rails has_many_through creates a double join, how to optimize? - ruby-on-rails

I have a situation in which Schools and EventLeg records are tied together through Photos, which belong to a given Event.
class Photo < ActiveRecord::Base
belongs_to :event
belongs_to :event_leg
has_and_belongs_to_many :schools
end
class Event < ActiveRecord::Base
has_many :event_legs, :through => :photos, :group => 'event_legs.id'
has_many :photos
end
class School < ActiveRecord::Base
has_and_belongs_to_many :photos
has_many :events, :through => :photos
has_many :event_legs, :through => :photos
end
class EventLeg < ActiveRecord::Base
has_many :photos
has_many :schools, :through => :photos
end
I need to get the schools that appeared in a given event leg at an event.
#event = Event.find 4088
#event.schools.joins(:photos).where('photos.event_leg_id' => 28034)
This results in SQL that joins the photos_schools table twice, once to schools and once to photos:
SELECT DISTINCT `schools`.* FROM `schools`
INNER JOIN `photos_schools` `photos_schools_join` ON `photos_schools_join`.`school_id` = `schools`.`id`
INNER JOIN `photos` `photos_schools_2` ON `photos_schools_2`.`id` = `photos_schools_join`.`photo_id`
INNER JOIN `photos_schools` ON `schools`.`id` = `photos_schools`.`school_id`
INNER JOIN `photos` ON `photos_schools`.`photo_id` = `photos`.`id`
WHERE `photos`.`event_id` = 4088 AND `photos`.`event_leg_id` = 28034
The second JOIN of photos_schools -> photos is unnecessary, the following query does the same thing and faster:
SELECT DISTINCT `schools`.* FROM `schools`
INNER JOIN `photos_schools` ON `schools`.`id` = `photos_schools`.`school_id`
INNER JOIN `photos` ON `photos_schools`.`photo_id` = `photos`.`id`
WHERE `photos`.`event_id` = 4088 AND photos.event_leg_id = 28034;
So why is AR creating this second set of joins, and how can I make it stop?

try skipping the joins(:photos) i guess it should work
#event.schools.where('photos.event_leg_id' => 28034)

Related

Join for has_many_through in rails

I have 3 models as following.
class Order
belongs_to :item
belongs_to :category
end
class Item
has_many :orders
belongs_to :category
end
class Category
has_many :items
has_many :orders, through: :items
end
I want to join the tables like Order.joins(:item).joins(:category), but it's not working.
Desired SQL is
SELECT * FROM `orders`
INNER JOIN `items` ON `items`.`id` = `orders`.`item_id`
INNER JOIN `categories` ON `items`.`category_id` = `categories`.`id`
I hope your helps.
I'm a little confused because Order and Item both belongs_to Category and Category already has_many Orders with that setup, the :through option is unnecesary.
For your desired output I guess you want to do a nested join (order > item > category) instead of multiple joins (order > item+category)
https://guides.rubyonrails.org/active_record_querying.html#joining-multiple-associations
12.1.3.1 Joining Nested Associations (Single Level)
Article.joins(comments: :guest)
This produces:
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
So, you should do something like Order.joins(item: :category)
The syntax you're looking for is
Order.joins(item: :category)
Check here for more information.
The proper way to setup these associations is:
class Order < ApplicationRecord
belongs_to :item
# This references items.category_id
has_one :category, through: :item
end
class Item < ApplicationRecord
has_many :orders
belongs_to :category
end
class Category < ApplicationRecord
has_many :item, through: :orders
has_many :orders
end
You want to remove the orders.category_id column (if it exists) and use an indirect association through the items table to avoid duplication. The semantics of belongs_to and has_one can be confusing but belongs_to assumes that the foreign key is on this model (orders), while has_one places it on the other models table (items).
This will let you join/include/eager_load the association with:
irb(main):002:0> Order.joins(:category)
Order Load (1.4ms) SELECT "orders".* FROM "orders" INNER JOIN "items" ON "items"."id" = "orders"."item_id" INNER JOIN "categories" ON "categories"."id" = "items"."category_id" LIMIT $1 [["LIMIT", 11]]
And as you can see Rails will handle joining the join table (items) automatically.
If you want both associations to be loaded you can use a hash or just list both:
Order.eager_load(item: :category)
Order.eager_load(:item, :category)

Joining Nested Associations (Multiple Level)

I have the following models and relationships:
A User has many Offers (where he/she is the seller), an Offer has many Purchases, a Purchase has many Accbooks
Models and associations:
class User < ApplicationRecord
has_many :offers, foreign_key: :seller_id
has_many :purchases, foreign_key: :buyer_id
end
class Offer < ApplicationRecord
has_many :purchases
belongs_to :seller, class_name: 'User'
end
class Purchase < ApplicationRecord
belongs_to :offer
belongs_to :buyer, class_name: 'User'
has_one :seller, through: :offer
has_many :accbooks, class_name: 'Admin::Accbook', foreign_key: 'purchase_id'
end
module Admin
class Accbook < ApplicationRecord
belongs_to :purchase
end
end
I want to get all the Accbooks of any given user (as a seller). The equivalent SQL statement would look like this:
SELECT "accbooks".*
FROM "accbooks"
INNER JOIN "purchases" ON "purchases"."id" = "accbooks"."purchase_id"
INNER JOIN "offers" ON "offers"."id" = "purchases"."offer_id"
INNER JOIN "users" ON "users"."id" = "offers"."seller_id"
WHERE "users"."id" = ?
So far I've tried this:
Admin::Accbook.joins( {purchase: :offer} )
Which gives me this SQL as a result:
SELECT "accbooks".*
FROM "accbooks"
INNER JOIN "purchases" ON "purchases"."id" = "accbooks"."purchase_id"
INNER JOIN "offers" ON "offers"."id" = "purchases"."offer_id"
Now I donĀ“t know how to add the join to the User model, and then how to add the Where condition.
Thanks for any insight.
You can joins the relations together and apply where clause on the joined relations:
Admin::Accbook
.joins(purchase: :offer)
.where(offers: { seller_id: 123 })
A thing to know, where uses the DB table's name. joins (and includes, eager_load, etc) uses the relation name. This is why we have:
Admin::Accbook
.joins(purchase: :offer)
# ^^^^^ relation name
.where(offers: { seller_id: 123 })
# ^^^^^^ table name
Try Adding following association in users.rb
has_many :accbooks, through: :purchases
So your problem is user is acting as 2 roles for same accounts. You can try something like below stuff
class User < ApplicationRecord
has_many :offers, foreign_key: :seller_id
has_many :purchases, foreign_key: :buyer_id
has_many :offers_purchases,
through: :offers,
:class_name => 'Purchase',
:foreign_key => 'offer_id',
:source => :purchases
end

Counter_culture for has_one relationship (rails)

I use counter_culture gem, but when I want to use it, it throws an error that it cannot find product_id field in Product model, which makes sense because it should look for 'id'.
Below is the query generated when I call 'Product.counter_culture_fix_counts'. As you can see, it produced 'LEFT JOIN products AS products ON product_categories.id = products.product_id', which is not correct and it should be 'LEFT JOIN products AS products ON product_categories.product_id = products.id'.
SELECT categories.id, categories.id, COUNT(products.id) AS count, categories.products_count FROM "categories" LEFT JOIN product_categories AS product_categories ON categories.id = product_categories.category_id LEFT JOIN products AS products ON product_categories.id = products.product_id AND (products.active = 't') GROUP BY "categories"."id" ORDER BY "categories"."id" ASC LIMIT 1000 OFFSET 0
And here are the models:
# Product Model
class Product
has_one :product_category, dependent: :destroy
has_one :category, :through => :product_category
counter_culture [:product_category, :category], :column_name => Proc.new { |product| 'products_count' }, :column_names => { ["products.active = ?", true] => 'products_count' }, :touch => true
end
# ProductCategory Model
class ProductCategory
belongs_to :product
belongs_to :category
end
# Category Model
class Category
has_many :product_categories, dependent: :destroy
has_many :products, :through => :product_categories
end
Any idea how to get this working?
Thank you, Miro

Active record query with includes does not include

I have got this 'query'
#product_collections = ProductCollection.
#includes(:products). #does not do anything
joins(:products). #this at least gives me the products (in articles table)
group("tags.id").
where("articles.category_id = ?", #category.id).
where("articles.available = ?", true).
order('tags.name asc')
this produces the following sql
SELECT "tags".* FROM "tags"
INNER JOIN "article_taggings" ON "tags"."id" = "article_taggings"."tag_id"
INNER JOIN "articles" ON "articles"."id" = "article_taggings"."article_id"
WHERE ("tags"."type" = 'ProductCollection')
AND (articles.category_id = 1)
AND (articles.available = 't')
GROUP BY tags.id
ORDER BY tags.name asc
how could I manage to get the products in one (or only one second) go?
Models:
class Article < ActiveRecord::Base
has_many :article_taggings
has_many :tags, :through => :article_taggings
end
class Product < Article
belongs_to :category
has_many :product_collections, :through => :article_taggings, :source => :tag
end
class ArticleTagging < ActiveRecord::Base
belongs_to :article
belongs_to :tag
end
class Tag < ActiveRecord::Base
has_many :article_taggings
has_many :articles, :through => :article_taggings
has_and_belongs_to_many :web_pages
end
class ProductCollection < Tag
has_many :products, :through => :article_taggings, :source => :article
end
You need to put the includes after joins. That will solve the problem.

has_many :through association through two different associations

I have four model classes:
class Group < ActiveRecord::Base
has_many :projects
has_many :personal_blogs
end
class Project < ActiveRecord::Base
has_many :events, :as => :event_producer
end
class PersonalBlog < ActiveRecord::Base
has_many :events, :as => :event_producer
end
class Event < ActiveRecord::Base
belongs_to :event_producer, :polymorphic => true
end
I want to find all of the events for a particular group. I figure this is a has_many :through association, but how do I specify a has_many on Group that finds all events in the projects or personal_blogs of a group? I could, of course, specify two associations and concatenate the results, but then I have to re-sort, limit, condition, etc. in Ruby, which could potentially be a performance nightmare with many events. I'd like to do this within ActiveRecord to avoid such a nightmare.
You could define a method in the Group class like next:
class Group < ActiveRecord::Base
has_many :projects
has_many :personal_blogs
def events
Event.find(:all, :conditions => ['(type = ? AND event_producer_id IN (?)) OR (type = ? AND event_producer IN (?))', 'project', project_ids, 'personal_blog', personal_blog_ids])
end
end
If you don't like SQL like the previous one, it's always possible to use Single Table Inheritance. This solution depends on your classes attributes and behavior, but will allow you to use a "has_many through" association.
Why not just do:
class Group < ActiveRecord::Base
has_many :projects
has_many :personal_blogs
def all_events
projects.events + personal_blogs.events
end
end
If you need an association that you can preload you could specify a custom 'from' query that includes the group id with the event.
class Group < ActiveRecord::Base
has_many :projects
has_many :personal_blogs
has_many :events, -> {from(<<~SQL)}
(
SELECT DISTINCT e.*, g.id as group_id
FROM events e
LEFT JOIN projects p ON p.event_id = e.id
LEFT JOIN personal_blogs pb ON pb.event_id = e.id
INNER JOIN groups g ON g.id IN (p.group_id, pb.group_id)
) as events
SQL
end

Resources