Ruby do-loop to break down an array/hash - ruby-on-rails

I made a self referring database using the has_many :through relationship:
**Product**
name
**Ingredient**
quantity
product_id
product_component_id
I can have an egg, carton of 12 eggs, and a flat of 16 cartons.
I am trying to write a loop that starts with a product and breaks down all the components of each product and those to the most basic state. The goal is to return an array of all the base products that go into any given product so the carton would return 12 eggs and the flat would return 192 Eggs.
I gave it a shot and this is how far I got:
def product_breakdown
results = []
ingredients.each do |ingredient|
if ingredient.product_component_id == nil
results += ingredient
else
Keep digging deeper?
end
end
return results
end
I am missing a whole concept when it comes to using the loop. If anyone has an advise on the name of the concepts that this requires, I would be very appreciative.
edit in order to be more clear I copied the relationships of the database.
class Product < ActiveRecord::Base
has_many :ingredients
has_many :product_components, :through => :ingredients
end
class Ingredient < ActiveRecord::Base
belongs_to :product
belongs_to :product_component, class_name: "Product", :foreign_key => "product_component_id"
end

I suggest using each_with_object to build the array. That way you don't even need the results variable, just return each_with_object's return value.
How do you differentiate between a unit, carton, and flat?

If I understand correctly, each ingredient has a component which can be nil, Carton, or Flat? And one carton always contains 12 units, and one flat 16 cartons? And a source, which is the type of ingredient (egg, milk, etc?)
In that case, I'd define a couple helper methods on Ingredient, an as_unit class method and a unit_quantity instance method:
def unit_quantity
case product_component_id
when nil
quantity
when CARTON_COMPONENT_ID
12 * quantity
when FLAT_COMPONENT_ID
192 * quantity
end
end
def self.as_unit ingredients
source_ids = ingredients.map(&:product_source_id).uniq
raise "Can't join different types together" if source_ids.count != 1
source_id = source_ids.first
quantity = ingredients.reduce(0) { |total, ingredient| total += ingredient.unit_quantity }
Ingredient.new quantity: quantity, product_component_id: nil, product_source_id: source_id
end
That way, you can rewrite products_breakdown to be:
def products_breakdown ingredients
ingredients.group_by(&:product_source_id).map do |_, ingredients|
Ingredient.as_unit ingredients
end
end
This should result in:
$ ingredients
#=> [<Ingredient: 3 Cartons of Egg>, <Ingredient: 2 Flats of Milk>, <17 Units of Egg>]
$ product_breakdown ingredients
#=> [<Ingredient: 53 Units of Egg>, <Ingredient: 384 Units of Milk>]
Is this at all what you were looking for? I'm not sure I fully understood your question...

Related

create nested attributes with conditions

I have three models Restocking, Product, and Size
#Product
has_many :sizes, as: :sizeable
#Size
belongs_to :sizeable, polymorphic: true
restocking.rb
class Restocking < ApplicationRecord
has_many :sizes, as: :sizeable
belongs_to :product
accepts_nested_attributes_for :sizes
def update_existing_product
product = self.product
product.update_attributes(
price: self.price,
buying_price: self.buying_price,
)
sizes = Size.where(sizeable_id: self.product_id)
self.sizes.each do |restocking_size|
sizes.each do |product_size|
if product_size.size_name == restocking_size.size_name
product_size.quantity += restocking_size.quantity
product_size.save
end
end
end
end
end
So the method update_existing_productupdate prices and quantity of existing sizes...
If a similar size_name is found it updates the existing size quantity otherwise it creates a new one...
I don't manage to correctly create new sizes...
I am supposed to use this Size.create method, but when I put it on the loop it creates the same size many times.
Size.create!(
sizeable_id: self.product_id,
sizeable_type: "Product",
size_name: restocking_size.size_name,
quantity: restocking_size.quantity,
)
Size is created many times because of how your loop is constructed.
Fix for your code can look like this:
self.sizes.each do |restocking_size|
if (existing_size = sizes.find{|s| s.size_name == restocking_size.size_name })
existing_size.tap{|s| s.quantity += restocking_size.quantity }.save!
else
# no existing, here goes create
end
end
But keep in mind, that handling this at application level can lead to race conditions, if this code happens to run at the same time when some other code updates same data.
For example:
we have 10 items of size A of item B
restoking another 5
code runs, fetches sizes, there we have 10 in quantity
at this moment someone buys one item of that size, 9 items left and is this written to db
restocking continues to run - adds 5 to 10, writes 15 to db
quantity is 15, while one item has been sold
Can be avoided by using record#with_lock{ here update happens } in every place where you update counter (but this reloads the record, can be inefficient for large volumes).

ActiveRecord group by on a join

Really been struggling trying to get a group by to work when I have to join to another table. I can get the group by to work when I don't join, but when I want to group by a column on the other table I start having problems.
Tables:
Book
id, category_id
Category
id, name
ActiveRecord schema:
class Category < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :category
end
I am trying to get a group by on a count of categories. I.E. I want to know how many books are in each category.
I have tried numerous things, here is the latest,
books = Book.joins(:category).where(:select => 'count(books.id), Category.name', :group => 'Category.name')
I am looking to get something back like
[{:name => fiction, :count => 12}, {:name => non-fiction, :count => 4}]
Any ideas?
Thanks in advance!
How about this:
Category.joins(:books).group("categories.id").count
It should return an array of key/value pairs, where the key represents the category id, and the value represents the count of books associated with that category.
If you're just after the count of books in each category, the association methods you get from the has_many association may be enough (check out the Association Basics guide). You can get the number of books that belong to a particular category using
#category.books.size
If you wanted to build the array you described, you could build it yourself with something like:
array = Categories.all.map { |cat| { name: cat.name, count: cat.books.size } }
As an extra point, if you're likely to be looking up the number of books in a category frequently, you may also want to consider using a counter cache so getting the count of books in a category doesn't require an additional trip to the database. To do that, you'd need to make the following change in your books model:
# books.rb
belongs_to :category, counter_cache: true
And create a migration to add and initialize the column to be used by the counter cache:
class AddBooksCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :books_count, :integer, default: 0, null: false
Category.all.each do |cat|
Category.reset_counters(cat.id, :books)
end
end
end
EDIT: After some experimentation, the following should give you close to what you want:
counts = Category.joins(:books).count(group: 'categories.name')
That will return a hash with the category name as keys and the counts as values. You could use .map { |k, v| { name: k, count: v } } to then get it to exactly the format you specified in your question.
I would keep an eye on something like that though -- once you have a large enough number of books, the join could slow things down somewhat. Using counter_cache will always be the most performant, and for a large enough number of books eager loading with two separate queries may also give you better performance (which was the reason eager loading using includes changed from using a joins to multiple queries in Rails 2.1).

Rails 3.2 Organizing a Report Based off of Complex Relationships

I am developing a program for a warehousing/shipping company with the following Data relationships. The skinny regarding the relationships below is that the warehouse receives raw materials(product) from various carriers(clients) and stores them until they are needed to be shipped to the manufacturing plant. When a shipment leaves the warehouse, the manufacturing facility must know which company each raw material originated from. Take a look at the following ERD.
EDIT: My relationships in text form.
shipment.rb
has_many :product_shipments, :dependent => :destroy
has_many :products, :through => :product_shipments
product_shipment.rb
belongs_to :product
belongs_to :shipment
product.rb
has_many :product_shipments
has_many :shipments, :through => :product_shipments, :dependent => :destroy
belongs_to :client
client.rb
has_many :products, :dependent => :destroy
I'm having trouble generating the queries in a manner that's formatted the way the requirements demand. The shipment report takes a date and must iterate through each client and list the products shipped to the manufacturing facility on that given date. It needs to be generated dynamically and formatted like the following.
Shipment Date: 2013-01-01
Client: 12 Name: ACME Widget Co
Product Name | Product Code | Qty Shipped
_________________________________________
nuts & bolts | gj-3423 | 25
steel sheet | 4g-2394 | 10
dc Motor | cj-c213 | 4
Client: 14 Name: Blah Blah, Inc
Product Name | Product Code | Qty Shipped
_________________________________________
spacers | gj-3423 | 15
epoxy | 4g-2394 | 10
dc Motor | 24-gj19 | 6
Client: 15 Name: Sample Co, Inc
Product Name | Product Code | Qty Shipped
_________________________________________
hot roll 48 | cg-3423 | 15
welding wir | 4g-2394 | 10
dc Motor | c2-1241 | 6
.
.
.
.
The problem is generating Queries using ActiveRecord. It's easy to grab the shipments and products for a given date for ex below. It is not easy to grab the original client that the raw material originated from, then iterate through shipments to the manufacturing facility for that client dynamically.
UPDATE: I am now able to group clients and shipments like above. Now I need to exclude clients where they DON'T have a shipment on the date specified. The below, although somewhat correct, still gives the dreaded O(n) query. Here is what I have.
#clients = Client.includes(:products => :shipments)
#clients.each do |c|
puts c.name
c.products.each do |p|
p.shipments.where("ship_date like '#{#ship_date.strftime("%Y-%m-%d")}%'").each do |s|
s.product_shipments.joins(:product).each do |ps|
puts s.bill_of_lading + " " + ps.product_id.to_s + " " + ps.product.product_name.to_s + " " + ps.product.product_code + " " +ps.qty_shipped.to_s + " " + s.ship_date.to_s
end
end
end
end
My issue is how do I organize the query to start with clients and then list products shipped on '2012-06-30. The query gets wacko from that perspective. I am unsure how to generate a query with active record when the relationship is that far removed.
UPDATE: Ok looking at the results you expect in the report, values from ProductShipment (like the quantity attribute) need to be pulled out, so product_shipments must be included in the nested association we're eager loading, otherwise ProductShipments aren't instantiated, it only serves as a join table.
Therefore instead of Client.includes(:products => shipments)... you want :
#clients = Client.includes(:products => {:product_shipments => :shipment}).
where("shipments.ship='#{ship_date}'")
Now I don't fully understand your domain model, but when there's a lot of nested associations, I like to spot the ones which hold the most information in a one to one relationship, because they can be seen as center piece. In this case product and shipment can both be understood as extensions of the "master model" product_shipment.
Thus you can write (respectfully to Demeter's law) :
class ProductShipment < AR
def report_details
s = shipment; p = product
"#{s.bill_of_lading} #{p.id} #{p.name} #{p.code} #{quantity} #{s.shipped_on}"
end
end
Here comes the tricky part: as it is written :products => {:product_shipments => :shipment} Rails understands
product_shipments.shipment but not product_shipment.products
The later would actually trigger a db call... (which we're trying to avoid). Thankfully Rails has another trick in it's pocket :
class ProductShipment < ActiveRecord::Base
belongs_to :product, inverse_of: :product_shipments
end
class Product < ActiveRecord::Base
has_many :product_shipments, inverse_of: :product
end
Having insured the mirroring of associations you can now fetch product_shipments through products and get your report with no O(n) calls on the DB :
#client.map {|c| c.products.map {|p| p.product_shipments} }.flatten.each do |ps|
puts ps.details
end
UPDATE END
You must eager load the associated models or you get in the infamous O(n) query.
Shipment.where(:date => someday).includes({:product => :client}).each do |shipmt|
puts shipmt.date
shipmt.product.group_by(&:client).each do |client, products|
puts client
products.each do |product|
puts product.details
end
end
end
BTW rails does the join directly from shipment to product assuming you have a has_many :through or has_and_belongs_to_many association here, so no need to use the join table (aka product_shipment)
Here's how I did it. Maybe not the best way to do it but it's close. Thanks for #charlysisto for sending me in the right direction with easy loading the relationships. I still get a O(n) on the join query for the lookup table, but could be worse. Any refinements please comment so I can improve the answer.
#clients = Client.order("clients.id ASC").includes(:products => :shipments).where("shipments.ship_date like '#{ship_date}'")
#clients.each do |c|
puts c.name
c.products.each do |p|
p.shipments.each do |s|
s.product_shipments.joins(:product).each do |ps|
puts shipment and products stuff
end
end
end

Adding related Ruby on Rails activeRecord objects to an array in a Controller

I am trying to add objects to an array in Ruby on Rails in a :helper_method in the application controller. In this example, other_roommates comes back as an array with 2 objects in it, of type house_tenant, which is exactly what I would expect.
#other_roommates = Array.new
#other_roommates = current_house.house_tenant.where("active = '1' AND person_id != ?", current_user.id)
What I want to do is make an array with the related object Person from each of those HouseTenants, but when I do this I get exactly the same array as above:
#other_roommates = Array.new
joins = current_house.house_tenant.where("active = '1' AND person_id != ?", current_user.id)
joins.each do |join|
#other_roommates << join.person
end
I would expect in the above case that the array would have the 2 Person objects that are related to the house_tenant objects in it. I can get to those Person objects in the view by enumerating through the other_roommate array and calling .person on each object there, but it doesn't make sense that it doesn't work here in the Application Controller. To further confuse me, I tried 2 more tests:
for x in 0..joins.count - 1
#other_roommates << joins[x]
end
and
for x in 0..joins.count - 1
#other_roommates << x
end
When I output the contents of the array, both examples give me the exact same thing "0, 1", or the value of x. Here's the loop that's in the view:
<% other_roommates.each do |roommate| %>
<%= roommate %>
<% end %>
For what it's worth, all this is running on a Heroku Cedar stack. I know this is probably a basic issue, but I have no idea what the heck I'm doing wrong. Any help is much appreciated!
EDIT: here's the relationships from the model classes. I can traverse the objects in exactly the same way if I do it in a view instead of in the ApplicationController, just for some reason here it doesn't work.
class HouseTenant < ActiveRecord::Base
belongs_to :house
belongs_to :person
end
class House < ActiveRecord::Base
has_many :house_tenant
end
class Person < ActiveRecord::Base
has_many :house_tenant
end
The schema for house_tenant has 'house_id' and 'person_id', and the relationships work just fine in the console as well.

How to sort by associated model within given value?

I have model Item and model Stats.
Item
has_many :stats
Stat
belongs_to :items
In the model (e.g. mysql table) Stat there is 3 fields:
rating
skin_id
item_id
So for Stat, it could be, like:
#item.stats => Array of stats for records with item_id = 1, with a differer skin_id
I need to sort all items, for a given skin_id by 'rating'.
Something like:
#items = Item.all.order('stats[currtnt_skin.id] DESC') (of course it doesn't work)
In other words i need to sort within array of:
#stats = #items.stats[current_skin.id]
#items.order (... by #stats ...)
How it could be done?
Firstly I'm presuming by belongs_to :items you mean belongs_to :item (singular) given the presence of the item_id foreign key.
Secondly, to solve your specific query you can use:
Stat.where(:skin_id => skin_id).joins(:item).order("items.rating DESC")
However, if skin_id refers to another model - i.e. Stat belongs_to :skin and Skin has_many :stats then it may make more sense to start from there:
skin = Skin.find(1)
stats = skin.stats.order("rating DESC").includes(:item)
To get the items then just loop through them:
stats = skin.stats.order("rating DESC").includes(:item)
stats.each do |stat|
stat.item
end
F
#items = Item.join(:stats).order('skin_id DESC')
I believe, though I might be mistaken that joining the table will do so on the association you've defined.
in rails 3 it will be something like:
Item.includes("stats").order("stats.skin_id desc")
Have you tried this ?
Item.includes("stats").where('stats.skin_id = ?', 1).order("stats.rating desc")

Resources