views:

284

answers:

4

I've overrode the method find for ActiveRecord::Base and that went pretty well, now I can customize it nicely.

def self.find(*args)
  # my custom actions 
  super(*args)
end

then

Album.find(1) #=> My custom result

But now I would like to override this:

Album.first.photos

and I'm not sure what method exactly I should override to get the job done... I'm thinking of something specifically for associative queries, but I'm not sure :(

I need to act on 'ActiveRecord::Base' suggesting dynamic classes so I couldn't create a method photos for that, but a method that will interact within all the models I try.

Thanks a lot


Update

For better explaining what I'm trying to achieve:

Is to create a pluggable ruby gem for use with rails and that gem can simply take you ID structure from the database and convert it on the fly to a shorten ID just like bit.ly system. but this is done on the fly with only declaring has_shortened :id to your model, and then all the interface and queries return for example DH3 instead of 12 for example, I've managed to get this working this the point I need to deal with associataions.

Follow the gem url http://github.com/ludicco/shortener so you can check it out, and feel free to come with ideas you might have to implement it if you think it's a nice idea.

Cheers

+1  A: 

Since Album.first returns an instance of Album, you should override the photos method on the Album class itself:

class Album < ActiveRecord::Base

  # ...

  def photos(*args)
    if my_condition_is_met
      // my custom handler
    else
      super
    end
  end

  # ...

end

Now if you are sure you need this for every single model instance, I'd rather isolate it on a module:

module Photo
  def photos(*args)
    if my_condition_is_met
      // my custom handler
    else
      super
    end
  end
end

And reopen ActiveRecord::Base class:

class ActiveRecord::Base
  include Photo
end

This way you get modularization for your solution, making it act like a "plugin".

One additional and unrelated note: you don't have to call super(*args), since a call to only super will pass along all the parameters you received.

EDIT: Minor formatting

kolrie
Thanks for the answer kolrie, the only thing I didn't get is how can I make this plugin or a gem if I will never know which class will be related to the first one? For example, in this case the class is `Photo` and one `Album has_many :photos`, but what if the user gets the `Album has_many :pages` or `pictures`?. I would like to override the method before that, on a super level, so with only that one it can be used for all the associations a model can have, without distinction :)Thanks again
ludicco
+2  A: 

Association accessors like you define with has_many are pretty complicated stuff in ActiveRecord behind the scene. They actually use a reflection class to return a proxy object that behaves like a model class, but restrict its finders to the foreign key (and maybe other conditions). If you like to get deeper into it, you might want to have a look at reflection.rb, associations.rb, associations/association_proxy.rb and associations/association_collection.rb in the ActiveRecord gem.

However, in the end, we can use an association accessor as if it would be the class itself and ActiveRecord takes care to pass the required additional conditions to the class finder.

That means, that if you call Album.first.photos, behind the scene, ActiveRecord calls Photo.find (with additional condition to only return records that have :album_id => Album.first.id).

So, if you override Photo.find, it'll return your modified results as well if you use it through Album.first.photos. That's ActiveRecord magic :)

My quick test (ran with Rails 2.3.8):

class Album < ActiveRecord::Base
  has_many :photos
end

class Photo < ActiveRecord::Base
  belongs_to :album

  def self.find (*args)
    puts "XXX running custom finder"
    super
  end
end

Rails console:

> Photo.all
XXX running custom finder
Photo Load (0.4ms)   SELECT * FROM "photos" 
=> [#<Photo id: 1, album_id: 1>, … ] 

> Album.first.photos
Album Load (0.4ms)   SELECT * FROM "albums" LIMIT 1
XXX running custom finder
Photo Load (0.3ms)   SELECT * FROM "photos" WHERE ("photos".album_id = 1) 
=> [#<Photo id: 1, album_id: 1>, … ] 
Andreas
Hi Andreas, thanks a lot for your answer, the only thing I still wondering is how to make this dynamic, for better explanation on what I'm trying to do I've updated my question with some more details.Thanks again
ludicco
This sounds more like you've got a different problem with "dynamically" overriding the find method from within your has_shortened method. You can't simply do "def self.something" inside a class method. You need to either mixin a module that holds the new method or define them dynamically using instance_eval/class_eval or one of the define_method/define_class_method helpers. But that's a different story…
Andreas
A: 

Is there an particular reason you can't do this with an association extension?

http://ryandaigle.com/articles/2006/12/3/extend-your-activerecord-association-methods

Messing with the proxy objects directly is asking for trouble. But if you just want to override the find, this might do it.

seriousken
A: 

Take a look at friendly_id plugin (http://github.com/norman/friendly_id http://norman.github.com/friendly_id/file.Guide.html), it does what you need: generate custom id alias for model. You can define custom method to generate slug.

Leave id field in model

For custom #find use :finder_sql option:

class Album < ActiveRecord::Base
  has_friendly_id :short_id

  has_many :photos, :finder_sql => 'select * from photos where album_id="#{short_id}"'
end

>> Album.last.photos
select * from photos where album_id="t54"
edbond
this sounds pretty cool edbond, exactly what I needed, thanks a lot mate
ludicco