Pivotal Labs

HasFinder -- It's Now Easier than ever to create complex, re-usable SQL queries

edit Posted by Nick Kallen on Sunday September 02, 2007 at 01:56AM

HasFinder is an extension to ActiveRecord that makes it easier than ever to create custom find and count queries.

Let's start with an example. Suppose you have an Article model; some articles are published, some are popular. Let's declare finders for each of these notions. You may be tempted to write something like the following:

class Article < ActiveRecord::Base
  def self.published
    find(:all, :conditions => {:published => true})
  end

  def self.popular
    ...
  end
  ...
end

But there are serious limitations to this approach. How do you find articles that are BOTH popular and published? How can you easily paginate published articles?

HasFinder Features

Let's define the equivalent finders using my new plugin, has_finder:

class Article < ActiveRecord::Base
  has_finder :published, :conditions => {:published => true}
  has_finder :popular, :conditions => ...
end

Query Composition

Now, you can elegantly compose queries:

Article.published.popular

This will return all articles that are both popular and published.

Calculations and Nested Finds

You can also easily paginate or call nested finders or do calculations:

Article.published.paginate(:page => 1)
Article.published.popular.count
Article.popular.find(:first)
Article.popular.find(:all, :conditions => {...})

Works with ActiveRecord has_many and has_many :through Associations

Furthermore, without any additional work, these finders will work with ActiveRecord associations.

class User
  has_many :articles
end

user.articles.popular.find(:first)
user.articles.published.popular.average(:view_count)

Finders are extendable just like ActiveRecord Associations

class Article
  has_finder :unpublished :conditions => {:published => false} do
    def published_all
      find(:all).map(&:publish)
    end
  end
end

Alternatively, you can use the :extend options:

class Article
  has_finder :unpublished, :conditions => ..., :extend => MyExtensionModule
end

Finders behave just like ActiveRecord Associations

For example, you can call #reload:

Article.published.popular.reload

Finders can take parameters

class Car
  has_finder :colored, lambda {|color| { :conditions => {:color => color} } }
end

Car.colored('red').paginate(:page => 1)

What makes HasFinder better than alternatives like scope_out and scoped_proxy?

There are already two plugins similar to HasFinder: scope_out and scoped_proxy. Both of them are excellent. In fact, Scoped Proxy was the model for HasFinder. Unfortunately, neither plugin provided all of the features I desired. Scope-Out lacks on-the-fly composition, a nice way to call a nested find, or the ability to do arbitrary calculations. Scoped Proxy is great, but it doesn't work with regular ActiveRecord Associations, it is not extendable like ActiveRecord associations, and it doesn't behave exactly like a regular ActiveRecord Association. Neither of them work out of the box with will_paginate. For all of these reasons and more, I rolled my own. It's now available as a gem.

Installation

% gem install has_finder

Usage

In environment.rb:

gem 'has_finder'
require 'has_finder'

See the examples above for usage.

Comments

  1. josh josh on September 02, 2007 at 03:09PM

    Very nice!

    Maybe make available as a plugin instead?

    Also - why "has_finder" - why not just an explicit "finder"?

  2. Ryan Ryan on September 02, 2007 at 06:16PM

    Awesome. I wrote something to enable the same call syntax the other day, but this is much better. I second the vote for a plugin!

  3. Nick Kallen Nick Kallen on September 02, 2007 at 08:05PM

    Josh/Ryan -- thanks for the feedback. I'll package this as a plugin soon. As for calling the method "finder" vs. "has_finder" -- I'm OK with both. Let's have a vote!

  4. Joel Joel on September 02, 2007 at 11:12PM

    Really sweet!

    But isn't with_scope going to be a protected method? Implications on this?

    Trying something like Page.published.find(params[:id])) in edge and it fails, something like @page.published.children.find(:all) works fine though..

  5. Nick Kallen Nick Kallen on September 03, 2007 at 03:08AM

    Joel -- I just updated the gem to use send :with_scope. I can't seem to get rake to run on a freshly generated edge rails app, so I'm unsure whether this will fix the defect or not. Let me know!

  6. Joel Joel on September 03, 2007 at 12:45PM

    That seems to do it. Much obliged =)

    (Did you try gem update --system; gem update rake; for edge rails?)

  7. Cyx Cyx on September 05, 2007 at 06:07AM

    Hi,

    For some reason there is a bug when using with scope. For example

    User.scoped_by_site(1) { User.superusers }

    and

    User.scoped_by_site(1) { User.superusers.find(:all) }

    for the first example, it doesn't respect the scope, for the second it does.

    Any thoughts?

  8. Nick Kallen Nick Kallen on September 05, 2007 at 04:46PM

    Cyx: That kind of usage is impossible to support, unfortunately. It's difficult to explain why that doesn't work, but let me try:

    User.superusers returns a proxy object, not an array. What you're doing:

    <code>User.scoped_by_site(1) { User.superusers }
    </code>

    is more or less equivalent to just

    <code>User.superusers
    </code>

    Since the more complex expression just returns the proxy object. The withscope only "lasts" as long as the block being passed to it; it's destroyed as soon as the block returns. In your first example, you'll only effect the find after scopedbysite returns (e.g., by calling #each or #find), at which point the withscope is long gone.

    This fact is, by far, the most complex part of how hasfinder works. If you want an object to encapsulate a scope (which is more or less what hasfinder does), composition is only possible if the objects no how to chain themselves together, such that method invocation leads to nested with_scopes. Look at the source code for details. In any case, I recommend you do the following:

    <code>class User
      has_finder :scoped_by_site lambda{|site_id| {...} }
    end
    </code>

    and then you should be OK.

  9. macovsky macovsky on September 06, 2007 at 09:29AM

    great idea! vote for plugin too :).

    for STI it doesn't work. for now i put has_finder's definition in a module and included it in tables-children. other way i've got that:

    The error occurred while evaluating nil.call from .../gems/has_finder-0.1.2/lib/has_finder/has_finder.rb:59:in 'published'

  10. grant grant on September 06, 2007 at 11:38PM

    One problem I'm having with nested scopes:

    Article.published.popular executes two queries, one to get published and another to get published AND popular AND published.

    SELECT * FROM articles WHERE (published = 1)

    SELECT * FROM articles WHERE (( ( published = 1 ) AND ( popular = 1 ) ) AND ( published = 1 ))

  11. Seth Seth on September 08, 2007 at 02:08AM

    I was hacking around earlier and ended up implementing a half-baked version of this that doesn't involve the use of AssociationProxies:

    http://pastie.caboo.se/95207

    It's definitely under-powered (I'm using has_finder instead), but folks might find the approach interesting.

  12. Seth Seth on September 08, 2007 at 02:08AM

    I was hacking around earlier and ended up implementing a half-baked version of this that doesn't involve the use of AssociationProxies:

    http://pastie.caboo.se/95207

    It's definitely under-powered (I'm using has_finder instead), but folks might find the approach interesting.

  13. nick nick on September 09, 2007 at 05:49PM

    macovsky - i'll fix this bug and release a new version in the next couple of days. grant - i'll see if I can reproduce this behavior.

  14. Zargony Zargony on September 10, 2007 at 07:31AM

    Great! Since I was playing around with scopeout and scopedproxy, I always wanted to somehow combine the advantages of both plugins. I didn't try hasfinder yet, but it looks like it does exactly what I am searching for, so +1 for a hasfinder plugin :-)

  15. Nick Kallen Nick Kallen on September 10, 2007 at 07:42AM

    macovsky/grant - both issues should be fixed and are now available as has_finder 0.1.3.

  16. Joel Joel on September 11, 2007 at 03:50PM

    Hi, thanks again for a excellent piece of software..

    Have you thought about calculations? eg. Order.completed.sum(:amount) ect. (That maybe out of scope for has_finder though)

    Just checking if you are implementing it before I try a stab at it myself..

    On another note, it's busted in edge again =) I'm getting

    vendor/rails/activerecord/lib/activerecord/base.rb:1277:in method_missing_without_paginate': undefined methodhasfinder' for # (NoMethodError)

    lots of stuff going on in edge now apparantly.

    I think it has to do with how you include the HasManyAssociations, changing from just using include to doing a class_eval seems to do it.

    like so:

    ActiveRecord::Base.send :include, HasFinder::ActiveRecord module ActiveRecord::Associations [HasManyAssociation, HasManyThroughAssociation].each do |klass| klass.class_eval do include HasFinder::HasManyAssociation aliasmethodchain :methodmissing, :hasfinder end end end

  17. Joel Joel on September 11, 2007 at 03:50PM

    whoops, busted rendering but you get the idea =)

  18. nick nick on September 11, 2007 at 05:19PM

    Joel -- I'll make those fixes later tonight... As for #sum, in theory they should be working (I thought they were already) since I thought with_scope affected calculations as well... I'll double check that stuff.

  19. Nick Kallen Nick Kallen on September 12, 2007 at 04:21AM

    Joel -- I fixed the issue with sum (was being delegated to the array rather than ActiveRecord). I could not reproduce your issue in edge (just tried with latest version right now), sorry!

    Try 0.1.5 and let me know if it's any better.

  20. oliver oliver on September 13, 2007 at 04:20AM

    two things. one, i'm unable to get hasfinder working. I get an error from ActiveRecord::Base: "in'method_missing':ArgumentError: no id given" on the line that calls "super" at the end. not sure what's going on there.

    also...Is there any way to include has_finder finders directly on an association? so for instance: User - has_finder :active, :conditions = {:deactivated = false}

    Group has_many :memberships has_many :members, :through = :memberships, :with_finder = :active

    that would seem easier than having to always access @group.members.active

  21. Nick Kallen Nick Kallen on September 13, 2007 at 05:55AM

    Hi oliver:

    1. There's not yet planned support for things like: hasmany ... :withfinder => :active Good idea though, so I'll consider adding it.

    2. I cannot reproduce the error you have with has_finder. I need more detail. Send me a full stack trace (nick @ pivotallabs.com), and any source code that you can... Something to do might be generate a new rails app, require the gem in your environment and write a few lines of code that fail... you can zip it up and send it all to me.

  22. Nick Kallen Nick Kallen on September 13, 2007 at 06:42AM

    Oliver, PS:

    <code>class Group
      has_many :memberships ...
      def active_memberships
        memberships.active
      end
    </code>

    end

    Not super-concise, but it gives you almost all that you want.

  23. Brandon Keepers Brandon Keepers on October 10, 2007 at 03:13PM

    Nick,

    HasFinder is quite a gem (in many ways), but do you have it available in an svn repo so it can be installed as a plugin?

  24. Nick Kallen Nick Kallen on October 16, 2007 at 05:56AM

    brandon: source code is in rubyforge in the pivotalrb project. thanks for your interest!

  25. Michael Michael on October 22, 2007 at 09:30PM

    I want to install as script/plugin is it posible?

  26. Michael Michael on October 22, 2007 at 09:30PM

    I want to install as script/plugin is it posible?

  27. Mark Mark on October 23, 2007 at 03:32PM

    can you use associations in the conditional:

    <code>class Song &lt; ActiveRecord::Base
    has_many :reviews
    has_finder :u2, {:conditions =&gt; {"songs.artist" =&gt; "U2", "reviews.rating" =&gt; 8 }, :include =&gt; :reviews }
    </code>
  28. Mark Mark on October 23, 2007 at 03:32PM

    can you use associations in the conditional:

    <code>class Song &lt; ActiveRecord::Base
    has_many :reviews
    has_finder :u2, {:conditions =&gt; {"songs.artist" =&gt; "U2", "reviews.rating" =&gt; 8 }, :include =&gt; :reviews }
    </code>
  29. Mark Mark on October 23, 2007 at 03:32PM

    can you use associations in the conditional:

    <code>class Song &lt; ActiveRecord::Base
    has_many :reviews
    has_finder :u2, {:conditions =&gt; {"songs.artist" =&gt; "U2", "reviews.rating" =&gt; 8 }, :include =&gt; :reviews }
    </code>
  30. Mike Pepper Mike Pepper on October 27, 2007 at 09:11AM

    @ those that were asking about using this as a plugin:

    Install it as a gem, then copy the 'has_finder-0.1.5' from the 'ruby/lib/ruby/gems/1.8/gems' directory into 'vendor/plugins', and put:

    require 'has_finder'

    into a new file named init.rb, inside that folder, then you dont need the two lines in your environment.rb file either.

    I think you can also safely delete the config and test folders, and the 'setup.rb' file, as they are then redundant. Oh and don't forget to restart the server (as I always do!)

    A lot of gems can be 'converted' this way as they share the same basic structure as rails plugins.

    And thanks nick, this is just what i've been looking for :)

  31. andy andy on November 02, 2007 at 06:34PM

    This plugin (gem) seems to have the same problem with :group option of AR.find method that scope_out suffers. Namely it does not seem to support it.

    ex...

    hasfinder :nonactive, :group => 'login'

    fails with...

    ActionView::TemplateError (Unknown key(s): group) on line #10 of app/views/users/_index.rhtml:

    /var/lib/gems/1.8/gems/activesupport-1.4.4/lib/activesupport/coreext/hash/keys.rb:48:in assert_valid_keys' /var/lib/gems/1.8/gems/activerecord-1.15.5/lib/active_record/base.rb:913:inwith_scope'

  32. andy andy on November 02, 2007 at 06:42PM

    FYI, a work around for the lack of :group support

    ex...

    hasfinder :nonactive, :select => 'distinct ...'

    seems to work for me, but the :group option would be nice - as it's a standard part of find method

  33. nick nick on November 04, 2007 at 03:50AM

    Mark -- your technique should work.

    Andy -- unfortunately, I rely on with_scope (as does scope_out) and it does not support group by. I can give you source code to make with_scope support group_by, (it's fairly straightforward), but I wouldn't want to include it in has_finder because of its potentially undesirable effect on the rest of the Rails app.

  34. DEkart DEkart on November 07, 2007 at 05:57AM

    It would be good to get an access to SVN repo. I want to install this plugin using Piston (http://piston.rubyforge.org/). It is much better than copying Gem

  35. nick nick on November 07, 2007 at 06:05AM

    DEkart -- http://pivotalrb.rubyforge.org/svn/hasfinder/trunk/hasfinder/

  36. Chris Chris on November 08, 2007 at 02:29PM

    I am currently using custom finders like this

    class Barcode < ActiveRecord::Base def self.findallbyclient(clientid) self.findallbyclientid(clientid, :include => {:item => :itemtype}, :order => "item_types.name, items.name") end end

    I would like to replace it with this but how can I pass the session variable session[:client] to the has_finder method?

    hasfinder :all, :conditions => {:clientid => session[:client]}, :include => {:item => :itemtype}, :order => "itemtypes.name, items.name"

  37. Chris Chris on November 08, 2007 at 02:31PM

    My underscores got stripped out of that post but you can still get the point.

  38. nick nick on November 08, 2007 at 05:49PM

    Chris -- only the controller (and view) have access to the session (for better or worse). The easiest thing to do is pass in the client_id as a parameter to the finder, as you did in your self.find_all_by...

    <code>class Barcode
      has_finder :all, lambda {|client_id| { :conditions =&gt; {:client_id =&gt; client_id} } }
    end
    
    class BarcodesController
      ...
        Barcode.all(session[:client_id])
    </code>

    In the example you gave above, it doesn't seem like you need your model to know about the session... But,

    If you want the session available in your model layer, this is an obstacle totally unrelated to has_finder. Use an around_filter to set (and unset) a class variable on AR::Base or a thread-local variable that AR::Base has access to. Better yet, alias_method_chain perform_actionto do the same thing (this will ensure your session is available in your models in the after_filters that happen after your around_filter).

    Don't let anyone give you a hard time about MVC. Dogma does not lead to good software design.

  39. Chris Chris on November 13, 2007 at 08:58PM

    Thanks, that works great for finding a collection of objects but when I try to find a single item for my show action, I get an method missing error. I think this is because has_finder is returning an array and my show action is treating it like an object.

    hasfinder :one, lambda {|id, clientid| { :conditions => {:id => id, :clientid => clientid} } }

    How can I write this to return a single object instead of an array of 1, or should I just use a custom method for finding one object instead of using has_finder?

  40. nick nick on November 14, 2007 at 05:11AM

    chris -- has finder is not really designed for the "one" case. A simple, but clumsy, implementation might be something like the following:

    <code>has_finder :ones, lambda {|id, client_id| {...} }
    
    def self.one
      ones.first
    end
    </code>
  41. Chris Chris on November 14, 2007 at 07:31PM

    Thanks Nick, you've been so helpful. One more question. I have the following defined.

    <code>
    has_finder :active, lambda {|client_id| { :conditions => {:client_id => client_id, :active => true}, :include => [:tiers, :sale_contracts], :order => "plans.monthly_fee" } }
    has_finder :business, lambda {|client_id| { :conditions => {:client_id => client_id, :is_business_plan => true}, :include => [:tiers, :sale_contracts], :order => "plans.monthly_fee" } } </code>

    When I call Plan.active.business(session[:client]) I get nothing in the result list because the conditions are merged and the has finder for active is supplying the condition client_id = null because I didn't pass it in the controller. To make it work I have to call Plan.active(session[:client]).business(session[:client]) and that works fine, I was just wondering if there was a prettier way to write it.

    By the time I got to the end of writing this post, I have decided to myself that its better this way but still let me know what you think. I'm still considering using the around filter that you mentioned before because the way my app is designed, every query in every model requires the client_id.

  42. Daniel Fischer Daniel Fischer on November 28, 2007 at 12:33AM

    This is perfect, just in the nick of time.

    Thanks!

  43. Daniel Fischer Daniel Fischer on November 28, 2007 at 09:22AM

    gem 'has_finder' = true require 'has_finder' NoMethodError: undefined method 'delegate' for HasFinder::FinderProxy:Class

    Eek, what's going on?

  44. nick nick on November 29, 2007 at 05:59AM

    delegate is a method from activesupport. You must be requring 'hasfinder' too early in your environment file--assuming you're using Rails. If you're not using Rails, make sure to require has_finder after active record

  45. Rob M Rob M on December 01, 2007 at 03:21AM

    YOU ROCK. This works great!

  46. nick nick on December 02, 2007 at 02:12AM

    Daniel:

    <code>class Writing
      has_many :pages
    end
    
    class Page
      has_finder :beginnings, :conditions =&gt; { :position =&gt; 1 }
    end
    </code>

    And voila:

    <code>my_writing.pages.openings
    </code>
  47. Daniel Fischer Daniel Fischer on December 02, 2007 at 03:52AM

    Haha, thanks! I ended up doing this before the reply:

    <code>first_scoped_page = self.writing.pages.find(:all, :conditions =&gt; ["position = 1"] )
    </code>

    Now I can refactor!

    Thanks!

  48. Vladimir Meremyanin Vladimir Meremyanin on December 07, 2007 at 12:48PM

    Absolutely Great thing, thanks a lot, man!

    I've noticed a bug when was using models in modules.

    <code>class Foo::Bar &lt; ActiveRecord::Base
      belongs_to :x
      belongs_to :y
    #...
    
      has_finder from_x, lambda {|x| {:conditions =&gt; {:from_id =&gt; x}}}
      has_finder in_y,  lambda {|y| {:conditions =&gt; {:y_id =&gt; y}}}
    end
    
    class Foo::Z &lt; ActiveRecord::Base
      belongs_to :x
      belongs_to :y
    #...
    
      def outgoing_bars
         Bar.from_x(x_id).in_y(y_id)
      end
    end
    </code>

    results in a 'X is not missing constant Bar' error.

    The workaround for this is to use absolute name - ::Foo::Bar, instead of just Bar.

    But it is possible that I did something wrong...

  49. Shifra Shifra on December 07, 2007 at 05:35PM

    Okay, we really need to package this as a plugin...

  50. nick nick on December 08, 2007 at 07:14PM

    Vlad -- if you give me a full stack trace I will look into it in more detail. I know that using AR's in modules in general can cause problems with associations like belongsto (at least it did with Rails 1.2.3), so it's possible hasfinder is unrelated, not sure. But I'll fix it if I can.

  51. Vladimir Meremyanin Vladimir Meremyanin on December 12, 2007 at 03:03AM

    Sure, here it is:

    <code>/var/lib/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:240:in `load_missing_constant'
    /var/lib/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:468:in `const_missing'
    /var/lib/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:470:in `send'
    /var/lib/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:470:in `const_missing'
    /app/models/foo/z.rb:15:in `outgoing_bars'
    /var/lib/gems/1.8/gems/activerecord-1.15.5/lib/active_record/associations/association_proxy.rb:123:in `send'
    /var/lib/gems/1.8/gems/activerecord-1.15.5/lib/active_record/associations/association_proxy.rb:123:in `method_missing'
    /app/views/shared/_document_bars.rhtml:5:in 
    ...
    </code>

    The call is like

    <code>document.z.outgoing_bars
    </code>

    To get the point what I'm trying to do: navigating kind of a graph, with multiple paths on the same vertices :)

  52. Steve Steve on December 14, 2007 at 09:31AM

    I thought I posted this already but I didn't see the comment appear. I have realized that in rails, when you do a find(:all) statement inside of an association (has_many :somethings, :thought => :other_things do; def completed; find(:all, :conditions => {:completed => 1}); end;) it breaks the :uniq => true statement in the association. I'm able to fix this in my models when i define finders to narrow a search down by just adding a .uniq to the end of the statement (find(:all).uniq) .. is there any way to add this functionality to has_finders? I'd love to use this gem and make my finders much easier to use and read, but i NEED the uniq fix and can't figure out how to do it myself with this gem. Any help?

  53. Steve Steve on December 14, 2007 at 10:26AM

    After playing around in the source, I added .uniq to the find(:all) statement in the load_found private method and it now only returns unique results. This'll do until an official fix is released.

  54. Andy Watts Andy Watts on January 26, 2008 at 01:29AM

    Those looking for a plugin version can try this checkout into their plugins directory..

    svn checkout http://pivotalrb.rubyforge.org/svn/has_finder/trunk/has_finder

  55. Richard Ibarra Richard Ibarra on January 30, 2008 at 01:23PM

    Hi!, I tried to use has_finder and has_many_polymorphs (tagging_extensions), but I get a strange behavior... I have two taggable models, in one of them I've added a finder (using has_finder)... and in that model I can't access the dynamic method added by tagging_extensions (tags for example). In the model where I haven't added any finder I can access these methods without any problem.

    For some reason, when I ask for the Tag model... this problem doesn't happen anymore.... ----- environment.rb require 'tagging_extensions' require 'has_finder' Tag The last line makes all work fine. This model Tag, adds to my two models a lot of methods via :extend options in has_many_polymorphs... I think that is the reason why the problem doesn't happen anymore.. but I think that is not the correct way for solving it..

    Any ideas? Thanks

  56. hosiawak hosiawak on January 31, 2008 at 12:56PM

    Finally found a scoping plugin that works with acts_as_tree out of the box. Thank you :)

  57. Joshua Joshua on January 31, 2008 at 01:47PM

    It seems that HasFinder overrides the model translating aspect of Globalize (an il8n plugin for Rails), at least in the case where one is using the default translation storage.

    It's a very nice system, otherwise!

  58. rubylicio.us rubylicio.us on February 07, 2008 at 01:37AM

    This is so great. One "problem" I found though, was that I've started using rails new partial renderthingie:

    controller: @posts = Post.a_has_finder_finder

    view: render :partial => @posts

    This doesn't work anymore.. probably since the objects in @posts aren't pure Post-classes anymore, but some has_finder proxy thingie.

    The workaround isn't that big of a deal though, just go back to using the "old way":

    view: render :partial "posts/post", :locals => bla bla ...

    .. Maybe something could be done so it works with the first way though?

  59. rubylicio.us rubylicio.us on February 07, 2008 at 01:46AM

    Oups, sorry for the dups.. didn't see my comments comming.

    And one correction, the way one has to do with has_finder in the view is:

    render :partial => "posts/post", :collection => @posts ... which isn't such a big deal.

  60. jeffb jeffb on February 08, 2008 at 08:30PM

    I'm trying to dynamically add has_finder methods to model classes. As my first exploration I tried the following in a model class:

    content_columns.select {|c| c.type == :string}.each do |column|
        has_finder column.name.to_sym, 
                         lambda {|param| { :conditions => {column.name.to_sym => param} } }
    end
    

    Unfortunately this isn't working. I'm seeing the following error many times when I true to use one of these finders:

    /path/to/my/mode_model.rb:42: warning: multiple values for a block parameter (0 for 1) from /path/to/my/rails_root/vendor/plugins/has_finder/lib/has_finder/has_finder.rb:55

    Any idea how I might be able to pull this off?

    Thanks.

  61. Jörg Battermann Jörg Battermann on February 24, 2008 at 01:16PM

    Hello there,

    has anyone used has_finder successfully with has_many_polymorphs associations?

    Basically I have an user model which has many items (which can be of type :a, :b and :c) and for example I want to get all UserItems which have been created yesterday (finding within the hmp user.items associations). I am kinda confused atm because it isn't working as expected :-/

    _j

  62. Jörg Battermann Jörg Battermann on February 24, 2008 at 01:29PM

    Markdown obviously messes with the _'es ;)

    Anyway - what I forgot to add was the setup: I have one User model, one UserItem model and several other models A, B and C. UserItem is the join model for the hmp associations. I placed e.g. this finder in there:

    has_finder :created_today, :conditions => ['created_at > ?', Date.yesterday]

    Now when I fetch an User u and do a u.items.created_today, I get a big, bad exception:

    ActiveRecord::StatementInvalid: Mysql::Error: Column 'created_at' in where clause is ambiguous: .... :-/

    A, B and C have all the typical 2 rails timestamp columns and so do the useritems.

    Any Idea what's wrong here?

  63. nick nick on February 24, 2008 at 06:04PM

    Jorg,

    HMP scares me - I've used it before and with no good effect. It looks like a simple workaround to your problem would be:

    has_finder :created_today, :conditions => ['users.created_at > ?', Date.yesterday]
    

    Note the prefix on the created_at column. Also, bear in mind that doing Date.yesterday is really dangerous as the statement gets evalled at class-eval time, meaning if your code is long-running, yesterday may be 5 days ago, etc.

    Two workarounds for that are: 1) use the lambda form, 2) put the date in the sql clause, not using bind (the question-mark).

  64. Jason Arora Jason Arora on March 03, 2008 at 02:35AM

    Hi Nick,

    I found a bug with finders that only do an "order by". For example:

    hasfinder :popular, :order => 'hits desc' hasfinder :alphabetical, :order => 'name asc'

    Topic.popular.alphabetical would only do "order by hits desc" and not "order by hits desc, name asc".

    You've created a wonderful gem. Any update on if and when it will be available as a plugin or can we hope to see this in the official Rails release soon?

    Regards, Jason

  65. Jason Arora Jason Arora on March 03, 2008 at 06:31AM

    Hi again! First, I have to say I am really digging this gem, man! This is exactly what Rails needed.

    I found another small bug - when I do something like this: forum.topics.popular

    The generated SQL checks forum_id twice: SELECT * FROM topics WHERE (topics.forum_id = 13) AND (topics.forum_id = 13) ORDER BY posts_count desc

    My finder is the following: has_finder :popular, :order => 'posts_count desc'

  66. Alexander Alexander on March 16, 2008 at 02:53PM

    I think that rails core team should consider has_finder as part of AR. It's useful thing like associations. I hope that some day it becomes part of AR.

  67. Xin Xin on March 16, 2008 at 09:07PM

    Thank you for releasing has_finder. It's a pleasure to use.

    I want to use it in conjunction Geokit. Geokit adds :origin and :within options to finders. In a has_finder, it does not recognise these options.

    Can anyone help at all?

  68. jochen jochen on March 17, 2008 at 06:37PM

    I encountered a "superclass mismatch for class Annotation" when trying to use with rails 2.0.2.

    How to fix that?

  69. dip00dip dip00dip on March 18, 2008 at 09:09AM

    My solution is ugly -but it works. You have to remove the spec directory from plugin's source.

  70. dip00dip dip00dip on March 18, 2008 at 09:25AM

    And there is second way.

    Just install it as a gem. (instructions here: http://pivotalrb.rubyforge.org/svn/has_finder/trunk/has_finder/README.txt)

    In such way it has no problems with rake.

  71. Xin Xin on March 18, 2008 at 03:45PM

    I had the same problem jochen. I also removed the spec directory!

    I want to chain a number of has_finders together. Some of these finders might have nil as parameter, in which case I'd like it to be ignored. Is there any way of doing this?

    i.e. Shoes.of_size(10).of_colour(nil).under(40)

    I tried messing with the lamda but didn't get very far.

  72. Jacob Atzen Jacob Atzen on March 25, 2008 at 02:57PM

    How does this plugin play along with paginating_find?

    I'm trying to do something like:

    has_finder :paged, :page => {:size => 100, :auto => true}, :order => :id
    

    Which should potentially allow:

    User.active.paged
    
  73. Jon Buda Jon Buda on April 11, 2008 at 06:47PM

    Am I correct in assuming has_finder will not work with HABTM associations? I've tried it and it doesn't produce the correct results. Just wondering if I'm doing something wrong or its just not supported.

    Other than that, been loving the plugin, thanks!

  74. Martin Zimmermann Martin Zimmermann on April 14, 2008 at 07:13PM

    Hi,

    nice gem!

    I wanted to point another use which was very helpful for me.

    I needed to compose several OPTIONAL queries... Say your Person#controller accepts params[:search] and params[:ver] (with a boolean field), and you can call it with either or both of params. In the controller I would like just to call:

    Person.search(params[:search]).verification(params[:ver])

    and I wanted it to work with no (eg. give me all Person), one or the above two params options.

    I managed to do this by defining the has_finder like:

    has_finder :search, lambda {|name| {:conditions => name.nil? ? nil : ["name LIKE ?", name+"%"] } } has_finder :ver, lambda {|v| {:conditions => v.nil? ? nil : :conditions => {:verification = v} } }

    Bye, Z

  75. soliiid soliiid on April 28, 2008 at 06:17AM

    Hi nick,

    great job!

    but i found that has_finder does not work with polymorphic :has_many.

    here is the test code.

    class Membership < ActiveRecord::Base belongs_to :joinable, :polymorphic => true

    has_finder :accepted, :conditions => { :accepted => true } end

    class Group < ActiveRecord::Base has_many :memberships, :as => :joinable end

    the snippet Group.find(:first).memberships produces the sql SELECT * FROM memberships WHERE (memberships.joinable_id = 1 AND memberships.joinable_type = 'Group')

    while Group.find(:first).memberships.accepted only produces the sql SELECT * FROM memberships WHERE (accepted_at is not NULL)

    am i missing something?

  76. soliiid soliiid on April 28, 2008 at 06:21AM

    Hi nick,

    great job!

    but i found that has_finder does not work with polymorphic :has_many.

    here is the test code.

    <code>
    class Membership < ActiveRecord::Base
    
      belongs_to :joinable, :polymorphic => true
    
      has_finder :accepted, :conditions => { :accepted => true }
    
    end
    
    class Group < ActiveRecord::Base
    
      has_many :memberships,    :as => :joinable
    
    end
    </code>

    the snippet

    Group.find(:first).memberships

    produces the sql

    <code>
    
    SELECT * FROM `memberships` WHERE (memberships.joinable_id = 1 AND memberships.joinable_type = 'Group')
    
    </code>

    while

    Group.find(:first).memberships.accepted

    only produces the sql

    <code>
    SELECT * FROM `memberships` WHERE (accepted_at is not NULL)
    </code>

    am i missing something?

  77. UVSoft UVSoft on April 28, 2008 at 09:39AM

    Hi there,

    Does has_finder cache the result of searches like usual AssociationProxies do?

    class Task < AR has_finder :resolved, :condititions => { :resolved => true } end

    Task.resolved Task.resolved

    produces two SQL quiries!!!

  78. Nick Kallen Nick Kallen on April 28, 2008 at 05:01PM

    UVSoft:

    It does cache results. x = Task.resolved has not yet hit the database. x.collect will hit the db one time. x.collect again will not hit the database. You do not want to cache Task.resolved as it's effectively a global variable that will persist across http requests.

Add a Comment (MarkDown available)