N+1 queries in ModelSerializer - ruby-on-rails

Comment.includes(:replies).without_replies
This way I get all the comments. I write API. And I use ActiveModelSerializer`
Comment have a relationship with the user. belongs_to :user
class CommentSerializer < ActiveModel::Serializer
has_many :replies, class_name: 'Comment'
attributes :id,
:user_image_url
def user_image_url
object.user.image_url
end
end
I need to get a picture of the user who left a comment.
Method user_image_url.
It's all good.
But the bullet displays a message.
GET /api/v1/comments?page=1&per_page=20
USE eager loading detected
Comment => [:user]
Add to your finder: :includes => [:user]
Call stack
/app/serializers/comment_serializer.rb:27:in `user_image_url'
I did this. Comment.includes(:replies, :user).without_replies But nothing, why?

Have you tried Comment.includes(:replies, :user).without_replies ?

Related

Retrieving model attribute from table+column name

Let's say you have the following models:
class User < ActiveRecord::Base
has_many :comments, :as => :author
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Let's say User has an attribute name, is there any way in Ruby/Rails to access it using the table name and column, similar to what you enter in a select or where query?
Something like:
Comment.includes(:author).first.send("users.name")
# or
Comment.first.send("comments.id")
Edit: What I'm trying to achieve is accessing a model object's attribute using a string. For simple cases I can just use object.send attribute_name but this does not work when accessing "nested" attributes such as Comment.author.name.
Basically I want to retrieve model attributes using the sql-like syntax used by ActiveRecord in the where() and select() methods, so for example:
c = Comment.first
c.select("users.name") # should return the same as c.author.name
Edit 2: Even more precisely, I want to solve the following problem:
obj = ANY_MODEL_OBJECT_HERE
# Extract the given columns from the object
columns = ["comments.id", "users.name"]
I don't really understand what you are trying to achieve. I see that you are using polymorphic associations, do you need to access comment.user.name while having has_many :comments, :as => :author in your User model?
For you polymorphic association, you should have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
end
And if you want to access comment.user.name, you can also have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments, :as => :author
has_many :comments
end
Please be more specific about your goal.
I think you're looking for a way to access the user from a comment.
Let #comment be the first comment:
#comment = Comment.first
To access the author, you just have to type #comment.user and If you need the name of that user you would do #comment.user.name. It's just OOP.
If you need the id of that comment, you would do #comment.id
Because user and id are just methods, you can call them like that:
comments.send('user').send('id')
Or, you can build your query anyway you like:
Comment.includes(:users).where("#{User::columns[1]} = ?", #some_name)
But it seems like you're not doing thinks really Rails Way. I guess you have your reasons.

Include with condition, but always show the primary table results

I have a function like :
# get all locations, if the user has discovered them or not
def self.getAll(user)
self.find(:all, :order => 'min_level asc', :include => 'discovered_locations',
:conditions => [ "discovered_locations.user_id = ? OR discovered_locations.id is null", user.id] )
end
self is actually the BossLocation model. I want to get a result set of the bosslocation and the discovered location IF that location was discovered by my user. However, if it was not discovered, i still need the bosslocation and no object as a discovered location. With the above code, if the user has not discovered anything, i don't get the bosslocations at all.
EDIT :
My associations are like :
class BossLocation < ActiveRecord::Base
has_many :discovered_locations
has_many :users, :through => :discovered_locations
class DiscoveredLocation < ActiveRecord::Base
belongs_to :user
belongs_to :boss_location
class User < ActiveRecord::Base
has_many :discovered_locations
has_many :boss_locations, :through => :discovered_locations
I think the problem is that you specify the user_id in the where conditions and not in the join condition. Your query will only give you the BossLocation if the user has discovered it or if no user at all has discovered it.
To make the database query match your need, you could change the include to the following joins:
:joins => "discovered_locations ON discovered_locations.boss_location_id = boss_locations.id
AND discovered_locations.user_id = '#{user.id}'"
BUT, I don't think it would help that much since the eager loading of Rails will not work when using joins like this instead of include.
If I where to do something similar, I would probably split it up. Perhaps by adding associations for user like this:
class User < ActiveRecord::Base
has_many :disovered_locations
has_many :discovered_boss_locations, :through => :discovered_locations
Update:
That way, in your Controller you can get all BossLocations and all discovered BossLocations like this:
#locations = BossLocation.all
#discovered = current_user.discovered_locations.all.group_by(&:boss_location_id)
To use these when you loop through them, do something like this:
<% #locations.each do |location| %>
<h1><%= location.name %></h1>
<% unless #discovered[location.id].nil? %>
<p>Discovered at <%= #discovered[location.id].first.created_at %></p>
<% end %>
<% end %>
What this does is it groups all discovered locations into a hash where the key is the boss_location_id. So when you loop through all boss_locations, you just see if there is an entry in the discovered hash that matches the boss_id.
"a result set of the bosslocation and the discovered location IF that location was discovered by my user."This is not a left outer join.You will get all bosslocations anytime.So,your conditions are wrong!This will get the bosslocation that it's discovered_locations.user_id = user.id OR discovered_locations.id is null".In this condition, this may be difficult for one sql statement. Also you can use union in your find_by_sql,but i suggest you use two find function.

(Rails Question) Merging multiple polymorphic has_many relationships

(This is not the actual code I'm using, although this sums up the idea of what I want to do)
class Connection < ActiveRecord::Base
belongs_to :connection1, :polymorphic => true
belongs_to :connection2, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :followers, :class_name => 'Connection', :as => :connection1
has_many :followings, :class_name => 'Connection', :as => :connection2
end
My question is that I want to know how I will be able to create a method called "network" such that what is returned isn't an array. Like so,
u = User.first
u.network # this will return a merged version of :followings and :followers
So that I'll still be able to do this:
u.network.find_by_last_name("James")
ETA:
Or hmm, I think my question really boils down to if it is possible to create a method that will merge 2 has_many associations in such a way that I can still call on its find_by methods.
Are you sure that you want a collection of Connections, rather than a collection of Users?
If it's a collection of Connections that you need, it seems like you'll be well served by a class method on Connection (or scope, if you like such things).
connection.rb
class Connection < ActiveRecord::Base
class << self
def associated_with_model_id(model, model_id)
include([:connection1, :connection2]).
where("(connection1_type IS #{model} AND connection1_id IS #{model_id})
OR (connection2_type IS #{model} AND connection2_id IS #{model_id})")
end
end
end
user.rb
class User < ActiveRecord::Base
def network
Connection.associated_with_model_id(self.class.to_s, id)
end
end
Probably not as useful as you'd like, but maybe it'll give you some ideas.

Rails: Query to get recent items based on the timestamp of a polymorphic association

I have the usual polymorphic associations for comments:
class Book < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
I'd like to be able to define Book.recently_commented, and Article.recently_commented based on the created_at timestamp on the comments. Right now I'm looking at a pretty ugly find_by_SQL query to do this with nested selects. It seems as though there must be a better way to do it in Rails without resorting to SQL.
Any ideas? Thanks.
For what it's worth, here's the SQL:
select * from
(select books.*,comments.created_at as comment_date
from books inner join comments on books.id = comments.commentable_id
where comments.commentable_type='Book' order by comment_date desc) as p
group by id order by null;
Sometimes it's just best to add a field to the object of which you are commenting. Like maybe a commented_at field of datetime type. When a comment is made on an object, simply update that value.
While it is possible to use SQL to do it, The commented_at method may prove to be much more scalable.
Not sure what your method has looked like previously but I'd start with:
class Book < ActiveRecord::Base
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', 5.minutes.ago])
end
end
This should find all the books that have had a comment created on them in the last 5 minutes. (You might want to add a limit too).
I'd also be tempted to create a base class for this functionality to avoid repeating the code:
class Commentable < ActiveRecord::Base
self.abstract_class = true
has_many :comments, :as => :commentable
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', Time.now - 5.minutes])
end
end
class Book < Commentable
end
class Article < Commentable
end
Also, you might want to look at using a plugin to achieve this. E.g. acts_as_commentable.

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources