Logo of AppSignal

Menu

ActiveRecord vs. Ecto
Part One

Elvio Viçosa Jr on

Data is a core part of most software applications. Mapping and querying data from a database is a recurring task in the life of a developer. Because of this, it is important to understand the process and be able to use abstractions that simplify the task.

In this post, the first of a series of two, you’ll find a comparison between ActiveRecord (Ruby) and Ecto (Elixir). We’ll see how both tools enable developers to migrate and map database schemas.

So we’ll be comparing Apples and Oranges. (Original) Batgirl, who never needed to say a word, versus Batman, explicitly stating ‘I’m Batman’. Implicit, convention over configuration, versus Explicit intention. Round one. Fight!

ActiveRecord

With more than 10 years since its release, chances are, you’ve already heard about ActiveRecord - the famous ORM that is shipped by default with Ruby on Rails projects.

ActiveRecord is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. ActiveRecord facilitates the creation and use of business objects whose data requires persistent storage in a database. It is an implementation of the ActiveRecord pattern which is itself, a description of an Object Relational Mapping system.

Although it is mostly known to be used with Rails, ActiveRecord can also be used as a standalone tool, getting embedded in other projects.

Ecto

When compared to ActiveRecord, Ecto is a quite new (and at the moment not as famous) tool. It is written in Elixir and is included by default in Phoenix projects.

Unlike ActiveRecord, Ecto is not an ORM, but a library that enables the use of Elixir to write queries and interact with the database.

Ecto is a domain specific language for writing queries and interacting with databases in Elixir.

By design, Ecto is a standalone tool, being used in different Elixir projects and not connected to any framework.

Aren’t you Comparing Apples and Oranges?

Yes we are! Although ActiveRecord and Ecto are semantically different, but common features like database migrations, database mappings, queries and validations are supported by both ActiveRecord and Ecto. And we can achieve the same results are achieved using both tools. For those interested in Elixir coming from a Ruby background we thought this would be an interesting comparison.

The Invoice System

Throughout the rest of the post, a hypothetical invoice system will be used for demonstration. Let’s imagine we have a store selling suits to super heroes. To keep things simple, we’ll only have two tables for the invoice system: users and invoices.

Below is the structure of those tables, with their fields and types:

users

Field Type
full_name string
email string
created_at (ActiveRecord) / inserted_at (Ecto) datetime
updated_at datetime

invoices

Field Type
user_id integer
payment_method string
paid_at datetime
created_at (ActiveRecord) / inserted_at (Ecto) datetime
updated_at datetime

The users table has four fields: full_name, email, updated_at and a fourth field that is dependent on the tool used. ActiveRecord creates a created_at field while Ecto creates an inserted_at field to represent the timestamp of the moment the record was first inserted in the database.

The second table is named invoices. It has five fields: user_id, payment_method, paid_at, updated_at and, similar to the users table, either created_at or inserted_at, depending on the tool used.

The users and invoices tables have the following associations:

Migrations

Migrations allow developers to easily evolve their database schema over time, using an iterative process. Both ActiveRecord and Ecto enable developers to migrate database schema using a high-level language (Ruby and Elixir respectively), instead of directly dealing with SQL.

Let’s take a look at how migrations work in ActiveRecord and Ecto by using them to create the users and invoices tables.

ActiveRecord: Creating the Users Table

Migration

1
2
3
4
5
6
7
8
9
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :full_name, null: false
      t.string :email, index: {unique: true}, null: false
      t.timestamps
    end
  end
end

ActiveRecord migrations enable the creation of tables using the create_table method. Although the created_at and updated_at fields are not defined in the migration file, the use of t.timestamps triggers ActiveRecord to create both.

Created Table Structure

After running the CreateUsers migration, the created table will have the following structure:

1
2
3
4
5
6
7
8
9
10
   Column   |            Type             | Nullable |              Default
------------+-----------------------------+----------+-----------------------------------
 id         | bigint                      | not null | nextval('users_id_seq'::regclass)
 full_name  | character varying           | not null |
 email      | character varying           | not null |
 created_at | timestamp without time zone | not null |
 updated_at | timestamp without time zone | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "index_users_on_email" UNIQUE, btree (email)

The migration is also responsible for the creation of a unique index for the email field. The option index: {unique: true} is passed to the email field definition. This is why the table has listed the "index_users_on_email" UNIQUE, btree (email) index as part of its structure.

Ecto: Creating the Users Table

Migration

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule Financex.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :full_name, :string, null: false
      add :email, :string, null: false
      timestamps()
    end

    create index(:users, [:email], unique: true)
  end
end

The Ecto migration combines the functions create() and table() to create the users table. The Ecto migration file is quite similar to its ActiveRecord equivalent. In ActiveRecord the timestamps fields (created_at and updated_at) are created by t.timestamps while in Ecto the timestamps fields (inserted_at and updated_at) are created by the timestamps() function.

There’s a small difference between both tools on how indexes are created. In ActiveRecord, the index is defined as an option to the field being created. Ecto uses the combination of the functions create() and index() to achieve that, consistent with how the combination is used to create the table itself.

Created Table Structure

1
2
3
4
5
6
7
8
9
10
   Column    |            Type             | Nullable |              Default
-------------+-----------------------------+----------+-----------------------------------
 id          | bigint                      | not null | nextval('users_id_seq'::regclass)
 full_name   | character varying(255)      | not null |
 email       | character varying(255)      | not null |
 inserted_at | timestamp without time zone | not null |
 updated_at  | timestamp without time zone | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_email_index" UNIQUE, btree (email)

The table created on running the Financex.Repo.Migrations.CreateUsers migration has an identical structure to the table created using ActiveRecord.

ActiveRecord: Creating the invoices Table

Migration

1
2
3
4
5
6
7
8
9
10
class CreateInvoices < ActiveRecord::Migration[5.2]
  def change
    create_table :invoices do |t|
      t.references :user
      t.string :payment_method
      t.datetime :paid_at
      t.timestamps
    end
  end
end

This migration includes the t.references method, that wasn’t present in the previous one. It is used to create a reference to the users table. As described earlier, a user has many invoices and an invoice belongs to a user. The t.references method creates a user_id column in the invoices table to hold that reference.

Created Table Structure

1
2
3
4
5
6
7
8
9
10
11
     Column     |            Type             | Nullable |               Default
----------------+-----------------------------+----------+--------------------------------------
 id             | bigint                      | not null | nextval('invoices_id_seq'::regclass)
 user_id        | bigint                      |          |
 payment_method | character varying           |          |
 paid_at        | timestamp without time zone |          |
 created_at     | timestamp without time zone | not null |
 updated_at     | timestamp without time zone | not null |
Indexes:
    "invoices_pkey" PRIMARY KEY, btree (id)
    "index_invoices_on_user_id" btree (user_id)

The created table follows the same patterns as the previously created table. The only difference is an extra index (index_invoices_on_user_id), which ActiveRecord automatically adds when the t.references method is used.

Ecto: Creating the invoices Table

Migration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
defmodule Financex.Repo.Migrations.CreateInvoices do
  use Ecto.Migration

  def change do
    create table(:invoices) do
      add :user_id, references(:users)
      add :payment_method, :string
      add :paid_at, :utc_datetime
      timestamps()
    end

    create index(:invoices, [:user_id])
  end
end

Ecto also supports the creation of database references, by using the references() function. Unlike ActiveRecord, which infers the column name, Ecto requires the developer to explicitly define the user_id column name. The references() function also requires the developer to explicitly define the table the reference is pointing to, which in this example, is the users table.

Created Table Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
     Column     |            Type             | Nullable |               Default
----------------+-----------------------------+----------+--------------------------------------
 id             | bigint                      | not null | nextval('invoices_id_seq'::regclass)
 user_id        | bigint                      |          |
 payment_method | character varying(255)      |          |
 paid_at        | timestamp without time zone |          |
 inserted_at    | timestamp without time zone | not null |
 updated_at     | timestamp without time zone | not null |

Indexes:
    "invoices_pkey" PRIMARY KEY, btree (id)
    "invoices_user_id_index" btree (user_id)
Foreign-key constraints:
    "invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)

Both migrations are also quite similar. When it comes to the way the references feature is handled, there are a few differences:

  1. Ecto creates a foreign-key constraint to the user_id field ("invoices_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)), which maintains the referential integrity between the users and invoices tables.

  2. ActiveRecord automatically creates an index for the user_id column. Ecto requires the developer to be explicit about that. This is why the migration has the create index(:invoices, [:user_id]) statement.

ActiveRecord: Data Mapping & Associations

ActiveRecord is known for its “conventions over configurations” motto. It infers the database table names using the model class name, by default. A class named User, by default, uses the users table as its source. ActiveRecord also maps all the columns of the table as an instance attribute. Developers are only required to define the associations among the tables. These are also used by ActiveRecord to infer the involved classes and tables.

Take a look at how the users and invoices tables are mapped using ActiveRecord:

users

1
2
3
class User < ApplicationRecord
  has_many :invoices
end

invoices

1
2
3
class Invoice < ApplicationRecord
  belongs_to :user
end

Ecto: Data Mapping & Associations

On the other hand, Ecto requires the developer to be explicit about the data source and its fields. Although Ecto has similar has_many and belongs_to features, it also requires developers to be explicit about the associated table and the schema module that is used to handle that table schema.

This is how Ecto maps the users and invoices tables:

users

1
2
3
4
5
6
7
8
9
10
defmodule Financex.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :full_name, :string
    field :email, :string
    has_many :invoices, Financex.Accounts.Invoice
    timestamps()
  end
end

invoices

1
2
3
4
5
6
7
8
9
10
defmodule Financex.Accounts.Invoice do
  use Ecto.Schema

  schema "invoices" do
    field :payment_method, :string
    field :paid_at, :utc_datetime
    belongs_to :user, Financex.Accounts.User
    timestamps()
  end
end

Wrap Up

In this post, we compared apples and oranges without a blink. We compared how ActiveRecord and Ecto handle database migrations and mapping. A battle of the implicit slient original Batgirl versus the explicit 'I’m Batman’ Batman.

Thanks to “convention over configuration”, using ActiveRecord usually involves less writing. Ecto goes in the opposite direction, requiring developers to be more explicit about their intents. Other than “less code” being better in general, ActiveRecord has some optimal defaults in place that save the developer from having to make decisions on everything and also having to understand all the underlying configurations. For beginners, ActiveRecord is a more suitable solution, because it makes “good enough” decisions by default as long as you strictly follow its standard.

The explicit aspect of Ecto makes it easier to read and understand the behavior of a piece of code, but it also requires the developer to understand more about the database properties and the features available. What might make Ecto look cumbersome at first glance, is one of its virtues. Based on my personal experience in both ActiveRecord and Ecto world, Ecto’s explicitness removes the “behind the scene” effects and uncertainty that is common in projects with ActiveRecord. What a developer reads in code, is what happens in the application and there is no implicit behavior.

In a second blog in a few weeks, in the two part “ActiveRecord vs Ecto” series, we’ll cover how queries and validations work in both ActiveRecord and Ecto.

We’d love to know what you thought of this article. We’re always on the lookout for new topics to cover, so if you have a subject you’d like to learn more about, please don’t hesitate to let us know at @AppSignal!

This post is written by guest author Elvio Vicosa. Elvio is the author of the book Phoenix for Rails Developers.

10 latest articles

Go back

Subscribe to

Elixir Alchemy

A true alchemist is never done exploring. And neither are we. Sign up for our Elixir Alchemy email series and receive deep insights about Elixir, Phoenix and other developments.

We'd like to set cookies, read why.