Table of Contents
Is there anyone out there in the Ruby community who doesn’t know Active Record? I don’t think so. Many of us start our adventure with Rails by writing AR models. And we write them a lot. We detect the N+1 issue many times. We refactor it much more. We doubt our ideas, solutions, and AR many, many times in our Ruby lives.
Okay, so what Active Record is, at its heart? Here’s one brief definition from the Ruby On Rails documentation:
"Active Record connects classes to relational database tables to establish an almost zero-configuration persistence layer for applications. The library provides a base class that, when subclassed, sets up a mapping between the new class and an existing table in the database. In the context of an application, these classes are commonly referred to as models. Models can also be connected to other models; this is done by defining associations."
Why, then, am I suffering from Stockholm syndrome if Active Record is merely a tool, rather than a part of our architecture or language? Why, if it’s just a “dumb detail” like the database (well, maybe less dumb)? Why do I like AR? Cause it’s the default option in Rails? Really?!
Well, these questions marked a watershed moment for me. I liked and hated Rails with AR simultaneously. But what’s wrong with this ORM?
- When I want to write more than one object in a single query, I need to write custom SQL—yes, it’s 2018, Big Data everywhere.
- When I want to write something more complicated than examples from the tutorial, I… also need to write custom SQL.
A couple of caveats for using AR:
- I need to know SQL syntax
- I don’t have SQL syntax checking by Ruby
- It’s not object-oriented
- From my own experience—it’s hard to maintain
- The model contains business logic, entity structure, additional actions, and more poorly defined logic.
Okay, it’s not exactly what I’d want from my best ORM. What’s worse? What does it say about my application, if the the key part of my choice doesn’t abide by a single responsibility principle? Making clean, well-designed code with SRP is hard even with a perfect stack, so if a tool like the models from AR are not that well-defined from the assumption, nobody will be able to create a perfect application.
Active Record is Bad—but with Arel It’s Quite Nice
Try to see how Arel sits with each point from the list above, but first off—how does it work? Simply put, Arel is Relational Algebra for Active Record, a kind of constructor for queries sent to AR. Sounds excellent, because that means that it’s a solution for first two points from my list. It’s easy to maintain, and it’s easy to understand. But tbh I don’t feel comfortable after coming back to Arel after a couple of weeks. It’s hard to understand in more complicated queries. If I wrote some magic in Arel, I’m entirely sure that any dev would have a lot of problems with reading this code a year later.
Okay, so it’s a great solution with a handful of disadvantages. A couple of weeks after learning about Arel, I met Sequel…
AR with Arel Is Quite Nice; Sequel Is Awesome!
Firstly, the most important thing about Sequel is the fact that it uses the PG extended query protocol. That makes Sequel much faster than AR and reduces server-side resource usage. Additionally, AR doesn’t use the whole potential of PG as opposed to Sequel.
When I first started working with this ORM, a product that I felt was very fresh to me, I had some trouble finding a good way to work with it. The first steps are quite hard, but its strength lies in the fact that you can learn it quite quickly and its incredible effectiveness. If I remember correctly, I needed between two and three weeks of learning Sequel to build the proficiency I needed for my domain. IMO, such a timeframe is strikingly short, but that’s mainly due to the very natural composition of the interface of that tool.
After another two weeks, I extracted the entire persistence logic from the (old) AR models and into simple repository classes. How do you think that works? How is it testable? Well, it’s awesome. For me, a young developer without considerable experience, it was a definite milestone in my skill building process. And, to cap it all off, Sequel tries hard to stay with zero issues on GitHub! That’s quite a feat—very few gems are this well-maintained like Jeremy Evans’ tool!
Using Sequel is not without its disadvantages, though—there is still a perceptible lack of gems supporting Sequel. However, if you always want to use gems, start by reading my most recent piece.
Why do developers like AR so much and why aren’t they interested in checking something new? It might be the considerable entry threshold, lack of clarity, or the complexity of Arel, or maybe the rather imperfect documentation of Sequel. It may be the convenience. Unfortunately, we are lazy. If we know something very well and it’s quite universal, we try to use it everywhere. It’s natural, but not a good practice. I can, however, assure you of one thing—the time you dedicate to testing various tools will not be spent in vain and will bear fruit.
What I’m Trying to Say
How many of you have used tools like Arel or Sequel? I believe many of you haven’t. (In one of my posts on how to update recursively with CTE I provided a good example you should explore.)
Description of the problem: Select all users from a DB with their parents which have the association by parent_id column.
Possible solution: Recursive Common Table Expression.
# Raw SQL
WITH RECURSIVE TEMP AS
(SELECT id, name
FROM users
UNION ALL
(SELECT t1.id, t1.name
FROM (SELECT * FROM users) AS t1
INNER JOIN TEMP ON (temp.id = t1.parent_id)))
SELECT *
FROM TEMP
# Sequel
def get_users_with_parents
db[:temp].with_recursive(
:temp,
db[:users].select(:id, :name),
db[:users].from_self.join(:temp, id: :parent_id).select(:t1__id, :t1__name),
)
end
# ActiveRecord
def raw_sql
<<-SQL
WITH RECURSIVE TEMP AS
(SELECT id, name
FROM users
UNION ALL
(SELECT t1.id, t1.name
FROM (SELECT * FROM users) AS t1
INNER JOIN TEMP ON (temp.id = t1.parent_id)))
SELECT *
FROM TEMP
SQL
end
ActiveRecord::Base.connection.execute(raw_sql)
# Arel
def get_users_with_parents
recursive = Arel::SelectManager.new
recursive.from(:users).project(:id, :name)
non_recursive = Arel::SelectManager.new
non_recursive.from(:users).project(:id, :name).join(temp).on(users_parent_id.eq(temp_id))
union = recursive.union(non_recursive)
as_statement = Arel::Nodes::As.new(temp, union)
manager = Arel::SelectManager.new
manager.with(:recursive, as_statement).from(temp).project(Arel.star)
end
It’s a very particular case, but let's be honest—sooner or later we’re all going to encounter that particular issue.
How about everyday use cases? Maybe inserting multiple records in a single query?
data = [
{ name: "Owner", email: "owner@example.com" },
{ name: "Employee", email: "employee@example.com" },
...
]
# Raw SQL
INSERT INTO users (name, email)
VALUES ("Owner", "owner@example.com"), ("Employee", "employee@example.com")
# Sequel
db[:users].multi_insert(data)
# ActiveRecord by #import
User.import(data.first.keys, data.map(&:values))
# Arel
table = Table.new(:users)
manager = Arel::InsertManager.new
manger.into(table)
manager.columns = [table[:name], table[:email]]
manager.values = manager.create_values_list(data.map(&values))
Who emerges the winner? For me, Sequel definitely takes the cake as it is the most readable and easiest to understand. :)
A Word of Conclusion
Don’t get me wrong—I had no intention of coercing you into switching to another ORM. That’s not the greatest idea, because although AR has some disadvantages, it still has a lot of use cases in my opinion. On the other hand, one of the most important skills a developer can have is an ability to draw on a variety of different solutions, patterns, and tools. Such an ability also opens new doors in our career paths. With this post, I simply wanted to offer you my experiences, and demonstrate the advantages and disadvantages of the three solutions discussed above. But above all, I wanted to show you great alternatives to what you may already know.
Meet Ruby on Rails experts
We’ve been doing Ruby on Rails since 2010 and our services quality and technical expertise are confirmed by actual clients. Work with experts recognized as #1 Ruby on Rails company in the World in 2017 by Clutch.