Rails Active Record - 如何为用户/配置文件场景建模

时间:2012-03-22 21:12:30

标签: ruby-on-rails ruby-on-rails-3 activerecord ruby-on-rails-3.1

我有一个rails应用程序,它有三种不同类型的用户,我需要它们共享相同的常用配置文件信息。但是,每个不同的用户本身也具有唯一的属性。我不确定如何分离出不同的领域。

  • 管理员(网站管理员)
  • 所有者(商店/等)
  • 会员(如合作社成员)

我正在使用设计进行身份验证,使用 cancan 进行授权。因此,我有一个User模型,其中包含一组可应用于用户的角色。这个课看起来像这样:

class User < ActiveRecord::Base
  # ... devise stuff omitted for brevity ...

  # Roles association
  has_many :assignments
  has_many :roles, :through => :assignments

  # For cancan: https://github.com/ryanb/cancan/wiki/Separate-Role-Model
  def has_role?(role_sym)
    roles.any? { |r| r.name.underscore.to_sym == role_sym }
  end
end

每个用户都有一个包含以下内容的个人资料:

  • 第一&amp;姓氏
  • 地址信息(city / st / zip / etc)
  • 电话

我不想用这个信息污染用户模型,所以我把它扔进了一个Profile模型。这部分很简单。这会将用户模型转换为以下内容:

class User < ActiveRecord::Base
  # ... devise stuff omitted for brevity ...
  # ... cancan stuff omitted for brevity ... 
  has_one :profile
end

其他字段是我对如何建模有一些不安的感觉......

如果用户是管理员,他们将拥有唯一的字段,例如:

  • admin_field_a:字符串
  • admin_field_b:字符串

如果用户是所有者,他们将拥有唯一的字段......

  • stripe_api_key:字符串
  • stripe_test_api_key:字符串
  • stripe_account_number:字符串
  • has_one:将#AR Refence存储到Admin和Member没有的其他模型。

如果用户是会员,他们会有一些额外的字段:

  • stripe_account_number:字符串
  • belongs_to:store#他们是
  • 成员的商店
  • has_many:note

...

并且商店模型将在成员上包含has_many,因此我们处理商店的成员。

问题在于其他字段。我将它们设置为不同的类吗?把它们变成另一种 我目前尝试了几种不同的方法来设置它:

一种方法是将用户模型设置为聚合根

class User < ActiveRecord::Base
  # ...

  # Roles association
  has_many :assignments
  has_many :roles, :through => :assignments

  # Profile and other object types
  has_one :profile
  has_one :admin
  has_one :owner
  has_one :member

  # ...
end

这种方法的好处是用户模型是根,可以访问所有内容。垮台是如果用户是“所有者”,那么“admin”和“member”引用将是nil(和其他可能性的笛卡尔 - 管理员但不是所有者或成员等)。

我想到的另一个选择是让每种类型的用户都从User模型继承:

class User < ActiveRecord::Base
  # ... other code removed for brevity
  has_one :profile
end

class Admin < User
  # admin fields
end

class Owner < User
  # owner fields
end

class Member < User
  # member fields
end

问题在于我用表中的所有类型的nil污染了User对象,其中一种类型不需要来自另一种类型/ etc的值。看起来有点乱,但我不确定。

另一种选择是将每个帐户类型创建为根,但将用户作为子对象,如下所示。

class Admin
  has_one :user
  # admin fields go here. 
end

class Owner
  has_one :user
  # owner fields go here. 
end

class Member
  has_one :user
  # member fields go here. 
end

上面的问题是我不确定如何在用户登录后加载正确的类。我将拥有他们的user_id,我将能够分辨他们是哪个角色(因为角色)关于用户模型的关联),但我不确定如何从用户UP转到根对象。方法?其他?

结论 我有几种不同的方法可以做到这一点,但我不确定正确的“轨道”方法是什么。在rails AR中对此进行建模的正确方法是什么? (MySQL后端)。如果没有“正确”的方法,那么上述最好的方法(我也对其他想法持开放态度)。

谢谢!

3 个答案:

答案 0 :(得分:6)

我的回答是假定给定用户只能是一种类型的用户 - 例如只有管​​理员或只有会员。如果是这样,这似乎是ActiveRecord's Polymorphic association的完美工作。

class User < ActiveRecord::Base
  # ...

  belongs_to :privilege, :polymorphic => true
end

此关联为User提供了一个称为“特权”的访问者(缺少更好的术语,并避免命名混淆,这将在以后变得明显)。因为它是多态的,所以它可以返回各种类。多态关系需要在相应的表上有两列 - 一列(accessor)_type(accessor)_id。在我的示例中,User表将获得两个字段:privilege_typeprivilege_id,ActiveRecord组合这些字段以在查找期间查找关联的条目。

您的管理员,所有者和会员类看起来像这样:

class Admin
  has_one :user, :as => :privilege
  # admin fields go here. 
end

class Owner
  has_one :user, :as => :privilege
  # owner fields go here. 
end

class Member
  has_one :user, :as => :privilege
  # member fields go here. 
end

现在你可以这样做:

u = User.new(:attribute1 => user_val1, ...)
u.privilege = Admin.new(:admin_att1 => :admin_val1, ...)
u.save!
# Saves a new user (#3, for example) and a new 
# admin entry (#2 in my pretend world).

u.privilege_type  # Returns 'Admin'
u.privilege_id    # Returns 2

u.privilege       # returns the Admin#2 instance.
# ActiveRecord's SQL behind the scenes: 
#    SELECT * FROM admin WHERE id=2 

u.privilege.is_a? Admin   # returns true
u.privilege.is_a? Member  # returns false

Admin.find(2).user        # returns User#3
# ActiveRecord's SQL behind the scenes: 
#    SELECT * FROM user WHERE privilege_type='Admin' 
#    AND privilege_id=2

如果您希望它是一组已知的值,我建议您将数据库上的(accessor)_type字段设为ENUM。一个ENUM,恕我直言,比Rails通常默认的VARCHAR255更好的选择,更容易/更快/更小的索引,但当你有数百万用户时,更难以/耗时更改。此外,正确索引关联:

add_column :privilege_type, "ENUM('Admin','Owner','Member')", :null => false
add_column :privilege_id, :integer, :null => false

add_index :user, [:privilege_type, :privilege_id], :unique => true
add_index :user, :privilege_type

第一个索引允许ActiveRecord快速查找反向关联(例如,查找具有Admin#2权限的用户),第二个索引允许您查找所有管理员或所有成员。

这个RailsCast有点陈旧,但仍然是关于多态关系的好教程。

最后一个注意事项 - 在您的问题中,您表示管理员,所有者或成员是用户的类型,这是足够合适的,但正如您可能看到的,我必须解释您的用户表将具有{{1 }} field。

答案 1 :(得分:1)

我可能不会给你一个铁路批准的建议,但是..

分享您的个人资料是一个很好的电话。考虑使用Decorator pattern作为角色。您可以拥有AdminUserDecorator,OwnerUserDecorator或MemberOwnerDecorator。您还可以直接在实例上动态添加其他字段(毕竟它是Ruby),但我认为这会变得丑陋和复杂。 (如果你真的想做坏事,可以使用访问者对象从用户类的方法中为你提供装饰器的实例。)

另外,为什么要将条带或付款配置放在所有者身上而不是成为商店信息的一部分?除非业主可以拥有多家商店并为每家商店使用相同的付款信息?

更新:我还应该建议使用TDD清除可行的内容。

答案 2 :(得分:1)

您已经接受了答案,但是对于它的价值,我有类似的情况,并选择使用您的“用户模型作为聚合根”方法。我的用户模型包含所有“个人资料”信息和用户,使用虚构的示例has_one:buyer和has_one:seller。我使用一个简单的tinyint字段作为用户持有角色的位标志,因为用户既可以是买家也可以是卖家(或者我将来需要的TBD其他角色)。如果设置了一个位,你可以假设相应的关联不是nil(这对我来说不是问题,因为我总是在使用关联引用之前检查位标志)。我的真实从属模型中实际上并没有太多独特的字段,但是当每个从属模型都有其他关联时,如果卖家has_one:merchant_account“和买家has_one:purchase_history等等,这会让事情变得非常有用。我避风港但是,当我这么做的时候,我会跟着这篇文章谈论我遇到的任何问题。