Associated Model Attribute Inheritance - ruby-on-rails

Rails novice here. I have associated models Team and Venue defined as follows:
class Team < ApplicationRecord
has_many :home_venues, class_name: "Venue"
end
and
class Venue < ApplicationRecord
belongs_to :team, optional: true, foreign_key: :team_id
end
Both models have attributes :city and :region. When I call team.home_venues.create, I would like for the :city and :region values of newly-created venue to default to the :city and :region values of the creating team unless otherwise specified.
What's the best way to achieve this functionality?

I would use before_validation hook - this way you will make sure you will have all validations run at correct time. In your Venue model:
before_validation :set_default_values
def set_default_values
self.city ||= self.team.try(:city)
self.region ||= self.team.try(:region)
end

Related

ActiveRecord has_one where associated model has two belongs_to associations

I have two ActiveRecord models that are associated with each other in this way:
class Address < ApplicationRecord
has_one :user, class_name: User.name
end
class User < ApplicationRecord
belongs_to :home_address, class_name: Address.name
belongs_to :work_address, class_name: Address.name
end
The User -> Address association works fine:
home_address = Address.new
#=> <Address id:1>
work_address = Address.new
#=> <Address id:2>
user = User.create!(home_address: home_address, work_address: work_address)
#=> <User id:1, home_address_id: 1, work_address_id: 2>
user.home_address
#=> <Address id:1>
user.work_address
#=> <Address id:2>
What I'm having trouble with is getting the Address's has_one to work properly. At first I got an error that User#address_id does not exist, which makes sense because that's not the name of the foreign key field. It would be either home_address_id or work_address_id (and I added these FKs with a migration). But I wasn't sure how to let it know which address to use, until I learned that you can pass a scope into a has_one declaration:
class Address < ApplicationRecord
has_one :user,
->(address) { where(home_address_id: address.id).or(where(work_address_id: address.id)) },
class_name: User.name
end
But this returns the same error as before: Caused by PG::UndefinedColumn: ERROR: column users.address_id does not exist. This is confusing, because nowhere in that scope did I declare that I'm looking on address_id. I'm guessing the has_one implicitly has a foreign_key of :address_id, but I don't know how I'd set this because there are technically two, :home_address_id and :work_address_id.
I feel like I'm close here - how do I fix this has_one association?
Update
My gut says that the solution here is to just create a user method that performs the query I'm looking to run, instead of declaring a has_one. It'd be great if has_one supports this functionality, but if not, I'll fall back to that.
class Address < ApplicationRecord
def user
User.find_by("home_address_id = ? OR work_address_id = ?", id, id)
end
end
Solution
Thanks to #max below! I ended up going with a solution based on his answer. I also use the Enumerize gem, which will come into play in the Address model.
class AddAddressTypeToAddresses < ActiveRecord::Migration[5.2]
add_column :addresses, :address_type, :string
end
class User < ApplicationRecord
has_many :addresses, class_name: Address.name, dependent: :destroy
has_one :home_address, -> { Address.home.order(created_at: :desc) }, class_name: Address.name
has_one :work_address, -> { Address.work.order(created_at: :desc) }, class_name: Address.name
end
class Address < ApplicationRecord
extend Enumerize
TYPE_HOME = 'home'
TYPE_WORK = 'work'
TYPES = [TYPE_HOME, TYPE_WORK]
enumerize :address_type, in: TYPES, scope: :shallow
# Shallow scope allows us to call Address.home or Address.work
validates_uniqueness_of :address_type, scope: :user_id, if: -> { address_type == TYPE_WORK }
# I only want work address to be unique per user - it's ok if they enter multiple home addresses, we'll just retrieve the latest one. Unique to my use case.
end
Each association in Rails can just have a single foreign key because what you would need is in terms of SQL is:
JOINS users
ON users.home_address_id = addresses.id OR users.work_address_id = addresses.id
Using a lambda to add a default scope for the association won't work here since ActiveRecord doesn't actually let you monkey with how it joins on an assocation level. Which is quite understandable if you consider how many different queries it generates and the number of edge cases that feature would cause.
If you REALLY want to go down the rabbit hole of having two different foreign keys on your users table you can solve it with Single Table Inheritance:
class AddTypeToAddresses < ActiveRecord::Migration[6.1]
def change
add_column :addresses, :type, :string
end
end
class User < ApplicationRecord
belongs_to :home_address, class_name: 'HomeAddress'
belongs_to :work_address, class_name: 'WorkAddress'
end
class HomeAddress < Address
has_one :user, foreign_key: :home_address_id
end
class WorkAddress < Address
has_one :user, foreign_key: :work_address_id
end
But I would put the foreign key on the other table and use a one-to-many association:
class Address < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :addresses
end
This lets you add as many address types as you want without borking your users table.
If you want to limit the user to one home and one work address you would do:
class AddTypeToAddresses < ActiveRecord::Migration[6.1]
def change
add_column :addresses, :address_type, :integer, index: true, default: 0
add_index :addresses, [:user_id, :address_type], unique: true
end
end
class Address < ApplicationRecord
belongs_to :user
enum address_type: {
home: 0,
work: 1
}
validates_uniqueness_of :type, scope: :user_id
end
class User < ApplicationRecord
has_many :addresses
has_one :home_address,
-> { home },
class_name: 'Address'
has_one :work_address,
-> { work },
class_name: 'Address'
end

Can't get STI to act as polymorphic association on model

I have a User model that can have an email and a phone number, both of which are models of their own as they both require some form of verification.
So what I'm trying to do is attach Verification::EmailVerification as email_verifications and Verification::PhoneVerification as phone_verifications, which are both STIs of Verification.
class User < ApplicationRecord
has_many :email_verifications, as: :initiator, dependent: :destroy
has_many :phone_verifications, as: :initiator, dependent: :destroy
attr_accessor :email, :phone
def email
#email = email_verifications.last&.email
end
def email=(email)
email_verifications.new(email: email)
#email = email
end
def phone
#phone = phone_verifications.last&.phone
end
def phone=(phone)
phone_verifications.new(phone: phone)
#phone = phone
end
end
class Verification < ApplicationRecord
belongs_to :initiator, polymorphic: true
end
class Verification::EmailVerification < Verification
alias_attribute :email, :information
end
class Verification::PhoneVerification < Verification
alias_attribute :phone, :information
end
However, with the above setup I get the error uninitialized constant User::EmailVerification. I'm unsure of where I'm going wrong.
How I structure this so that I can access email_verifications and phone_verifications on the User model?
When using STI you don't need (or want) polymorphic associations.
Polymorphic associations are a hack around the object-relational impedance mismatch problem used to setup a single association that points to multiple tables. For example:
class Video
has_many :comments, as: :commentable
end
class Post
has_many :comments, as: :commentable
end
class Comment
belongs_to :commentable, polymorphic: true
end
The reason they should be used sparingly is that there is no referential integrity and there are numerous problems related to joining and eager loading records which STI does not have since you have a "real" foreign key column pointing to a single table.
STI in Rails just uses the fact that ActiveRecord reads the type column to see which class to instantiate when loading records which is also used for polymorphic associations. Otherwise it has nothing to do with polymorphism.
When you setup an association to a STI model you just have to create an association to the base inheritance class and rails will handle resolving the types by reading the type column when it loads the associated records:
class User < ApplicationRecord
has_many :verifications
end
class Verification < ApplicationRecord
belongs_to :user
end
module Verifications
class EmailVerification < ::Verification
alias_attribute :email, :information
end
end
module Verifications
class PhoneVerification < ::Verification
alias_attribute :email, :information
end
end
You should also nest your model in modules and not classes. This is partially due to a bug in module lookup that was not resolved until Ruby 2.5 and also due to convention.
If you then want to create more specific associations to the subtypes of Verification you can do it by:
class User < ApplicationRecord
has_many :verifications
has_many :email_verifications, ->{ where(type: 'Verifications::EmailVerification') },
class_name: 'Verifications::EmailVerification'
has_many :phone_verifications, ->{ where(type: 'Verifications::PhoneVerification') },
class_name: 'Verifications::PhoneVerification'
end
If you want to alias the association user and call it initiator you do it by providing the class name option to the belongs_to association and specifying the foreign key in the has_many associations:
class Verification < ApplicationRecord
belongs_to :initiator, class_name: 'User'
end
class User < ApplicationRecord
has_many :verifications, foreign_key: 'initiator_id'
has_many :email_verifications, ->{ where(type: 'Verifications::EmailVerification') },
class_name: 'Verifications::EmailVerification',
foreign_key: 'initiator_id'
has_many :phone_verifications, ->{ where(type: 'Verifications::PhoneVerification') },
class_name: 'Verifications::PhoneVerification',
foreign_key: 'initiator_id'
end
This has nothing to do with polymorphism though.

Renamed belongs_to association, it currently can't find the associated model

My user.location is returning nil currently.
My model looks like:
# id
# user_location_id
class User < ApplicationRecord
belongs_to :location, class_name: "UserLocation"
end
user = User.find(1)
user.location.id # returns nil
Do I have to tell me model how to find the association in the UserLocation model?
Be sure you have user_location_id as foreign_key to user table.
You can add it to you association.
belongs_to :location, class_name: "UserLocation", foreign_key: "user_location_id"
I hope this help you.

Possible to alias a belongs_to association in Rails?

I have a model with a belongs_to association:
class Car < ActiveRecord::Base
belongs_to :vendor
end
So I can call car.vendor. But I also want to call car.company! So, I have the following:
class Car < ActiveRecord::Base
belongs_to :vendor
def company
vendor
end
end
but that doesn't solve the assignment situation car.company = 'ford', so I need to create another method for that. Is there a simple alias mechanism I can use for associations? Can I just use alias_method :company, :vendor and alias_method :company=, :vendor=?
No it doesn't look for company_id for instance change your code as follows
In Rails3
class Car < ActiveRecord::Base
belongs_to :vendor
belongs_to :company, :class_name => :Vendor,:foreign_key => "vendor_id"
end
In Rails4
We can use alias attribute.
alias_attribute :company, :vendor
In Rails 4, you should simply be able to add alias_attribute :company, :vendor to your model.
Short Version:
Generate model with migration
$ rails generate model Car vendor:references name:string ...
Add following line in Car model i.e car.rb file
class Car < ActiveRecord::Base
belongs_to :company, :class_name => 'Vendor', :foreign_key => 'vendor_id'
end
Now you have #car.company instance method.
For a Detailed explanation read ahead [Optional if you understood the above !!]
Detailed Version:
The model Car will have an association with the model Vendor (which is obvious). So there should be a vendor_id in the table cars.
In order to make sure that the field vendor_id is present in the cars table run the following on the command line. This will generate the right migration. The vendor:references is important. You can have any number of attributes after that.
$ rails generate model Car vendor:references name:string
Or else in the existing migration for create_table :cars just add the line t.references :vendor
class CreateCars < ActiveRecord::Migration
def change
create_table :cars do |t|
t.string :name
...
t.references :vendor
t.timestamps
end
end
end
The final thing that you need to do is edit the model Car. So add this code to your car.rb file
class Car < ActiveRecord::Base
belongs_to :company, :class_name => 'Vendor', :foreign_key => 'vendor_id'
end
After you do the third step you will get the following instance methods for the model Car provided by Rails Associations
#car.company
When you do #car.company it will return a #<Vendor ...> object. To find that #<Vendor ...> object it will go look for the vendor_id column in the cars table because you have mentioned :foreign_key => 'vendor_id'
You can set the company for a car instance by writing
#car.company = #vendor || Vendor.find(params[:id]) #whichever Vendor object you want
#car.save
This will save the id of that Vendor object in the vendor_id field of the cars table.
Thank You.
class Car < ActiveRecord::Base
belongs_to :vendor
belongs_to :company, :class_name => :Vendor
end

How do I use an in-memory object when working with :has_many and :belongs_to

I am using Active Record with Rails 3. I have a class User that has_many Categories. I would like to instantiate categories from user that shares the same in-memory user instance.
The default Active record behavior creates a new instance of user for each category. There is any way to change this behavior?
Here are some snippets of my code. You can notice that I tried to use :inverse_of, without success...
class Category < ActiveRecord::Base
attr_accessor :name
attr_accessible :name, :user
belongs_to :user, :inverse_of => :categories
end
class User < ActiveRecord::Base
attr_accessor :key
attr_accessible :categories, :key
has_many :categories, :inverse_of => :user
end
I have the following spec to test the desired behavior:
user = User.first
user.key = #key # key is an :attr_accessor, not mapped to db
category = user.categories.first
category.user.key.should == user.key # saddly, it fails.. :(
Any suggestions?
Thank you for reading my question!

Resources