ActiveRecord is reinventing Sequel
For those who don’t know, Sequel is an ORM very similar to ActiveRecord, in a way that it also implements the Active Record pattern. As of this writing it’s 9 years old. I’ve already written about some of the main advantages of Sequel over ActiveRecord (and other people have as well: 1, 2).
I’m using Sequel for over a year now, and am finding it to be consistently better than ActiveRecord. But that’s just my opinion, right? You can’t really say that one tool is objectively better than the other, each tool has its tradeoffs.
Well, sometimes you simply can. What I’ve noticed is that, whenever a new shiny ActiveRecord feature comes, Sequel has already had the same feature for quite some time. That would be ok if these were a few isolated incidents, but they’re really not. ActiveRecord appears to have been consistently reinventing Sequel.
Wait, that can’t be right. ActiveRecord is insanely popular and it’s part of Rails, the Rails team surely wouldn’t work so hard reimplementing something that already exists. Anyway, that is a huge accusation, how can I possibly prove my claims? Give me a chance, I really do have evidence. A lot of evidence.
What I will do is walk you through ActiveRecord’s most notable updates, and look for Sequel’s equivalents. I will also compare times when a feature landed on both ORMs. I will list the features roughly in reverse chronological order (from newest to oldest), so that we start from fresh memories.
ActiveRecord 5
OR
The ActiveRecord::Relation#or
query method allows use of the OR operator
(previously you’d have to write SQL strings):
Post.where(id: 1).or(Post.where(id: 2))
# => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
Implementing this feature required a lot of discussion. The feature finally landed in ActiveRecord (commit), only 8 years behind Sequel (code).
Left joins
The ActiveRecord::Relation#left_joins
query method generates a LEFT OUTER
JOIN (previously kind of possible via #eager_load
):
User.left_joins(:posts)
# => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
The feature landed in ActiveRecord in 2015 (PR). On the other hand, Sequel has had support for all types of JOINs since 2008, and added “association joins” in 2014 (commit).
Attributes API
The attributes API allows specifying/overriding types of columns/accessors in your models, as well as querying with instances of those types, and bunch of other things.
It’s difficult to point out at a specific equivalent in Sequel since the area of ActiveRecord’s attributes API is so broad. In my opinion you can roughly achieve the same features with serialization, serialization_modification_detection, composition, typecast_on_load, and defaults_setter plugins.
Views
The ActiveRecord::ConnectionAdapters::AbstractAdapter#views
method defined on
connection adapters returns an array of database view names:
ActiveRecord::Base.connection.views #=> ["recent_posts", "popular_posts", ...]
Sequel implemented #views
in 2011
(commit),
4 years before ActiveRecord (commit).
Indexing Concurrently
This PostgreSQL feature is crucial for zero-downtime migrations on larger tables, ActiveRecord has had adding indices concurrently since 2013 (commit), and dropping concurrently since 2015 (commit).
Sequel supported both adding and dropping indices concurrently since 2012 (commit).
In batches
ActiveRecord::Relation#in_batches
yields batches of relations, suitable for
batched updates or deletes:
Person.in_batches { |people| people.update_all(awesome: true) }
Sequel doesn’t have an equivalent, because there is no one right way to do batched updates, it depends on the situation. For example, the following Sequel implementation in my benchmarks showed to be 2x faster than ActiveRecord’s:
(Person.max(:id) / 1000).times do |i|
Person.where(id: (i*1000 + 1)..((i+1) * 1000)).update(awesome: true)
end
Aborting hooks
Before Rails 5, returning false
in any before_*
callback resulted in
halting of callback chain. The new version
removes this behaviour and
requires you to be explicit about it:
class Person < ActiveRecord::Base
before_save do
throw(:abort) if some_condition
end
end
This is actually one of the rare cases where Sequel added the equivalent
cancel_action
method
being inspired by ActiveRecord’s change .
ActiveRecord 4
Adequate Record
Adequate Record is a set of performance improvements in ActiveRecord that
makes common find
and find_by
calls and some association queries up to 2x
faster.
However, running the ORM benchmark shows that Sequel is still much, much faster than ActiveRecord, even after the Adequate Record merge.
Postgres JSON, array and hstore
ActiveRecord 4 added support for Postgres JSON, array and hstore columns, along with automatic typecasting. From looking at the commits we can say that ActiveRecord received these features roughly at the same time as Sequel (pg_json, pg_array, pg_hstore), which is around the time these features got added to Postgres. Note that Sequel on top of this also has an API for querying these types of columns (pg_json_ops, pg_array_ops, pg_hstore_ops), which greatly improves readability.
Mutation detection
ActiveRecord 4.2+ automatically detects in-place changes to columns values, and marks the record as dirty. Sequel added this feature through modification_detection plugin after ActiveRecord. But note that in Sequel this is opt-in, so that users can decide whether they want the performance hit.
Where not
The where.not
query construct allows negating a where
clause, eliminating
the need to write SQL strings:
Person.where.not(name: "John")
It was added in 2012 (commit),
in which time Sequel’s equivalent exclude
was existing already for 5 years
(code).
Rewhere
In 2013 ActiveRecord::Relation#rewhere
was
added
allowing you to overwrite all existing WHERE conditions with new ones:
Person.where(name: "Mr. Anderson").rewhere(name: "Neo")
Sequel has had unfiltered
, which removes existing WHERE and HAVING conditions,
since 2008, 5 years before this ActiveRecord update
(commit).
Enum
ActiveRecord::Base#enum
was added to ActiveRecord 4.1
(commit),
giving the ability to map names to integer columns:
class Conversation < ActiveRecord::Base
enum status: [:active, :archived]
end
While Sequel doesn’t have this database-agnostic feature, it has the pg_enum plugin for Postgres’ enum type, although it was added only 1 year after ActiveRecord’s enum.
Automatic inverse associations
ActiveRecord 4.1 added a feature to automatically detect inverse associations,
instead of having to always use :inverse_of
(commit).
Sequel had this basically since it added associations in 2008, which was about 5 years before ActiveRecord’s update.
Contextual validations
Contextual validations allow passing a symbol when validating, and doing validations depending on the existence or absence of the given symbol.
Sequel doesn’t have this feature, since it’s a code smell to have this in the model, but Sequel’s instance-level validations allow you to validate records from service objects, which is a much better way of doing contextual validation.
Reversibility improvements
ActiveRecord 4.0 improved writing reversible migrations by allowing destructive
methods like remove_column
to be reversible, as well as adding a really handy
ActiveRecord::Migration#reversible
method allowing you to write everything in
a change
, not having to switch to up
and down
.
Sequel’s reversing capabilities are a bit lacking compared to ActiveRecord’s, they are currently about the same as ActiveRecord’s before this change.
Null relation
ActiveRecord 4.0 added a handy ActiveRecord::Relation#none
which represents
an empty relation, effectively implementing a null object pattern for relations.
Sequel added a null_dataset plugin as an inspiration to ActiveRecord’s feature.
ActiveRecord 3
EXPLAIN
In 2011 ActiveRecord 3.2 added ActiveRecord::Relation#explain
for EXPLAIN-ing
queries
(commit).
Sequel has had EXPLAIN support for Postgres since 2007
(code),
and for MySQL was added only later in 2012
(commit).
Pluck
ActiveRecord 3.2 added ActiveRecord::Relation#pluck
in 2011
(commit),
and added support for multiple columns in 2012 (commit).
Sequel’s equivalent Sequel::Dataset#select_map
existed since 2009
(commit),
and support for multiple columns was added in 2011 (commit).
Uniq
ActiveRecord 3.2 added SELECT DISTINCT through ActiveRecord::Relation#uniq
in
2011 (commit).
Sequel has had equivalent Sequel::Dataset#distinct
since 2007
(code),
4 years ahead of ActiveRecord.
Update column
ActiveRecord 3.1. added ActiveRecord::Base#update_column
for updating
attributes without executing validations or callbacks
(commit).
The equivalent behaviour in Sequel, user.this.update(...)
, at that moment
already existed for 4 years.
Reversible migrations
ActiveRecord 3.1 added support for reversible migrations via change
(commit).
Soon after that, and inspired by ActiveRecord, Sequel added its support for
reversible migrations (commit).
Arel
Finally, we come probably to ActiveRecord’s biggest update: the chainable query interface and extraction of Arel. For those who don’t know, ActiveRecord prior to 3.0 didn’t have a chainable query interface.
Sequel already had this chainable query interface, before Nick Kallen started working on Arel (source), meaning he was obviously inspired by Sequel. Also, building queries with Arel looks very different than through models (it’s arguably more clunky), while Sequel’s low-level interface gives you the exact same API as you have through models.
Alternative to Arel for building complex queries is Squeel. Beside the obvious insipration indicated by the anagram in the name (even though there is no mention of it in the README), the interface obviously mimics Sequel’s virtual row blocks.
Aftermath
In this detailed overview, even though Sequel was ahead of ActiveRecord in vast majority of cases, there were a few cases where ActiveRecord was leading the way:
- Reversible migrations
- Aborting hooks
- Mutation detection
- Enum (kind of)
- Null relation
We see that Sequel was closely keeping up with ActiveRecord, but ActiveRecord wasn’t keeping up with Sequel. Note that on GitHub Sequel maintains 0 open issues, while ActiveRecord circles around 300 open issues. It’s also worth mentioning that Sequel is maintained mainly by one developer, while ActiveRecord is developed by most of the Rails team.
Conclusion
I want that you think about this. ActiveRecord was mainly implementing features that Sequel already had. That could be justified if ActiveRecord had some other advantages over Sequel, but I’m failing to see them. I don’t classify integration with Rails as an advantage (you can just make a sequel-rails for that), I mean advantages that actually help interacting with databases.
I wished that I used Sequel from day one, instead of starting with ActiveRecord and slowly realizing that Sequel is better. The only reason ActiveRecord is so popular is because it’s part of Rails, not because it’s better. There is a reason why hanami-model and ROM use Sequel under-the-hood and not ActiveRecord. It hurts me that so many developer hours are put into ActiveRecord, and I don’t see for what purpose; a better tool already exists and is excellently maintainted. Let’s direct our energy towards the better tool.