Cam PedersenEngineer, Runner, Baker, Photographer

Missing records in Rails 6.1

September 18, 2020

Cunningham's Law says "the best way to get the right answer on the internet is not to ask a question; it's to post the wrong answer."

Maybe a good extension would be "the best way to get a feature in an open source project is not to commit the code, but to complain about the lack of said feature."

A couple of years ago I published a post about finding Unassociated records in Rails. I thought I would get around to building out my idea of without(:model) someday, but you know how life goes.

Thankfully someone else found the lack of this feature annoying too. I was googling for solutions to an unrelated problem yesterday and stumbled on the Rails PR Finding Orphan Records, which adds missing(:model).

It looks like it was submitted by Tom Rossi in December 2018, but only merged in January 2020, finding its way into Rails 6.1. The linked thread indicates he and Rafael França figured this out at Rails Conf! Maybe I should go someday 😂

The implementation is much simpler than I thought it would be, and more powerful than I had thought to make it. It actually allows you to left join multiple tables:

Post.where.missing(:author, :comments)

# SELECT "posts".* FROM "posts"
# LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
# WHERE "authors"."id" IS NULL AND "comments"."id" IS NULL

This was acheived in the following method in activerecord/lib/active_record/relation/query_methods.rb:

def missing(*args)
  args.each do |arg|
    reflection = @scope.klass._reflect_on_association(arg)
    opts = { reflection.table_name => { reflection.association_primary_key => nil } }
    @scope.left_outer_joins!(arg)
    @scope.where!(opts)
  end

  @scope
end

We can see through their clever use of _reflect_on_association how the previous way to do this had to be implemented:

Post
  .left_joins(:author).where(authors: {id: nil})
  .left_joins(:comments).where(comments: {id: nil})

It seems small but I love to see improvements like this. Rails continues to be a masterpiece of developer ergonomics, and I'd like to thank Tom Rossi and Rafael França for helping push it forward!

Want to know when I post?
🙂