2015-11-10 106 views
1

我有一个网站,允许用户通过多种服务(LinkedIn,电子邮件,Twitter等)登录。ActiveRecord多态关联与唯一约束

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

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

schema

的关联如下(仅用于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 

什么办法解决此问题?

回答

3

我会这么做 - 我已将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 

编辑 - 而不是作出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 =方法作为“虚拟属性”,例如,如果你有一个形式字段像"user[linkedin_identity_id]",具有LinkedinIdentity的id,然后您可以按照通常的方式在控制器中执行@user.update_attributes(params[:user])

+1

你为什么要明确设置'linkedin_identity'的表名?我问,因为离开它作为'linkedin_identities'是有道理的,因为它对我来说。 –

+0

以“identity.identity”命名的混淆本质的好处。我喜欢“身份”表上的唯一性约束,以防止给定的'user_id'和'_type'组合创建多个记录。但是,原来的问题仍然存在 - 我不能称之为用户。linkedin_identity“,因为该关联是通过”:identity“(单数),不存在的。谢谢! – user2490003

+1

如果你改变'through'选项是'通过:标识符'吗?> –

1

这里有一个想法,在这里奇妙地工作,例如案件。 (我的情况有点不同,因为所有标识符都在同一个表中,同一基类型的子类)。

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 
+0

有趣!我希望有一个更原生的Rails方式来解决这个问题,但从你的解决方案看,没有:(但我喜欢这种方法,所以如果没有本地方式弹出,我可能会使用它,谢谢! – user2490003

+0

它实际上是本地的因为ActiveRecord添加了关系扩展,请检查文档,为了获得最佳效率,您可以为关系添加更多接口方法! – muichkine