appsignal

ActiveRecord vs. Ecto Part One

Elvio Viçosa Jr

Elvio Viçosa Jr on

ActiveRecord vs. Ecto
Part One

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

FieldType
full_namestring
emailstring
created_at (ActiveRecord) / inserted_at (Ecto)datetime
updated_atdatetime

invoices

FieldType
user_idinteger
payment_methodstring
paid_atdatetime
created_at (ActiveRecord) / inserted_at (Ecto)datetime
updated_atdatetime

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:

  • A user has many invoices
  • An invoice belongs to a user

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

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

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

1defmodule Financex.Repo.Migrations.CreateUsers do
2  use Ecto.Migration
3
4  def change do
5    create table(:users) do
6      add :full_name, :string, null: false
7      add :email, :string, null: false
8      timestamps()
9    end
10
11    create index(:users, [:email], unique: true)
12  end
13end

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

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

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

1defmodule Financex.Repo.Migrations.CreateInvoices do
2  use Ecto.Migration
3
4  def change do
5    create table(:invoices) do
6      add :user_id, references(:users)
7      add :payment_method, :string
8      add :paid_at, :utc_datetime
9      timestamps()
10    end
11
12    create index(:invoices, [:user_id])
13  end
14end

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     Column     |            Type             | Nullable |               Default
2----------------+-----------------------------+----------+--------------------------------------
3 id             | bigint                      | not null | nextval('invoices_id_seq'::regclass)
4 user_id        | bigint                      |          |
5 payment_method | character varying(255)      |          |
6 paid_at        | timestamp without time zone |          |
7 inserted_at    | timestamp without time zone | not null |
8 updated_at     | timestamp without time zone | not null |
9
10Indexes:
11    "invoices_pkey" PRIMARY KEY, btree (id)
12    "invoices_user_id_index" btree (user_id)
13Foreign-key constraints:
14    "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

1class User < ApplicationRecord
2  has_many :invoices
3end

invoices

1class Invoice < ApplicationRecord
2  belongs_to :user
3end

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

1defmodule Financex.Accounts.User do
2  use Ecto.Schema
3
4  schema "users" do
5    field :full_name, :string
6    field :email, :string
7    has_many :invoices, Financex.Accounts.Invoice
8    timestamps()
9  end
10end

invoices

1defmodule Financex.Accounts.Invoice do
2  use Ecto.Schema
3
4  schema "invoices" do
5    field :payment_method, :string
6    field :paid_at, :utc_datetime
7    belongs_to :user, Financex.Accounts.User
8    timestamps()
9  end
10end

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!

Share this article

RSS

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps