How do I query with '.includes' across more than two tables? - ruby-on-rails

I have the following associations:
class Captain
has_many :boats
end
class Boat
belongs_to :captain
has_many :classifications
end
class Classification
has_many :boats
end
I want to find out which captains have boats that have classifications with :name attributes of "catamaran."
This has been my best guess so far:
Captain.includes(:boats, :classifications).where(:boats => {:classifications => {:name => "catamaran"}})

Try this
Captain.joins(boats: :classifications).where(classifications: { name: "catamaran" })
This query results in following SQL query
SELECT * FROM `captains`
INNER JOIN `boats` ON `boats`.`captain_id` = `captains`.`id`
INNER JOIN `join_table` ON `join_table`.`boat_id` = `boat`.`id`
INNER JOIN `classifications` ON `join_table`.`classification_id` = `classifications`.id

#Sujan Adiga has right!
If you use the include method, active record generate 2 separates sql query. The first for your main Model, and the second for your inclued model. But you don't have access on the included model in your first query.
When you use the joins method, active record generate sql query with joins statement. So you can use the joined model in your clause where.

Related

Unique join table reference inside model scope

I have 2 models with a has_many association:
class Log < ActiveRecord::Base
has_many :log_details
end
and
class LogDetail < ActiveRecord::Base
belongs_to :log
end
The Log table has an action_type string column. The LogDetail table has 2 columns: key and value, both string, and a reference back to the Log table with a log_id.
I want to write 3 scopes on the Log model to query for some details joining the Log table with LogModel twice. Here's a sample:
has_many :payment_gateways, -> {where(key: 'payment_gateway')}, class_name: 'LogDetail', foreign_key: :log_id
has_many :coupon_codes, -> {where(key: 'coupon_code')}, class_name: 'LogDetail', foreign_key: :log_id
scope :initiate_payment, -> {where(action_type: 'INITIATE PAYMENT')}
scope :payment_gateway, -> (pg) {joins(:payment_gateways).where(log_details: {value: pg}) unless pg.blank?}
scope :coupon_code, -> (cc) {joins(:coupon_codes).where(log_details: {value: cc}) unless cc.blank?}
Using the above scopes, if I try to query for
Log.initiate_payment.payment_gateway('sample_pg').coupon_code('sample_cc')
I get the SQL query:
SELECT
`logs`.*
FROM
`logs`
INNER JOIN
`log_details` ON `log_details`.`log_id` = `logs`.`id`
AND `log_details`.`key` = 'payment_gateway'
INNER JOIN
`log_details` `coupon_codes_logs` ON `coupon_codes_logs`.`log_id` = `logs`.`id`
AND `coupon_codes_logs`.`key` = 'coupon_code'
WHERE
`logs`.`action_type` = 'INITIATE PAYMENT'
AND `log_details`.`value` = 'sample_pg'
AND `log_details`.`value` = 'sample_cc'
instead of: (notice the difference in the last AND condition)
SELECT
`logs`.*
FROM
`logs`
INNER JOIN
`log_details` ON `log_details`.`log_id` = `logs`.`id`
AND `log_details`.`key` = 'payment_gateway'
INNER JOIN
`log_details` `coupon_codes_logs` ON `coupon_codes_logs`.`log_id` = `logs`.`id`
AND `coupon_codes_logs`.`key` = 'coupon_code'
WHERE
`logs`.`action_type` = 'INITIATE PAYMENT'
AND `log_details`.`value` = 'sample_pg'
AND `coupon_codes_logs`.`value` = 'sample_cc'
The first query, because it doesn't resolve the join table references properly, gives me zero results.
How can I modify my scopes/models in such a way to generate the correct query? I think I need a reference to the join table alias inside the scope's where clause, but I'm not sure how to get that reference.
Sadly, ActiveRecord has no built-in way to specify the alias used when joining an association. Using merge to try merging the two scopes also fails as the condition is overridden.
3 solutions:
Use Arel to alias a joins, but that's a bit hard to read, and you still need to repeat the association definition for payment_gateways and coupon_codes
Join directly in SQL:
scope :payment_gateway, -> (pg) { joins(<<-SQL
INNER JOIN log_details payment_gateways
ON payment_gateways.log_id = logs.id
AND payment_gateways.key = 'payment_gateway'
AND payment_gateways.value = #{connection.quote(pg)}
SQL
) if pg.present? }
But you need to add manually the conditions already defined in the associations
Finally, my favorite, a solution that sticks to ActiveRecord:
scope :payment_gateway, -> (pg) do
where(id: unscoped.joins(:payment_gateways).where(log_details: {value: pg})) if pg.present?
end
scope :coupon_code, -> (cc) do
where(id: unscoped.joins(:coupon_codes).where(log_details: {value: cc})) if cc.present?
end
Gotcha #1: if you use Rails < 5.2, you might need to use class methods instead of scopes.
Gotcha #2: Solution #3 might be less performant than #2, make sure to EXPLAIN ANALYZE to see the difference.

Get dynamic table alias in Rails scope

I have two scopes that are shared by the majority of my models. They have raw SQL that directly refers to the model's table name, and that doesn't play nicely with Arel:
class ApplicationRecord < ActiveRecord::Base
valid = lambda do |positive = true|
if %w[validForBegin validForEnd].all? { |c| base_class.column_names.include?(c) }
condition = "NOW() BETWEEN #{base_class.table_name}.validForBegin AND #{base_class.table_name}.validForEnd"
condition = "!(#{condition})" unless positive
where(condition)
end
end
scope :valid, valid
scope :invalid, -> { valid(false) }
end
# Sample usage
class Party < ApplicationRecord
has_one :name,
-> { valid },
class_name: 'PartyName',
foreign_key: :partyId,
has_many :expired_names,
-> { invalid },
class_name: 'PartyName',
foreign_key: :partyId,
end
Since my scope refers directly to the model's table_name, I can't join on both associations at once:
Party.joins(:name, :expired_names).first
# Produces this sequel statement
SELECT `party`.*
FROM `party`
INNER JOIN `party_name` ON `party_name`.`partyId` = `party`.`id`
AND (NOW() BETWEEN party_name.validForBegin AND party_name.validForEnd)
INNER JOIN `party_name` `expired_names_party` ON `expired_names_party`.`partyId` = `party`.`id`
AND (!(NOW() BETWEEN party_name.validForBegin AND party_name.validForEnd))
ORDER BY `party`.`id` ASC LIMIT 1
Note that both 'AND' conditions on the joins are referring to the table party_name. The second one should instead be referring to expired_names_party, the dynamically generated table alias. For more complicated Rails queries where Arel assigns an alias to EVERY table, both joins will fail.
Is it possible for my scope to use the alias assigned to it by Arel at execution time?
I created this repo to help test your situation:
https://github.com/wmavis/so_rails_arel
I believe the issue is that you are trying to use the same class for both relationships. By default, rails wants to use the name of the class for the table associated with that class. Therefore, it is using the table name party_name for both queries.
To get around this issue, I created an ExpiredName class that inherits from PartyName but tells rails to use the expired_names table:
https://github.com/wmavis/so_rails_arel/blob/master/app/models/expired_name.rb
class ExpiredName < PartyName
self.table_name = 'expired_names'
end
This seems to fix the issue in my code:
Party.joins(:name, :expired_names).to_sql
=> "SELECT \"parties\".* FROM \"parties\"
INNER JOIN \"party_names\"
ON \"party_names\".\"party_id\" = \"parties\".\"id\"
INNER JOIN \"expired_names\"
ON \"expired_names\".\"party_id\" = \"parties\".\"id\""
Let me know if it doesn't work for you and I'll try to help.

Rails 5 select from two different tables and get one result

I have 3 models, Shop, Client, Product.
A shop has many clients, and a shop has many products.
Then I have 2 extra models, one is ShopClient, that groups the shop_id and client_id. The second is ShopProduct, that groups the shop_id and product_id.
Now I have a controller that receives two params, the client_id and product_id. So I want to select all the shops (in one instance variable #shops) filtered by client_id and product_id without shop repetition. How can I do this??
I hope I was clear, thanks.
ps: I'm using Postgresql as database.
Below query will work for you.
class Shop
has_many :shop_clients
has_many :clients, through: :shop_clients
has_many :shop_products
has_many :products, through: :shop_products
end
class Client
end
class Product
end
class ShopClient
belongs_to :shop
belongs_to :client
end
class ShopProduct
belongs_to :shop
belongs_to :product
end
#shops = Shop.joins(:clients).where(clients: {id: params[:client_id]}).merge(Shop.joins(:products).where(products: {id: params[:product_id]}))
Just to riff on the answer provided by Prince Bansal. How about creating some class methods for those joins? Something like:
class Shop
has_many :shop_clients
has_many :clients, through: :shop_clients
has_many :shop_products
has_many :products, through: :shop_products
class << self
def with_clients(clients)
joins(:clients).where(clients: {id: clients})
end
def with_products(products)
joins(:products).where(products: {id: products})
end
end
end
Then you could do something like:
#shops = Shop.with_clients(params[:client_id]).with_products(params[:product_id])
By the way, I'm sure someone is going to say you should make those class methods into scopes. And you certainly can do that. I did it as class methods because that's what the Guide recommends:
Using a class method is the preferred way to accept arguments for scopes.
But, I realize some people strongly prefer the aesthetics of using scopes instead. So, whichever pleases you most.
I feel like the best way to solve this issue is to use sub-queries. I'll first collect all valid shop ids from ShopClient, followed by all valid shop ids from ShopProduct. Than feed them into the where query on Shop. This will result in one SQL query.
shop_client_ids = ShopClient.where(client_id: params[:client_id]).select(:shop_id)
shop_product_ids = ShopProduct.where(product_id: params[:product_id]).select(:shop_id)
#shops = Shop.where(id: shop_client_ids).where(id: shop_product_ids)
#=> #<ActiveRecord::Relation [#<Shop id: 1, created_at: "2018-02-14 20:22:18", updated_at: "2018-02-14 20:22:18">]>
The above query results in the SQL query below. I didn't specify a limit, but this might be added by the fact that my dummy project uses SQLite.
SELECT "shops".*
FROM "shops"
WHERE
"shops"."id" IN (
SELECT "shop_clients"."shop_id"
FROM "shop_clients"
WHERE "shop_clients"."client_id" = ?) AND
"shops"."id" IN (
SELECT "shop_products"."shop_id"
FROM "shop_products"
WHERE "shop_products"."product_id" = ?)
LIMIT ?
[["client_id", 1], ["product_id", 1], ["LIMIT", 11]]
Combining the two sub-queries in one where doesn't result in a correct response:
#shops = Shop.where(id: [shop_client_ids, shop_product_ids])
#=> #<ActiveRecord::Relation []>
Produces the query:
SELECT "shops".* FROM "shops" WHERE "shops"."id" IN (NULL, NULL) LIMIT ? [["LIMIT", 11]]
note
Keep in mind that when you run the statements one by one in the console this will normally result in 3 queries. This is due to the fact that the return value uses the #inspect method to let you see the result. This method is overridden by Rails to execute the query and display the result.
You can simulate the behavior of the normal application by suffixing the statements with ;nil. This makes sure nil is returned and the #inspect method is not called on the where chain, thus not executing the query and keeping the chain in memory.
edit
If you want to clean up the controller you might want to move these sub-queries into model methods (inspired by jvillians answer).
class Shop
# ...
def self.with_clients(*client_ids)
client_ids.flatten! # allows passing of multiple arguments or an array of arguments
where(id: ShopClient.where(client_id: client_ids).select(:shop_id))
end
# ...
end
Rails sub-query vs join
The advantage of a sub-query over a join is that using joins might end up returning the same record multiple times if you query on a attribute that is not unique. For example, say a product has an attribute product_type that is either 'physical' or 'digital'. If you want to select all shops selling a digital product you must not forget to call distinct on the chain when you're using a join, otherwise the same shop may return multiple times.
However if you'll have to query on multiple attributes in product, and you'll use multiple helpers in the model (where each helper joins(:products)). Multiple sub-queries are likely slower. (Assuming you set has_many :products, through: :shop_products.) Since Rails reduces all joins to the same association to a single one. Example: Shop.joins(:products).joins(:products) (from multiple class methods) will still end up joining the products table a single time, whereas sub-queries will not be reduced.
Below sql query possibly gonna work for you.
--
-- assuming
-- tables: shops, products, clients, shop_products, shop_clients
--
SELECT DISTINCT * FROM shops
JOIN shop_products
ON shop_products.shop_id = shops.id
JOIN shop_clients
ON shop_clients.shop_id = shops.id
WHERE shop_clients.client_id = ? AND shop_products.product_id = ?
If you'll face difficulties while creating an adequate AR expression for this sql query, let me know.
Btw, here is a mock

ActiveRecord find categories which contain at least one item

Support I have two models for items and categories, in a many-to-many relation
class Item < ActiveRecord::Base
has_and_belongs_to_many :categories
class Category < ActiveRecord::Base
has_and_belongs_to_many :items
Now I want to filter out categories which contain at least one items, what will be the best way to do this?
I would like to echo #Delba's answer and expand on it because it's correct - what #huan son is suggesting with the count column is completely unnecessary, if you have your indexes set up correctly.
I would add that you probably want to use .uniq, as it's a many-to-many you only want DISTINCT categories to come back:
Category.joins(:items).uniq
Using the joins query will let you more easily work conditions into your count of items too, giving much more flexibility. For example you might not want to count items where enabled = false:
Category.joins(:items).where(:items => { :enabled => true }).uniq
This would generate the following SQL, using inner joins which are EXTREMELY fast:
SELECT `categories`.* FROM `categories` INNER JOIN `categories_items` ON `categories_items`.`category_id` = `categories`.`id` INNER JOIN `items` ON `items`.`id` = `categories_items`.`item_id` WHERE `items`.`enabled` = 1
Good luck,
Stu
Category.joins(:items)
More details here: http://guides.rubyonrails.org/active_record_querying.html#joining-tables
please notice, what the other guys answererd is NOT performant!
the most performant solution:
better to work with a counter_cache and save the items_count in the model!
scope :with_items, where("items_count > 0")
has_and_belongs_to_many :categories, :after_add=>:update_count, :after_remove=>:update_count
def update_count(category)
category.items_count = category.items.count
category.save
end
for normal "belongs_to" relation you just write
belongs_to :parent, :counter_cache=>true
and in the parent_model you have an field items_count (items is the pluralized has_many class name)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
in a has_and_belongs_to_many relation you have to write it as your own as above
scope :has_item, where("#{table_name}.id IN (SELECT categories_items.category_id FROM categories_items")
This will return all categories which have an entry in the join table because, ostensibly, a category shouldn't have an entry there if it does not have an item. You could add a AND categories_items.item_id IS NOT NULL to the subselect condition just to be sure.
In case you're not aware, table_name is a method which returns the table name of ActiveRecord class calling it. In this case it would be "categories".

Finding records with no associated records in rails 3

class Person < ActiveRecord::Base
has_many :pets
scope :with_dog, join(:pets).where("pets.type = 'Dog'")
scope :without_pets ???????????????????????????????????
end
class Pet < ActiveRecord::Base
belongs_to :people
end
I'd like to add a scope to the Person model that returns people who have no pets. Any ideas? I feel like this is obvious, but it's escaping me at the moment.
scope :without_pets, lambda { includes(:pets).where('pets.id' => nil) }
Try something like this:
Person.joins('left outer join pets on persons.id=pets.person_id').
select('persons.*,pets.id').
where('pets.id is null')
I haven't tested it but it ought to work.
The idea is that we're performing a left outer join, so the pets fields will be null for every person that has no pets. You'll probably need to include :readonly => false in the join since ActiveRecord returns read-only objects when join() is passed a string.
Mark Westling's answer is correct. The outer join is the right way to go. An inner join (which is what the joins method generates if you pass it the name/symbol of an association and not your own SQL) will not work, as it will not include people who do not have a pet.
Here it is written as a scope:
scope :without_pets, joins("left outer join pets on pets.person_id = persons.id").where("pets.id is null")
(If that doesn't work, try replacing 'persons' with 'people' -- I'm not sure what your table name is.)
You must use a LEFT OUTER JOIN in order to find records without associated records. Here's an adapted version of a code I use:
scope :without_pets, joins('LEFT OUTER JOIN pets ON people.id = pets.person_id').group('people.id').having('count(pets.id) = 0')
Im not sure if your pet model has a person id, but maybe this attempt helps you somehow
scope :with_dog, joins(:pets).where("pets.type = 'Dog'")
scope :without_pets, joins(:pets).where("pets.person_id != persons.id")
Update: Corrected the query method name from 'join' to 'joins'.

Resources