Alternative to a union with ActiveRecord - sql

I think I want to do a union in Rails, but according to this post rails union hack, how to pull two different queries together unions aren't natively supported in Rails. I'm wondering if there is a better way to approach this problem.
I have table of items, each item has many prices, but I only want to join one price to each item.
To determine the proper price for an item I have two additional foreign keys in the price model: category_id and discount_id. Each could independently declare a price for an item.
Ex.
Item + Category = Price1 and Item + Discount = Price 2
If discount_id matches a passed id I want to exclude the price results FOR THAT ITEM ONLY that match Item + Category. Also I'm trying not to loose lazy loading.
Hopefully the problem is clear! If not I'll try to clarify more, thanks in advance.

Your models would start off looking something like this:
class Price < ActiveRecord::Base
belongs_to :item
belongs_to :category
belongs_to :discount
scope :category, where("prices.category_id IS NOT NULL")
scope :discount, where("prices.discount_id IS NOT NULL")
end
class Item < ActiveRecord::Base
has_many :prices
end
class Category < ActiveRecord::Base
has_many :prices
end
class Discount < ActiveRecord::Base
has_many :prices
end
One way of doing this is to add a class method to Price that encapsulates this logic:
class Price < ActiveRecord::Base
def self.used
discount_items_sql = self.discount.select("prices.item_id").to_sql
where("prices.discount_id IS NOT NULL OR prices.item_id NOT IN (#{discount_items_sql})")
end
end
This is effectively the same as this query:
SELECT * FROM prices
WHERE prices.discount_id IS NOT NULL -- the discount_id is present on this record,
OR prices.item_id NOT IN ( -- or no discount_id is present for this item
SELECT item_id FROM prices WHERE discount_id IS NOT NULL)
You can add these helper methods on your Item model for simplicity:
class Item < ActiveRecord::Base
def category_price
prices.category.first
end
def discount_price
prices.discount.first
end
def used_price
prices.used.first
end
end
Now you can easily get each individual 'type' of price for a single item (will be nil for prices that aren't available):
item.category_price
item.discount_price
item.used_price

Related

Rails: query children and grandchildren, run method on child, SQL SUM on grandchildren -- resulting in array?

Models:
class Category < ApplicationRecord
has_many :inventories
has_many :stocks, through: :inventories
end
class Inventory < ApplicationRecord
belongs_to :category
has_many :stocks
end
class Stock < ApplicationRecord
belongs_to :inventory
end
Goal:
Achieve an efficient ActiveRecord query that builds an array like this:
[
{ name: "Supplies", count: 10.00, total_value: 40.00 },
{ name: "Materials", count: 25.00, total_value: 30.00 }
]
name -> just a regular attribute in Inventory model
count -> a SQL SUM on the :count column in stocks table
total_value -> from a method in the Inventory model that does some math
This could be a total fantasy but I have a large dataset so am trying to make this hyper efficient. Any ideas?
Edit to answer question:
total_value is a method on Inventory that then calls a sum of a method on Stock:
def total_value
stocks.map do |stock|
stock.total_cost
end.sum
end
total_cost is a method on Stock:
def total_cost
cost_per_unit * count
end
Here you go: query = Inventory.group(:id, :name).select(:id, :name).left_joins(:stocks).select("SUM(stocks.count) AS count").select("SUM(stocks.cost_per_unit * stocks.count) AS total_value")
query.as_json gives what you're looking for.
You can also access the data via find_each: query.find_each { |record| puts "record #{record.name} has a total value of #{record.total_value}" }
If you want to avoid duplicating the logic of total_value in SQL, you have to load stocks records, which considerably slows down the computation if there are many:
Upgrade the model
class Inventory < ApplicationRecord
def stocks_count
stocks.sum(&:count)
end
def total_value
stocks.sum(&:total_cost)
end
end
And the query
Inventory.preload(:stocks).map do |inventory|
{
name: inventory.name,
count: inventory.stocks_count,
total_value: inventory.total_value
}
end
If you want to optimize your query to the max, you may consider caching 2 columns total_value and stocks_count on the inventories table. You would update them everytime one of its stocks changes (creation, deletion, update). It's harder to maintain but that's the fastest option.

Rails multiple joins condition query for exact tags with has many through relationships

I have a problem with filtering products by exact tags as my current query does not return exact matches and I can't seem to get the right query conditions.
Example
Area = ["small","big"] ,Surface = ["smooth","rough"]
Product A has only ["small","smooth","rough"] as tags
If I filter products using ["small","big","smooth","rough"] as the tags, I get product A in my search results but ideally, it should not return any search results.
I have three models, Product,Area and Surface. Area & Surface are linked to Product by a has_many through relationship.
class Product < ActiveRecord::Base
has_many :product_areas
has_many :areas, :through => :product_areas
has_many :product_surfaces
has_many :surfaces, :through => :product_surfaces
class Area < ActiveRecord::Base
#Surface model looks exactly the same as Area model
has_many :product_areas,dependent: :destroy
has_many :products, :through => :product_areas
My Query
area_ids = params[:area_ids]
surface_ids = params[:surface_ids]
#products = Product.where(nil)
#products = #products.joins(:areas).where('areas.id' => area_ids).group('products.id').having("count(areas.id) >= ?",area_ids.count) unless area_ids.blank?
#products = #products.joins(:surfaces).where('surfaces.id' => surface_ids).group('products.id').having("count(surfaces.id) >= ?",surface_ids.count) unless surface_ids.blank?
I solved this problem just now with this solution.
First I used the names of the models for Area & Surface as their unique identifer as they can have conflicting ids and added them to an array.
Next I looped through the products and created an array of the name identifers and compared the two arrays to check if they intersect. Intersection would mean that the search filters were a correct match and we add the product ID to a third array which stores all the product_ids before doing a query to get the products with those product ids.
#area = Area.all
area_ids = params[:area_ids]
#uniq_names = #area.where(id: area_ids).collect { |m| m.name }
#products.each do |product|
#names = product.areas.map { |m| m.name }
# if intersect, then we add them to filtered product
if (#uniq_names - #names).empty?
product_ids << product.id
end
end
#products = Product.where(id: product_ids)

Resume of product query

I have the following schema table:
I have three activerecord models with their associations. I am struggling with a query which will show the following information for each product:
Product Name, Money Total, Quantity Sold Total
It should also take account on the status of the order that the product_line are associated with, which it should be equal to "successful".
I also want a second one query which it will show the above but it will have restriction based on the month (based on the orders.created_at column). For example if I want the sales for January of this product.
Product Name, Total Money so far, Quantity total, Month
I managed to create something but I think it isn't very optimized and I used ruby's group_by which it is doing many additional queries on the view. I would appreciate how you usually start thinking about creating a query like that.
Update
I think I almost managed to solve the first query and it is the following:
products = Product.joins(:product_lines).select("products.name, SUM(product_lines.quantity) as sum_amount, SUM(product_lines.quantity*products.price) as money_total"),group("products.id")
I tried to split each columns separately and find out how I could calculate it. I haven't take into account the order status though.
The associations are the following:
ProbudtLine
class ProductLine < ActiveRecord::Base
belongs_to :order
belongs_to :cart
belongs_to :product
end
Product
class Product < ActiveRecord::Base
has_many :product_lines
end
Order
class Order < ActiveRecord::Base
has_many :product_lines, dependent: :destroy
end
I finally did it.
First query:
#best_products_so_far = Product.joins(product_lines: :order)
.select("products.*, SUM(product_lines.quantity) as amount_total, SUM(product_lines.quantity*products.price) as money_total")
.where("orders.status = 'successful'")
.group("products.id")
Second query:
#best_products_this_month = Product.joins(product_lines: :order)
.select("products.*, SUM(product_lines.quantity) as amount_total, SUM(product_lines.quantity*products.price) as money_total")
.where("orders.status = 'successful'")
.where("extract(month from orders.completed_at) = ?", Date.today.strftime("%m"))
.group("products.id")

ActiveRecord : find associated records with ALL conditions

I am trying to perform an activerecord query that essentially does the below.
I have 3 models in a many-to-many relationship
class Item < ActiveRecord::Base
has_many :item_modifiers, dependent: :destroy
has_many :modifiers, through: :item_modifiers
end
class ItemModifier < ActiveRecord::Base
belongs_to :item
belongs_to :modifier
end
class Modifier < ActiveRecord::Base
has_many :item_modifiers
has_many :items, through: :item_modifiers
end
Now I want to find all items that have modifiers with IDs 1 and 2
I have tried several things like:
Item.includes(:modifiers).where(modifiers: {id: 1}).where(modifiers: {id: 2})
This fails because it searches for modifiers where ID = 1 AND ID = 2 which is always false.
This also doesn't work
Item.includes(:modifiers).where(modifiers: {id: [1, 2]})
Because this does an IN (1, 2) query so it returns items with modifiers of either 1 or 2. I want items that have any modifiers as long as they have AT LEAST 1 modifier with ID 1 and AT LEAST 1 modifier with ID 2
I seem to be missing something quite simple but I just can't get my head around it.
Thanks in advance.
It could like:
Item.joins(:item_modifiers).where("item_modifiers.modifier_id=1 OR
item_modifiers.modifier_id=2").group("items.id").having("COUNT(item_modifiers.id)=2")
If write in plain SQL, it could be:
SELECT I.*, COUNT(IM.id) FROM items as I INNER JOIN item_modifiers AS IM on I.id=IM.item_id
WHERE IM.modifier_id=1 OR IM.modifier_id=2 GROUP BY I.id HAVING COUNT(IM.id)=2
It will get all the items with its modifers' id include 1 and 2. Maybe in different DB, the statement need a slight change.

Can I push this rails calculation into the database?

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
;