logo <

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 :smiley:.

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.

Janko Marohnić

comments powered by Disqus