Rails 5 model custom validation with relation has many - ruby-on-rails

In my Rails 5.2 app, I have the following Model relations.
A Job has many Contracts and has many Expertises. Resulting in the following Ruby on Rails models.
job.rb
class Job < ApplicationRecord
has_many :attached_expertises, as: :expertisable, dependent: :destroy
has_many :expertises, through: :attached_expertises
has_many :attached_contracts, as: :contractable, dependent: :destroy
has_many :contracts, through: :attached_contracts
end
contract.rb
class Contract < ApplicationRecord
has_many :attached_contracts, dependent: :destroy
end
attached_contract.rb
class AttachedContract < ApplicationRecord
belongs_to :contract
belongs_to :contractable, polymorphic: true
end
expertise.rb
class Expertise < ApplicationRecord
has_many :attached_expertises
end
attached_expertise.rb
class AttachedExpertise < ApplicationRecord
belongs_to :expertise
belongs_to :expertisable, polymorphic: true
end
I need to perform a validation in Job depending in the values of the associated models.
I have 3 types of Contracts and 3 types of Expertises as well.
Lets say contracts can be P, C or E, and Expertises J, L, S.
When creating or updating a Job if Contract of type E is selected, Expertise must be of type J, otherwise if should raise an error.
I have tried this creating custom validation ActiveModel::EachValidator. But was not able to get it to work. Neither with a custom method in the model it self.
Whats is the best way to achieve this?
And where should I place the validator file? In app/models/concerns/?

You can use validates_with and write a custom method to do the checking
class Person < ActiveRecord::Base
validates_with GoodnessValidator
end
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
record.errors[:base] << "This person is evil"
end
end
end

Related

Can a scope that looks up on a polymorphic be turned into an association between the two models?

Suppose an Invoice belongs_to Invoiceable, a polymorphic, being the possible invoiceable_types Subscription, SubscriptioCart and Purchase.
The Invoice table has the columns invoiceable_type and invoiceable_id. So for example, if I want to retrieve all Invoices related to a SubscriptionCart through the polymorphic, I can do Invoice.where(invoiceable_type: "SubscriptionCart").
Now how can I transform such scope into a direct association between Invoice and a SubscriptionCart through the polymorphic?
I've tried adding belongs_to :subscription_cart to the Invoice model, resulting in #invoice.subscription_cart returning nil .
This makes sense as the table invoices doesn't have a column subscription_cart_id (nor should it, as that's why we use the polymorphic).
But how do I specify what to look for in Invoiceable then?
I've tried class_name: :SubscriptionCart and foreign_key: subscription_cart_id but it still returns nil.
Stripped down models:
Invoice model:
class Invoice < ApplicationRecord
belongs_to :invoiceable, polymorphic: true
scope :subscription_cart, -> {
where(invoiceable_type: "SubscriptionCart")
}
end
SubscriptionCart model:
class SubscriptionCart < ApplicationRecord
include ::Invoiceable::Subscription
belongs_to :subscription
has_many :invoices, as: :invoiceable
end
Subscription model:
class Subscription < ApplicationRecord
include ::Invoiceable::Subscription
belongs_to :user
has_many :subscription_carts, dependent: :restrict_with_exception
has_many :invoices, as: :invoiceable, dependent: :restrict_with_exception
end
Invoiceable concern:
module Invoiceable::Subscription
extend ActiveSupport::Concern
include Invoiceable
included do
def attributes_for_invoice_items
{}.tap do |attributes|
attributes["flat_fee"] = plan_invoice_item
attributes["delivery_price"] = delivery_price_item_invoice_item if plan.deliverable?
attributes["setup_fee"] = setup_invoice_item if setup_fee_billing_pending?
attributes["per_unit"] = per_unit_invoice_item if base_plan.per_unit?
end
end
end
end

Guarantee that model is associated to same parent

Assuming i have 3 models associated to each other:
class Farm < ApplicationRecord
has_many :horses
has_many :events
end
class Horse < ApplicationRecord
belongs_to :farm
has_many :events_horses, class_name: 'Event::EventsHorse'
has_many :events, through: :events_horses, source: :event, dependent: :destroy
end
class Event
belongs_to :farm
has_many :events_horses, class_name: 'Event::EventsHorse'
has_many :horses, through: :events_horses, source: :horse, dependent: :destroy
end
class Event::EventsHorse < ApplicationRecord
self.table_name = "events_horses"
belongs_to :horse
belongs_to :event
audited associated_with: :event, except: [:id, :event_id]
end
How to guarantee that each of the Horse belongs to same Farm as event? Possible solution is using custom validation, but i was wondering if there is some other way. I have few other models like Horse, so it force me to do custom validation method to each of them.
class Event
...
validate :horses_belongs_to_farm
private
def horses_belongs_to_farm
horses.all? {|h| h.farm_id == farm_id}
end
end
I think the model you are using is setting up too many id's between the tables that require consistency checking.
If you set the model up this way, then you don't need to validate that a horse's farm and event are consistent since the data ensures it:
class Farm < ApplicationRecord
has_many :horses
has_many :events
end
class Horse < ApplicationRecord
belongs_to :farm
has_many :events, through: :farm
end
class Event < ApplicationRecord
belongs_to :farm
has_many :horses, through: :farm
end
If you need efficient access to horses from events or events from horses, you can use joins. This gives some simplicity, clarity, and consistency.
You should also have a look at Choosing Between has_many_through and has_and_belongs_to_many.
[Edit based upon updated question and comments] Now that your model and question are a little more clear, my hunch is that putting the validation in the Event model causes redundant validations. Since your intent is to make sure that, in a given event, the horse and farm are consistent, I would put the validation in EventsHorses:
class Event::EventsHorse < ApplicationRecord
...
validate :horse_belongs_to_farm
private
def horse_belongs_to_farm
horse.farm_id == event.farm_id
end
end
As an aside, thy do you have Event::EventsHorse rather than simply have a separate model for EventsHorse?

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.

Rails 4 has_many association not working when model has 2 subclasses

I'm using Rails 4.
I have an Anomaly Records class called Ar that inherits from the following classes as follows:
class RecordBase < ActiveRecord::Base
self.abstract_class = true
end
class ArAndEcrBase < RecordBase
self.abstract_class = true
# Relations
belongs_to :originator, class_name: 'User', foreign_key: 'originator_id'
has_many :attachments
end
class Ar < ArAndEcrBase
end
I want to share some relations with a class that handles another type of records in the Ar subclass however the has_many relationship doesn't work.
The following works:
> Ar.last.originator
=> #<User id: 1, ...
The following crashes:
> Ar.last.attachments
Mysql2::Error: Unknown column 'attachments.ar_and_ecr_base_id'
For some reason the has_many relationship doesn't work well. It should look for column attachments.ar_id and not attachments.ar_and_ecr_base_id
Am I doing something wrong? Or is this a Rails bug?
Atm the only way to get the code working is to move the has_many relation to the Ar class:
class Ar < ArAndEcrBase
has_many :attachments
end
If you want several models to have association to the same other model you probably need a polymorphic association
class Picture < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
class Employee < ActiveRecord::Base
has_many :pictures, as: :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, as: :imageable
end

Dynamic has_many class_name using polymorphic reference

I am trying to associate a polymorphic model (in this case Product) to a dynamic class name (either StoreOnePurchase or StoreTwoPurchase) based on the store_type polymorphic reference column on the products table.
class Product < ActiveRecord::Base
belongs_to :store, polymorphic: true
has_many :purchases, class_name: (StoreOnePurchase|StoreTwoPurchase)
end
class StoreOne < ActiveRecord::Base
has_many :products, as: :store
has_many :purchases, through: :products
end
class StoreOnePurchase < ActiveRecord::Base
belongs_to :product
end
class StoreTwo < ActiveRecord::Base
has_many :products, as: :store
has_many :purchases, through: :products
end
class StoreTwoPurchase < ActiveRecord::Base
belongs_to :product
end
StoreOnePurchase and StoreTwoPurchase have to be separate models because they contain very different table structure, as does StoreOne and StoreTwo.
I am aware that introducing a HABTM relationship could solve this like this:
class ProductPurchase < ActiveRecord::Base
belongs_to :product
belongs_to :purchase, polymorphic: true
end
class Product < ActiveRecord::Base
belongs_to :store, polymorphic: true
has_many :product_purchases
end
class StoreOnePurchase < ActiveRecord::Base
has_one :product_purchase, as: :purchase
delegate :product, to: :product_purchase
end
However I am interested to see if it is possible without an extra table?
Very interesting question. But, unfortunately, it is impossible without an extra table, because there is no polymorphic has_many association. Rails won't be able to determine type of the Product.purchases (has_many) dynamically the same way it does it for Product.store (belongs_to). Because there's no purchases_type column in Product and no support of any dynamically-resolved association types in has_many. You can do some trick like the following:
class Product < ActiveRecord::Base
class DynamicStoreClass
def to_s
#return 'StoreOnePurchase' or 'StoreTwoPurchase'
end
end
belongs_to :store, polymorphic: true
has_many :purchases, class_name: DynamicStoreClass
end
It will not throw an error, but it is useless, since it will call DynamicStoreClass.to_s only once, before instantiating the products.
You can also override ActiveRecord::Associations::association to support polymorphic types in your class, but it is reinventing the Rails.
I would rather change the database schema.

Resources