I currently have the following model structure and relations
class Fleet < ApplicationRecord
# properties id, created_at, updated_at
# A fleet should only consists of the same type, either a fleet or cars or a fleet of trains. Never both
has_many :cars
has_many :trains
end
class Car < ApplicationRecord
# properties id, created_at, updated_at, number_of_gears, is_sports_car
belongs_to :fleet
end
class Train < ApplicationRecord
# properties id, created_at, updated_at, number_of_wagons, people_capacity, has_first_calass_section
belongs_to :fleet
end
The problem i have is thata fleet should only consists of the same type, either a fleet of cars or a fleet of trains. Never both.
With the current relations on fleet, it seems error prone to have both has_many associations (or more coming like Airplanes) in terms of integrity and ease of use when calling #fleet._cars-or-trains, i would have to know what i can call.
STI is not an option since the properties are very different on a Car and a Train. There are many more properties, for the sake of simplicity i have shortened them in this example.
What is the right way in Rails to do this?
I would use two different models: CarsFleet and TrainsFleet. Both can extend a Fleet model but define a different has_many association.
Then "Car" would belong to a cars_fleet and "Train" would belong to a trains_fleet.
This is what polymorphic relationships are for.
A Fleet could have many vehicles, and a Vehicle could be either a Car or a Train and, importantly, never both.
class Fleet < ApplicationRecord
belongs_to :vehicles, polymorphic: true # notice the belongs_to, it's the only way polymorphism works from memory
end
class Car < ApplicationRecord
has_one :fleet, as: :vehicle
end
class Train < ApplicationRecord
has_one :fleet, as: :vehicle
end
That way you can call fleet.vehicles and not worry about the class.
This code is untested but it should get you across the line, and it's the most Railsy-way to solve your issue.
You could add a model validation to check this:
class Fleet < ApplicationRecord
has_many :cars
has_many :trains
validates :cars, absence: true, if: -> { trains.any? }
validates :trains, absence: true, if: -> { cars.any? }
end
The above should stop fleets from being saved when they have both cars and trains present. However you can still create cars and trains separately and link them to a fleet.
# assuming a valid fleet exist
fleet = Fleet.create # should save
# you can still create both cars and trains
fleet.cars.create # should save
fleet.trains.create # should save
# however when you retry to save the fleet it fails on the validation
fleet.save # should fail
In the above scenario we would like to also fire the fleet validations when creating a car or train. To do this add a validates_associated:
validates_associated :fleet
In both the Car and Train model. This now should withhold the above car and train to be saved.
Note that this answer doesn't prevent anything SQL related and records are still technically allowed in the database. If you don't want this you have to build a trigger that runs AFTER INSERT and AFTER UPDATE. Since this is SQL related I would suggest asking a new question if you have any issues with trigger creation. Mention the database used since the trigger syntax is not the same for all database types (MySQL, SQL Server, ...).
Related
I am working on a small collection tracker where I feel like STI could really simplify this problem but it seems the general consensus is to avoid STI whenever possible so I have broken my models apart. Currently, they are all the same but I do have a few different bits of metadata that I can see myself attaching to them.
Anyways, the root is a Platform which has many Games, Systems, Peripherals, etc. and I am trying to show all of these relations on a view in a dynamic table that is filterable, sortable and searchable.
For example a query could be #platform.collectables.search(q).order(:name).
# Schema: platforms[ id, name ]
class Platform < ApplicationRecord
has_many :games
has_many :systems
has_many :peripherals
end
# Schema: games[ id, platform_id, name ]
class Game < ApplicationRecord
belongs_to :platform
end
# Schema: systems[ id, platform_id, name ]
class System < ApplicationRecord
belongs_to :platform
end
# Schema: peripherals[ id, platform_id, name ]
class Peripheral < ApplicationRecord
belongs_to :platform
end
In the above, the polymorphism comes into play when I add them to a Collection:
# Schema: collections[ id, user_id, collectable_type, collectable_id ]
class Collection < ApplicationRecord
belongs_to :user
belongs_to :collectable, polymorphic: true
end
Now, when I view a Platform, I expect to see all of its games, systems and peripherals which I refer to as collectables. How would I query all of these while being able to sort as a whole (ie: "name ASC"). Below works in theory but this changes the relation to an Array which stops me from further filtering, searching or reordering at the database level so I can't tag on another scope or order.
class Platform < ApplicationRecord
...
def collectables
games + systems + peripherals
end
end
I stumbled on Delegated Types which kind of sounds like the step in the direction that I am looking for but maybe I am missing something.
I'm tempted to try the STI route, I don't see these models diverging much and things that are different could be stored inside of a JSONB column cause it's mostly just metadata for populating a view with and not really searching against. Basically a model such as this but it seems so frowned upon, I feel like I must be missing something.
# Schema: collectables[ id, platform_id, type, name, data ]
class Collectable < ApplicationRecord
belongs_to :platform
end
class Platform < ApplicationRecord
has_many :collectables
def games
collectables.where(type: 'Game')
end
def systems
collectables.where(type: 'System')
end
...
end
One solution here would be Delegated Type (a relatively new Rails feature) which can basically be summarized as Multiple Table Inheritance through polymorphism. So you have a base table containing the shared attributes but each class also has its own table - thus avoiding some of the key problems of STI.
# app/models/concerns/collectable.rb
# This module defines shared behavior for the collectable "subtypes"
module Collectable
TYPES = %w{ Game System Peripheral }
extend ActiveSupport::Concern
included do
has_one :base_collectable, as: :collectable
accepts_nested_attributes_for :base_collectable
end
end
# This model contains the base attributes shared by all the collectable types
# rails g model base_collectable name collectable_type collectable_id:bigint
class BaseCollectable < ApplicationRecord
# this sets up a polymorhic association
delegated_type :collectable, types: Collectable::TYPES
end
class Game < ApplicationRecord
include Collectable
end
class Peripheral < ApplicationRecord
include Collectable
end
class System < ApplicationRecord
include Collectable
end
You can then setup a many to many assocation through a join model:
class Collection < ApplicationRecord
belongs_to :user
has_many :collection_items
has_many :base_collectables, through: :collection_items
has_many :games,
through: :base_collectables,
source_type: 'Game'
has_many :peripherals,
through: :base_collectables,
source_type: 'Peripheral'
has_many :systems,
through: :base_collectables,
source_type: 'Systems'
end
class CollectionItem < ApplicationRecord
belongs_to :collection
belongs_to :base_collectable
end
# This model contains the base attributes shared by all the collectable types
# rails g model base_collectable name collectable_type collectable_id:bigint
class BaseCollectable < ApplicationRecord
# this sets up a polymorhic association
delegated_type :collectable, types: %w{ Game System Peripheral }
has_many :collection_items
has_many :collections, through: :collection_items
end
This lets you treat it as a homogenius collection and order by columns on the base_collectables table.
The big but - the relational model still doesn't like cheaters
Polymorphic assocations are a dirty cheat around the object relational impedence missmatch by having one columns with a primary key reference and another storing the class name. Its not an actual foreign key since the assocation cannot resolved without first pulling the records out of the database.
This means you can't setup a has_many :collectables, through: :base_collectables association. And you can't eager load all the delegated types or order the entire collection by the columns on the games, peripherals or systems tables.
It does work for the specific types such as:
has_many :games,
through: :base_collectables,
source_type: 'Game'
Since the table can be known beforehand.
This is simply a tough nut to crack in relational databases which are table based and not object oriented and where relations in the form of foreign keys point to a single table.
VS STI + JSON
The key problem here is that all the data you're stuffing into the JSON column is essentially schemaless and you're restricted by the 6 types supported by JSON (none of which is a date or decent number type) and working with the data can be extremely difficult.
JSON's main feature is it simplicity which worked well enough as a transmission format. As a data storage format its not really that great.
It's a huge step up from the EAV table or storing YAML/JSON serialized into a varchar column but its still has huge caveats.
Other potential solutions
STI. Fixes one the polymorphism problem and introduces a bunch more.
A materized view that is populated with a union of the three tables can be treated as table and thus can simply have an ActiveRecord relation attached to it.
Union query. Basically the same idea as above but you just use the raw query results or feed them into a model representing a non-sensical table.
Non-relational database. Document based databases like MongoDB let you create flexible documents and non-homogenius collections by design while having better type support.
So, after thinking on this for a while, I have no idea what the proper way to model this is.
I have a website focused on sharing images. In order to make the users happy, I want them to be able to subscribe to many different collections of images.
So far, there's two types of collections. One is a "creator" relationship, which defines people who worked on a specific image. That looks like this:
class Image < ActiveRecord::Base
has_many :creations
has_and_belongs_to_many :locations
has_many :creators, through: :creations
end
class Creator < ActiveRecord::Base
has_many :images, ->{uniq}, through: :creations
has_many :creations
belongs_to :user
end
class Creation < ActiveRecord::Base
belongs_to :image
belongs_to :creator
end
Users may also tag an image with a subjective tag, which is something not objectively present in the image. Typical subjective tags would include "funny" or "sad," that kind of stuff. That's implemented like this:
class SubjectiveTag < ActiveRecord::Base
# Has a "name" field. The reason why the names of tags are a first-class DB model
# is so we can display how many times a given image has been tagged with a specific tag
end
class SubjectiveCollection < ActiveRecord::Base
# Basically, "User X tagged image Y with tag Z"
belongs_to :subjective_tag
belongs_to :user
has_many :images, through: :subjective_collection_member
end
class SubjectiveCollectionMember < ActiveRecord::Base
belongs_to :subjective_collection
belongs_to :image
end
I want users to be able to subscribe to both Creators and SubjectiveTags, and to display all images in those collections, sequentially, on the home page when they log in.
What is the best way to do this? Should I have a bunch of different subscription types - for example, one called SubjectiveTagSubscription and one called CreatorSubscription? If I do go this route, what is the most efficient way to retrieve all images in each collection?
What you want to use is a Polymorphic Association.
In your case, it would look like this:
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribeable, polymorphic: true
end
The subscriptions table would need to include the following fields:
user_id (integer)
subscribeable_id (integer)
subscribeable_type (string)
This setup will allow a subscription to refer to an instance of any other model, as ActiveRecord will use the subscribeable_type field to record the class name of the thing being subscribed to.
To produce a list of images for the currently logged in user, you could do this:
Subscription.where(user_id: current_user.id).map do |subscription|
subscription.subscribeable.images.all
end.flatten
If the performance implications of the above approach are intolerable (one query per subscription), you could collapse your two types of subscribeables into a single table via STI (which doesn't seem like a good idea here, as the two tables aren't very similar) or you could go back to your initial suggestion of having two different types of subscription models/tables, querying each one separately for subscriptions.
I'm building my first Rails project and it's a checkout system for a computer loaner pool. Creating Technicians (who perform checkouts) and CheckOuts (like transactions) makes perfect sense. However, I'm struggling with the relationship between CheckOuts and LoanerComputers.
Technician and CheckOut have a 1:N relationship, and CheckOut and LoanerComputerhave a 1:1 relationship. I believe in my Rails-n00b heart that it would be nice to have association proxy, e.g. Technician.check_outs.loaner_computers or even better Technician.loaner_computers, but from what I've learned that would mean that my LoanerComputer class must contain the belongs_to, and that assumes that the LoanerComputer table in my database has a check_out_id column.
I've tried thinking about it from a "rental" approach, but I see lots of solutions that have a fourth model to store state changes of the thing being "rented." To me, it makes more sense to have technician_id and loaner_computer_id in a single CheckOut entry, but then how could I easily access a technician's checked-out loaner computers using association proxy? Is it possible to use :delegate in this instance, or does it look like I'd have to make a custom method to read loaner computers via technicians? Here's example code:
class Technician < ActiveRecord::Base
has_many :check_outs
end
class CheckOut < ActiveRecord::Base
belongs_to :technician
# has_one :loaner_computer
# OR
# belongs_to :loaner_computer (which means I need to have a "loaner_id" column in the db, right?)
end
class LoanerComputer < ActiveRecord::Base
# belongs_to :check_out (which means I need to have a "check_out_id" column in the db)
# OR
# has_one :check_out
end
P.S. Do I just have it all backwards? Should I say Technicians has_many LoanerComputers, and LoanerComputers has_many CheckOuts?
Thanks for your time! Let me know if anything needs clarification!
I think, what you're looking for - is "has_many through" association.
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
In Your case - It'll look like this
class Technician < ActiveRecord::Base
has_many :check_outs
has_many :loaners, through: :check_outs
end
class CheckOut < ActiveRecord::Base
# Must have technician_id and check_out_id fields in DB
belongs_to :technician
belongs_to :check_out
end
class Loaner < ActiveRecord::Base
has_many :technicians, through: :check_outs
end
With this - You'll be able to access loaners from technician and reverse
Technician.loaners
Loaner.technicians
You can also access check_outs from both models
Imagine the following situation:
I have a dog model and a house model. A dog can belong to a house, and a house can have many dogs, so:
Class Dog < ActiveRecord::Base
belongs_to :house
end
Class House < ActiveRecord::Base
has_many :dogs
end
Now, imagine that I also want to create dogs that don't have a house. They don't belong to house. Can I still use that relationship structure and simply don't inform a :house_id when creating it?
Is there a better practice?
Obs.: I used this analogy to simplify my problem, but my real situation is: I have a model a user can generate instances of it. He can also create collections of those instances, but he can leave an instance outside a collection.
Be careful with this in Rails 5...
#belongs_to is required by default
From now on every Rails application will have a new configuration
option config.active_record.belongs_to_required_by_default = true, it
will trigger a validation error when trying to save a model where
belongs_to associations are not present.
config.active_record.belongs_to_required_by_default can be changed to
false and with this keep old Rails behavior or we can disable this
validation on each belongs_to definition, just passing an additional
option optional: true as follows:
class Book < ActiveRecord::Base
belongs_to :author, optional: true
end
from: https://sipsandbits.com/2015/09/21/whats-new-in-rails-5/#belongs_toisrequiredbydefault
I think it is absolutely normal approach.
You can just leave house_id with null value in database for the models which don't belong to other.
I can't seem to wrap my head around this, so I thought I'd post and see if anyone could help me out (please pardon the question if it's insultingly simple: it's complicated to me right now!)
I have these models:
order
service
customer
I think they speak for themselves: a service is what the customer buys when they place an order.
Ok.
So, naturally, I setup these relationships:
# a customer can have many orders
class Customer
has_many :orders
end
# an order belongs to a single customer and can have many services
class Order
belongs_to :customer
has_many :services
end
... but here's where I trip up:
# a service can belong to many orders
class Service
# belongs_to :order ???
end
Because my understanding of belongs_to is that--if I put it there--a service could only belong to one order (it would have only one value in the order_id key field--currently not present--tying it to only one order, where it needs to be able to belong to many orders).
What am I missing here?
There are two ways to handle this. The first is a rails-managed many-to-many relationship. In this case, you use a "has_and_belongs_to_many" relationship in both the Order and Service models. Rails will automatically create a join table which manages the relationships. The relationships look like this:
class Order
has_and_belongs_to_many :services
end
class Service
has_and_belongs_to_many :orders
end
The second way is to manage the join table yourself through an intermediate model. In this case, you might have another model called "LineItem" that represents a Service in the context of an Order. The relationships look like this:
class LineItem
belongs_to :order
belongs_to :service
end
class Order
has_many :line_items
end
class Service
has_many :line_items
end
I prefer the second myself. It's probably just me, but I don't get as confused about what's going on when it's explicit. Plus if I ever want to add some attributes to the relationship itself (like perhaps a quantity in your case) I'm already prepared to do that.
class Customer
has_many :orders
end
class Service
has_many :orders
end
class Order
belongs_to :customer
belongs_to :service
end
The Order should have customer_id and service_id, because it is in a many-to-one relationship with both.
I think this Railscast will help you out - basically you have 2 options. You can use has_and_belongs_to_many or has_many :through.
You will also find that has_and_belongs_to_many has been deprecated in favor of has_many :though => model_name which gives the same (and more) functionality.
I think you have realized this but your order is really a composite domain model, sometimes called an aggregate in DDD speak.
Your Service is a really a listing of some product/service that someone can order. Your order aggregate records what someone ordered.
The aggregate as someone else said is made up of a header, the Order, which includes things like who ordered, the date, does it include taxes, shipping charge, etc. And the Order has_many OrderLineItem's. The OrderLineItem belongs_to Service and contains things like the quantity ordered, belongs_to the product/service, etc.
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
class OrderLineItem < ActiveRecord::Base
belongs_to :Order
end
I personally use the OrderLineItem model name in deference to LineItem because in a system that needs to ship real products, as opposed to services, you might have allocations that link orders to inventory which would have line items and shipments that get the allocated product to the client, which also have line items. So the line item term can become very overloaded. This likely is not the case in your model because you're only doing services.