I'll be short and to the point. I am trying to join three tables together to get a total of invoiced hours times the invoiced rate from all non-archived clients. This is what i currently have in my controller:
#total_due = Client.find(:all, :joins => [:invoices, :invoice_line_items], :select => "SUM(invoice_line_items.hours * invoice_line_items.rate) as total", :group => "clients.archive, invoices.paid_status HAVING clients.archive = false AND invoices.paid_status = false")
My models look like this:
# /models/clients.rb
class Client < ActiveRecord::Base
has_many :invoices
has_many :invoice_line_items, :through => :invoices
end
# /models/invoices.rb
class Client < ActiveRecord::Base
belongs_to :client
has_many :invoice_line_items
end
# /models/invoice_line_items.rb
class Client < ActiveRecord::Base
belongs_to :invoice
end
So, as you can see from the code in my controller i am trying to get the total of all hours times rate for all invoices related to non-archived clients, and they have not paid yet. The above code in my controller is duplicating data somewhere because my number is a lot higher than it should be.
The query i am trying to build is this:
SELECT SUM(invoice_line_items.hours * invoice_line_items.rate) AS total FROM clients INNER JOIN invoices ON clients.id = invoices.client_id INNER JOIN invoice_line_items ON invoices.id = invoice_line_items.invoice_id GROUP BY clients.archive, invoices.paid_status HAVING clients.archive = false AND invoices.paid_status = false
Can someone explain or figure out why i am getting duplicated data in my rails query? I have a feeling it is just joining the tables differently than i am expecting. I am still fairly new to rails as well. Thanks!
UPDATE:
This is the query that rails is generating:
select sum(invoice_line_items.hours * invoice_line_items.rate) as total
from clients
inner join invoices on invoices.client_id = clients.id
inner join invoices invoices_clients_join on invoices_clients_join.client_id = clients.id
inner join invoice_line_items on invoice_line_items.invoice_id = invoices_clients_join.id
group by clients.archive, invoices.paid_status
having clients.archive = false and invoices.paid_status = false
Ok, i was able to get rails to generate the query i needed by just removing the invoices join in my controller. I guess that combined with the invoice_line_items join and the :through relationship was making creating many joins.
My controller now looks like this:
#total_due = Client.find(:all, :joins => :invoice_line_items, :select => "SUM(invoice_line_items.hours * invoice_line_items.rate) as total", :group => "clients.archive, invoices.paid_status HAVING clients.archive = false AND invoices.paid_status = false")
Related
I have the following code my Track.rb model. Is there any way to write this using more Rails syntax? Im using Rails 3.2
#track = Track.find(7)
Submission.joins("LEFT JOIN missions ON missions.id = submissions.mission_id")
.joins("LEFT JOIN tracks ON tracks.id = missions.track_id")
.where("missions.track_id = ?", track.id)
Models:
Track.rb
has_many :missions
Mission.rb
belongs_to :track
has_many :submissions
Submission.rb
belongs_to :mission
First of all, do you really need left join for missions? You filter by missions.track_id, so you do not need submissions without missions. Inner join would be more appropriate in this case.
Next, why do you need to join tracks - you do not use this table in the next sql.
With these thoughts, you could rewrite your code as:
Submission.joins(:mission).where(missions: { track_id: track.id })
I have the following models in my app:
class Company < ActiveRecord::Base
has_many :gallery_cards, dependent: :destroy
has_many :photos, through: :gallery_cards
has_many :direct_photos, class_name: 'Photo'
end
class Photo < ActiveRecord::Base
belongs_to :gallery_card
belongs_to :company
end
class GalleryCard < ActiveRecord::Base
belongs_to :company
has_many :photos
end
As you can see, Company has_many :photos, through: :gallery_cards and also has_many :photos. Photo has both a gallery_card_id and a company_id column.
What I want to be able to do is write a query like #company.photos that returns an ActiveRecord::Relation of all the company's photos. In my Company model, I currently have the method below, but that returns an array or ActiveRecord objects, rather than a relation.
def all_photos
photos + direct_photos
end
I've tried using the .merge() method (see below), but that returns an empty relation. I think the reason is because the conditions that are used to select #company.photos and #company.direct_photos are different. This SO post explains it in more detail.
#company = Company.find(params[:id])
photos = #company.photos
direct_photos = #company.direct_photos
direct_photos.merge(photos) = []
photos.merge(direct_photos) = []
I've also tried numerous combinations of .joins and .includes without success.
this might be a candidate for a raw SQL query, but my SQL skills are rather basic.
For what it's worth, I revisited this and came up (with help) another query that grabs everything in one shot, rather than building an array of ids for a second query. This also includes the other join tables:
Photo.joins("
LEFT OUTER JOIN companies ON photos.company_id = #{id}
LEFT OUTER JOIN gallery_cards ON gallery_cards.id = photos.gallery_card_id
LEFT OUTER JOIN quote_cards ON quote_cards.id = photos.quote_card_id
LEFT OUTER JOIN team_cards ON team_cards.id = photos.team_card_id
LEFT OUTER JOIN who_cards ON who_cards.id = photos.who_card_id
LEFT OUTER JOIN wild_cards ON wild_cards.id = photos.wild_card_id"
).where("photos.company_id = #{id}
OR gallery_cards.company_id = #{id}
OR quote_cards.company_id = #{id}
OR team_cards.company_id = #{id}
OR who_cards.company_id = #{id}
OR wild_cards.company_id = #{id}").uniq
ActiveRecord's merge returns the intersection not the union of the two queries – counterintuitively IMO.
To find the union, you need to use OR, for which ActiveRecord has poor built-in support. So I think you're correct that its best to write the conditions in SQL:
def all_photos
Photo.joins("LEFT OUTER JOIN gallery_cards ON gallery_cards.id = photos.gallery_card_id")
.where("photos.company_id = :id OR gallery_cards.company_id = :id", id: id)
end
ETA The query associates the gallery_cards to photos with a LEFT OUTER JOIN, which preserves those photo rows without associated gallery card rows. You can then query based on either photos columns or on associated gallery_cards columns – in this case, company_id from either table.
You can leverage ActiveRecord scope chaining to join and query from additional tables:
def all_photos
Photo.joins("LEFT OUTER JOIN gallery_cards ON gallery_cards.id = photos.gallery_card_id")
.joins("LEFT OUTER JOIN quote_cards ON quote_cards.id = photos.quote_card_id")
.where("photos.company_id = :id OR gallery_cards.company_id = :id OR quote_cards.company_id = :id", id: id)
end
I have the following model
class Recipe < ActiveRecord::Base
has_many :recipe_allergens
has_many :allergens, through: :recipe_allergens
end
I'm trying to find all the recipes that do not have a given set of allergens, so I tried joins(:allergens).where.not(allergens: { id: allergens }).
Unfortunately, this does not account for the null case, where a Recipe may not have any associated Allergen.
I thought the LEFT OUTER JOIN that includes does would handle this.
How are you supposed to write this query?
EDIT:
I got this working with the following, though it seems really gross:
joins("LEFT OUTER JOIN recipe_allergens ON recipe_allergens.recipe_id = recipes.id")
.joins("LEFT OUTER JOIN allergens ON allergens.id = recipe_allergens.allergen_id")
.where(
Allergen.arel_table[:id].not_in(allergen_ids)
.or(Allergen.arel_table[:id].eq(nil))
)
Please tell me there's a better way!
I would do it this way:
allergenes = Allergene.where #....
allergene_ids = allergenes.pluck(:id)
bad_recipe_ids = RecipeAllergenes.where(allergene_id: allergene_ids)
.pluck(:recipe_id)
recipes = Recipe.where('id NOT IN ?', bad_recipe_ids)
I'm trying to increase my app's efficiency by doing work in the database rather than in the app layer, and I'm wondering if I can move this calculation into the database.
Models:
class Offer < ActiveRecord::Base
has_many :lines
has_many :items, :through => :lines
end
class Line < ActiveRecord::Base
belongs_to :offer
belongs_to :item
# also has a 'quantity' attribute (integer)
end
class Item < ActiveRecord::Base
has_many :lines
has_many :offers, :through => :lines
# also has a 'price' attribute (decimal)
end
What I want to do is calculate the price of an offer. Currently I have a price method in the Offer class:
def price
self.lines.inject(0) do |total, line|
total + line.quantity * line.item.price
end
end
I suspect it may be possible to do a Offer.sum calculation instead that would get the answer directly from the DB rather than looping through the records, but the Calculations section of the ActiveRecord query guide doesn't have enough detail to help me out. Anybody?
Thanks!
You're correct that you can do this with sum. Something like this:
class Offer < ActiveRecord::Base
# ...
def price
self.lines.sum 'lines.quantity * items.price', :joins => :item
end
end
When you call e.g. Offer.find( some_id ).price the above will construct a query something like this:
SELECT SUM( lines.quantity * items.price ) AS total
FROM lines
INNER JOIN items ON items.id = lines.item_id
WHERE lines.offer_id = <some_id>
;
Sometimes you're better off with SQL.
SELECT SUM( lines.quantity * items.price ) AS total
FROM offers
INNER JOIN lines ON offers.id = lines.offer_id
INNER JOIN items ON items.id = lines.item_id
WHERE offers.id = 1
;
I have three models: Products, Placements, Collections
I'm trying to write a name scope that only chooses products NOT in a certain collection.
products has_many :collections, :through => :placements
collections has_many :products, :through => :placements
I got about this far:
scope :not_in_front, joins(:collections).where('collections.id IS NOT ?', 4)
But that generated the opposite of what I expected in the query:
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "placements" ON "products"."id" = "placements"."product_id" WHERE "placements"."collection_id" = 4
Any idea how to write this to only select the products not in that particular collection?
Instead of collections.id IS NOT 4 try collections.id != 4
The named scope was getting too ugly, so I went with this. Not sure it's the best way, but, it works...
def self.not_on_top_shelf
top_shelf = Collection.find_by_handle('top-shelf')
products = Product.find(:all, :order => "factor_score DESC")
not_on_top_shelf = products.map {|p| p unless p.collections.include?(top_shelf)}
not_on_top_shelf.compact #some products may not be in a collection
end