具有唯一约束的 ActiveRecord 多态关联
ActiveRecord polymorphic association with unique constraint
我有一个允许用户通过多种服务(LinkedIn、电子邮件、Twitter 等)登录的网站。
我设置了以下结构来模拟 User
及其多重身份。基本上一个用户可以有多个身份,但只能有一个给定类型(例如,不能有 2 个 Twitter 身份)。
我决定将其设置为多态关系,如下图所示。基本上有一个中间 table identities
将 User
条目映射到多个 *_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
我有一个允许用户通过多种服务(LinkedIn、电子邮件、Twitter 等)登录的网站。
我设置了以下结构来模拟 User
及其多重身份。基本上一个用户可以有多个身份,但只能有一个给定类型(例如,不能有 2 个 Twitter 身份)。
我决定将其设置为多态关系,如下图所示。基本上有一个中间 table identities
将 User
条目映射到多个 *_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