I have a very special cases. I understand maybe db design is not very awesome, but I cannot change that.
class Employer < ApplicationRecord
has_many :contract_employers
has_many :contracts, through: :contract_employers
has_many :crm_contacts, through: :contract_employers
# typical join table, with key: contract_id and employer_id
class ContractEmployer < ApplicationRecord
belongs_to :contract
belongs_to :employer
has_many :crm_contacts
# CrmContact table has key: contract_employer_id
class CrmContact < ApplicationRecord
belongs_to :contract_employer
has_one :employer, through: :contract_employer
Given
employer = Employer.create
I have no issue to run
employer.contracts.create
However, if I try to run
employer.crm_contacts.create
It raise error
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection: Cannot modify association 'Employer#crm_contacts' because the source reflection class 'CrmContact' is associated to 'ContractEmployer' via :has_many.
I checked the rails source code, basically it states very clearly
# Construct attributes for :through pointing to owner and associate. This is used by the
# methods which create and delete records on the association.
#
# We only support indirectly modifying through associations which have a belongs_to source.
# This is the "has_many :tags, through: :taggings" situation, where the join model
# typically has a belongs_to on both side. In other words, associations which could also
# be represented as has_and_belongs_to_many associations.
#
# We do not support creating/deleting records on the association where the source has
# some other type, because this opens up a whole can of worms, and in basically any
# situation it is more natural for the user to just create or modify their join records
# directly as required.
So only typical join table supports model.associations.create? Any suggestion for my user case?
Take my case for example, even rail is willing to do the job. How could employer.crm_contacts.create create middle table record ContractEmployer? Yes, it knows employer.id, but it has no clue what contract.id is, right?
Rails can not create middle table record in this case, but you can.
And I am completely agree with this (comments in rails source code /activerecord/lib/active_record/associations/through_association.rb):
in basically any situation it is more natural for the user to just
create or modify their join records directly as required
I don't see a problem here.
class Employer < ApplicationRecord
# ...
def create_crm_contact
ActiveRecord::Base.transaction do
contract = contracts.create # will create both `contract` and associated `contract_employer`
# find the `contract_employer` that has been just created
contract_employer = contract_employers.find_by(contract_id: contract.id)
contract_employer.crm_contacts.create
end
end
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.
I have a user in my application that can have multiple assessments, plans, and materials. There is already a relationship between these in my database. I would like to show all these in a single tab without querying the database too many times.
I tried to do a method that joins them all in a single table but was unsuccessful. The return was the following error: undefined method 'joins' for #<User:0x007fcec9e91368>
def library
self.joins(:materials, :assessments, :plans)
end
My end goal is to just itterate over all objects returned from the join so they can be displayed rather than having three different variables that need to be queried slowing down my load times. Any idea how this is possible?
class User < ApplicationRecord
has_many :materials, dependent: :destroy
has_many :plans, dependent: :destroy
has_many :assessments, dependent: :destroy
end
class Material < ApplicationRecord
belongs_to :user
end
class Assessment < ApplicationRecord
belongs_to :user
end
class Plan < ApplicationRecord
belongs_to :user
end
If all you want to do is preload associations, use includes:
class User < ApplicationRecord
# ...
scope :with_library, -> { includes(:materials, :assessments, :plans) }
end
Use it like this:
User.with_library.find(1)
User.where(:name => "Trenton").with_library
User.all.with_library
# etc.
Once the associations are preloaded, you could use this for your library method to populate a single array with all the materials, assessments and plans of a particular user:
class User < ApplicationRecord
# ...
def library
[materials, assessments, plans].map(&:to_a).flatten(1)
end
end
Example use case:
users = User.all.with_library
users.first.library
# => [ ... ]
More info: https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Prefer includes over joins unless you have a specific reason to do otherwise. includes will eliminate N+1 queries, while still constructing usable records in the associations: you can then loop through everything just as you would otherwise.
However, in this case, it sounds like you're working from a single User instance: in that case, includes (or joins) can't really help -- there are no N+1 queries to eliminate.
While it's important to avoid running queries per row you're displaying (N+1), the difference between one query and three is negligible. (It'd cost more in overhead to try to squish everything together.) For this usage, it's just unnecessary.
I have a Rails app with the following relationship:
region.rb
class Region < ActiveRecord::Base
has_many :facilities
end
facility.rb
class Facility < ActiveRecord::Base
belongs_to :region
end
I want to expand functionality a bit so that facilities can belong to more than one region at a time. I believe I can do this with a has_many_through relationship but I'm needing some guidance on converting the existing has_many into a has many through. I understand how to create and wire up the join table, but how would I take existing data and translate it?
So for instance. On a facility object there is region_id, since the facilities can belong to more than one region I'd probably need a region_ids field and shovel the collection of regions into that column which should then populate the other side of the association via the join table. I have this part pretty much figured out as far as moving forward and wiring up the association. But I'm unsure as to how to take existing data and translate it over so the app doesn't break when I change the model association.
Any advice would be greatly appreciated.
I suggest you to always use has_many :through instead of HBTM.
To establish this kind of relation you'll need the following set up:
# region.rb
class Region
has_many :facility_regions
has_many :facilities, through: :facility_regions
end
# facility.rb
class Facility
has_many :facility_regions
has_many :regions, through: :facility_regions
end
# facility_region.rb
class FacilityRegion
belongs_to :facility
belongs_to :region
end
Also, of course, you'll need to create a migration:
rails g migration create_facility_regions facility_id:integer region_id:integer
# in this migration create a uniq index:
add_index :facility_regions, %I(facility_id region_id), name: :facility_region
rake db:migrate
UPD
As to migration from one database state to another one.
I think it should not be a problem.
1) Do not delete the relations you had before (leave has_many :facilities and belongs_to :region in models).
2) When new table is created and new associations added to the classes (which I showed) create a new migration:
rails g migration migrate_database_state
3) Write the script, which will create new records in db (to reflect the current state of things):
ActiveRecord::Base.transaction do
Facility.where.not(region_id: nil).find_each do |facility|
next if FacilityRegion.find_by(falicity_id: facility.id, region_id: facility.region_id)
FacilityRegion.create!(facility_id: facility.id, region_id: facility.region_id)
end
end
4) Put this script into last created migration and run it (or in console without migration, effect would be the same).
5) After script is successfully run, create new migration in which you delete region_id from facilities table and remove these associations definitions (has_many :facilities and belongs_to :region) from models.
It must be it. I might have made some typos or so, make sure I did not miss anything and
You need to add another model, a "middle guy" called FacilityRegion.rb, like this:
facility.rb
class Facility < ActiveRecord::Base
has_many :falicity_regions
has_many :regions, through: falicity_regions
end
facility_region.rb
class FacilityRegion < ActiveRecord::Base
belongs_to :region
belongs_to :facility
end
region.rb
class Region < ActiveRecord::Base
has_many :falicity_regions
has_many :facilities, through: falicity_regions
end
If you want to use belongs_and_has_many relationship, you need to:
rails g migration CreateJoinTableRegionsFacilities regions facilities
Then,
rake db:migrate
Now, your relationships should be:
Region.rb:
class Region < ApplicationRecord
has_and_belongs_to_many :facilities
end
Facility.rb
class Facility < ApplicationRecord
has_and_belongs_to_many :regions
end
In order to populate the new join table, you will need to in your console:
Region.all.find_each do |r|
Facility.where(region_id: r.id).find_each do |f|
r.facilities << f
end
end
Now, you can either leave the columns region_id and facility_id in Facility and Region table, respectively, or you can create a migration to delete it.
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
I have a Sponsors model and a Promo Codes model.
A sponsor can have zero or more promo codes
A promo code can have zero or one sponsors
Thus a promo code should have an optional reference to a sponsor, that is, a sponsor_id that may or may not have a value. I'm not sure how to set this up in Rails.
Here's what I have so far:
# app/models/sponsor.rb
class Sponsor < ActiveRecord::Base
has_many :promo_codes # Zero or more.
end
# app/models/promo_code.rb
class PromoCode < ActiveRecord::Base
has_one :sponsor # Zero or one.
end
# db/migrate/xxxxx_add_sponsor_reference_to_promo_codes.rb
# rails g migration AddSponsorReferenceToPromoCodes sponsor:references
# Running migration adds a sponsor_id field to promo_codes table.
class AddSponsorReferenceToPromoCodes < ActiveRecord::Migration
def change
add_reference :promo_codes, :sponsor, index: true
end
end
Does this make sense? I'm under the impression that I have to use belongs_to in my Promo Codes model, but I have no basis for this, just that I've haven't seen a has_many with has_one example yet.
In Rails 5, belongs_to is defined as required by default. To make it optional use the 'optional' option :)
class User
belongs_to :company, optional: true
end
Source: https://github.com/rails/rails/issues/18233
This looks like a simple has_many and belongs_to relationship:
# app/models/sponsor.rb
class Sponsor < ActiveRecord::Base
has_many :promo_codes # Zero or more.
end
# app/models/promo_code.rb
#table has sponsor_id field
class PromoCode < ActiveRecord::Base
belongs_to :sponsor # Zero or one.
end
has_one isn't appropriate here, as it would replace has_many: ie, you either have "has_many" and "belongs_to" OR "has_one" and "belongs_to". has_one isn't generally used much: usually it is used when you already have a has_many relationship that you want to change to has_one, and don't want to restructure the existing tables.
Unless you specify validation, relationships are optional by default.
The belongs_to is to tell rails the other half of the relationship between those two objects so you can also call #promo_code.sponsor and, vice versa, #sponsor.promo_codes.