has_many with a scope super slow with STI - ruby-on-rails

class User < ApplicationRecord
has_many :buckets, -> { order(ranked_row: :asc) }
delegate :incomes, :fixed_costs, :financial_goals, to: :buckets
...
end
I have Buckets, that are STI'd. If I add that scope to the has_many, my page takes forever on 9 records, and seems to be loading something that should be cached
If I remove the scope, all is well
Any idea how the scope on the has_many is affecting the STI?? ranked_row has an index, but it's the same, regardless. I am using active_model_serializers, but I'm not sure if there's a correlation.
Update
Definitely something with active_model_serializers. ActiveModel::SerializableResource.new(user) is in the controller, and bogs down in the console, too. I removed everything from the from the serializer, and calling the scoped has_many is the thing. I'll hit up github.
Code
https://gist.github.com/dudo/f25767f00c874842a005
That's the smallest bit of code I could get to cause the issue. Again, It works fine without the scope on the has_many, and it also works with removing the percent_complete method from Bucket... that method doesn't look too nasty. What could be in that included_transactions method that's bringing it to a crawl when a scope is present??

When adding scope to a has_many you need to explicitly declare the inverse_of.
https://github.com/rails/rails/blob/7f18ea14c893cb5c9f04d4fda9661126758332b5/guides/source/4_1_release_notes.md

Related

Reduce N+1 Queries

N+1 queries detected what does that mean what should i do to make it work.
I am using Bullet gem for showing N+1 queries
user: kalyan
N+1 Query detected
Emp => [:functions]
Add to your finder: :include => [:functions]
N+1 Query method call stack
/home/Documents/app/views/tics/show.html.haml:20:in `_app_views_tics_show_html_haml___2301551533476719406_237290860'
This is the message from bullet gem.
_app_views_tics_show.html.haml
- if (#tic.assigned_to == current_user.emp) or (current_user.emp_functions.map{|x| x.id}.include?(1) if current_user.emp_functions.present? )
= best_in_place #tic, :subject
- else
= #tic.subject
help me to reduce n+1 query problem
emp.rb
has_many :emp_functions, inverse_of: :emp,dependent: :restrict_with_exception
belongs_to :user, inverse_of: :emp
emp_functions.rb
belongs_to :emp , inverse_of: :emp_functions
belongs_to :function
function.rb
has_many :emp_functions, dependent: :restrict_with_exception
has_many :emp, through: :emp_functions
user.rb
has_one :emp, inverse_of: :user, dependent: :restrict_with_exception
Imho, the most efficient way to get rid of this N+1 query is to alter the current_user method, adding includes(:emp_functions) to the User.find call. The way you do it depends on the way you handle your authentication. If you're using some kind of gem (Devise or Sorcery for example) you'll need to reopen those classes and alter the method, not forgetting to use super. If youэму written your own authentication, you'd be able to do that more easily.
Second thing I noticed is that you don't actually need to use map on your user.emp_functions in the view, provided that emp_functions is a has_many association on the User model. You can just flatten it to current_user.emp_function_ids.include?(1). This will help you get rid of the N+1 query problem, but only in this particular case. If you do tackle the emp_functions of the current user often in many places, I'd recomend using the first path I described.
Third third thing, not directly relevant to the question is that I really don't think that code belongs in the view. You should move it to a helper or a decorator, keeping the view clean and readable.
If emp_functions is a has_many association on your user model (and it seems like it is), You should be doing:
current_user.emp_function_ids.include?(1)
This is a very specific solution to your issue and I advise against using it in a view
change current_user.emp_functions.map{|x| x.id}.include?(1) to
current_user.emp_functions.where(id: 1).exists?
This will result in 1 single query to the db everytime you hit the page containing it.
You can also remove that if statement to remove another db query.

Undefined method error for scope on STI subclass

The Setup
I have an STI setup like so:
class Transaction < ActiveRecord::Base
belongs_to :account
scope :deposits, -> { where type: Deposit }
end
class Deposit < Transaction
scope :pending, -> { where state: :pending }
end
class Account < ActiveRecord::Base
has_many :transactions
end
If I call:
> a = Account.first
> a.transactions.deposits
...then I get what I expect, a collection of Deposit instances, however if I look at the class of what's returned:
> a.transactions.deposits.class
...then it's actually not a Deposit collection, it's still a Transaction collection, ie. it's a Transaction::ActiveRecord_AssociationRelation
The Problem
So, to the problem, if I then want to call one of the Deposit scopes on that collection it fails:
> a.transactions.deposits.pending
NoMethodError: undefined method `pending' for #<Transaction::ActiveRecord_Associations_CollectionProxy:0x007f8ac1252d00>
Things I've Checked
I've tried changing the scope to Deposit.where... which had no effect, and also to Deposit.unscoped.where... which actually returns the right collection object, but it strips all the scope, so I lose the account_id=123 part of the query so it fails on that side.
I've checked this and the problem exists for both Rails 4.1 and 4.2. Thanks for any pointers on how to make this work.
I know there's a workaround, but...
I know I could work around the issue by adding a has_many :deposits into Account, but I'm trying to avoid that (in reality I have many associated tables and many different transaction subclasses, and I'm trying to avoid adding the dozens of extra associations that would require).
Question
How can I get what's returned by the deposits scope to actually be a Deposit::ActiveRecord_Association... so that I can chain my scopes from Deposit class?
I created an isolated test for your issue here:https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope-rb and it has the error you mentioned.
I came across this post from pivotal http://pivotallabs.com/merging-scopes-with-sti-models/ about using were_values in a scope to get all the conditions. I then used them on unscope to force the expected class, basically this:
def self.deposits
conditions = where(nil).where_values.reduce(&:and)
Deposit.unscoped.where(conditions)
end
This test asserts that it returns a Deposit::ActiveRecord_Relation https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope2-rb
Update
You can also write this as a scope if you prefer:
scope :deposits, -> { Deposit.unscoped.where where(nil).where_values.reduce &:and }
As a quick workaround you can do > a.transactions.deposits.merge(Deposit.pending), but can't think of a different way of solving it. I'll think and try more options later and come back if I find anything.
You might want to say that an Account has_many :deposits
class Account < ActiveRecord::Base
has_many :transactions
has_many :deposits
end
Then you should be able to query
a.deposits.pending

Referring to instance in has_many (Rails)

I have a Game model which has_many :texts. The problem is that I have to order the texts differently depending on which game they belong to (yes, ugly, but it's legacy data). I created a Text.in_game_order_query(game) method, which returns the appropriate ordering.
My favourite solution would have been to place a default scope in the Text model, but that would require knowing which game they're part of. I also don't want to create separate classes for the texts for each game - there are many games, with more coming up, and all the newer ones will use the same ordering. So I had another idea: ordering texts in the has_many, when I do know which game they're part of:
has_many :texts, :order => Text.in_game_order_query(self)
However, self is the class here, so that doesn't work.
Is there really no other solution except calling #game.texts.in_game_order(#game) every single time??
I had a very similar problem recently and I was convinced that it wasn't possible in Rails but that I learned something very interesting.
You can declare a parameter for a scope and then not pass it in and it will pass in the parent object by default!
So, you can just do:
class Game < ActiveRecord
has_many :texts, -> (game) { Text.in_game_order_query(game) }
Believe or not, you don't have to pass in the game. Rails will do it magically for you. You can simply do:
game.texts
There is one caveat, though. This will not work presently in Rails if you have preloading enabled. If you do, you may get this warning:
DEPRECATION WARNING: The association scope 'texts' is instance dependent (the scope block takes an argument). Preloading happens before the individual instances are created. This means that there is no instance being passed to the association scope. This will most likely result in broken or incorrect behavior. Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
Following up using PradeepKumar's idea, I found the following solution to work
Assuming a class Block which has an attribute block_type, and a container class (say Page), you could have something like this:
class Page
...
has_many :blocks do
def ordered_by_type
# self is the array of blocks
self.sort_by(&:block_type)
end
end
...
end
Then when you call
page.blocks.ordered_by_type
you get what you want - defined by a Proc.
Obviously, the Proc could be much more complex and is not working in the SQL call but after there result set has been compiled.
UPDATE:
I re-read this post and my answer after a bunch of time, and I wonder if you could do something as simple as another method which you basically suggested yourself in the post.
What if you added a method to Game called ordered_texts
def ordered_texts
texts.in_game_order(self)
end
Does that solve the issue? Or does this method need to be chainable with other Game relation methods?
Would an Association extension be a possibility?
It seems that you could make this work:
module Legacy
def legacy_game_order
order(proxy_association.owner.custom_texts_order)
end
end
class Game << ActiveRecord::Base
includes Legacy
has_many :texts, :extend => Legacy
def custom_texts_order
# your custom query logic goes here
end
end
That way, given a game instance, you should be able to access instance's custom query without having to pass in self:
g = Game.find(123)
g.texts.legacy_game_order
Here is a way where you can do it,
has_many :texts, :order => lambda { Text.in_game_order_query(self) }
This is another way which I usually wont recommend(but will work),
has_many :texts do
def game_order(game)
find(:all, :order => Text.in_game_order_query(game))
end
end
and you can call them by,
game.texts.game_order(game)
Im not sure what your order/query looks like in the in_game_order_query class method but i believe you can do this
has_many :texts, :finder_sql => proc{Text.in_game_order_query(self)}
Just letting you know that I have never used this before but I would appreciate it if you let me know if this works for you or not.
Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many for more documentation on :finder_sql
I think if you want runtime information processed you should get this done with:
has_many :texts, :order => proc{ {Text.in_game_order_query(self)} }

Rails, trouble with scopes

I'm not sure what I'm doing wrong here but I want to create scope for a model I have, but I want it to evaluated a count on a related model... like say:
class Thing < ActiveRecord::Base
has_many :photos
scope :with_images, self.photo.count > 0
end
class Photo < ActiveRecord::Base
belongs_to :thing
end
I should then have a scope which would work like
Thing.where('some conditions').with_images
I get a NoMethodError on photos, why wouldn't this be available as a relation? I don't want to use it as a method.
There are two things going on here. First, you're trying to call photo, instead of photos.
However, the you're still going to get an error because at the time of execution, self refers to the constant Thing, and not an instance of Thing. The declaration has_many :photos defines a method photos for instances of Thing. Therefore, Thing (the constant) doesn't have a method called photos.
tl;dr Just use a :joins argument since it will only find records that have photos
scope :with_images, :joins => :photos
It should be:
self.photos.count > 0
or if you're using counter cache:
self.photos_count > 0

How can I access ActiveRecord Associations in class callbacks in rails?

Updated
Appears to be a precedence error and nothing to do with the question I originally asked. See discussion below.
Original question
Is it possible to use active record associations in callbacks? I've tested this code in the console and it works fine as long as it isn't in a callback. I'm trying to create callbacks that pull attributes from other associated models and I keep getting errors of nil.attribute.
If callbacks are not the correct approach to take, how would one do a similar action in rails? If the associations are simple, you could use create_association(attributes => ), but as associations get more complex this starts to get messy.
For example...
class User < ActiveRecord::Base
belongs_to :b
before_validation_on_create {|user| user.create_b} #note, other logic prevents creating multiple b
end
class B < ActiveRecord::Base
has_many :users, :dependent => destroy
after_create{ |b| b.create_c }
has_one :c
end
class C < ActiveRecord::Base
belongs_to :b
after_create :create_alert_email
private
def create_alert_email
self.alert_email = User.find_by_b_id(self.b_id).email #error, looks for nil.email
end
end
Off course associations are available in your callbacks. After all, the create_after_email is simply a method. You can even call it alone, without using a callback. ActiveRecord doesn't apply any special flag to callback methods to prevent them from working as any other method.
Also notice you are running a User#find query directly without taking advantage of any association method. An other reason why ActiveRecord association feature should not be the guilty in this case.
The reason why you are getting the error should probably searched somewhere else.
Be sure self.b_id is set and references a valid record. Perhaps it is nil or actually there's no User record with that value. In fact, you don't test whether the query returns a record or nil: you are assuming a record with that value always exists. Are you sure this assumption is always statisfied?

Resources