Is overriding an ActiveRecord relation's count() method okay? - ruby-on-rails

Let's say I have the following relationship in my Rails app:
class Parent < ActiveRecord::Base
has_many :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
end
I want parents to be able to see a list of their chatty kids, and use the count in paginating through that list. Here's a way to do that (I know it's a little odd, bear with me):
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
proxy_association.owner.kids.where(:chatty => true)
end
end
end
But! Some parents have millions of kids, and p.kids.for_chatting.count takes too long to run, even with good database indexes. I'm pretty sure this cannot be directly fixed. But! I can set up a Parent#chatty_kids_count attribute and keep it correctly updated with database triggers. Then, I can:
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
parent = proxy_association.owner
kid_assoc = parent.kids.where(:chatty => true)
def kid_assoc.count
parent.chatty_kids_count
end
end
end
end
And then parent.kids.for_chatting.count uses the cached count and my pagination is fast.
But! Overriding count() on singleton association objects makes the uh-oh, I am being way too clever part of my brain light up big-time.
I feel like there's a clearer way to approach this. Is there? Or is this a "yeah it's weird, leave a comment about why you're doing this and it'll be fine" kind of situation?

Edit:
I checked the code of will_paginate, seems like it is not using count method of AR relation, but i found that you can provide option total_entries for paginate
#kids = #parent.kids.for_chatting.paginate(
page: params[:page],
total_entries: parent.chatty_kids_count
)
This is not working
You can use wrapper for collection like here
https://github.com/kaminari/kaminari/pull/818#issuecomment-252788488​,
just override count method.
class RelationWrapper < SimpleDelegator
def initialize(relation, total_count)
super(relation)
#total_count = total_count
end
def count
#total_count
end
end
# in a controller:
relation = RelationWrapper.new(#parent.kids.for_chatting, parent.chatty_kids_count)

Related

Find all parent's parents of a set of deeply nested child objects in Rails 4

This maybe sounds confusing but it's easy to explain. Let's say I have those 3 deeply nested models:
//boo.rb
class Boo < ActiveRecord::Base
has_many :foos
end
//foo.rb
class Foo < ActiveRecord::Base
belongs_to :boo
has_many :goos
end
//goo.rb
class Foo < ActiveRecord::Base
belongs_to :foo
end
Now if I got a set of Goos in #goos, is it possible to get all connected Foos in a lean way? I am using this atm but it's not very lean because I only get the ids in the first step and not the object themselves:
#goos.pluck(:foo_id)
And if there is a better way to do that, is there also a way to get all parents of the connected Foos? So that I have the one set of all Boos connected to the objects in #goos?
Hope this question is not too weird but I am not sure if there are technical terms for this!
Works like this:
#Get all Foos
#foos = #goos.map(&:foo).compact.uniq
#Get all Boos
#boos = #goos.map(&:foo).map(&:boo).compact.uniq
Although it's not really great cause it return an array instead of an active record relation meaning I can't call where and order on it.
Edit:
If it's important for some reason to preserve the active record relation class, this is the better way:
#Get all Foos
#foos = Foo.where(id: #goos.map(&:foo.id).uniq)
#Get all Boos
#boos = Boo.where(id: #goos.map(&:foo).map(&:boo_id).uniq)

Ruby on rails associations parents

I would like to know, whatever the association is (simple belongs_to, polymorphic ...), when I make an association like :
class Toto < ActiveRecord::Base
belongs_to :test_one
belongs_to :test_two
end
class TestOne < ActiveRecord::Base
has_many :totos
end
class TestTwo < ActiveRecord::Base
has_many :totos
end
and then
test_one = TestOne.create
test_two = TestTwo.create
test1 = test_one.totos.create
test2 = test_two.totos.create
I would like to know into a callback of Toto what object instantiate me. In this case, it's obviously test_one and then test_two. I know I could check ids for example but the problem is when i do :
test3 = test_one.totos.create(test_two: test_two)
I can't know if test3 was created through test_one or test_two.
Thank you.
According to your example, I understand that you want to identify the type of object which is associated to your totos object (has_many :totos).
Since there are multiple different objects that might be associated to your totos object through the has_many and belongs_to associations, you might want to perform some kind of verification first to identify the type of the associated object.
First Answer:
This will only work if you know beforehand all the object types that has_many :totos
if test3.respond_to?(:test_one)
test = test3.test_one
elsif test3.respond_to?(:test_two)
test = test3.test_two
end
Second Answer:
I found this on Stackoverflow, and it somehow answeres your question. So if I rephrase the answer to:
def get_belongs_to(object)
associated = []
object.class.reflect_on_all_associations(:belongs_to).map do |reflection|
associated << object.try(reflection.name)
end
associated.compact
end
This method will return an array of all objects associated to your totos object. This will also work when totos belongs to multiple objects say test_one and test_two at the same time. So the following:
associated_objects = get_belongs_to(test3)
and in your case associated_objects[0] will yield the object you desire.
Hope this helps.
Rails does not persist the data you're looking for, so you'll have to store it yourself if you want it. This means you'll need a migration for the new field:
rails generate migration AddOriginalParentTypeToTotos original_parent_type:string
rake db:migrate
You can then override the assignment methods so that the first parent assigned will assign the original_parent_type attribute (and it will remain the same once assigned):
class Toto < ActiveRecord::Base
def test_one=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_one_id=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_two=(val)
self[:original_parent_type] ||= 'test_two'
super
end
def test_two_id=(val)
self[:original_parent_type] ||= 'test_two'
super
end
end
You can then use send to add an original_parent method:
class Toto < ActiveRecord::Base
def original_parent
send(original_parent_type) if original_parent_type
end
end

activerecord - getting the most popular category

I have a model Category and class method tickets_num which return the number of tickets that belongs to this category:
Category.first.tickets_num # => 2
class Category < ActiveRecord::Base
has_many :tickets
def tickets_num
self.tickets.count
end
end
I would like to do controller method "popular" which will return me 3 categories with the highest number of tickets_num , how can I do it in the most elegant way?
def popular
#categories = Category.order(tickets_num).all.limit(3)
end
my method doesn't work.
The following active record query should get you what you need. Not sure if there is a cleaner way to do that.
Category.select("categories.*, COUNT(tickets.id) AS t_count").joins(:tickets).group("categories.id").order("t_count DESC").limit(3)

Having trouble chaining ActiveRecord::Associations record

I'm trying to retrieve an associated column named "contribution_amount" for each user but I'm getting undefined method error and I can't figure out why.
Controller has:
#payments = Payment.where(:contribution_date => Date.today).pluck(:user_id)
#users = User.where(:id => #payments).find_each do |user|
user.payments.contribution_amount
end
models have:
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
Exact error in console is
`undefined method `contribution_amount' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Payment:0x007fb89b6b2c08>`
user.payments is a scope; that is, it represents a collection of Payment records. The contribution_amount method is only available on individual Payment records. You could say user.payments.first.contribution_amount, but I'm not sure that's your goal.
Are you trying to sum the contribution amounts? In that case, you'd want to use a method which aggregates collections of records: user.payments.sum(:contribution_amount).
Veering off-topic for a moment, it is generally better to push scoping methods down into your models. For example:
class User < ActiveRecord::Base
def self.with_payment_contribution_after(date)
joins(:payments).merge(Payment.with_contribution_after(date))
end
def self.with_contribution_amount
joins(:payments).group("users.id")
.select("users.*, sum(payments.contribution_amount) as contribution_amount")
end
end
class Payment < ActiveRecord::Base
def self.with_contribution_after(date)
where(:contribution_date => date)
end
end
# In your controller
#users = User.with_payment_contribution_after(Date.today)
.with_contribution_amount
# In a view somewhere
#users.first.contribution_amount
The advantages to structuring your code this way are:
Your scopes are not longer locked away in a controller method, so you can easily reuse them other places.
Your controller method can become simpler and more declarative. That is, it can express what information it wants, not how that information is acquired.
Breaking scopes down into smaller pieces implies that our code is better decomposed, and that which has been decomposed can be recomposed.
It's easier to test scopes via model unit tests then via controller testing.

Avoid scope hitting database if association already loaded

I have 2 models like so:
class Country < ActiveRecord::Base
has_many :cities
end
class City < ActiveRecord::Base
belongs_to :country
scope :big, where("population > 1000000")
end
Then, in the code I load a country with it's cities, like so:
country = Country.include(:cities).find(id)
But when I execute:
country.cities.big
It makes a hit to the db with this query:
SELECT * FROM cities where country_id = 1 AND population > 1000000
Which works fine, but it's not necessary since the cities where all already loaded by the :include.
Is there a way to tell the scope to not hit the db if the association is already loaded?
I can do it with an association extension, but not for a regular scope. On extensions I do something like:
has_many :cities do
def big
if loaded?
detect {|city| city.population > 1000000}
else
where("population > 1000000")
end
end
end
But this would be repeating the scope in 2 places and I want to reuse the scope on the city model.
The scope logic uses methods that work with Arel under the hood, and ruby Enumerables don't know how to use them. You may be able to refactor your logic to an abstraction that can be translated to use either the Arel or Enumerable methods, but this won't always be possible:
def self.build_scope(abstracted)
where(abstracted.map(&:to_s).join(' '))
end
def self.build_enum(abstracted)
select{|city| city.send(abstracted[0]).send(*abstracted[1..2]) }
end
def self.abstract_big
[:population, ">", 10000]
end
scope :big_scope, build_scope(abstract_big)
def self.big_enum
build_enum abstract_big
end
You could then do:
country.cities.big_enum
A much better idea would be to only eagerly load according to the scope that you want (if you know it in advance):
country = Country.include(:cities).merge(City.big).find(id)

Resources