ActiveRecord vs. Ecto Part One
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
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
invoices
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
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 endend
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:
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
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) endend
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
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
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 endend
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
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
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]) endend
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
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:
- 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. - 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 thecreate 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
class User < ApplicationRecord has_many :invoicesend
invoices
class Invoice < ApplicationRecord belongs_to :userend
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
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() endend
invoices
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() endend
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.