Setting attributes on active record join tables - ruby-on-rails

I have a standard has_many through association in active record with an added "quantity" attribute on the join table.
When using this syntax to add an association, how do I set a value for the quantity attribute in the join table.
order.products << Product.find(params[:product_id])
The only way I have been able to accomplish this is by creating the "join object" directly like this:
order.product_orders.create(:product => Product.find(params[:product_id], :quantity => 5)
The second approach kind of bugs me because I am addressing the join directly.
Is there an alternate/better way to approach this?
Thanks!

I think your better approach is 1. You have to separete product and order item.

Related

Rails order results with multiple joins to same table

--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

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.

ActiveRecord calculating multiple averages

I have a scores table with both score and time per user. I want to calculate both averages grouped by user. I can successfully calculate one of them but not sure how to do both at once.
#scores = SpellingScore.where(:user_id => users).average(:score, :group => :user)
Will produce sql like the following
SELECT AVG(`spelling_scores`.`score`) AS average_score, user_id AS user_id
FROM `spelling_scores`
WHERE (`spelling_scores`.`user_id` IN (78767, 78772, 78775)) GROUP BY user_id
I know how to do it in SQL but can't work out the ActiveRecord way.
This is what I want to do...
SELECT AVG(`spelling_scores`.`score`) AS average_score, AVG(`spelling_scores`.`time`) AS average_time, user_id AS user_id
FROM `spelling_scores`
WHERE (`spelling_scores`.`user_id` IN (78767, 78772, 78775)) GROUP BY user_id
Cheers,
Tim
Thanks for your help Macarthy.
I ended up doing it this way
SpellingScore.select("AVG(`spelling_scores`.`score`) AS average_score,
AVG(`spelling_scores`.`time`) AS average_time, COUNT(*) AS question_count, user_id AS
user_id").where(:user_id => users).group(:user_id).includes(:user).order('users.last')
At least I've retained some ActiveRecord chaining.
Just use SQL. Forget the ActiveRecord way, SQL is better for something like this. If you want to keep your logic in your model just create a new method in your model
Wouldn't this work SpellingScore.group(:user).average(:score)
Can't test this without a schema though

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

Find all objects with broken association

I have two models in my rails app with a has many and belongs to association.
Category has many items and Item belongs to category.
These models are associated in the normal way through a category_id column in the Item model.
I'm looking for a quick way of finding all elements in the database with broken associations.
i.e. find all categories that exist with no associated items and items that exist with no associated category.
For example, if I have an item with a category_id of 7, but the category with id 7 has been deleted then this would be considered broken.
For your example, to find items with category_id's for categories which don't exist any more:
Item.where('NOT EXISTS (SELECT * FROM categories where category.id = item.category_id)')
You might want to look at this as well:
A rake task to track down missing database indexes (not foreign keys though, but indexes): https://github.com/eladmeidar/rails_indexes
A very effective way is using find_by_sql to let the database do the heavy lifting:
uncategorized_items = Item.find_by_sql("select * from items where category_id IS NULL")
Another way is a named scope:
class Item < ActiveRecord::Base
scope :uncategorized, where(:category_id => nil) # rails 3
# or...
named_scope :uncategorized, :conditions => 'category_id IS NULL'
end
These are just a couple of ideas. I assume that once you've found these broken associations you plan to fix them, right? You might want to use validates_associated in both models if it's important to you that this not happen again.
You can use find_by_sql and a left outer join to find all the items in one table but not another. Here, I use a downloads table and an image_files table (I've only included the SQL):
SELECT d.*, d.image_file_id
from downloads as d
LEFT OUTER JOIN image_files as i
ON i.id = d.image_file_id
WHERE d.image_file_id IS NULL

Resources