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
Related
I have 3 tables.
product
product_attribute_mappings
product_attribute_values
Here are some rows of each table.
products
id |name
1058|shoes
product_attribute_mappings
id | product_id | product_attribute_id
438 | 1058 | 9
product_attribute_values
id | product_attribute_mapping_id | value
2001 | 438 | 18 oz
2002 | 438 | 19 oz
As you can see here,
product.id = product_attribute_mappings.product_id
product_attribute_values.product_attribute_mapping_id = product_attribute_mappings.id
I want to get all product attributes values like
product.product_attribute_values # ["18 oz", "19 oz"]
But I am not sure how I can make models with associations to get as I want.
Does anyone have any idea?
What you have is a really strange backwards variant of the Entity Attribute Value (EAV) pattern. It would make more sense if you had the normalized attribute definitions (eg volume, weight, number of doodads, etc) on one table and the entitiy, attribute and value on one table.
class Product < ApplicationRecord
has_many :product_attributes
has_many :product_attribute_types, through: :product_attributes
# eager loading scope
def self.eager_load_attributes
eager_load(product_attributes: :product_attribute_types)
end
end
# This is the normalization table that stores the definition of an attribute
# rails g model ProductAttribute name:string unit:string
class ProductAttributeType< ApplicationRecord
has_many :product_attributes
has_many :product_attribute_types, through: :product_attributes
end
# This is the actual table that defines the attributes
# rails g model ProductAttribute product:belongs_to product_attribute_type:belongs_to value:jsonb
class ProductAttribute < ApplicationRecord
belongs_to :product # the entity
belongs_to :product_attribute_type # the attribute
# just shortcuts
delegates :name, to: :product_attribute_type
delegates :unit, to: :product_attribute_type
end
This uses a JSON column as the value to alieviate one of the classical issues with EAV which is that you have to cast everything into a single (usually string) type column. JSON can store numbers (not terribly well), strings, arrays and objects.
This lets you iterate through the products and attributes with:
# eager loading avoids a n+1 query
#products = Product.eager_load_attributes.all
#products.each do |product|
product.product_attributes.each do |attr|
puts "#{attr.name}: #{attr.value}{attr.unit}"
end
end
You can change your association name from product_attribute_values to product_attribute_values_association, then define product_attribute_values as instance methods
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...
I got asked to build a website where users can make an appointment for a car testdrive.
The calendar is not limited to say, 10 days, so I cannot specify the dates beforehand. The car dealers should have the ability to block certain days or timeslots.
I came up with a Testdrive table looking something like this:
Testdrive
---------
- id
- user_id
- date
- timeslot
- client_title
- client_name
- client_firstname
- client_company
- client_street
- client_house_nr
- client_postal_code
- client_city
- client_email
- client_phone
- client_mobile
However, I'm now not sure how to model the "blocked" slots/dates thing. Was thinking of making another table like "TestdriveDate" or something, but then I'd be limiting the calendar to what's in that table... and I don't want the dealers to have to enable every day/timeslot, nor do I want to put that much data in my database. So, I guess I should have something like "BlockedDate", or "BlockedTimeSlot". In that case, however, I would have to check every date in the list on my frontend against this table.. which also doesn't feel right.
I guess the 'BlockedDate' approach would be the best way to go though? Looking for some help in modeling this so its useable for me as a developer, and for my users (the car dealers).
Do this:
#app/models/slot.rb
class Slot < ActiveRecord::Base
#columns id | day | time | created_at | updated_at
#This will be populated with all the available "slots" -- EG day 0, time 1
enum day: [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday]
enum time: [:0900, :1000, :1100, :1130, :1200, :1300, :1330, :1400]
has_many :test_drives
has_many :clients, through: :test_drives
end
#app/models/test_drive.rb
class TestDrive < ActiveRecord::Base
#columns id | client_id | slot_id | created_at | updated_at
belongs_to :client
belongs_to :slot
end
#app/models/client.rb
class Client < ActiveRecord::Base
#columns id | title | name | company | street | house_nr | postal_code | city | email | phone | mobile | created_at | updated_at
has_many :test_drives
has_many :slots, through: :test_drive
def firstname
name.split(" ").first
end
end
This might be a bit overkill but it should give you the ability to do the following:
#client = Client.create name: "x", etc etc
#slot = Slot.find_by day: "saturday", time: "1400"
#client.test_drives.create slot: #slot
You'd be able to add a validation to test_drive on whether a particular slot has been taken.
You'd also be able to add a validation to the slot model to determine which date/time combinations are permissible:
#app/models/slot.rb
class Slot < ActiveRecord::Base
...
validate :day_times
private
def day_times
permissible_times: {monday: [:0900, :1000], tuesday: [:1200]}
errors.add(:time, "Sorry, this time is unavailable on this day") unless permissible_times[day.to_sym].include? time
end
end
You can read up about enum here.
One of the main issues you have at the moment is that you've populated your TestDrives table with client_ fields. Whilst this will work, it's a faux pas - it will quickly become cumbersome and overburdened.
You'll be much better with a has_many :through association, as described above...
How about this approach.
Block
date
timeslot
TestDrive
block_id
user_id
client_title
client_name
client_firstname
client_company
client_street
client_house_nr
client_postal_code
client_city
client_email
client_phone
client_mobile
A Block model can be created either by the car dealer or by the appointment.
For example there can be a block with id=1. If there is a TestDrive that has block_id=1 then this is an appointment. If no TestDrive is found with block_id=1 then this is just a blocked slot.
So a Block has_one :test_drive and a TestDrive belongs_to :block. A TestDrive must be associated with a Block but a Block can have no TestDrives (Zero-to-many relationship)
I need some help building a table and then getting data from that table in Rails 3.
Here's the break down:
Models - 3 models involved here they are:
Thread has many participants
Participants belong to thread
Users
Activity table:
id | thread_id | participants
Example records would look something like:
1 | 300 | 3,1,5,67,13
2 | 333 | 3,12
3 | 433 | 1,12
4 | 553 | 1,12, 67
Where participants, is a list of user_ids, if there is a better way to store the user_ids please let me know. I haven't built this yet.
After I populate the activity table. I then want to be able to query along the lines of:
Select all Activity records where the participant_id of 67 is included in the participants field.
I hope the above is clear, if not please let me know. Ideas? Thoughts? Suggestions.
Thanks
While it's tempting to store multiple values in a column, it always ends up with someone getting hurt. You're better off building a join table to relate the models.
For example you could do this:
class DiscussionThread < ActiveRecord::Base
has_many :participations
has_many :participants, :through => :participations
end
class Participation < ActiveRecord::Base
belongs_to :discussion_thread
belongs_to :participant, :class_name => "User", :foreign_key => :user_id
end
class User < ActiveRecord::Base
has_many :participations
has_many :dicussion_threads, :through => :participations
end
That gives you three tables:
table: discussion_threads
columns: id
table: participations
columns: id | discussion_thread_id | user_id
table: users
columns: id
To find the threads in which a user is participating, just do:
#user.discussion_threads
And to find the users participating in a thread:
#discussion_thread.participants
Note: Thread is a reserved word in Ruby, so I've renamed it DiscussionThread
EDIT
mind showing an example of how to serialize an array of ids and then query against them?
You awaken in the middle of the night, and under the power of a strange compulsion you go to your computer and create this migration:
rails g model Abomination horror_ids:text
and model:
class Abomination < ActiveRecord::Base
serialize :horror_ids
end
You test it to make sure it can store an array:
#abomination = Abomination.create(:horror_ids=>[2,33,42])
#abomination.horror_ids # => [2,33,42]
So what? You know that behind the scenes Rails converts it to YAML, which looks like this:
---\n
- 2\n
- 33\n
- 42\n
Again compelled by that wierd urging, you wonder "How could I search for a particular id stored in this column?". Well, it's just a string, right? You know how to find that in a text field:
cthulhu = 33
Abomination.where(["horror_ids LIKE '%- ?\n%'",cthulhu]).first
With increasing dread, you realize someone might stumble across this and think it was actually a good idea. It must be destroyed! But you are unable to type rm -rf *, instead the strange force makes you consider the quirks that a future mindless follower of Cthulhu developer might need to know, such as
#abomination = Abomination.create
#abomination.horror_ids # => nil
#abomination = Abomination.create(:horror_ids=>[])
#abomination.horror_ids # => []
#abomination = Abomination.create(:horror_ids=>"any string value can go here")
#abomination.horror_ids # => "any string value can go here"
And the fact that serialized data can get corrupted when the column size is too small to accommodate it all.
You make one last ditch effort to kick out the power cord, but it is too late, the gibbering, insane consciousness that has taken control of you posts the code on StackOverflow for the whole world to see. Finally you collapse into a troubled sleep. The next day, realizing what you've perpetrated, you give up coding forever and become an accountant.
Moral
Don't do this
There seem to be a number of ways to handle a multiple foreign key association. Each way I have approached this has their draw backs, and as I am new to Rails I am convinced others have come across a similar scenario and I am probably working on something solved long ago.
My question is:
What would be an efficient way of handling a multiple index key association, while still retaining all other Rails sql modifiers (such as :include etc)?
My scenario is:
I have a table association as follows (simplified), which is used to connect people to other people via links:
People
+----+-----------+
| id | name |
+----+-----------+
| 1 | Joe |
+----+-----------+
| 2 | Sally |
+----+-----------+
| 3 | Bob |
+----+-----------+
Links
+----+-----------+---------+
| id | origin_id | rcvd_id |
+----+-----------+---------+
| 1 | 2 | 1 |
+----+-----------+---------+
| 2 | 1 | 3 |
+----+-----------+---------+
| 3 | 3 | 2 |
+----+-----------+---------+
From row 1 of the above Links table, one can see that a Person (Sally = 2) is linked to another Person (Joe = 1).
It is easy for me to find all of a Persons Links if my foreign key was "origin_id". But this would only show People originating a Link. In my scenario I need to see all links regardless if they were originated or received by a Person. If for example I were to ask for all of Sally's links (Sally = 2), the result I would want would be:
Links
+----+-----------+---------+
| id | origin_id | rcvd_id |
+----+-----------+---------+
| 1 | 2 | 1 |
+----+-----------+---------+
| 3 | 3 | 2 |
+----+-----------+---------+
Hence I have 2 index keys, both "origin_id" and "rcvd_id".
One way this could be solved is with a Method:
class Person < ActiveRecord::Base
has_many :link_origins, :class_name => "Link", :foreign_key => :origin_id, :dependent => :destroy
has_many :link_rcvds, :class_name => "Link", :foreign_key => :rcvd_id, :dependent => :destroy
def links
origin_person + rcvd_person
end
However, this is not efficient. For example this requires the entire collection to be gathered from the database and only then does the paginate method work (I am using the will_paginate gem), which defeats the point as paginate should speed up the process by limiting the number of records called. Not limit the records after the entire collection is already done.
Also, the above will not allow me to call for example, Joe.links(:first).origin_id.name. Not exactly this code but meaning I could not call the Person details on the origin_id of a selected link, as the links method does not know that origin_id is related to the People table.
So far the most workable solution seems to be the :finder_sql.
class Person < ActiveRecord::Base
has_many :links, :finder_sql => 'SELECT * FROM links WHERE (links.origin_id = #{id} or links.rcvd_id = #{id})'
This gives all links where the Person_id matches either the Links.origin_id or the Links.rcvd_id.
The down side of this option, is that using :finder_sql, rules out all the other sql modifiers since Rails
doesn't know how to parse and modify the SQL you provide. For example I would not be able to use the :include option with the :finder_sql.
So, right now I am using the :finder_sql, solution. But it seems there might be a away of getting this association done in such a way that I don't need a :finder_sql. For example, is there a way to write a custom sql string while retaining the Rails sql modifiers that Active Record supplies.
Any ideas on the above?
I did find the solution to this, however it turned out I was probably asking the wrong question. I have not found away to have multiple index keys as I asked without implementing some custom sql which breaks different rails helpers.
I guess my question still stands, but how I did resolve this was to look at the problem differently. I just created the associations as they are:
belongs_to :rcvd_person, :class_name => 'Person', :foreign_key => :rcvd_id
belongs_to :origin_person, :class_name => 'Person', :foreign_key => :origin_id
And a custom sql statement:
class Person...
has_many :links, :finder_sql => 'SELECT * FROM links WHERE origin_id = #{id} OR rcvd_id = #{id}'
end
Then I was able to manipulate the records how I wanted in my view. In case anyone else is doing something similar, I did:
<% person.links.each do |link| %>
<% if link.origin_id == person.id %>
<%= link.rcvd_person.given_name %>
<% else %>
<%= link.origin_person.given_name %>
<% end %>
<% end %>
I'm not sure you can really support an association with multiple keys in the same table because Rails won't know which key to set if you attempt to create a relationship.
However, if you just want person.links, Rails 3 provides a way that is better than :finder_sql
class Link
def self.by_person(person)
where("origin_id => :person_id OR rcvd_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
# You can include the has_many relationships if you want, or not
def links
Link.by_person(self)
end
end
This lets you do things like #person.links.limit(3) (which currently appears to be broken when using :finder_sql)