This gem implements basic filtering of ActiveRecord models on the class and instance level based on the amount of relationships belonging to the object or its associations.
Benefits/ Raison d'Etre
Defining a scope or model method that implements such basic having/count functionality on an ActiveRecord model is trivial:
class Joker << ActiveRecord::Base has_many :jokes def self.with_at_least_n_jokes(n) self.joins(:jokes).group("jokes.id HAVING count(joker_id) >= n") end end
But what if we want to filter a jester's jokes as well? ActiveSupport defines concerns to make module inclusion really simple:
module Humor extend ActiveSupport::Concern included do has_many :jokes def with_at_least_n_jokes(n) self.joins(:jokes).group("jokes.id HAVING count(joker_id) >= n") end end end class Joker < ActiveRecord::Base include Humor end
Great! We can find the really humurous
But the following might break, as the hard-coded foreign key "joker_id" might not exist in the jesters table.
class Jester < ActiveRecord::Base include Humor end
The problem is quite clear: the scopes define queries using specific implementation details, making reuse nearly impossible.
But there's also a larger issue here: jokes are only one possible association. If we want the Jester model to include, say, music_instruments -- and to be able to filter on their amount -- we're back at square one, writing another module for each association.
This gem provides relief. Instead of defining modules with hard-coded method names and queries, it uses ActiveRecord's reflection capabilities to enable amount-based filtering on all models and their associations:
# Class-based filters Joker.with_at_least(2, :music_instruments) Joker.with_at_least(200, :jokes)
# Instance-based association filters @first_joker = Joker.find(1) @first_joker.jokes.with_at_least(10, :tomatoes)
These also call scoped() on all queries and return an instance of ActiveRecord::Relation, meaning that these methods are chainable:
@first_joker.performances.with_at_least(10, :dramatic_moments).where(:duration > 10)
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install numbered_relationships
This gem extends ActiveRecord, meaning the installation immediately offers the following methods:
Joker.with_at_least(1, :joke) Joker.with_at_most(2, :jokes) Joker.with_exactly(2, :jokes) Joker.without(2, :jokes) Joker.with_more_than(2, :jokes) Joker.with_less_than(2, :jokes) j = Joker.find(123) j.jokes.with_at_least(1, :laugh) j.jokes.with_at_most(2, :laughs) j.jokes.with_exactly(2, :laughs) j.jokes.without(11, :laughs) j.jokes.with_more_than(18, :laughs) j.jokes.with_less_than(2, :laughs)
It's also possible to use class methods or scopes defined on the association class:
Joker.with_at_least(1, [:dirty], :joke)
As long as the class method or scope :dirty is defined on Joker:
class Joke def self.dirty where(funny: true) end end
this code will properly filter the results before attempting to group them. It outputs the following SQL:
SELECT jesters.* FROM jesters INNER JOIN jokes ON jokes.jester_id = jesters.id WHERE jokes.funny = 't' GROUP BY jesters.id HAVING count(jokes.id) >= 1
These methods, like all other class methods, are chainable, meaning the following would also work:
Joker.with_at_least(1, [:dirty, :insulting, :crying_on_the_floor], :joke)
A call to a non-existent association will -- at least for the moment -- simply return:
- Improve exception handling.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature)
- Commit your changes (
git commit -am 'Added some feature')
- Push to the branch (
git push origin my-new-feature)
- Create new Pull Request