Rails - association issue for complex models - ruby-on-rails

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

Related

How to use Rails 5's ActiveRecord attributes to provide a virtual column

I want to add virtual columns to some of my models, but to have their values returned by ActiveRecord statements like Product.first, so that I can use statements like Product.first.to_json to output the product, with the virtual columns, on an API request.
The values of the columns depend on other model attributes. I don't want these columns persisted to the database.
I tried this:
class Product < ApplicationRecord
def total
price + tax
end
end
but Product.first did not include the total.
class Product < ApplicationRecord
attribute :total, :decimal, default: -> { 0.0 }
end
adds a total: 0.0 to the returned object, but
class Product < ApplicationRecord
attribute :total, :decimal, default: -> { price + tax }
end
fails with messages such as
#<NameError: undefined local variable or method `price' for #<Class:0x0000557b51c2c960>>
and
class Product < ApplicationRecord
attribute :total, :decimal, default: -> { 0.0 }
def total
price + tax
end
end
still returns total: 0.0.
I'm not even sure if attribute is the right way to do this, as the docs seem to imply that it binds to a column.
To sum up:
the products table should not contain a total column.
accessing Product through ActiveRecord should return a Product object that includes a total key with a computed value based on other attributes of the model.
Is this even possible?
I really don't want to have to replace every to_json call with a lot of code manually inserting these virtual columns…
You can use methods option
class Product < ApplicationRecord
def total
price + tax
end
end
Product.first.to_json(methods: :total)
Override as_json in your model to include your method.
This won't include total in your retrieved Product object, but it will include it when calling .to_json on the object.
class Product < ApplicationRecord
attribute :total, :decimal, default: -> { 0.0 }
def total
price + tax
end
def as_json(options = {})
super(methods: [:total])
end
end
A virtual/generated column (assuming MySQL/MariaDB) in your database would solve what you need. Because it is generated from data from other columns, you can't write to it and it is only updated during read operations. There is the option to persist the data, but that's not the question here.
In my example, I want to add a virtual column "age" to my People database that is the difference between person.birthday and curdate().
I generate the column:
rails generate migration AddAgeToPeople age:virtual
Then I edit the migration file so that add_column :people, :age, :virtual
becomes
class AddAgeToPeople < ActiveRecord::Migration[5.2]
def change
add_column :people, :age, :int, as: "timestampdiff(year, birthday, curdate())"
end
end
The end result would be SQL that looks like:
ALTER TABLE people ADD COLUMN age GENERATED ALWAYS AS (timestampdiff(year, birthday, curdate()));
| Field | Type | Null | Key | Default | Extra |
| age | int(11) | YES | | NULL | VIRTUAL GENERATED |
The end result is an attribute in the model that I can interact with normally (read only though)

Count items in arrays cross 100's of thousands of records

I have a Rails app with a Postgres database that has an Artists table with a jsonb genres column.
There are hundreds of thousands of rows.
Each genre column in the row has an array like ["rock", "indie", "seen live", "alternative", "indie rock"] with different genres.
What I want to do is output a count of each genre in JSON across all the rows.
Something like: {"rock": 532, "power metal": 328, "indie": 862}
Is there a way to efficiently do that?
Update...here's what I've got at the moment...
genres = Artist.all.pluck(:genres).flatten.delete_if &:empty?
output = Hash[genres.group_by {|x| x}.map {|k,v| [k,v.count]}]
final = output.sort_by{|k,v| v}.to_h
Output is a hash instead of JSON, which is fine.
But already feels pretty slow, so I'm wondering if there's a better way to do it.
This is an extremely trivial task if you just use a decent relational db design:
class Artist < ApplicationRecord
has_many :artist_genres
has_many :genres, through: :artist_genres
end
class Genre < ApplicationRecord
has_many :artist_genres
has_many :artists, through: :artist_genres
end
class ArtistGenre < ApplicationRecord
belongs_to :artist
belongs_to :genre
end
You could then get the result by:
class Genre < ApplicationRecord
has_many :artist_genres
has_many :genres, through: :artist_genres
# This will instanciate a record for each row just like your average scope
# and return a ActiveRecord::Relation object.
def self.with_artist_counts
self.joins(:artist_genres)
.select('genres.name, COUNT(artist_genres.id) AS artists_count')
.group(:id)
end
# This pulls the columns as raw sql results and creates a hash with the genre
# name as keys
def self.pluck_artist_counts
self.connection.select_all(with_artist_counts.to_sql).inject({}) do |hash, row|
hash.merge(row["name"] => row["artists_count"])
end
end
end
On re-reading your question you state that the column IS a JSONb type. So the answer below will not work since you need to first get the array from the jsonb column. This should work better:
output = Artist.connection.select_all('select genre, count (genre) from (select id, JSONB_ARRAY_ELEMENTS(genres) as genre from artists) as foo group by genre;')
=> #<ActiveRecord::Result:0x00007f8ef20df448 #columns=["genre", "count"], #rows=[["\"rock\"", 5], ["\"blues\"", 5], ["\"seen live\"", 3], ["\"alternative\"", 3]], #hash_rows=nil, #column_types={"genre"=>#<ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb:0x00007f8eeef5d500 #precision=nil, #scale=nil, #limit=nil>, "count"=>#<ActiveModel::Type::Integer:0x00007f8eeeb4c060 #precision=nil, #scale=nil, #limit=nil, #range=-2147483648...2147483648>}>
output.rows.to_h
=> {"\"rock\""=>5, "\"blues\""=>5, "\"seen live\""=>3, "\"alternative\""=>3}
As mentioned in the comments, if you can change the DB to normalize it, go for it. An anonymous array in a jsonb column is just going to be painful going forward. If you need to use this answer I would at least think about adding a view to the DB so that you can get the genre count as a table that has a corresponding model in rails (that you can just create in your model definitions).
Original answer when I thought your column was a regular array column type in Postgres.
Here is a SQL way to do it in Rails:
genre_count = Artist.connection.select_all('SELECT
UNNEST(genres),
COUNT (UNNEST(genres))
FROM
artists
GROUP BY
UNNEST(genres);')
You can then use the method of your choice to turn a much smaller dataset into JSON.
I am not familiar enough with UNNEST know why I can't alias it like any other column to make it prettier. But it works.
http://sqlfiddle.com/#!15/30597/21/0

Ruby do-loop to break down an array/hash

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

Rails appointment website - block certain timeslots/days

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)

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

Resources