Backend Development 15 min read

Comparing ActiveRecord and Ecto: ORM Approaches in Ruby on Rails and Elixir

This article compares Ruby on Rails' ActiveRecord and Elixir's Ecto, explaining their mapping definitions, data retrieval, query syntax, write operations, validations, callbacks, and associations, while highlighting the trade‑offs between the convenience of ActiveRecord and the explicitness of Ecto.

Liulishuo Tech Team
Liulishuo Tech Team
Liulishuo Tech Team
Comparing ActiveRecord and Ecto: ORM Approaches in Ruby on Rails and Elixir

ActiveRecord is the model layer of Ruby on Rails and an ORM (Object‑relational mapping). Ecto is an Elixir library that serves a similar purpose. Both abstract database operations so developers can work without writing raw SQL and map table rows to language structures.

For a typical users table the definitions look like this:

# ActiveRecord
class User < ActiveRecord::Base
end
# Ecto
defmodule User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
  end
end

ActiveRecord relies on inheritance from ActiveRecord::Base and a naming convention that maps the class User to the plural table name users . Ecto requires an explicit schema definition, making the table name and fields visible in the code.

To extract the part of an email before the @ as a name, the two libraries use the following code:

# ActiveRecord
class User < ActiveRecord::Base
  def name
    email.split('@').first
  end
end

irb> user = User.first
=> #<User id: 1, email: "[email protected]">
irb> user.name
=> "foo"
# Ecto
defmodule User do
  def name(user) do
    user.email |> String.split("@") |> List.first
  end
end

iex> user = Ecto.Repo.get_by User, id: 1
%User{id: 1, email: "foo.example.com"}
iex> User.name(user)
"foo"

This comparison also reflects the underlying language paradigms: ActiveRecord stores data and behavior together in an object (object‑oriented), while Ecto separates data (a struct) from behavior (functions), which often results in more explicit code.

From a testing perspective the difference is visible in how test data is prepared:

# ActiveRecord
user = User.new(email: "[email protected]")
assert user.name == "foo"
# Ecto
user = %User{email: "[email protected]"}
assert User.name(user) == "foo"

ActiveRecord automatically initializes objects from attribute hashes, whereas Ecto builds a struct and requires explicit functions for any processing.

Fetching a user with id = 2 demonstrates query handling:

# ActiveRecord
user = User.where(id: 2).first
# Ecto
user = Ecto.Repo.one(from u in User, where: u.id == 2)

ActiveRecord performs the query through the User class alone, while Ecto involves three modules: Ecto.Repo , Ecto.Query , and the schema module.

Query syntax differs as well. ActiveRecord offers chainable methods such as where , order , group , joins , and select . Ecto provides a LINQ‑like DSL that resembles raw SQL:

from(p in Post,
  group_by: p.category,
  select: {p.category, count(p.id)})

Inserting a record also shows the contrast:

# ActiveRecord
class User < ActiveRecord::Base
  validates_presence_of :email
end

irb> User.create!(email: "[email protected]")
# Ecto
defmodule User do
  import Ecto.Changeset
  def changeset(user, params \\ %{}) do
    user
    |> cast(params, ~w(email))
    |> validate_required([:email])
  end
end

iex> changeset = User.changeset(%User{}, %{email: "[email protected]"})
iex> Ecto.Repo.insert(changeset)

ActiveRecord embeds validations directly in the model class, automatically running them on create or update . Ecto separates validation logic into an Ecto.Changeset , allowing different changesets for different contexts (e.g., email signup, phone signup, guest, OAuth).

Callbacks are another divergence: ActiveRecord supports lifecycle callbacks (before_save, after_create, etc.), while Ecto 2.0 removed callbacks to keep schema and database operations decoupled, encouraging explicit handling of side effects.

Both libraries support associations (one‑one, one‑many, many‑to‑many). The definitions are similar:

# ActiveRecord
class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end
# Ecto
defmodule User do
  schema "users" do
    has_many :posts, Post
  end
end

defmodule Post do
  schema "posts" do
    field :title, :string
    belongs_to :user, User
  end
end

When accessing user.posts , ActiveRecord triggers a database query automatically, whereas Ecto returns a Ecto.Association.NotLoaded struct until the developer explicitly calls Repo.preload . This explicitness helps avoid hidden N+1 queries.

In summary, ActiveRecord offers a concise, convention‑driven API that hides much of the database plumbing, making it quick to start but potentially obscuring performance costs. Ecto, built later, emphasizes explicit schema definitions, separate validation pipelines, and explicit association loading, providing greater transparency and modularity at the cost of more boilerplate.

backendORMRubyactiverecordEctoElixir
Liulishuo Tech Team
Written by

Liulishuo Tech Team

Help everyone become a global citizen!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.