Rails order results with multiple joins to same table - ruby-on-rails

--Edit--
I wanted to simplify this question.
With this model structure:
has_one :pickup_job, class_name: 'Job', source: :job
has_one :dropoff_job, class_name: 'Job', source: :job
What I want to do is:
Package.joins(:dropoff_job, :pickup_job).order(pickup_job: {name: :desc})
This is obviously not valid, the actual syntax that should be used is:
.order('jobs.name desc')
However the way that rails joins the tables means that I cannot ensure what the table alias name will be (if anything), and this will order by dropoff_job.name instead of pickup_job.name
irb(main):005:0> Package.joins(:dropoff_job, :pickup_job).order('jobs.name desc').to_sql
=> "SELECT \"packages\".* FROM \"packages\" INNER JOIN \"jobs\" ON \"jobs\".\"id\" = \"packages\".\"dropoff_job_id\" INNER JOIN \"jobs\" \"pickup_jobs_packages\" ON \"pickup_jobs_packages\".\"id\" = \"packages\".\"pickup_job_id\" ORDER BY jobs.name desc"
Also I am not in control of how the tables are joined, so I cannot define the table alias.
--UPDATE--
I have had a play with trying to extract the alias names from the current scope using something like this:
current_scope.join_sources.select { |j| j.left.table_name == 'locations' }
But am still a little stuck and really feel like there should be a MUCH simpler solution.
--UPDATE--
pan's answer works in some scenarios but I am looking for a solution that is a bit more dynamic.

Use the concatenation of association name in plural and current table name as a table alias name in the order method:
Package.joins(:dropoff_job, :pickup_job).order('pickup_jobs_packages.name desc')

You can try something like this
Package.joins("INNER JOIN locations as dropoff_location ON dropoff_location.id = packages.dropoff_location_id INNER JOIN locations as pickup_location ON pickup_location.id = packages.pickup_location_id)
Idea is to create your own alias while joining the table

Related

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('...')

ActiveRelation where statement on child attribute

I have a has_one condition that I'm trying to access but am having a little trouble
Solicitation belongs_to :lead
Lead has_many :solicitations
My first statement grabs all solicitations for a user
#solicitations = current_user.solicitations.includes(:lead)
I can already access the attribute lead.case_type and could just cycle through the relation and put them in their places manually, but I figure their is an easier way.
What I am trying to do is something similar to
#solicitations.where("lead.case_type = ?", "Civil")
I have tried these and receive an unknown column error lead.case_type
Solicitation.all(:conditions => {:lead => {:case_type => 'Civil'}}, :joins => :lead)
The problem is that you are using lead.case_type, but (if you're following Rails' conventions) your table name is leads. This should work:
#solicitations = current_user.solicitations.includes(:lead).where("leads.case_type = ?", "Civil")
You could also use joins for that:
#solicitations = current_user.solicitations.joins(:lead).where("leads.case_type = ?", "Civil")
includes does an outer join, whereas joins does an inner join. Since you're querying the joined table an inner join would be better here.
In where you always have to use the table name (plural), but in includes and joins it depends on the relationship. In this case solicitation belongs to lead, so you have to use :lead (singular). It's a bit confusing, but I hope this clears it up for you.

Translating a (slightly complex) raw SQL query to ActiveRecord/Arel?

I have a very simple Rails app with a very simple relational database: Category has many Samples. I'd simply like to load the categories that have X number of samples.
In plain SQL I would do something like this:
SELECT
categories.*
FROM
categories
JOIN
(SELECT
category_id, COUNT(*) as sample_count
FROM
samples
GROUP BY
category_id
) AS subselect
ON
categories.id=subselect.category_id
WHERE
subselect.sample_count = X; -- where X is whatever
That works just fine, by the way, but it's not terribly Rails-like to use raw SQL. And obviously I'd like to get those categories as model instances, so:
How would I go about re-writing something like that to an ActiveRecord or Arel query? Is it even feasible, or should I go with the plain SQL? Is there perhaps an altogether simpler way of doing it?
A possible nice way would be to use counter_cache, as described on this page: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Add a column named samples_count to your Category model:
add_column :categories, :samples_count, :integer
In your Sample model update belongs_to as follows:
belongs_to :category , :counter_cache => true
You can now use the count as a condition, for example:
Category.where(:samples_count => 7)

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