Join for has_many_through in rails - ruby-on-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)

Related

In Rails, how do I query two has_many associations in a single finder?

I’m using Rails 4.2. I have the following user model with a couple of has_many associations
class User < ActiveRecord::Base
…
has_many :roles, through: :roles_users
has_many :addresses, dependent: :destroy, as: :addressable, inverse_of: :addressable
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
has_many :roles_users
class RolesUser < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
alias :user :addressable
I would like to find all users of a specific role without any addresses. I thought the below would do it
> users = User.includes(:roles, :addresses).where(:roles => {:name => 'User'}, :addresses => {:user_id => nil})
But when I check the results, I’m still getting results that have addresses …
2.7.1 :012 > users.last.addresses.count
…
=> 2
What’s the proper way to write a finder that queries these two has_many associations?
Checking for children records with a nil parent id is like the way of doing this in Rails 4. But if that doesn't work, you could use the NOT IN clause combination:
User
.where
.not(
id: User
.joins(:addresses, :roles)
.where(roles: { name: 'User' })
.select(:id)
)
It's basically filtering out by the user id all those user rows that have an address associated, have a role, and the role name is exactly "User". You end up with a SQL query like this:
SELECT "users".*
FROM "users"
WHERE "users"."id" NOT IN (
SELECT "users"."id"
FROM "users"
INNER JOIN "roles_users" ON "roles_users"."user_id" = "users"."id"
INNER JOIN "roles" ON "roles"."id" = "roles_users"."role_id"
WHERE "roles"."name" = 'User'
)

Rails: accessing a has_many relation from ActiveRecord_AssociationRelation

class Article < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :article
end
Is there a way to access all books of a set of authors ?
For example: Article.where(...).books that will return the associated books of selected articles?
You can do this using a subquery:
Book.where(article: Article.where(...))
Note that Activerecord still turns this into a single SQL query:
SELECT `books`.* FROM `books` WHERE `books`.`article_id` IN (SELECT `articles`.`id` FROM `articles` WHERE (...))

How get all the objects in a polymorphic nested relationship in Rails 4

I have the following classes
class State < ActiveRecord::Base
has_many :cities
has_many :products, as: :geography
end
class City < ActiveRecord::Base
has_many :neighborhoods
has_many :products, as: :geography
end
class Neighborhood < ActiveRecord::Base
has_many :products, as: :geography
end
class Product < ActiveRecord::Base
belongs_to :geography, polymorphic: true
end
class User < ActiveRecord::Base
belongs_to :state
end
And obviously I have another class User. The user just has permissions for see the products in his/her state (except if the user is admin, in which case he/she can see all the products) How I can get all the products of a User?
I tried to add some has_many, :through to State as follows
has_many :cities_activities, through: :cities, source: :products
has_many :neighborhoods_activities, through: :neighborhoods, source: :products
def owned_activities
self.products + self.cities_activities + self.neighborhoods_activities
end
but owned_activities returns an Array not a ActiveRecord::Relation (I need some way of return a ActiveRecord::Relation, so I can apply on it chained scopes).
I am patching the code with if-blocks and the code is getting messing and ugly, How can I do this in a clean, rails way?
#JTG suggested using merge, but apparently it don't work in a nested way (or for more of three tables/models):
Started GET "/api/products.json?order=total_cost&page=1&per_page=18" for 127.0.0.1 at 2014-07-20 09:27:29 -0500
Processing by ProductsController#index as JSON
Parameters: {"order"=>"total_cost", "page"=>"1", "per_page"=>"18"}
Geokit is using the domain: localhost
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1
Role Load (0.3ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = $1 ORDER BY "roles"."id" ASC LIMIT 1 [["id", 5]]
State Load (0.2ms) SELECT "states".* FROM "states" WHERE "states"."id" = $1 ORDER BY "states"."id" ASC LIMIT 1 [["id", 17]]
(9.2ms) SELECT COUNT(*) FROM "products" INNER JOIN "cities" ON "products"."geography_id" = "cities"."id" AND "products"."geography_type" = 'City' INNER JOIN "polygons" ON "products"."geography_id" = "neighborhoods"."id
" AND "products"."geography_type" = 'Neighborhood' INNER JOIN "cities" ON "neighborhoods"."cities_id" = "cities"."id" WHERE "products"."geography_id" = $1 AND "products"."geography_type" = $2 AND "cities"."states_id" = $1 [["geography_id", 17], ["geography_type", "State"], ["states_id", 17]]
PG::DuplicateAlias: ERROR: table name "cities" specified more than once
: SELECT COUNT(*) FROM "products" INNER JOIN "cities" ON "products"."geography_id" = "cities"."id" AND "products"."geography_type" = 'City' INNER JOIN "neighborhoods" ON "products"."geography_id" = "neighborhoods"."id" AND "products"."geography_type" = 'Neighborhood' INNER JOIN "cities" ON "neighborhoods"."cities_id" = "cities"."id" WHERE "products"."geography_id" = $1 AND "products"."geography_type" = $2 AND "cities"."states_id" = $1
ERROR: table name "cities" specified more than once
-- Clase:
You can't use merge will combine the queries with AND and what you need is a SQL OR query. That is unfortunately not supported by any ActiveRecord method. You can use arel to create this query, but as it will be very complex and contain a lot of subqueries do I find it easier to just find the id's you need with normal queries and then use those.
To do that can you implement something like this in State.
class State < ActiveRecord::Base
has_many :cities
has_many :neighborhoods, through: :cities
has_many :products, as: :geography
has_many :city_products, through: :cities, source: :products
has_many :neighborhood_products, through: :neighborhoods, source: :products
def all_products
state_product_ids = product_ids
city_product_ids = city_products.pluck(:id)
neighborhood_product_ids = neighborhood_products.pluck(:id)
all_product_ids = [state_product_ids, city_product_ids, neighborhood_product_ids].flatten.uniq
Product.where(id: all_product_ids)
end
end
You can now query a user for all products like this user.state.all_products.
A polymorphic association is not suited optimally for these relationships...
You can use a nested has_many.
class State < ActiveRecord::Base
has_many :cities
has_many :products, through: :cities
end
class City < ActiveRecord::Base
has_many :neighborhoods
has_many :products, through: :neighborhoods
end
class Neighborhood < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :neighborhood
end
class User < ActiveRecord::Base
belongs_to :state
end
Then you should be able to run user.state.products
Even better would be to add has_many :products, through: :state to user.rb so you could run user.products

Rails: Order by two freign referencees

I have rails model as this:
Stock:
has_one :location
has_one :product
Product:
belongs_to :stock
Location:
belongs_to :stock
In DB Stock has two foreign key columns:
location_id
product_id
When querying stock, I want to order by name column of location and product (location and then product), how to join these three table and sort?
Currently my biggest challenge is how to join (even two tables), I constantly get wrong column name in SQL output, here is an example:
SELECT "stocks".* FROM "stocks" INNER JOIN "locations" ON "locations"."id" = "stocks"."id"
Where it should be:
SELECT "stocks".* FROM "stocks" INNER JOIN "locations" ON "locations"."id" = "stocks"."location_id"
I learned that I may be missing some foreign_key attribute in models, but don't know how to add them.
It seems like you placed the foreign keys in a wrong table http://guides.rubyonrails.org/association_basics.html#the-has-one-association
'products' and 'locations' tables both should have 'stock_id' for this code to work:
class Stock < ActiveRecord::Base
has_one :location
has_one :product
end
class Location < ActiveRecord::Base
belongs_to :stock
end
class Product < ActiveRecord::Base
belongs_to :stock
end

rails has_many_through creates a double join, how to optimize?

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)

Resources