ActiveRecord has_many through polymorphic has_many - sql

It seems like rails still not support this type of relation and throws ActiveRecord::HasManyThroughAssociationPolymorphicThroughError error.
What can I do to implement this kind of relation?
I have following associations:
Users 1..n Articles
Categories n..n Articles
Projects 1..n Articles
And here is Subscription model
Subscription 1..1 User
Subscription 1..1 Target (polymorphic (Article, Category or User))
And I need to select articles through Subscription#target#article according to user#subscriptions.
I have no idea hot to implement this
Ideally I want to get instance of Association class
UPDATE 1
Here is little example
Let say user_1 has 4 Subscription records:
s1 = (user_id: 1, target_id: 3, target_type: 'User')
s2 = (user_id: 1, target_id: 2, target_type: 'Category')
s3 = (user_id: 1, target_id: 3, target_type: 'Project')
s4 = (user_id: 1, target_id: 8, target_type: 'Project')
I need method User#feed_articles, that fetches all articles, that belong to any of target, I subscribed.
user_1.feed_articles.order(created_at: :desc).limit(10)
UPDATE 2
I separate articles sources by type in User model:
has_many :out_subscriptions, class_name: 'Subscription'
has_many :followes_users, through: :out_subscriptions, source: :target, source_type: 'User'
has_many :followes_categories, through: :out_subscriptions, source: :target, source_type: 'Category'
has_many :followes_projects, through: :out_subscriptions, source: :target, source_type: 'Project'
has_many :feed_user_articles, class_name: 'Article', through: :followes_users, source: :articles
has_many :feed_category_articles, class_name: 'Article', through: :followes_categories, source: :articles
has_many :feed_project_articles, class_name: 'Article', through: :followes_projects, source: :articles
But how can I merge feed_user_articles with feed_category_articles and feed_project_articles without loss of perfomance
UPDATE 3.1
The only way I found is to use raw SQL join query. Looks like it works fine, but I'm not sure.
def feed_articles
join_clause = <<JOIN
inner join users on articles.user_id = users.id
inner join articles_categories on articles_categories.article_id = articles.id
inner join categories on categories.id = articles_categories.category_id
inner join subscriptions on
(subscriptions.target_id = users.id and subscriptions.target_type = 'User') or
(subscriptions.target_id = categories.id and subscriptions.target_type = 'Category')
JOIN
Article.joins(join_clause).where('subscriptions.user_id' => id).distinct
end
(This is just for Users and Categories)
It supports scopes and other features. The only thing interests me: does this query lead to some undesirable effect?

I think that from DB performance prospective using UNION ALL multiquery will be more efficient than using polymorphic multijoin. Also it will be more readable. I tried to write an Arel query as example but it does not play nice (I failed to make order by clause work properly) so I think you have to put it via raw SQL. You can use SQL template apart from ORDER BY clause for drying it up.

You are correct that Rails doesn't support has_many :through w/ polymorphic associations. You could mimic this behavior by defining an instance method on your User class. That would look something like this:
def articles
Article.
joins("join subscriptions on subscriptions.target_id = articles.id and subscriptions.target_type = 'Article'").
joins("join users on users.id = subscriptions.user_id")
end

Related

Rails Many-to-many relationship with extension generating incorrect SQL

I'm having an issue where a many-to-many relationship with an "extension" is generating incorrect SQL.
class OrderItem < ApplicationRecord
belongs_to :buyer, class_name: :User
belongs_to :order
belongs_to :item, polymorphic: true
end
class User < ApplicationRecord
has_many :order_items_bought,
-> { joins(:order).where.not(orders: { state: :expired }).order(created_at: :desc) },
foreign_key: :buyer_id,
class_name: :OrderItem
has_many :videos_bought,
-> { joins(:orders).select('DISTINCT ON (videos.id) videos.*').reorder('videos.id DESC') },
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
end
user.videos_bought.confirmed generates this SQL:
Video Load (47.0ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" "order_items_videos_join" ON
"order_items_videos_join"."item_id" = "videos"."id" AND
"order_items_videos_join"."item_type" = $1 INNER JOIN
"orders" ON "orders"."id" = "order_items_videos_join"."order_id" INNER JOIN
"order_items" ON "videos"."id" = "order_items"."item_id" WHERE
"order_items"."buyer_id" = $2 AND ("orders"."state" != $3) AND "order_items"."item_type" = $4 AND
"orders"."state" = $5 ORDER BY videos.id DESC, "order_items"."created_at" DESC LIMIT $6
Which returns some Video records which are joined with orders that do NOT have state confirmed. I would expect all orders to have state confirmed.
If I use raw SQL everything works fine:
has_many :videos_bought,
-> {
joins('INNER JOIN orders ON orders.id = order_items.order_id')
.select('DISTINCT ON (videos.id) videos.*')
.reorder('videos.id DESC')
},
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
Now user.videos_bought.confirmed generates this SQL:
Video Load (5.4ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" ON
"videos"."id" = "order_items"."item_id" INNER JOIN orders ON
orders.id = order_items.order_id WHERE
"order_items"."buyer_id" = $1 AND ("orders"."state" != $2) AND
"order_items"."item_type" = $3 AND "orders"."state" = $4 ORDER BY
videos.id DESC, "order_items"."created_at" DESC LIMIT $5
Which seems more succinct because it avoids the auto generated order_items_videos_join name. It also only returns orders that have state confirmed.
Any idea what is going on? Does ActiveRecord just generate faulty SQL sometimes?
Using rails 5.1.5. Upgrading to latest made no difference.
I'm hoping to get an explanation on why Rails generates the order_items_videos_join string in the first case but not in the second case. Also, why the second SQL query produces incorrect results. I can edit the question with more code and data samples if needed.
ActiveRecord does not just generate faulty SQL sometimes, but there's a little nuance to it such that starting simple is best when it comes to defining relationships. For example, let's rework your queries to get that DISTINCT ON out of there. I've never seen a need to use that SQL clause.
Before chaining highly customized association logic, let's just see if there's simpler way to query first, and then check to see whether there's a strong case for turning your queries into associations.
Looks like you've got a schema like this:
User
id
Order
id
state
OrderItem
id
order_id
buyer_id (any reason this is on OrderItem and not on Order?)
item_type (Video)
item_id (videos.id)
Video
id
A couple of tidbits
No need to create association extensions for query conditions that would make perfectly good scopes on the model. See below.
A perfectly good query might look like
Video.joins(order_item: :order).
where(order_items: {
buyer_id: 123,
order: {
state: 'confirmed'
}).
# The following was part of the initial logic but
# doesn't alter the query results.
# where.not(order_items: {
# order: {state: 'expired'}
# }).
order('id desc')
Here's another way:
class User < ActiveRecord::Base
has_many :order_items, foreign_key: 'buyer_id'
def videos_purchased
Video.where(id: order_items.videos.confirmed.pluck(:id))
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order, class_name: 'User', foreign_key: 'buyer_id'
belongs_to :item, polymorphic: true
scope :bought, -> {where.not(orders: {state: 'cancelled'})}
scope :videos, -> {where(item_type: 'Video')}
end
class Video < ActiveRecord::Base
has_many :order_items, ...
scope :confirmed, -> {where(orders: {state: 'confirmed'})}
end
...
user = User.first()
user.videos_purchased
I might have the syntax a little screwy when it comes to table and attribute names, but this should be a start.
Notice that I changed it from one to two queries. I suggest running with that until you really notice that you have a performance problem, and even then you might have an easier time just caching queries, than trying to optimize complex join logic.

How to join three tables in Rails?

I have my records set up as follows
Subscription: id, plan_id <- belongs to plan
Plan: id, role_id <- belongs to role
Role: id, name
Given this data
Subscription: id: 1, plan_id: 5
Plan: id: 5, role_id: 10
Role: id: 10, name: 'Gold'
I'm trying to write a join so that I can find subscriptions based on their associated roles, i.e.:
Subscription.joins(:plan).joins(:role).where("roles.name = ?", 'Gold')
But this approach doesn't work. Any help would be appreciated.
Thanks.
If you have proper associations then use this:
Subscription.includes(:plan => [:role]).where("roles.name = 'Gold'").first
You can also write the query manually:
Subscription.joins("INNER JOIN plans ON plans.id = subscriptions.plan_id
INNER JOIN roles ON roles.id = plans.role_id").where("roles.name = 'Gold'").first
If you're trying to find all the subscriptions for a given role, why not start with the role? Rails can take care of these "one step removed" associations if we configure them correctly.
gold_role = Role.where(name: 'Gold').first
gold_role.subscriptions
This is possible via Rails' has_many :through relations. I think this is the correct setup for your models:
class Subscription < ActiveRecord::Base
belongs_to :plan
has_one :role, through: :plan
end
class Plan < ActiveRecord::Base
has_many :subscriptions
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :plans
has_many :subscriptions, through: :plans
end

How to write this ActiveRecord Query using Join instead of subquery in Rails 4

Consider the following:
class User < ActiveRecord::Base
has_many :events
end
class Event < ActiveRecord::Base
belongs_to :user #this user is the event owner
has_many :members
end
class Members < ActiveRecord::Base
belongs_to :user
belongs_to :event
end
Now, I need to list all the members for which current_user is the owner. so I have come up with this:
#members = Member.where event_id: current_user.events
which produces the following query:
SELECT "members".* FROM "members" WHERE "members"."event_id" IN (SELECT "events"."id" FROM "events" WHERE "events"."user_id" = 1)
This works as expected but uses subqueries instead of JOIN. Does anyone know a better way to write this same query?
Add a has_many :through association to your User model:
class User < ActiveRecord::Base
has_many :events
has_many :members, :through => :events
end
Now you can query for all a user's members through the members association:
user.members
The SQL generated will look something like:
SELECT "members".* FROM "members" INNER JOIN "events" ON "members"."id" = "events"."member_id" WHERE "events"."user_id" = 1
Transformed to JOIN syntax (with table aliases to make it shorter and easier to read):
SELECT m.*
FROM events e
JOIN members m ON m.event_id = e.id
WHERE e.user_id = $1
I guess this will work.
Member.joins(:event).where("events.user_id = ?" , current_user.id)
You could do something like :
Member.joins(:event).where(events: {user_id: current_user.id})

How to optimise this ActiveRecord query?

I'm still learning ruby, rails and ActiveRecord everyday. Right now I'm learning SQL through a new small app I'm building but the problem is that the main view of my app currently does ~2000 queries per page refresh, oouuuppps.
So now that I know I have all the required information in my DB and that I can display them correctly, it is time for me to optimise them but I just don't know where to start to be honest.
These are my models associations
class League < ActiveRecord::Base
belongs_to :user
has_many :league_teams
has_many :teams, :through => :league_teams
end
class Team < ActiveRecord::Base
has_many :gameweeks
has_many :league_teams
has_many :leagues, :through => :league_teams
end
class Gameweek < ActiveRecord::Base
belongs_to :team
has_and_belongs_to_many :players
has_and_belongs_to_many :substitutes, class_name: "Player", join_table: "gameweeks_substitutes"
belongs_to :captain, class_name: "Player"
belongs_to :vice_captain, class_name: "Player"
end
class Player < ActiveRecord::Base
serialize :event_explain
serialize :fixtures
serialize :fixture_history
has_many :gameweeks, class_name: "captain"
has_many :gameweeks, class_name: "vice_captain"
has_and_belongs_to_many :gameweeks
has_many :player_fixtures
end
So this is my controller:
#league = League.includes(teams: [{gameweeks: [{players: :player_fixtures} , :captain]}]).find_by(fpl_id:params[:fpl_id])
#teams = #league.teams
#defense_widget_leaderboard = #league.position_based_leaderboard_stats(#teams, ['Defender', 'Goalkeeper'])
And this is one of the method in my League Model:
def position_based_leaderboard_stats(teams,positions_array)
leaderboard = []
teams.each do |team|
position_points = 0
gameweeks = team.gameweeks
gameweeks.each do |gameweek|
defense = gameweek.players.where(type_name:positions_array)
defense.each do |player|
player.player_fixtures.where(gw_number: gameweek.number).each do |p|
position_points += p.points
end
end
end
leaderboard << [team.team_name,position_points]
end
return leaderboard.sort_by {|team| team[1]}.reverse
end
I have 4 methods that look more or less the same thing as the one above. Each are doing between 300 and 600 queries.
As far as I read it only, it is a typical case of N+1 queries. I tried to reduce with the includes in the #league but it got me down from 2000 to 1800 queries.
I looked into group_by, joins and sum but I couldn't make it work.
The closest thing I got to working was this
players = PlayerFixture.group("player_id").sum(:points)
Where I could then query by doing players[player.id] but that doesn't give me the right results anyway because it doesn't take into account the Gameweeks > Players > Player_fixtures relationship.
How can I reduce the numbers of queries I'm doing? I went on #RubyOnRails on freenode and people told me it can be done in 1 query but wouldn't point me in any directions or help me...
Thanks
In your position_based_leaderboard_stats N+1 problem appears, too. So you can preload all your associations before each cycles:
def position_based_leaderboard_stats(teams,positions_array)
leaderboard = []
Team.preload(gameweeks: players).where('players.type_name=?', positions_array )
your code
Also, you could add player_fixtures to preload statement, but I can't understand dependencies of those associations, sorry.
Spend some time with SQL. Finally found the query that can help me with that. I also discovered SQL Views and how to use them through Activerecord which is pretty neat.
Final successful query
CREATE VIEW team_position_points AS
select teams.id as team_id, teams.team_name, players.type_name, sum(points) as points
from teams
inner join gameweeks on teams.id = gameweeks.team_id
inner join gameweeks_players on gameweeks.id = gameweeks_players.gameweek_id
inner join players on gameweeks_players.player_id = players.id
inner join player_fixtures on players.id = player_fixtures.player_id AND player_fixtures.gw_number = gameweeks.number
group by teams.id, players.type_name

Order by polymorphic belongs_to attribute

How do I make an ActiveRecord query that orders by an attribute of a polymorphic belongs_to association?
For example I have model called Tagging that has a polymorphic belongs_to association named tagged_item.
Unfortunately Tagging.joins(:tagged_item) throws a ActiveRecord::EagerLoadPolymorphicError. So I can't do something like Tagging.joins(:tagged_item).order("tagged_item.created_at DESC").
Any suggestions?
You can't make a join directly with a polymorphic relationship because the polymorphic objects' data are in different tables. Still you could try to do it manually as in the following example.
class Tagging < ActiveRecord::Base
belongs_to :tagged_item, :polymorphic => true
end
class Post
has_many :image_tagging, :as => :tagged_item
end
class Comment
has_Many :image_tagging, :as => :tagged_item
Tagging.select("taggins.*, COALESCE(posts.created_at, comments.created_at) AS tagged_item_created_at").
joins("LEFT OUTER JOIN posts ON posts.id = tagging.tagged_item_id AND tagging.tagged_item_type = 'Post'").
joins("LEFT OUTER JOIN comments ON comments.id = tagging.tagged_item_id AND tagging.tagged_item_type = 'Comment'").
order("tagged_item_created_at DESC")
COALESCE chooses the first column provided if exists otherwise the other one. It's the same as IFNULL in mysql or you could even use CASE WHEN ... IS NULL THEN ... ELSE ... END