#189 Embedded Association
- Download:
- source codeProject Files in Zip (106 KB)
- mp4Full Size H.264 Video (18.2 MB)
- m4vSmaller H.264 Video (13.5 MB)
- webmFull Size VP8 Video (38.8 MB)
- ogvFull Size Theora Video (25.1 MB)
在上一集我们创建了一个基于角色的权限验证系统。在这个应用的注册页面上有几个复选框(checkbox),它用来为我们的用户指定一个或多个角色。
在这个应用中有一个模型Role
,它和模型User
通过一个关联表进行了多对多关联。如下图,在数据库中有三条roles的数据。
>> Role.all +----+-----------+-------------------------+-------------------------+ | id | name | created_at | updated_at | +----+-----------+-------------------------+-------------------------+ | 1 | Admin | 2009-11-16 21:22:59 UTC | 2009-11-16 21:22:59 UTC | | 2 | Moderator | 2009-11-16 21:23:06 UTC | 2009-11-16 21:23:06 UTC | | 3 | Author | 2009-11-16 21:23:16 UTC | 2009-11-16 21:23:16 UTC | +----+-----------+-------------------------+-------------------------+ 3 rows in set
现在的主要问题是数据库中的角色数据和文件authorization_rules
中为角色定义的权限代码产生了紧密耦合
role :author do includes :guest has_permission_on :articles, :to => [:new, :create] has_permission_on :articles, :to => [:edit, :update] do if_attribute :user => is { user } end end
如果这样定义角色和权限,我们就不能在不修改ruby代码的情况下去给角色表做修改,否则将角色存储在数据库中的优势就没有了。下面将演示如何去除耦合,将角色只定义在代码中,和数据库脱离关系。
我们不再需要模型Role
,在模型User
中,也将把下面两行代码删掉。
has_many :assignments has_many :roles, :through => :assignments
做完这些我们需要创建角色,因为角色和用户是关联的,所以我们在模型User
中定义一个ROLE的常量
class User < ActiveRecord::Base acts_as_authentic has_many :articles has_many :comments ROLES = %w[admin moderator author] def role_symbols roles.map do |role| role.name.underscore.to_sym end end end
现在,当我们想要修改角色的时候只需要修改这个常量就可以了。但问题仍然没有全部解决:如何将角色和用户进行关联?由于在数据库中没有了角色表,我们不得不在其他地方将角色和模型User
进行关联。
下面我们将演示两种关联类型不同的做法。
嵌入一对多关联
目前在我们的应用中,角色和用户是多对多的关联关系。现在我们将其修改为一对多关系,使用户只能是角色中的一种。角色将存储在数据库表users
当中,因为现在的角色是一个单一的值,所以我们只需要一个字符型的字段来存储它。现在让我们生成数据库迁移文件
script/generate migration add_role_to_users role:string
执行文件为表添加一个新的字段
rake db:migrate
下一步是在注册页面的表单中使用下拉菜单(select menu)代替先前使用的复选框(checkbox),像下面代码一样修改/app/views/users/new.html.erb
<%= form.label :roles %> <% for role in Role.all %> <%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role) %> <%=h role.name %><br /> <% end %>
修改为
<%= form.label :role %> <%= form.collection_select :role, User::ROLES, :to_s, :humanize %>
我们使用collection_select
来生成一个下拉菜单(select menu),这个方法通常需要一个模型做参数,但在这这样做也没有问题。当我们将参数设置为模型的时候,通常我们是传递一串模型对象数组和这些对象的属性来设置下拉菜单的值(options的value)和显示字符(options的innerText),现在我们传递了在模型User中定义的角色数组,我们为下拉菜单的值设置为角色.to_s
方法和为显示字符设置调用:humanize
方法。最终显示如下,我们只能选择一个角色。
像我们在前面声明的权限验证一样,这里我们需要对方法role_symbols稍作修改,让它返回一个symbol类型
def role_symbols [role.to_sym] end
现在我们创建一个用户,并且把角色存储到数据库表Users
的role
字段中
>> User.last.role => "moderator"
多对多关联
现在我们在一个模型中实现了一对多关系,但是在不恢复模型Role
的情况下,我们如何实现一个用户指定多个角色的功能呢?这稍微有点复杂,我们可以把多个值序列化到表users的一个字段中去
一个解决方案是我们为表users
建立一个text类型的字段roles
,然后使用Rails的serialize
方法对要存储的用户角色进行封装。这个方法在数据进入数据库之前会对字段调用to_yaml
方法。这个方法确实能够达到存储多个角色的目的,但它在遇到根据指定角色进行查询的时候就行不通了。所以我们还得找一个其他的方法
取而代之的是我们使用一种叫位掩码(bitmask)的方法,它能够实现在一个integer类型的字段中存储多个值,并且能够根据角色查询出指定的用户来。
首先我们要为表users添加一个新字段--roles_mask
,integer类型
script/generate migration add_roles_mask_to_users roles_mask:integer
执行迁移文件
rake db:migrate
为了方便位掩码(bitmask)和角色数组的转换,在模型User
中我们需要为roles_mask
写一个getter方法和一个setter方法
def roles=(roles) self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum end def roles ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? } end
这个setter方法把一个角色数组转换为位掩码(bitmask)然后赋值给roles_mask
。getter方法通过遍历返回在位掩码中存储的角色。有一些插件可以完成这个工作,但因为在这我们只有一个字段需要处理并且也不需要写太多代码,所以我们就没有引入进来。
我们需要修改role_symbols
方法来实现修改角色。如下代码,我们可以这样将角色数组转换为symbol类型。
def role_symbols roles.map(&:to_sym) end
最后一步,我们还得修改视图代码,我们仍需要使用checkbox这样在用户注册的时候可以选择多个角色
<%= form.label :roles %> <% for role in User::ROLES %> <%= check_box_tag "user[roles][]", role, @user.roles.include?(role) %> <%= h role.humanize %> <% end %> <%= hidden_field_tag "user[roles][]"%>
这里创建多对多关系使用的checkbox和第17集[视频 文章]有类似之处。user[roles][]
最后的这个空的方括号的作用是:当我们对表单进行提交时系统会把选中的checkbox当做数组传递给模型User
,User再对它们进行转换操作。check_box_tag
方法的最后一个参数是当用户已经是某个角色的时候,这个checkbox默认会选中。隐藏表单确保即使没有给改用户分配角色系统也会传递一个空数组到后台。
让我们回到注册页面,现在我们注册一个新用户并给他分配两个角色。
我们可以在控制台查看这个用户的角色以及为它分配的位掩码(roles_mask
)
>> User.last.roles => ["admin", "author"] >> User.last.roles_mask => 5
每一个角色的值都会有两种情况(Each role has a value twice that of the role next to it),最小值是1.所以5是通过公式(1*1) + (2*0) + (4*1)
计算出来的。
下面的问题是,如何才能找出某种角色的所有用户呢?假设我们想要找出所有角色是moderator的用户。我们可以为我们的模型User
添加如下代码
named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} }
这个named scope需要一个角色作为参数,根据这个参数按位操作去检查某一个用户是否具有这个角色。我们可以在控制台测试一下,如下图,我们查找所有角色为admin的用户。
>> User.with_role("admin") +----+----------+-------------+-------------+-------------+-------------+------------+ | id | username | email | crypted_... | password... | persiste... | roles_mask | +----+----------+-------------+-------------+-------------+-------------+------------+ | 6 | paul | paul@tes... | cffada11... | FDGoNtM1... | 35a7d8c8... | 5 | +----+----------+-------------+-------------+-------------+-------------+------------+ 1 row in set
结果返回了自从我们创建字段roles_mask
后添加的匹配用户。
当你需要再添加新角色的时候就要小心了,因为前面我们设置的位掩码(bitmask)是基于角色数组ROLES
位置的,如果你在这个数组的开始或中间添加一个新的角色,那么已经存在的角色也将会受到影响。
ROLES = %w[admin moderator author editor]
这一集到此结束。位掩码(bitmasking)在应用于多对多关系时非常好用,它仅需要一个integer类型字段。你可能会说,这只有在记录不属于数据库的时候才有用。我们的角色列表是绑定到代码当中的,在我们无需修改角色的情况下把角色列表定义到别处是没有意义的。如果你想在不改变代码的情况下添加或修改角色,你可以将它们存储到多对多关系数据库也是个不错的主意。