Polymorphism, Filtering, and the Geocoder Gem
The Geocoder Gem is great. It does everything you could ever want, from working with PostgreSQL to MongoDB, Google to Yandex, and Rails to Rack. Unfortunately though, there are situations where you can get into trouble with Active Record and joining tables. Here's what I know to work best.
You may find yourself wanting to abstract your geocoded functionality into model for code/information structure reuse purposes. I'm going to refer to this model as Location
. This model is primarily responsible for three things, having an inputted address, geocoding that into a latitude, longitude
, and referencing a polymorphic owner (which we'll call "located").
Then consider an Event
model. Also take into account that we may have multiple "located" models such as Venue
or Waypoint
.
Right, so now if we wanted to retrieve a list of Events near a Location we'd run this query; There are a lot of ways this is bad, but we'll start with the basics and move onto a better solution.
There are two things wrong with this query:
- This is an N+1 query (slow!), the "located" model should be included.
- Any kind of "located" model will be discovered (we only want Venues).
There are two new things we might want this query to do:
- There are no filters on the "located" model (Events under $10).
- This query exclusively orders by distance from San Francisco's center, what if we also wanted to order the query by the price of the event?
The N+1 Solution
This is the simplest part. Just tack an includes(:located)
onto the query. No matter how many different types of "located" models there are they will be loaded using IN
rather than one-by-one.
Filtering
Filtering actually relies on first bringing the subset of "located" models down to one type of model. In order to join the filtered model there can only be one type of model to join. You may have seen this error before when joining on a polymorphic association:
ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :located
The way to get around this is two fold, first by defining an association on the Location
model for one kind of "located" association only:
The second is to filter out any other kind of models before joining that new association:
Sweet, so by joining a single kind of "located" association we are able to filter it by it's properties in the same query as the Geocoder near
scope! We're on our way to making a great event listing page.
(Re)Ordering
Now that we have the Events
joined in our query, orering them by another column is as easy as:
Note: just using order
does not work because it will always be added after the distance
ordering. We speicifcally want to "group" by price in this example, and then order those by distance.
The final query looks like this:
Hopefully that helps get you over some of the hurdles around constructing a query that works with the Geocoder gem. This was all done and tested with Rails 4.0.1. I have published the Rails app for this post on Github.
Polymorphic Association JOIN
All things said and done, this has less to do with the Geocoder gem and more to do with joining & filtering polymorphic associations. I think this is beautiful solution to a frustrating problem.
Did you even know you can define associations dynamically? I didn't until I tried :)
This would make our geocoder filtering query very simple!
I'm considering gemifying this scope so it can be added to any/all models. Primarily concerned with the language used to describe this functionality so that other people can find the gem and the solution. Email me if you have any suggestions or problems with this solution.