Skip to content

Instantly share code, notes, and snippets.

@jlecour
Created September 22, 2010 07:49
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jlecour/591321 to your computer and use it in GitHub Desktop.
Save jlecour/591321 to your computer and use it in GitHub Desktop.
Geokit-rails and Rails 3

In my effort for porting the geokit-rails plugin to Rails 3, I've found that it would really benefit a full rewrite of the query composition parts.

My work can be followed here : http://github.com/jlecour/geokit-rails/ (make sure to look at the gem branch until it is merged into master) Any help is welcome.

The current version, compatible with Rails 2, has 2 main methods (find and count) and some utility methods, that are making some changes to the select/conditions/limit/order/… parts of the options hash passed to ActiveRecord and then call the super related method on the model's class.

With ActiveRecord 3 and its scope approche, I think we don't need to mess with the hash anymore if we use the power of scopes. I would be cleaner, more chainable and reusable, …

That said, we have to consider the kind of additions Geokit makes to a typical query. As a brief summary, here is how it works.

Any model can be extended to become mappable with the acts_as_mappable method called in the model definition. It has to have a latitude and a longitude column (the name can be changed) and some defaults are set (the formula, the distance unit, …). To be able to find entries in the database that are, for example, within 10 miles from an origin point (some coordinates, of another entry), we need to add a virtual column in the select part of the query to get a distance value and a condition in the where part of the query to restrict the results. Some utility methods (like closest, farthest, …) even need to add some limit to the query.

All this can be done with some scopes, but they need to play well with previous of future scopes in the chain. For example, if we want to get the distances between our results and a point or reference, we need to add the virtual column to the query result. In this case, the select methods can be used like this

select("*, #{distance_formula} AS distance")

But what if we don't want all the other fields, but only the primary key and the distance value ? Maybe it is possible to look for select scopes added before ours and add our distance column to them. For the moment I don't know if it is even possible, but I guess it is. More complicated : if there is another select scope added after our, how will it behave ? Will it reset the whole select SQL clause (that would'nt be good) ?

Another question came up : if a utiliy method (or scope) is adding a limit, is it possible to make it impossible to add another contradictory limit further in the chain ?

Last, but not least, the current version of geokit-rails can be used with associations. That's something I've never really used in my apps, but it is a good feature. I haven't even begun to think about how this can be done with ActiveRecord 3. Maybe it will be very hard, maybe very easy. But at least, I know it'll be something to deal with.


Update : I'm currently reading more ActiveRecord and Arel code, and I've seen that I can access the select clause. I can be done from Arel's low level projections or from AR's higher level select_values. For example :

$ l = Location.select('id, name')
$ l.select_values
# => ['id, name']

The problem is that It can be done only at the instance level, not in a scope definition, at least I've not found how. I'm having some NoMethodError for select_values

@tenderlove
Copy link

It's possible to stack scopes. I think this might be what you want. Consider a model like this:

class Location < ActiveRecord::Base
  scope :foo, lambda {
    { :select => 'latitude as bar' }
  }

  scope :bar, lambda {
    { :select => 'longitude as baz' }
  }

  def self.with_star
    select('*').foo.bar
  end
end

Location.foo will generate SELECT latitude as bar

Location.foo.bar will generate SELECT latitude as bar, longitude as baz.

Location.with_star will generate SELECT *, latitude as bar, longitude as baz

@jlecour
Copy link
Author

jlecour commented Oct 4, 2010

@tenderlove Thanks for the tips. I didn't know that select() was cumulative (like order).

I've tried this and it's working, but it's rather strange for the user because :

  • if add the "star" like you did, the user can't choose the fields he wants to retrieve (all the attributes are always retrieved)
  • if I don't add the "star", the user has to add it himself to actually retrieve the model's attributes

@jlecour
Copy link
Author

jlecour commented Oct 28, 2010

@tenderlove

I've made some good progress on this one, and now I have my core methods working. Most of the tests are passing with the new syntax, excepts the ones that are using the distance condition in a where clause.

You can find the latest code on github : http://github.com/jlecour/geokit-rails3

For example, this is working : Location.nearest(:origin => other_location)
But this is not (distance column not found in table locations) : Location.geo_scope(:origin => other_location).where('distance < 3')

I have 2 solutions. Either I say that using a distance condition in a where clause is not supported. It's easy,but not very good because I can't build my query iteratively.
Or I can find a way to substitute the "distance" table name in the where method.

I'm for the 2nd option, but I can't get it to work.

Ernie Miller's advice has been to hook into build_arel. As hard (and conscientiously) as I read AR::QueryMethods source code, I understand the code, but not what I can do with it. I don't know if I have to monkey patch or hook, and how.

Thanks for your help.

@ernie
Copy link

ernie commented Oct 29, 2010

Hey, sorry I'm late to this party. Anyway, what I was basically suggesting was that you might want to take a look at this:

http://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/query_methods.rb#L191

And of course, this:

http://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/query_methods.rb#L240

The latter method, build_select, could be overridden by including a module into ActiveRecord::Relation.

so where it currently says:

def build_select(arel, selects)
  unless selects.empty?
    @implicit_readonly = false
    arel.project(*selects)
  else
    arel.project(Arel.sql(@klass.quoted_table_name + '.*'))
  end
end

... you might rewrite it to say:

module Mappable # To be included into AR::Relation
  def build_select(arel, selects)
    if self.mappable?
      selects += [Arel.sql(@klass.quoted_table_name + '.*')] if selects.empty?
      selects += ['<distance formula> as distance']
    end
    super
  end
end

Of course, taking a page from what Aaron said above, I may be overthinking this. You should be able to just set up a few scopes on the model and make things happen. I think this way is kinda sexy though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment