简体   繁体   中英

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM