I have a parent ProductCategory and child Product. For example:
ProductCategory --- Product
Drill --- DeWalt DWD 112
--- Black & Decker 5 C
Bicycle --- Motobecane Turino ELITE Disc Brake
--- Shimano Aluminum
For a given ProductCategory, there are a set of attributes that all Products should be comparable with each other on (i.e., have data on). However, this set of attributes is likely to vary between ProductCategories
For example, for the ProductCategory of Drill, the attributes might be Voltage, Amps, Corded vs Cordless. Every Product Drill needs to have such information. However, for the ProductCategory of Bicycle, the attributes should be Size, Road vs Mountain, and every Product bicycle needs to have this information. (Sorry I don't know anything about either drills or bikes... why I picked this was stupid)
I'm trying to design the DB such that for a given Product, the attributes are something that I can easily search. For example, ideally I can run this command:
drills = Product.where(product_category_id:1)
drills.where("voltage >= ?", 5)
-> returns the individual drills, which may include DeWalt but not Black & Decker
This seems to present an interesting trade-off... because then I'd have to have Product have columns for every attribute for every ProductCategory, even those that aren't relevant to it. For example:
# Product columns
:voltage, :integer #for Drill
:amps, :integer #for Drill
:corded, :boolean #for Drill
:size, :integer #for Bicycle
:mountain, :boolean #for Bicycle
...
This doesn't seem sustainable... you can see very quickly that for just a few ProductCategories there will soon be an infinite number of Product columns!
At the other end of the spectrum, I thought about having defining attributes required of each Product in the parent ProductCategory, and then requesting these attributes/storing them on Product as a stringified data:
# ProductCategory has a column...
:required_attributes, :text
ProductCategory.where(name:"Drill").first.required_attributes
-> "voltage,amps,corded"
ProductCategory.where(name:"Bicycle").first.required_attributes
-> "size,mountain"
# Product has a column...
:attribute_data, :text
Product.where(name:"DeWalt").first.attribute_data
-> "{'voltage':5,'amps':5,'corded':5}"
With the design above, I could create a front end that enforced that, upon Product creation, one has to provide information for each required_attributes after it's been split based on commas. But of course, this makes searching much less efficient, at least I THINK it does... so this is my question. How can I efficiently search stringified data? If I'm searching for all Drills with at least 5 volts, so complete the below.
drills = ProductCategory.where(name:"Drill")
drills.where("attribute_data ...")
The simplest solution is just to use a JSON or HSTORE datatype column on products to store the specifications.
But if you want to have a bit more control and validations per specification you can use a design with a join table:
class Product
has_many :specs
has_many :definitions, through: :specs
end
# This is the "normalized" table that defines an specification
# for example "Size". This just holds the name for example and the acceptable values.
class Definition
has_many :specs
has_many :products, through: :specs
end
# this contains the actual specs of a product (the value)
# specs.payload is a JSON column
class Spec
belongs_to :definition
belongs_to :product
delegate :name, to: :definition
def value
payload[:value]
end
def value=(val)
payload[:value] = val
end
end
One of the classic problems with using a design like this would be that the specs table would have to store the value as a text (or varchar) column and deal with the issues of type casting. But most modern DB's support dynamic column types like HSTORE or JSON that you can use to store the actual value.
The downside is that you have to use a special SQL syntax when querying:
Spec.where("payload->>'value' = ?", "foo")
This is a sort of normalized variation on what is called the Entity–attribute–value model which can be an anti-pattern but is often the only good solution to dynamic attributes in a relational database.
See:
ActiveRecord and PostgreSQL
ActiveRecord::ActsAs - Multi Table Inheritance for AR
Another way to approach the problem which avoids the issue of EAV tables is to use multi-table inheritance. This example uses ActiveRecord::ActsAs.
class Product < ActiveRecord::Base
actable
belongs_to :brand
validates_numericality_of :universal_product_code, length: 12
validates_presence_of :model_name, :price
end
class Bicycle < ActiveRecord::Base
acts_as :product
validates_numericality_of :gears
validates_presence_of :size
end
class PowerTool < ActiveRecord::Base
acts_as :product
validates_numericality_of :voltage, :amps
validates_presence_of :voltage, :amps
end
This would store the base information on a products table:
change_table :products do |t|
t.decimal :price
t.sting :model_name
t.integer :universal_product_code, length: 12
t.integer :actable_id
t.string :actable_type
end
And it uses a polymorphic association to store the subtypes of products on more specific tables:
create_table :bicycles do |t|
t.integer :gears
t.integer :size
end
create_table :power_tools do |t|
t.boolean :corded
t.integer :amps
t.integer :size
end
The advantage here is that you have a defined schema instead of a bunch of loose attributes.
The drawback is if you are designing a generic web shop then a fixed schema will not cut it.
Related
Say we have 3 different applications - serviceapp, subscriptionapp, ecomapp, all written in ruby on rails and uses the same database in backend and tables in the backend. So the user table for all these three applications are same. If a user is part of serviceapp using the same email and credentials he can login into subscriptionapp or ecomapp and vice versa.
The reason behind choosing same user table and other table for all the application is puerly business perspective - same single crm and ticketing system for sales and cdm team to track everything. Devise is being used in all three applications along with LDAP so login and signup works fine without any issue.
Problem:
Till now users' last_login_at is a single column so we really can't tell which app he last logged in at. But now we have to start logging these details separately, like when did he last login at serviceapp, ecomapp, subscription app separetly.
Also we are starting to use a new crm of one particular app - subscriptionapp and for the clients(users) of that particular app we have to store extra information like unq_id from crm and so on.
My intial thought is to add these columns in the user table itself. But in the future we might add few extra information to user table which are app specific. Hence adding it to the main user table won't be a good idea for this. How shall I proceed in this case? I though of creating three different tables like subscriptionapp_client, ecomapp_client, serviceapp_client had associating them with the user table like user has_one ***_client.
If the association is present like if user.subscriptionapp_client.present? he is a client of that app and we can store the last login at, crm_uniq_id and all in there in that table itself.
Is there anyother good approach that might fit the problem here? I am reading about MTI but it looks like it won't solve the problem.
Single table inheritance with JSON.
class CreateClientAccount < ActiveRecord::Migration[5.2]
def change
create_table :client_accounts do |t|
t.references :user
t.string :uid # the unique id on the client application
t.string :type
t.integer :sign_in_count
t.datetime :last_sign_in_at
t.jsonb :metadata
t.timestamps
end
add_index :client_accounts, [:user_id, :type], unique: true
end
end
class User
has_many :client_accounts
has_one :service_account, class_name: 'ServiceApp::ClientAccount'
# ...
end
class ClientAccount < ApplicationRecord
belongs_to :user
validates_uniqueness_of :user_id, scope: :type
end
module ServiceApp
class ClientAccount < ::ClientAccount
end
end
module SubscriptionApp
class ClientAccount < ::ClientAccount
end
end
module EcomApp
class ClientAccount < ::ClientAccount
end
end
This avoids the very unappealing duplication of having X number of tables in the schema to maintain and the JSONB column still gives you a ton of flexibility. However its in many ways just an upgrade over the EAV pattern.
It also has a lot in common with MTI. In MTI you would use an association to another table which fills the same purpose as the JSON column - to make the relational model more flexible. This can either be polymorphic or you can have X number of foreign keys for each specific type.
One table for each type.
class User < ApplicationRecord
has_one :subscription_account
has_one :service_account
# ...
end
class ClientAccount < ApplicationModel
self.abstract_class = true
belongs_to :user
end
class SubscriptionAccount < ClientAccount
end
class ServiceAccount < ClientAccount
end
# ...
This is the most flexible option but if you want to add a feature you will have to create migrations for each and every table. And this also means that you can't query a single homogenous collection if you want all the types. You have to perform X number of joins.
This is not really that appealing unless the requirements for each type are wildly different.
Context
In the context of a Ruby on Rails application, in a school's project.
Let's consider the context of a team-based game, with many characters to choose from. I want to represent affinities between two characters in different context, which means whether two characters are being teamed-up or are facing each other or even when one is present in the game while the other is missing.
I would then have tables that looks something like this in my database
Characters
Ally-Relation
Enemy-Relation
PlayingSingle-Relation
Each of these <name>-Relation tables represents a many-to-many relation between Characters, with an additional score that represents the strongness of the relation
Of course, relations between character are subject to changes. We might decide for any reason that a relation has become irrelevant, or another relation that we didn't thought of before just appeared.
In terms of display, we want to look for both the best and worst other characters in a specific relation.
Question
I came up with something like this.
class Relation < ActiveRecord::Base
scope :best, ->(character_id) {
Character.find(where(character_left: character_id).order("score desc").limit(5).pluck(:character_right))
}
end
Where character_left and character_right are the two characters to be considered for in the relation and the score is the strenght of the bond.
However, when fetching data, my teacher thinks it would be best to have scopes in the Characters model to find both the best and worst other character in a specific relation. This, because the teammate that is doing, say, the HTML code don't give a damn about the structure of the Relations when he wants to display characters. He told me about using has_and_belongs_to_many and he sketched me some code he would expects that looks something like Character.best(:relation) to fetch the data.
While I think what I did is better (obviously :) ). Having the scopes that will fetch Characters from within the Relation models, as they subject to appear and disappear keeps the request relation specifics. This prevents us from modifying the Characters model every time we fumble with the Relations.
Having somethings that looks like Relation.best(:hero) seems cleaner to me.
What do you think about it ? What are good practices around this very specific situation. Are there any right way to apply and use modular many-to-many relation s in a Ruby on Rails application ?
Your on the right track with a score column and using that to order the relations. However you need to account for the fact that a character can be in either column in the join model.
class Character
has_many :relationships_as_left, foreign_key: 'left_id'
has_many :relationships_as_right, foreign_key: 'right_id'
end
# renamed to not get it mixed up with ActiveRecord::Relation
class Relationship
belongs_to :left, class_name: 'Character'
belongs_to :right, class_name: 'Character'
end
You want to make sure to setup a unique index and the correct foreign keys:
class CreateRelationships < ActiveRecord::Migration
def change
create_table :relationships do |t|
t.references :left, index: true, foreign_key: false
t.references :right, index: true, foreign_key: false
t.integer :score, index: true
t.timestamps null: false
end
add_foreign_key :relationships, :characters, column: :left_id
add_foreign_key :relationships, :characters, column: :right_id
add_index :relationships, [:left_id, :right_id], unique: true
end
end
Querying this table is kind of tricky since Character can be referenced in relationships.left_id or relationships.right_id.
class Relationship < ActiveRecord::Base
belongs_to :left, class_name: 'Character'
belongs_to :right, class_name: 'Character'
def self.by_character(c)
sql = "relationships.left_id = :id OR relationships.right_id = :id"
where( sql, id: c.id )
end
def self.between(c1, c2)
where(left_id: [c1,c2]).merge(where(right_id: [c1,c2]))
end
def other_character(c)
raise ArgumentError unless c == left || c == right
c == left ? right : left
end
end
The between method requires a little explaination:
where(left_id: [c1,c2]).merge(where(right_id: [c1,c2]))
This generates the following query:
SELECT
"relationships".* FROM "relationships"
WHERE
"relationships"."left_id" IN (1, 2)
AND
"relationships"."right_id" IN (1, 2)
Also both you and your professor are wrong - a scope on Character will not work since scopes are class level, what you want is to check the relations on an instance.
class Character
def worst_enemies(limit = 10)
relations = Relationship.joins(:left, :right)
.by_character(self)
.order('relationship.score ASC')
.limit(limit)
relations.map do |r|
r.other_character(self)
end
end
end
You could possibly do this more elegantly with a subquery.
I've struggled with this for the past few hours, I'm trying add the capability to a model where they are able to enter more than one price options to a model.
For example
Regular(price)
Luxury (price) etc..
I currently have a model
Pass.rb
belongs_to :user
validates_numericality_of :price,
greater_than: 49,
message: "must be at least 50 cents"
validates :title
My form for this model is normal letting a user enter a price. In order to have more than one price option how would I design that in the database or is this done more within the controller. Lastly if you make it available for user choice does it save as the official price for that model or would it be something extra to include when referencing the model's price within another controller. Example I'm using stripe to handle payment so if I have
sale = Sale.new do |s|
s.amount = pass.price
Would that represent the price the user selected. Sorry if this explained badly I haven't been able to find any resources to help me get started at solving this issue
In order to have more than one price option how would I design that in the database
class PriceOption < ActiveRecord::Base
belongs_to :pass
...
end
class Pass < ActiveRecord::Base
has_many :price_options
...
end
create_table "price_options", force: :cascade do |t|
t.integer "pass_id"
...
end
I'm rather new to Rails and have a question about how to successfully add sub-categories to an existing Join Table relationship.
For example, assume I'm building a job board. Each job has 5 major filter categories ( Job_Type [General Management, Finance & Operations, etc.], Industry [ Technology, Healthcare, etc.], Region [Northwest, Southeast, etc.], and so on ). I want each of those to have subcategories as well ( ex. This job post has a Region > Southeast > South Carolina > Greenville ).
Setting up an initial Join Table association for the 5 major filter types made sense for future filtering and searchability.
EDIT
Here is my current Posting Model
class Posting < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
belongs_to :recruiter, class_name: "User"
belongs_to :company
has_many :interests
has_many :comments, as: :commentable
has_and_belongs_to_many :job_types
has_and_belongs_to_many :industries
has_and_belongs_to_many :regions
has_and_belongs_to_many :market_caps
has_and_belongs_to_many :ownerships
has_many :users, through: :interests
acts_as_followable
end
I'm currently using join tables instead of an array directly on the ActiveRecord for speed sake and for filtering/searching capabilities later on. It also allows me to use these join table in conjunction with a plethora of other necessary ActiveRecord associations.
Here is a snippet of what job_type looks like:
class JobType < ActiveRecord::Base
has_and_belongs_to_many :postings
has_and_belongs_to_many :news_items
has_and_belongs_to_many :companies
has_and_belongs_to_many :users
has_and_belongs_to_many :mkt_ints
end
This allows me access to a simple array of associated models, but I'm confused as to how to move past that to an array of arrays with potential further nested arrays. It feels clunky to add additional join tables for the first join tables. I'm sure there's a better solution and would love to get any insight you might have.
SECOND EDIT
Here's a representational picture of what I am trying to do if it helps.
Thanks!
SOLVED
The data for this table will, most likely, not be changing which eases the complexity and presented a more straightforward solution.
I created separate roles within a single table to limit queries and join tables. The resulting Region ActiveRecord looks like this:
class CreateRegions < ActiveRecord::Migration
def change
create_table :regions do |t|
t.string :role # role [ region, state, city ]
t.integer :parent # equal to an integer [ state.id = city.parent ] or null [ region has no parent ]
t.string :name # [ ex: Southeast, South Carolina, Charleston ]
t.timestamps null: false
end
end
end
This gives me all of the fields necessary to create relationships in a single table and easily sort it into nested checkboxes using the parent.id.
Thank you to everyone who viewed this question and to Val Asensio for your helpful comment and encouragement.
I have a model that I'm building in my Rails app for a product's Price:
class Price < ActiveRecord::Base
validates_presence_of :country_code, :product, :amount
validates_uniqueness_of :product, :scope => [:country_code]
end
Appropriately, the DB model is as follows:
create_table :prices do |t|
t.string :country_code
t.integer :product_id
t.integer :amount
end
The complication is that, at the moment, Product isn't an actual ActiveRecord model -- it's a Struct wrapped around a single ID attribute (item_code) with a lot of constants and helper methods. There are plans in a future sprint to refactor it back into the database properly, but right now, much of our app code uses Product as if it's coming from the DB already, and ideally, I'd like to isolate as many changes for once Product becomes a proper table to just the models that would have direct relationships in the DB.
The question, then, is to cleanly handle the foreign key relationship in the model when there is no actual other table in the underlying DB. To get around it logically, I've added the following validation to Price:
validates_inclusion_of :product, :in => Product.all
Where Product.all is a method that simulates these objects coming from a table without being an actual ActiveRecord class.
All this together leads to the issue that, as far as I understand it, because there's no Product table, ActiveRecord isn't creating :product accessors (and is instead using :product_id directly). However, if I rely on :product_id, I've made it harder to migrate once Product becomes an actual ActiveRecord class.
Is there a simple way to 'alias' the :product attribute to the :product_id field so that the code is isolated within the Product (or even the Price model) in such a way that, for all intents and purposes, :product is automatically mapped as if there was a relationship in the database tables? The ideal solution, obviously, would be some method or alias instruction that could just be removed at some future date, and suddenly everything would act as though product_id has always pointed to the products table.
If you just want to fake the fields as if you had a has_one relationship setup in the Price model, you can just define the methods yourself in your Price model. As you didn't give an example of how you select from the Product Struct, the below is a code example if you were trying to re-implement it for an ActiveModel.
def product
if self.product_id
if !#product or #product.id != self.product_id
#product = Product.find(self.product_id)
else
#product
end
end
end
def product=(product)
#product = product
self.product_id = product._id
end
This will duplicate a has_one relationship where you can change it by setting it through product or product_id and it will only lookup a product once, and then again if it changes.