Ecto vs ActiveRecord

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