A few years ago, I wrote an article about moving from Django to Rails. As I said there, I don’t find it too hard moving from one programming language to another, as long as you have good knowledge on concepts they all share. Each language, of course, brings something on its own.
Last year, I decided to try something new. As a Ruby developer, Elixir proved to be a logical choice, and I have to admit, I am not disappointed. I fell in love with it at first sight, and I enjoy working with Elixir/Phoenix now.
One thing that I especially like is Ecto, a database wrapper, and a domain-specific language for writing queries in Elixir. Although it’s not entirely the same thing as Rails’ Active Record (Ecto is not ORM), they serve the same purpose.
In this article, I’ll write about few things I like about Ecto that aren’t available in Active Record. It’s not meant to be a “Phoenix is better than Rails” rant as both frameworks provide really great things and we can all learn from each of them.
Changesets
When you want to insert or update the data in your database with Ecto, you should use changesets. As the official documentation says, they allow you to filter, cast and validate changes before you apply them to the data. When I first saw the concept of changesets, I thought of a form object pattern that I often use in Rails. Changesets, like form objects, allow you to have different ‘forms’ for various use cases. Think of a registration form vs. update profile form. Both operate on the same model (User
), but they have different validations.
def registration_changeset(struct, params) do
struct
|> cast(params, [:email, :password])
|> validate_required([:email, :password])
|> unique_constraint(:email)
|> put_password_hash()
end
def update_profile_changeset(struct, params) do
struct
|> cast(params, [:first_name, :last_name, :age])
|> validate_required([:first_name, :last_name])
end
To accomplish this in Active Record, you have to use conditional validations (if/unless) which is ugly and hard to reuse, or you would have to use external gems like Active Type or Reform.
Ecto provides it out-of-the-box. The whole data manipulation philosophy in Ecto relies on the concept of small, reusable and modular changesets making it a powerful tool.
Validations use database constraints
Ecto can transform database constraints, like unique indexes or foreign key checks, into errors. This allows for a consistent database while giving users proper feedback and friendly error messages.
Let’s say we have a unique index on email
field in users
table.
create unique_index(:users, [:email])
If we try to insert a user with an email that already exists, an exception will be raised with a message like this:
** (Ecto.ConstraintError) constraint error when attempting to
insert struct:
* unique: users_email_index
If you would like to convert this constraint into an error, please
call unique_constraint/3 in your changeset and define the proper
constraint name.
Let’s call the unique_constraint/3
function in our changeset.
def changeset(struct, params) do
struct
|> cast(params, [:email])
|> unique_constraint(:email)
end
Now, if we try to do the same thing as above, Ecto will catch the exception and transform it into a readable changeset error:
changeset.errors
[email: {"has already been taken", []}]
On the other hand, Active Record does not catch errors raised by the database, but rather checks the database state right before performing a validation. This could lead to unwanted situations in case of high traffic and a lot of parallel requests.
Preloading
We all know that N+1 queries are a bad thing. In most cases, you want to lower the number of database queries in a single request. Both Active Record and Ecto provide a way to specify a list of associations that need to be fetched along with the main query.
The difference is that Ecto will enforce preloading if you want to use the associated resource (it will raise errors in case you don’t), while Active Record will load that resource at the time you use it. As I tend to forget to preload associations and because I don’t want bad things in my apps, I like Ecto’s enforcement.
user = MyApp.Repo.get(MyApp.User, 1)
user.company
#Ecto.Association.NotLoaded<association :company is not loaded>
Batch inserts
In Rails, we could use a gem like Active Record Import or similar, but there is no built-in solution. Although Active Record’s create
method can accept an array of objects, it does not perform a batch insert, but rather inserts each record one by one.
Ecto provides a built-in function called insert_all/3
that performs batch inserts and it works well.
MyApp.Repo.insert_all(
User,
[
[
first_name: "John",
last_name: "Doe",
email: "john.doe@example.com"
],
[
first_name: "Jane",
last_name: "Doe",
email: "jane.doe@example.com"
]
]
)
Repo.one
Sometimes you expect only one record to be returned by your query. If you get more than one, you know something is wrong. Ecto provides a function called one
that will fetch a single result from the query. It raises an exception if more than one entry is returned.
Imagine we have a unique index on nickname
field in users
table.
Somewhere in our code, we have a query to find a user with the specified nickname:
query = from u in User, where: u.nickname == "mickey"
Repo.one(query)
As we know we have a unique index, we are expecting only one result.
After some time, our business logic changes, and we add another field to our unique index – group_id
. Now we can have multiple users with the same nickname, but in different groups.
Let’s assume that we changed our index, but forgot to change the query in the code; when we query on a nickname that is the same for multiple users, Ecto will raise the following error:
** (Ecto.MultipleResultsError) expected at most one result but
got 2 in query
If Repo.one
didn’t raise the error on multiple results, we probably wouldn’t easily notice that something is wrong. Our business logic would continue to work with the first user returned by the database, not necessarily the right one.
If we compare this with Active Record, methods like find_by
would return the first record matching the specified conditions.
Ecto query language
Last, but not least, I like the way queries are written in Ecto. They just feel natural and SQL-ish to me. Let me give you an example of a query written in Ecto:
from user in User,
left_join: company in assoc(user, :company),
where: user.age < 25 or user.age > 60,
select: %{
first_name: user.first_name,
last_name: user.last_name,
age: user.age,
company: company.name
}
Isn’t that beautiful?
While there are some things I didn’t cover in this article, it’s a brief overview of things I love about Ecto. Feel free to comment about things you love about each of these libraries.