Building complex ActiveRecord queries with tap

I recently ran across the need to build a complex ActiveRecord query which called for conditional joins and lookups based on certain parameters.

My options were to either write a raw SQL query which would be hard to maintain or end up doing in-memory operations after a certain point which would be detrimental to performance. So I decided to take a different approach which would let me cleanly write the query while avoiding those pitfalls.

The best way to demonstrate the technique is by showing you an example. So say we wanted to list all the upcoming music gigs in a city and optionally search for a specific venue/band and paginate the results using a scope like this:

Gig.upcoming({venue: 'Fillmore', band: 'Doors', 'page': 1})

To implement a scope like that, we’ll need to make use of something like tap which is built into Ruby 1.9+. It’s basically a helper for method chaining. Let’s look at how tap is implemented at it’s source:

VALUE rb_obj_tap(VALUE obj)
{
  rb_yield(obj);
  return obj;
}

That basically translates to this in Ruby:

class Object
  def tap
    yield self
    self
  end
end

So let’s make use of that to implement the scope like this:

class Gig
  delegate :genre, :to => :band
  belongs_to :band
  belongs_to :venue
  delegate :region, :to => :venue

  scope upcoming(options = {}), -> {
    where('start_date > ?', DateTime.now).tap do |query|
      #Perform conditional logic here and return the query...
      query
    end
  }
end

But looks like there is a problem. None of the conditional queries inside the tap block seem to be taking effect. Taking a second look at how tap is implemented, it makes sense. tap passes it’s object into the given block and after the block finishes, it returns the object. But in our case, what we need is the return value of the block itself not the object.

So let’s make that change and introduce a separate method which can do exactly that. We’ll call it let and add it to Ruby’s Object class (open classes FTW!):

class Object
  def let
    return yield self
  end
end

Now we can implement the upcoming scope on the Gig model something like this:

class Gig
  delegate :genre, :to => :band
  belongs_to :band
  belongs_to :venue
  delegate :region, :to => :venue

  scope upcoming(options = {}), -> {
    where('start_date > ?', DateTime.now).let do |query|
      if options[:venue].present?
        query = query.include(:venue).search(venues_name_cont: options[:venue]).result #ransack DSL
      else
        query = query.where(featured: true)
      end

      if options[:search].present?
        query = query.search(name_and_description_cont: options[:search]).result #ransack DSL
      end

      if options[:page].present?
        query = query.page(options[:page]).per(20) #kaminari DSL
      end

      query
    end
  }
end

Now I know this particular example can be written in many different ways and may not make the best case for making use of a let method. But in more complicated cases this isn’t possible which is where let would come in handy.

Anyway I’d love to hear about what other techniques are out there for building complex AR queries. Share your thoughts in the comments.

If you liked this post, 🗞 subscribe to my newsletter and follow me on 𝕏!