In this post, I am going to take a look at Ecto, a data mapping and database query library designed to provide seamless integration between Elixir code and database operations and compare it to Ruby on Rail's ActiveRecord.
Ecto and ActiveRecord are both popular data access libraries in their communities, but they have different design philosophies and approaches to handling database operations.
Design Pattern
Ecto follows the Repository design pattern, where all data mapping and query operations are centralized through a repository.
It is best to take a look at a simple example to illustrate this:
user = Repo.get_by(MyApp.User, name: "Han")
changes = Ecto.Changeset.change(user, name: "Sam")
Repo.update!(changes)
As you can see, in Ecto data operations are orchestrated through the Repo
Module which provides a structured and centralized way to interact with the database. This separation of concerns between data access and domain logic contributes to a distinct programming paradigm in Ecto compared to ActiveRecord, which follows the Active Record design pattern, where data access and persistence logic are directly tied to the model classes.
In other words, all operations in Active Record are directly performed on the model class itself. This includes queries, updates, and other database actions. For example, fetching and updating a User record in ActiveRecord would be done as follows:
user = User.find_by(name: "Han")
user.update!(name: "Sam")
Explicitness of Operations
Ecto emphasizes explicitness in database operations, making queries and data manipulation actions clear and easy to reason about in the code.
In contrast, ActiveRecord often relies on "magic" methods and conventions to handle database operations automatically. While this can simplify development in some cases, it may introduce complexity and make it harder to understand the underlying database interactions.
Let's explore another code example that highlights this difference between the two. Here's how you would insert a new user record into the database using Ecto:
# User Schema Definition in Elixir (Ecto):
defmodule User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :age, :string
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :age])
|> validate_required([:name, :age])
end
end
# Creating a new user record using Ecto
changeset = User.changeset(%User{}, %{name: "Alice", age: 30})
case Repo.insert(changeset) do
{:ok, user} ->
IO.puts("User inserted: #{inspect(user)}")
{:error, changeset} ->
IO.puts("Error inserting user: #{inspect(changeset.errors)}")
end
In this Elixir code snippet, the process of creating a new user record involves explicitly creating a changeset, validating the data, and then inserting the record into the database using Repo.insert()
. The code clearly outlines the steps involved in inserting a new user record.
Let's see how you would insert a new user record into the database using ActiveRecord:
# Example of User model defined in ActiveRecord
class User < ApplicationRecord
validates :name, presence: true
validates :age, presence: true
end
# Creating a new user record using ActiveRecord
user = User.new(name: "Alice", age: 30)
if user.save
puts "User inserted: #{user.inspect}"
else
puts "Error inserting user: #{user.errors.full_messages}"
end
In Ruby, the process of creating and saving a new user record is simplified using ActiveRecord's 'magic' methods. The User.new
method initializes a new user object, and user.save
handles the insertion into the database, abstracting away some of the explicit steps involved.
The above example also highlights another very important key difference between Ecto and Active Record's design philosophy.
Functional vs Object Oriented Paradigm
Ecto leverages Elixir's functional programming capabilities, with changesets highlighting the functional approach to data manipulation. As you have already seen changesets are built through a series of function calls, ensuring data validity before persistence.
To further demonstrate the functional step-by-step approach of Ecto changesets, we can enhance the changeset/2
function above to include additional steps such as data transformation and custom validations:
# User Schema Definition in Elixir (Ecto)
defmodule User do
use Ecto.Schema
schema "users" do
field :name, :string
field :age, :integer
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :age])
|> validate_required([:name, :age])
|> transform_age_to_integer
|> validate_age_range
end
defp transform_age_to_integer(changeset) do
case get_field(changeset, :age) do
nil -> changeset
age -> put_change(changeset, :age, String.to_integer(age))
end
end
defp validate_age_range(changeset) do
case get_field(changeset, :age) do
nil -> changeset
age when age < 18 or age > 100 ->
add_error(changeset, :age, "Age must be between 18 and 100")
_ -> changeset
end
end
end
ActiveRecord, on the other hand, is more object-oriented, treating database records as objects with built-in methods for validating data.
To mirror the functionality demonstrated in the Ecto example above, we can enhance the User
model in ActiveRecord to include custom validations for the age range and data transformation for the age attribute.
# User Model Definition in Ruby (ActiveRecord)
class User < ApplicationRecord
validates :name, presence: true
validates :age, presence: true
validate :age_range_validation
before_validation :transform_age_to_integer
private
def transform_age_to_integer
self.age = age.to_i if age.present?
end
def age_range_validation
if age.present? && (age < 18 || age > 100)
errors.add(:age, "Age must be between 18 and 100")
end
end
end
Querying the Database
Ecto provides a query DSL (Domain-Specific Language) that closely resembles SQL but is integrated into Elixir. This DSL allows developers to write complex queries with advanced filters, joins, and ordering in a way that feels natural within the Elixir ecosystem.
# Ecto Query Example in Elixir (Ecto)
query = from u in User,
where: u.age > 18,
select: u.name
Repo.all(query)
# Raw SQL Equivalent to Ecto Query above
SELECT name
FROM users
WHERE age > 18;
In the example above, the from
macro is used to construct a query that selects the names of users with an age greater than 18. As you can see, the syntax of Ecto queries closely resembles raw SQL statements, making it intuitive for developers familiar with writing SQL queries.
Here is the same database operation written in Ruby using Active Record:
# ActiveRecord Query Example in Ruby (ActiveRecord)
User.where("age > ?", 18).pluck(:name)
In the Ruby example above, the where
method is used to filter users based on age greater than 18, and the pluck
method is used to select only the names of the filtered users. ActiveRecord queries in Ruby use a different syntax compared to raw SQL statements, providing a more abstract and object-oriented approach to querying the database.
This was a short post on Ecto, the popular data mapping and database query library from the Elixir community comparing it to Ruby's equivalent Active Record.
In a future post, I am going to dig much deeper into the topic of Ecto.
Thank you for reading.