具有唯一约束的 ActiveRecord 多态关联

ActiveRecord polymorphic association with unique constraint

我有一个允许用户通过多种服务(LinkedIn、电子邮件、Twitter 等)登录的网站。

我设置了以下结构来模拟 User 及其多重身份。基本上一个用户可以有多个身份,但只能有一个给定类型(例如,不能有 2 个 Twitter 身份)。

我决定将其设置为多态关系,如下图所示。基本上有一个中间 table identitiesUser 条目映射到多个 *_identity tables.

关联如下(仅针对LinkedInIdentity显示,但可以推断)

# /app/models/user.rb
class User < ActiveRecord::Base
  has_many :identities
  has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"

  ...
end


# /app/models/identity
class Identity < ActiveRecord::Base
  belongs_to :user
  belongs_to :identity, polymorphic: true

  ...
end


# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
  has_one :identity, as: :identity
  has_one :user, through: :identity

  ...
end

我 运行 遇到的问题是 User 模型。由于可以有多个身份,所以我用has_many :identities。但是,对于给定的身份类型(例如 LinkedIn),我使用 has_one :linkedin_identity ...

问题是 has_one 语句是 through: :identity,并且没有称为 :identity 的单数关联。只有复数 :identities

> User.first.linkedin_identity
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :identity in model User

有什么解决办法吗?

我会这样做 - 我已经将 Identity 和其他人之间的关系名称更改为 external_identity,因为说 identity.identity 只会让人感到困惑,尤其是当你没有得到身份记录回来。我还会对身份进行唯一性验证,这将防止为任何用户创建相同类型的第二个身份。

class User < ActiveRecord::Base
  has_many :identities
  has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
end


# /app/models/identity
class Identity < ActiveRecord::Base
  #fields: user_id, external_identity_id
  belongs_to :user
  belongs_to :external_identity, polymorphic: true
  validates_uniqueness_of :external_identity_type, :scope => :user_id
  ...
end


# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
  # Force the table name to be singular
  self.table_name = "linkedin_identity"

  has_one :identity
  has_one :user, through: :identity

  ...
end

EDIT - 而不是为 linkedin_identity 建立关联,您总是可以只使用 getter 和 setter 方法。

#User
def linkedin_identity
  (identity = self.identities.where(external_identity_type: "LinkedinIdentity").includes(:external_identity)) && identity.external_identity
end    

def linkedin_identity_id
  (li = self.linkedin_identity) && li.id
end    

def linkedin_identity=(linkedin_identity)
  self.identities.build(external_identity: linkedin_identity)
end

def linkedin_identity_id=(li_id)
  self.identities.build(external_identity_id: li_id)
end

EDIT2 - 重构以上内容以使其更加形式友好:您可以将 linkedin_identity_id= 方法用作 "virtual attribute",例如,如果您有一个像 "user[linkedin_identity_id]" 这样的表单字段,使用 LinkedinIdentity 的 ID,然后您可以按照通常的方式在控制器中执行 @user.update_attributes(params[:user])

这里有一个想法在这里非常适用于这种情况。 (我的情况有点不同,因为所有身份都在相同的 table,相同基类型的子类中)。

class EmailIdentity < ActiveRecord::Base
  def self.unique_for_user
    false
  end

  def self.to_relation
    'emails'
  end
end

class LinkedinIdentity < ActiveRecord::Base
   def self.unique_for_user
     true
  end

  def self.to_relation
    'linkedin'
  end
end

class User < ActiveRecord::Base
   has_many :identities do
      [LinkedinIdentity EmailIdentity].each do |klass|
        define_method klass.to_relation do
          res = proxy_association.select{ |identity| identity.is_a? klass }
          res = res.first if klass.unique_for_user
          res
        end
      end
   end
end

然后你可以

@user.identities.emails
@user.identities.linkedin