No commit activity in last 3 years
No release in over 3 years
Amount-based filtering for AR Models and Associations
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

~> 3.2.7
 Project Readme

NumberedRelationships

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

Joker.with_at_least_n_jokes(200)

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)

Installation

Add this line to your application's Gemfile:

gem 'numbered_relationships'

And then execute:

$ bundle

Or install it yourself as:

$ gem install numbered_relationships

Usage

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:

[]

TODO:

  1. Improve exception handling.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Code Climate Travis CI Status