is there a way to eager load polymorphic association's associations? - ruby-on-rails

artists have many activities (basically a cache of interactions between users):
class Activity < ActiveRecord::Base
belongs_to :receiver, :class_name => 'Artist', :foreign_key => :receiver_id #owns the stuff done "TO" him
belongs_to :link, :polymorphic => true
belongs_to :creator, :class_name => 'Artist', :foreign_key => :creator_id #person who initiated the activity
end
For example:
Activity.create(:receiver_id => author_id, :creator_id => artist_id, :link_id => id, :link_type => 'ArtRating')
I want to create an activity stream page for each artist, consisting of a list of different types of events, ArtRatings (likes, dislikes), Favoriting, Following etc.
The controller looks like this:
class ActivityStreamController < ApplicationController
def index
#activities = #artist.activities.includes([:link,:creator,:receiver]).order("id DESC").limit(30)
end
end
The db call correctly eagerly loads the polymorphic link objects:
SELECT "activities".* FROM "activities" WHERE (("activities"."receiver_id" = 6 OR "activities"."creator_id" = 6)) ORDER BY id DESC LIMIT 30
ArtRating Load (0.5ms) SELECT "art_ratings".* FROM "art_ratings" WHERE "art_ratings"."id" IN (137, 136, 133, 130, 126, 125, 114, 104, 103, 95, 85, 80, 73, 64)
SELECT "follows".* FROM "follows" WHERE "follows"."id" IN (14, 10)
SELECT "favorites".* FROM "favorites" WHERE "favorites"."id" IN (25, 16, 14)
But when I display each ArtRating, I also need to reference the post title, which belongs to a post. In the view, if I do:
activity.link.post
It does a separate DB call for each art_rating's post. Is there a way to eagerly load the post as well?
UPDATE TO THE QUESTION:
If there is no way to achieve eager loading of posts using 'includes' syntax, is there a way to manually do the eager loading query myself and inject it into the #activities object?
I see in the DB log:
SELECT "art_ratings".* FROM "art_ratings" WHERE "art_ratings"."id" IN (137, 136, 133, 130, 126, 125, 114, 104, 103, 95, 85, 80, 73, 64)
Is there a way I can access this list of ids from the #activities object? If so, I could do 2 additional queries, 1 to get the art_ratings.post_id(s) in that list, and another to SELECT all posts IN those list of post_ids. Then somehow inject the 'post' results back into #activities so that it's available as activity.link.post when I iterate through the collection. Possible?

TL;DR my solution makes artist.created_activities.includes(:link) eager load everything you want
Here's my first attempt at it: https://github.com/JohnAmican/music
A few notes:
I'm relying on default_scope, so this isn't optimal.
It looks like you're using STI. My solution doesn't. That means you can't simply call activities on an artist; you have to reference created_activities or received_activities. There might be a way around this. I'll update if I find anything.
I changed some names around because it was confusing to me otherwise.
If you go into console and do created_activities.includes(:link), the appropriate stuff gets eager-loaded:
irb(main):018:0> artist.created_activities.includes(:link)
Activity Load (0.2ms) SELECT "activities".* FROM "activities" WHERE "activities"."creator_id" = ? [["creator_id", 1]]
Rating Load (0.3ms) SELECT "ratings".* FROM "ratings" WHERE "ratings"."id" IN (1)
RatingExplanation Load (0.3ms) SELECT "rating_explanations".* FROM "rating_explanations" WHERE "rating_explanations"."rating_id" IN (1)
Following Load (0.3ms) SELECT "followings".* FROM "followings" WHERE "followings"."id" IN (1)
Favorite Load (0.2ms) SELECT "favorites".* FROM "favorites" WHERE "favorites"."id" IN (1)
=> #<ActiveRecord::Relation [#<Activity id: 1, receiver_id: 2, creator_id: 1, link_id: 1, link_type: "Rating", created_at: "2013-10-31 02:36:27", updated_at: "2013-10-31 02:36:27">, #<Activity id: 2, receiver_id: 2, creator_id: 1, link_id: 1, link_type: "Following", created_at: "2013-10-31 02:36:41", updated_at: "2013-10-31 02:36:41">, #<Activity id: 3, receiver_id: 2, creator_id: 1, link_id: 1, link_type: "Favorite", created_at: "2013-10-31 02:37:04", updated_at: "2013-10-31 02:37:04">]>
At the very least, this proves that Rails has the ability to do this. Circumventing default_scope seems like an issue with telling Rails what you want to do rather than a technical limitation.
UPDATE:
Turns out that when you pass a scope block to an association and call that association, self within that block refers to the relation. So, you can reflect on it and act appropriately:
class Activity < ActiveRecord::Base
belongs_to :creator, class_name: 'Artist', foreign_key: :creator_id, inverse_of: :created_activities
belongs_to :receiver, class_name: 'Artist', foreign_key: :receiver_id, inverse_of: :received_activities
belongs_to :link, -> { self.reflections[:activity].active_record == Rating ? includes(:rating_explanation) : scoped }, polymorphic: true
end
I updated my code to reflect (haha) this.
This can be cleaned up. For example, maybe you don't always want to eager load rating_explanations when accessing activity links. There are a number of ways to solve that. I could post one if you'd like.
But, I think the most important thing that this shows is that within the association's scope block, you have access to the ActiveRecord::Relation being built. This will allow you to do things conditionally to it.

Try this:
#artist.activities.includes([{:link => :post},:creator,:receiver])...
See the Rails docs for more.

If I get this right, you not only want to load a polymorphic association's association, but a polymorphic association's polymorphic association.
This basically means joining one defined table with another defined table through a bunch of undefined tables that come from some fields in the database.
Since both the activity and the post are joined through a polymorphic object they both have a something_id and something_type column.
Now I think active record doesn't let you do this out of the box but basically you want something like:
class Activity
has_one :post, :primary_key => [:link_id, :link_type], :foreign_key => [:postable_id, :postable_type]
end
(assuming your polymorphic association on Post is belongs_to :postable)
That would then give you a query sort-of direct association between Post and Activity that is a very weird take on habtm with a 'polymorphic join table' (I just made that term up). Because both share the same polymorphicly associated object, they can be connected. Sort of like a friends-of-my-friends thing.
CAVEAT: Like I said, as far as I know, AR doesn't let you do this out of the box (though it would be awesome) but there are some gems that give you composite foreign and/or primary keys. Maybe you can find one to help you solve your problem in this way.

Given your update to the question (that this isn't feasible using AR's include), you could try the following to get the associated Posts with a single extra query:
fetch the Activities, then build an Array of their linked post_ids.
#activities = #artist.activities.includes([:link,:creator,:receiver])
activity_post_ids = #activities.map{|a| a.link.post_id}.compact
then load the posts in one query and store them in a Hash, indexed by their id
activity_posts = Hash[Post.where(id: activity_post_ids).map{|p| [p.id, p]}]
#=> {1 => #<Post id:1>, 3 => #<Post id:3>, ... }
finally loop over #activities and set the post attribute of each associated link
#activities.each{|a| a.link.post = activity_posts[a.link.post_id]}

This simple ActiveRecord should work:
#artist.activities.includes([:link => :post,:creator,:receiver]).order("id DESC").limit(30)
If not, if you are getting an error like "Association named 'post' was not found; perhaps you misspelled it?", then you have a model problem, because at least one of your link associations doesn't have the post association.
Polymorphic associations are intended to be used with a common interface. If you ask for the post for any of the polymorphic association, then ALL of your associations should implemented also that association (the post one).
Check that all of your models used in the polymorphic association implement the post association.

Related

How to fetch all parent records on a self-join model

Suppose we have the following setup:
class Category < ApplicationRecord
belongs_to :parent, class_name: 'Category', foreign_key: :parent_id
has_many :children, class_name: 'Category', foreign_key: :parent_id
end
In other words: A category can have sub-categories. (And if the category has no parent, then it's a "top-level" category.)
What I'd like is: Given a collection of categories, merge this with their parent categories.
If it makes things easier, for now it's safe to assume that each category will be at most "one level deep", i.e. there are no "grand-child" categories. (But this might change one day, so ideally I'd like the code to be robust against that!)
My use case is that I'd like to be able to do something like this:
post.categories = categories.with_parents
..Since that will make all of the existing parent-specific category logic simpler to handle going forwards.
Naturally I could handle this by doing something like:
post.categories = (categories + categories.map(&:parent)).compact.uniq
...but I'm hoping there's a way to achieve this more elegantly and efficiently.
Here's the best way I've found so far, but it feels... wrong 😬 -- surely there a way of doing this with a join/union/merge??
class Category < ApplicationRecord
# ...
def self.with_parents
self.or(unscoped.where(id: select(:parent_id)))
end
end
If you just want to assign category and parent to post, this is as simple as I could make it:
def self.with_parent_ids
pluck(:id, :parent_id).flatten.compact.uniq
end
>> categories = Category.limit(2).reverse_order.with_parent_ids
Category Pluck (0.8ms) SELECT "categories"."id", "categories"."parent_id" FROM "categories" ORDER BY "categories"."id" DESC LIMIT $1 [["LIMIT", 2]]
=> [6, 3, 5, 2]
>> Post.first.category_ids = categories
I don't know if this is more elegant, but if parent_id is the only thing you have and you want to get all the parents up the tree:
>> Category.find_by_sql <<~SQL
WITH RECURSIVE tree AS (
( #{Category.where(id: 5).to_sql} )
UNION
( SELECT "categories".* FROM "categories" JOIN tree ON categories.id = tree.parent_id )
) SELECT * FROM tree
SQL
=>
[#<Category:0x00007f7fe354aa78 id: 5, name: "Audiobooks", parent_id: 2>, # started here
#<Category:0x00007f7fe354a9b0 id: 2, name: "Books", parent_id: 1>, # parent
#<Category:0x00007f7fe354a8e8 id: 1, name: "Products", parent_id: nil>] # grand parent
# etc

Return name in ActiveRecord relation along with foreign key id

I have a Sub-Component model which can belong to other sub-components. My Model looks like this:
class SubComponent < ApplicationRecord
belongs_to :parent, class_name: "SubComponent", foreign_key: "parent_sub_component_id", optional: true
has_many :child_sub_components, class_name: "SubComponent", foreign_key: "parent_sub_component_id"
validates_presence_of :name
end
This model is fairly simple, it has a name field and a parent_sub_component_id which as the name suggests is an id of another SubComponent.
I'd like to generate a query that returns all of the SubComponents (with their id, name, and parent_sub_component_id) but also includes the actual name of it's parent_sub_component.
This seems like it should be pretty simple but for the life of me I can't figure out how to do it. I'd like for this query to be done in the database rather than doing an each loop in Ruby or something like that.
EDIT:
I'd like for the output to look something like this:
#<ActiveRecord::Relation [#<SubComponent id: 1, name: "Parent Sub", parent_sub_component_id: nil, parent_sub_component_name: nil created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">, #<SubComponent id: 2, name: "Child Sub", parent_sub_component_id: 1, parent_sub_component_name: "Parent Sub" created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">]>
You can do this efficiently using an each loop if you use includes:
SubComponent.all.includes(:parent).each do |comp|
comp.parent.name # this gives you the name of the parent
end
What includes does is it pre-fetches the specified association. That is, ActiveRecord will query all subcomponents, and then in a single query also pull down all the parents of those subcomponents. When you subsequently access comp.parent in the loop, the associated parent will already be loaded, so this will not result in a so-called N+1 query.
The queries that AR will generate for you automatically will look something like this:
SELECT `subcomponents`.* FROM `subcomponents`
SELECT `subcomponents`.* FROM `subcomponents` WHERE `subcomponents`.`id` IN (1, 3, 9, 14)
If you need to use the name of the parent in a where condition, includes will not work and you will have to use joins instead to actually generate an SQL JOIN.
This is untested, but should get you started in the right direction, you can do this in Arel by doing something like
def self.execute_query
parent_table = Arel::Table.new(:sub_component).alias
child_table = Arel::Table.new(:sub_component)
child_table.join(parent_table, Arel::Nodes::OuterJoin).on(child_table[:parent_sub_component_id].eq(parent_table[:id]).project(child_table[:id], child_table[:name], parent_table[:id], parent_table[:name])
end
This results in a query like
SELECT "sub_component"."id", "sub_component"."name", "sub_component_2"."id", "sub_component_2"."name" FROM "sub_component" LEFT OUTER JOIN "sub_component" "sub_component_2" ON "sub_component"."parent_sub_component_id" = "sub_component_2"."id"
this is just off the top of my head by looking at Rails/Arel and probably needs a some work, but the query looks about what I would expect and this should get you going.

How to get serialized attribute in rails?

I have serialized column of Post model
serialize :user_ids
after save I can see in my console:
> p = Post.last
=> #<Post id: 30, title: "almost done2", created_at: "2017-05-08 15:09:40", updated_at: "2017-05-08 15:09:40", attachment: "LOGO_white.jpg", user_ids: [1, 2]>
I have permitted params :user_ids at my controller
def post_params
params.require(:post).permit(:title, :attachment, :user_ids)
end
All I need is to store #user_ids and copy to other model as atrribute,
but can't get instance like #user_ids = #Post.last.user_ids
Get error
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.post_id: SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
Thanks for help!
I think you've chosen really wrong name for your serialized attribute, unfortunately!
See, Rails is trying to be helpful and relation_ids method is allowing you to call ids of all related object. Consider those two models
class User
belongs_to :post
end
class Post
has_many :users
end
You can call something like
Post.last.user_ids
Which will then try to find all users that belong to this post and give you back their ids.
Look at query that you get:
SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
This is exactly what Rails is trying to do. Select USERS that belong to the post and give you their ids. However User does not have post_id column so this fails.
You're better off changing the name of your serialized column to something that won't confuse Rails. However, if you want to keep your column, you can override the method by putting this on the Post model
def user_ids
self[:user_ids]
end
This is non-ideal however, but will leave it up for you to decide what to do. My preference would be column rename, really. Will save you a lot of headeache

Destroy associated object from rails console

I am getting nil:NilClass errors, so I diagnosed the problem on rails c, see below:
irb(main):016:0> a = Product.find(216)
=> #<Product id: 216, ....>
irb(main):017:0> a.related_products
RelatedProduct Load (1.4ms) SELECT "related_products".* FROM "related_products" WHERE "related_products"."product_id" = 216
=> [#<RelatedProduct id: 162, product_id: 216, related_id: 248, created_at: "2013-08-20 15:37:03", updated_at: "2013-08-20 15:37:03">]
The problem here is related_id: 248 doesn't exist and confirmed it on the console:
Product.find(248)
I get:
SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT 1 [["id", 248]]
ActiveRecord::RecordNotFound: Couldn't find Product with id=248
My question is:
1) How do I delete: a.related_products on rails console, I tried a.related_products.destroy and destroy!
Bonus: is there a way to delete all Products that has empty related_products from rails console with one command?
Thank you in advance for your help!
You should be able to do something like
a.related_products.destroy_all
This will destroy ALL of the related products in the list.
Product.destroy_all(related_products: nil)
This will loop over all products and look to see if the related_products field is empty.
You may also want to setup dependent destroys. Something like
class MyModel < ActiveRecord::Base
has_many :related_products, dependent: :destroy
end
To delete all products where related_products are empty check destroy_all Product.destroy_all(related_products: nil)
Also check the dependent: :destroy in your model.

Correctness of using methods in model that joins other models in Rails

Maybe the title is confusing, but I didn't know how to explain my doubt.
Say I have the following class methods that will be helpful in order to do chainings to query a model called Player. A Player belongs_to a User, but if I want to fetch Players from a particular village or city, I have to fetch the User model.
def self.by_village(village)
joins(:user).where(:village => "village")
end
def self.by_city(city)
joins(:user).where(:city => "city")
end
Let's say I want to fetch a Player by village but also by city, so I would do...
Player.by_city(city).by_village(village).
This would be doing a join of the User twice, and I don't think that is correct.. Right?
So my question is: What would be the correct way of doing so?
I haven't tried that, but I would judge the answer to your question by the actual sql query ActiveRecord generates. If it does only one join, I would use it as you did, if this results in two joins you could create a method by_village_and_city.
OK. Tried it now:
1.9.2p290 :022 > Player.by_city("Berlin").by_village("Kreuzberg")
Player Load (0.3ms) SELECT "players".* FROM "players" INNER JOIN "users" ON "users"."id" = "players"."user_id" WHERE "users"."city" = 'Berlin' AND "users"."village" = 'Kreuzberg'
=> [#<Player id: 1, user_id: 1, created_at: "2012-07-28 17:05:35", updated_at: "2012-07-28 17:05:35">, #<Player id: 2, user_id: 2, created_at: "2012-07-28 17:08:14", updated_at: "2012-07-28 17:08:14">]
So, ActiveRecors combines the two queries, does the right thing and I would use it, except:
I had to change your implementation though:
class Player < ActiveRecord::Base
belongs_to :user
def self.by_village(village)
joins(:user).where('users.village' => village)
end
def self.by_city(city)
joins(:user).where('users.city' => city)
end
end
and what you're doing is usually handled with parameterized scopes:
class Player < ActiveRecord::Base
belongs_to :user
scope :by_village, lambda { |village| joins(:user).where('users.village = ?', village) }
scope :by_city, lambda { |city| joins(:user).where('users.city = ?', city) }
end

Resources