Bitmask
bitmask(位掩码),是利用二进制位,表示多种状态的组合,例如:4个状态的数据,有16种组合,那么就可以利用4位的二进制数,去表示这个16种组合,然后在通过按位的逻辑运算(OR,AND, XOR),来达到使用极少的空间存储和表示数据。
解决的问题
在大部分的应用程序中,我们都会遇见模型的多对多关联,比如用户和用户所在的分组,这就存在了多对多关联,如果按照正常的思路的话,我们会使用,中间表关联两张数据表,已到达多对多。但是使用关联表,会增加查询的复杂度,需要使用Join语句关联。
其实实现这个关联的办法还有一个,那就是使用bitmask,当然了,这个方法也仅限于,N:M中,M的数量不多的情况下,大概在M < 10,具体的实现方式就是:
class User < ActiveRecord::Base
5.times do |i|
const_set("GROUP#{i + 1}", 1 << i)
end
# GROUP1 = 1
# GROUP2 = 2
# GROUP3 = 4
# GROUP4 = 8
# GROUP5 = 16
end
那如果一个用户属于分组2和分组3 那么它的掩码就是:6 查询就是:
User.where(‘groups & 6 = 6’)
不在分组2和3的查询就是:
User.where(‘groups & 6 = 0’)
优点:查询语句简单,不需要二外的两张表,和join查询
缺点:使用 where groups & mask = mask 的方式是无法使用索引的,这样性能就会下降
第三种方案,升级版的bitmask
要让bitmask的查询方式使用到索引,我们可以将用 按位与的查询语句替换成待遇 in 语句的查询语句这样就可以使用到索引了。
还是原先的例子查询属于分组1和2的用户:
User.where(groups: (1...(1 << 5)).select{|x| x & 3 == 3})
这个原理就是,先生成出所有可能出现的掩码,然后在对掩码进行筛选,找出包含1和2的掩码也就是,按位与 3 得 3 的掩码,这样求出的集合就可以用于 in 语句查询了。
关于这里使用索引,根据索引的原理,被索引数据的选择性,是很注意的指标,在 bitmask中如果状态的组合数低的话,索引的效果是不明显的。
优点:不需要二外的两张表,和join查询,并且可以使用索引性能也不错。
缺点:查询的代码稍微复杂一点。不过我们可以是用Ruby的元编程,改进它
class User < ActiveRecord::Base
GROUP_SIZE = 5
GROUP_SIZE.times do |i|
const_set("GROUP#{i + 1}", 1 << i)
end
# GROUP1 = 1
# GROUP2 = 2
# GROUP3 = 4
# GROUP4 = 8
# GROUP5 = 16
BITMASKS = (1...(1 << GROUP_SIZE)).to_a
#
# Find User with groups
#
# @example
# User.with_groups(User::GROUP1, User::GROUP3)
#
# @param [Integer] group mask
# @return [ActiveRecord::Relation]
#
def self.with_groups(*groups)
mask = groups.sum
self.where(groups: BITMASKS.select {|x| x & mask == mask})
end
end
其实我们上面做的事情,已经有gem帮我们实现了bitmask-attributes
使用 bitmask_attributes
class User < ActiveRecord::Base
bitmask :groups, :as => [:group1, :group2, :group3, :group4]
end
然后我们的查询就可以使用它提供的 named scope
User.with_groups(:group1, :group2)
cancan
在 cancan 中有一个 bitmask 实际应用的例子,就是在基于角色的权限验证中,cancan 提供了,用户多角色的功能,它的默认实现就是,通过为 users 表添加一个名为 roles_mask 的 bitmask 字段,然后在 User模型中添加下面的方法,来操作角色。
class User < ActiveRecord::Base
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
def roles
ROLES.reject do |r|
((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero?
end
end
end
这个通过 bitmask 实现的角色控制,还被抽取成了单独的Gem叫role_model
总结
利用 bitmask,存储状态可以为我们节省很多的存储空间,同样它也会增加代码的复杂度,还好Ruby生态圈有很多很好的工具帮我们解决复杂度的问题,但是想利用 bitmask还是要看的实际应用场景,比如上面利用 bitmask去代替表关联的场景要谨慎使用,因为虽然减少了数据模型,但是它只能表示数量少的关联并且这样的模型设计是不符合数据库设计范式的。