【问题标题】:ActiveRecord polymorphic association with unique constraint具有唯一约束的 ActiveRecord 多态关联
【发布时间】:2015-11-10 08:57:37
【问题描述】:

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

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

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

关联如下(仅针对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

有什么办法吗?

【问题讨论】:

    标签: ruby-on-rails activerecord models


    【解决方案1】:

    我会这样做 - 我已将 Identity 和其他人之间的关系名称更改为 external_identity,因为说 identity.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
    

    编辑 - 您可以始终只使用 getter 和 setter 方法,而不是为 linkedin_identity 建立关联。

    #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= 方法用作“虚拟属性”,例如,如果您有一个像"user[linkedin_identity_id]" 这样的表单字段,ID 为LinkedinIdentity,您可以然后以通常的方式在控制器中执行@user.update_attributes(params[:user])

    【讨论】:

    • 为什么要显式设置表名linkedin_identity?我问是因为将其保留为 linkedin_identities 对我来说很有意义。
    • 关于将其命名为 identity.identity 的混淆性质的好点。我喜欢identities 表上的唯一性约束,它可以防止为给定的user_id_type 组合创建多个记录。然而,原来的问题仍然存在——我不能打电话给user.linkedin_identity,因为该关联是通过:identity(单数),它不存在。谢谢!
    • through选项改成through :identities是否有效?>
    • @MaxWilliams - 不幸的是,Rails 不喜欢 through: 参数是复数集合 ---- ActiveRecord::HasOneThroughCantAssociateThroughCollection: Cannot have a has_one :through association 'User#linkedin_identity' where the :through association 'User#identities' is a collection. Specify a has_one or belongs_to association in the :through option instead.
    • 也许该关联不起作用:改用一种方法。查看我的编辑。
    【解决方案2】:

    这是一个非常有效的想法,例如案例。 (我的情况有点不同,因为所有 identites 都在同一个表中,是相同基类型的子类)。

    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
    

    【讨论】:

    • 有趣!我希望有一种更原生的 Rails 方法来解决这个问题,但从你的解决方案中看起来没有 :( 但我确实喜欢这种方法,所以如果没有原生方法弹出,我可能会使用它。谢谢!
    • 它实际上是原生的,因为 ActiveRecord 添加了关系扩展。检查它的文档。您可以为您的关系添加更多接口方法以获得最佳效率!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多