Building complex ActiveRecord queries with tap
23 Aug 2015I 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.