Unique join table reference inside model scope - ruby-on-rails

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.

Related

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.

How do I query with '.includes' across more than two tables?

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.

How to write complex query in Ruby

Need advice, how to write complex query in Ruby.
Query in PHP project:
$get_trustee = db_query("SELECT t.trustee_name,t.secret_key,t.trustee_status,t.created,t.user_id,ui.image from trustees t
left join users u on u.id = t.trustees_id
left join user_info ui on ui.user_id = t.trustees_id
WHERE t.user_id='$user_id' AND trustee_status ='pending'
group by secret_key
ORDER BY t.created DESC")
My guess in Ruby:
get_trustee = Trustee.find_by_sql('SELECT t.trustee_name, t.secret_key, t.trustee_status, t.created, t.user_id, ui.image FROM trustees t
LEFT JOIN users u ON u.id = t.trustees_id
LEFT JOIN user_info ui ON ui.user_id = t.trustees_id
WHERE t.user_id = ? AND
t.trustee_status = ?
GROUP BY secret_key
ORDER BY t.created DESC',
[user_id, 'pending'])
Option 1 (Okay)
Do you mean Ruby with ActiveRecord? Are you using ActiveRecord and/or Rails? #find_by_sql is a method that exists within ActiveRecord. Also it seems like the user table isn't really needed in this query, but maybe you left something out? Either way, I'll included it in my examples. This query would work if you haven't set up your relationships right:
users_trustees = Trustee.
select('trustees.*, ui.image').
joins('LEFT OUTER JOIN users u ON u.id = trustees.trustees_id').
joins('LEFT OUTER JOIN user_info ui ON ui.user_id = t.trustees_id').
where(user_id: user_id, trustee_status: 'pending').
order('t.created DESC')
Also, be aware of a few things with this solution:
I have not found a super elegant way to get the columns from the join tables out of the ActiveRecord objects that get returned. You can access them by users_trustees.each { |u| u['image'] }
This query isn't really THAT complex and ActiveRecord relationships make it much easier to understand and maintain.
I'm assuming you're using a legacy database and that's why your columns are named this way. If I'm wrong and you created these tables for this app, then your life would be much easier (and conventional) with your primary keys being called id and your timestamps being called created_at and updated_at.
Option 2 (Better)
If you set up your ActiveRecord relationships and classes properly, then this query is much easier:
class Trustee < ActiveRecord::Base
self.primary_key = 'trustees_id' # wouldn't be needed if the column was id
has_one :user
has_one :user_info
end
class User < ActiveRecord::Base
belongs_to :trustee, foreign_key: 'trustees_id' # relationship can also go the other way
end
class UserInfo < ActiveRecord::Base
self.table_name = 'user_info'
belongs_to :trustee
end
Your "query" can now be ActiveRecord goodness if performance isn't paramount. The Ruby convention is readability first, reorganizing code later if stuff starts to scale.
Let's say you want to get a trustee's image:
trustee = Trustee.where(trustees_id: 5).first
if trustee
image = trustee.user_info.image
..
end
Or if you want to get all trustee's images:
Trustee.all.collect { |t| t.user_info.try(:image) } # using a #try in case user_info is nil
Option 3 (Best)
It seems like trustee is just a special-case user of some sort. You can use STI if you don't mind restructuring you tables to simplify even further.
This is probably outside of the scope of this question so I'll just link you to the docs on this: http://api.rubyonrails.org/classes/ActiveRecord/Base.html see "Single Table Inheritance". Also see the article that they link to from Martin Fowler (http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html)
Resources
http://guides.rubyonrails.org/association_basics.html
http://guides.rubyonrails.org/active_record_querying.html
Yes, find_by_sql will work, you can try this also:
Trustee.connection.execute('...')
or for generic queries:
ActiveRecord::Base.connection.execute('...')

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