Ecto is a database library that is used by default in Phoenix, but it can be used in any other Elixir project. It's important to mention Ecto is not an ORM. We can think of Ecto as a tool for mapping any type of data.
Ecto is different from Active Record. It's easy for Rails developers who start using the Phoenix framework to think of Ecto as an alternative for ActiveRecord.
We created an app to compare ActiveRecord and Ecto queries side-by-side.
Let's talk about the main ideas behind Ecto, and try to compare it with ActiveRecord.
Main difference
ActiveRecord: We can represent data using: behaviors + state.
Ecto: We need to represent data using: functions.
If we keep this in mind, it will help our understanding of Ecto.
Active Record pattern
ActiveRecord has a general pattern of accessing data used in Object-oriented languages. So, this is not specfically the Active Record pattern.
Using ActiveRecord, we can do:
artist = Artist.get(1)
artist.name = "Name"
artist.save
This makes a lot of sense for Object-Oriented languages. Data has behavior and state. This is pretty straightforward. How does Ecto handle that?
Repository Pattern
As a functional language we don't have data with state, nor do we have behavior. We only have functions.
In general, if you want to talk with the database, you need to talk with the Repository first.
artist = Repo.get(Artist, 1)
changeset = Artist.changeset(artist, name: "Changed name")
Repo.update(changeset)
If we check side-by-side what Active Record and repository does, we cannot see when Active Record touches the Database. We just do a save and it hits the database implicitly. In Ecto, you always interact with the database explicitly.
Ecto will not talk to the database without you asking it to. Everything is totally explicit. Any interaction with the database should pass through the Repository.
Migrations
Ecto also has migrations. This is not really different from what ActiveRecord offers to us.
defmodule SlackPosting.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :text, :text
add :user_slack_id, :string
add :user_name, :string
timestamps()
end
end
end
Schemas
Schema is normally a map between your types and your database. But not necessarily.
If we check the documentation:
An Ecto schema is used to map any data source into an Elixir struct. One of such use cases is to map data coming from a repository, usually a table, into Elixir structs.
An interesting thing to mention is that we don't need a schema for using Ecto. We can bypass the use of Schemas by using the table name as a string. Schemas are very flexible.
Here is an example of Schema definition in Ecto.
defmodule SlackPosting.Journals.Post do
use Ecto.Schema
import Ecto.Changeset
alias SlackPosting.Journals.Post
schema "posts" do
field :user_slack_id, :string
field :user_name, :string
field :text, :string
many_to_many :tags, SlackPosting.Journals.Tag, join_through: SlackPosting.Journals.PostTag
has_many :comments, SlackPosting.Journals.Comment
timestamps()
end
@doc false
def changeset(%Post{} = post, attrs) do
post
|> cast(attrs, [:text, :user_slack_id, :user_name])
|> validate_required([:text, :user_slack_id])
end
end
Changeset
A changeset handles the entire lifecycle of database updates. Filtering, casting, validations, handling errors, etc.
For our Post, we have validations for text and user_slack_id. We are using the cast to only get the correct attributes from our post.
def changeset(%Post{} = post, attrs) do
post
|> cast(attrs, [:text, :user_slack_id, :user_name])
|> validate_required([:text, :user_slack_id])
end
Any validation will be here.
Associations
Ecto also offers us associations. In this example, we are doing a has_many association and a many_to_many association.
schema "posts" do
field :user_slack_id, :string
field :user_name, :string
field :text, :string
many_to_many :tags, SlackPosting.Journals.Tag, join_through: SlackPosting.Journals.PostTag
has_many :comments, SlackPosting.Journals.Comment
This is pretty similar to what ActiveRecord does.
Lazy loading
Ecto does not support Lazy Loading. Consider our Post which has_many :comments. If we got a single post, it does not load by default the comments of this post. We need to tell Ecto explicitly to preload the comments. One of the benefits of doing that is it helps us avoid N+1 queries.
In our case, we have Posts. Post has_many Comments and Tags. How can I preload the tags and comments using Ecto? We just need to use the preload function.
def list_posts do
Repo.all(Post)
|> Repo.preload([:comments, :tags])
end
Active Record and Ecto side by side
As a Rails developer, I got used to using ActiveRecord. For me, I would have something comparing the queries using Ecto and Active Record.
With this in mind, I created a catalog of queries, which anyone can contribute to. It's called Ecto vs ActiveRecord. It's alive!
The idea is to compare common queries side-by-side in Ecto and Active Record.
Contribute!

Summary
Today we saw what Ecto can provide to us and some comparison with ActiveRecord.
Here's a great video from ElixirConf that goes into Ecto in detail and explains all this extremely well.
Resources