+ -
当前位置:首页 → 问答吧 → 二进制运算在扩展组合属性中的使用

二进制运算在扩展组合属性中的使用

时间:2009-05-07

来源:互联网

本帖最后由 xuer 于 2009-5-7 17:01 编辑

注意:这里的格式可能比较乱,请大家下载PDF手册阅读!




关键词 : 扩展属性 扩展会员组 扩展角色 二进制运算 检索


这里的所说的扩展属性, 最常见就是以下几种情况:


1.某个会员不但属于一个主会员组, 而且从属多个扩展会员组, 如何以更好的方式保存某个会员的扩展组,并可以非常方便地查阅出某个扩展组下面的所有用户?或很方便地检索某个用户是否属于某扩展会员组?


2.基于角色的权限控制系统(RBAC)很是流行。系统中有很多角色,但是某个用户拥有多个角色,以何种格式保存用户的角色信息?并可以更方便地检索出某个角色下的所有用户,或判断用户是否属于某个角色?


3.一篇文章属于某个主栏目,但同时又属于其它多个栏目(扩展栏目),如何保存其扩展栏目信息? 在检索文章时,很容易地检索出属于某个栏目的文章?(扩展栏目)
4.在CMS系统中,往往存在推荐位,一篇文章往往属于多个推荐位,如何保存这些每个文章的推荐位信息?并在非常方便地检索出某个推荐位下的所有文章?


以上的问题,都是笔者在开发系统中遇到过的问题.


一 . 在这种一对多的关系中,数据库字段一般的处理方法都是将这些信息保存以逗号分隔(或其它符号相隔)的字符串,如第一个问题,最常见普通的数据库结构可能是这样的:
----会员分组表user_group----


groupid             name


1                    管理员


2                    注册会员


3                    VIP会员


4                    收费会员
5                    金牌会员


6                    企业会员



-----会员表 user_list ---


userid       username   groupid   extgroup


1             lee        5          3,4,6


2             sandy      6          2,3,4



简单解释一下,lee主会员组是5,即金牌会员,但他同时属于组3,4,5,6 ,我们用逗号分隔的组ID保存了其扩展组信息; 同理sandy的主会员组是6,但他同时又属于组2,3,4,5 , 我们也用逗号分隔的组ID保存了他的扩展组信息;


但现在存在一个检索的问题


1 .如何检索出所有收费会员(属于组4)? 看看上面的表,给我们检索带来问题,我们无法从上面的表中直接检索出这个信息!


2 .假设组ID是X, 如何判断lee是否属于组X ? 这个比上面简单些,先取出lee的groupid和extgroup,进行合并,得到了3,4,5,6这样的字符串,如果使用PHP程序,你可能需要以下代码片断($X表示需要判断的组的ID):



$groupids = '3,4,5,6' ;  //这是从用户lee 的组信息,经过数据库查询取得


if(in_array($x,explode(',',$groupids))) {


//用户属于组$x


}


//不要被上面的代码迷惑了,简化一下表示为:


$groupids = '3,4,5,6' ;


$groupids = explode(',',$groupids);


if(in_array($x,$groupids)) {


//用户属于组$x


}



如果你看不懂上面的代码,建议你先学一下相关知识.



好,现在新的问题产生了! 我如何取得所有收费会员(属于会员组4)列表?看看,从上面的表中,因为扩展组信息不可直接查询,所以我们无法直接得到这些信息,很难受,是吧?你可能想到遍历所有会员记录,对一个有成千上万会员的表来说,这样做的效率可想而知!



所以,针对这个问题,我们重新设计user_list表的extgroup字段,让其成为int/bigint类型,其保存的值如下:


----会员表 user_list ---


userid       username   groupid   extgroup   extgroupids            


1             lee         5          88          3,4,6


2             sandy       6          28                          2,3,4



大家可能迷惑了,88和28是怎么来的?
88 == 23|24|26


28 == 22|23|24


乘幂并进行二进制位的或运算,写成php代码:


88 == pow(2,3)|pow(2,4)|pow(2,6)


28 == pow(2,2)|pow(2,3)|pow(2,4)


如果写成更直接的位运算符:
88 == (2<<2)|(2<<3)|(2<<5)


28 == (2<<1)|(2<<2)|(2<<3)


记住了,某个数左移一位,相当于它的值乘以2, 左移两位,相当于乘以4,两种方法都是殊途同归,曲径通幽的.


当然用逗号分隔的组ID信息,还是有用的,因为我们无法通过88很容易得到3,4,6这样的结果,所以为了修改用户信息,我们还是需要保存一份的.



现在,如何取得所有属于组4的用户呢?非常好办了:


select * from user_list where 4&(pow(2,groupid)) or (extgroup!=0 and 4&(extgroup)) ;


或者简化一下:


select * from user_list where 4&(pow(2,groupid)



不要迷惑了,因为group是十进制,我们需要将其进行二进制运算,才能计算出正确的二进制的或结果,后面的4&(extgroup)就是检查扩展组了,现在,觉得是不是很方便,so easy .



这样,我们就很方便地查阅到了某个会员组(包含扩展组)下的所有会员了.


现在假设会员M的主会员组是 $X, 扩展会员组是$Y(已经通过或运算得到的结果,非逗号分隔的字符串), 我们需要检查M是否属于组$Z(会员组ID为$Z) ,如何做?


if(pow(2,$Z)&(pow(2,$X)|($Y)))  {


//会员属于组$Z


}



因为在这里$X, $Z都是原始的整数(没有通过二进制运算),所以需要进行二进制转化运算.



如果深入到基本原理,可以这样认为,一个32bit的整数,共有32个二进制位,那么每一个二进制位都可以用来表示一组信息,因为系统中不可能有重复组ID,故我们进行pow(2,$x)运算,得到一个二进制位的点位符,不会造成二进制位数据覆盖, 说简单一些就是:


pow(2,$x)!=pow(2,$y) 当且仅当$x!=$y,这里的$x, $y相当于会员组ID号,它们不可能有重复.



所以,如果使用inti类型,就是说某个用户可以属于多达32个组:pow(2,32),这在一般系统中已经完全够用,不必担心. 如果不够,请使用long int类型吧.



二 . 回到我们的RBAC(基于角色的权限控制)


在类似的系统中,我们往往会记录某个模块所能操作的角色ID, 我们当然记录角色ID的位运算结果,作为主要的检索条件,可能你的权限会这样设计表:


--- 权限表 user_priv----


id       module          role     roleids


1         report          11       1,2,8


2         print            8        8



如果不明白11和8是怎么来的,请看第一节的说明,已经很详细了,此处不再赘述.


假设当前用户角色ID为8,16,那么可以这样查询出当前用户可以操作的模块:


<?php


$_role = pow(2,8)|pow(2,16);


$sql = "select id,module from user_priv where {$_role}&role";



//查询当前用户是否有权限操作模块$module


$sql = "select id,module from user_priv where module='$module' and  {$_role}&role";



?>


不要惊讶,权限检测现在就是这么简单快速!


三 . 回到文章属于多个栏目的问题,在这种系统中,你必然要设计一个栏目(有的也叫分类)表(至于是否无限检分类,与这里的关系不大),如下结构:


-- 文章栏目表 category--


catid       name


1            财经


2            法制


3            人物专访


4            环境


5            人文


6             旅游


--  文章表article  --


id            catid        title         content    extcatid       catids


12            2           法制文章标题 内容        0               


13            3            ...           ...           3             1,2


14            2            ...           ...           48            4,5,6



id为4的文章,主栏目是2,但同时属于栏目4,5,6 ,48==pow(2,4)|pow(2,5)|pow(2,6)



那么,现在要检索属于栏目4的所有文章(包含扩展栏目):


"select * from article where catid=4  or (extcatid!=0 and pow(2,4)&(extcatid))" ;


两部分,一个是检测主栏目,后台是检测扩展栏目,使用extcatid!=0进行了约束,否则,查询出的结果就会有问题了.


或使用下面的方式:


"select * from article where pow(2,4)&(pow(2,catid)|extcatid)" ;


将主栏目ID与扩展栏目先合并,再计算.



看看,多简单. phpcms2008的方法是建立了一个新的表,用来处理这个问题,其实,它如果使用这个方法,会减少不少的表呢.



说到这里,就会有一个问题了,我刚才说到的32位,如果您的栏目很多,多达几十个呢?这时pow(2,50)==1.1258999068426e+015, 是一个非常大的数了,bigint最大值大约是184千亿亿,宽度是20位



大约栏目ID为67的时候,已经是极限,那么用这种方式, 这无疑就是一个限制了.


但另一方面,如果67个栏目还不够用, 你应该考虑一下,是否你的系统架构存在问题?



是否需要建立分站?或是子系统?



四 . 推荐位的问题.每个文章都可能属于多个推荐位,每个推荐位都有一个唯一的数字ID , 这与上面的问题在本质上完全一样的,不过同时将文章所属于的各个推荐位ID进行2的升幂运算后,再或运算得到一个结果,保存下来就可以了.



记住一个原则:扩展属性使用pow(2,val)|pow(2,val) 运算后保存,查询时使用


where pow(2,value) & extvalue 来查询 .



如果您的理解或使用此种方式中有什么疑惑,请联系我,欢迎一起探讨,笔者的信箱是[email protected]




月 影  2009-5-7 于北京  

二进制在扩展属性中的使用.pdf (163.7 KB)

下载次数:11

2009-5-7 17:01

作者: xuer   发布时间: 2009-05-07

先顶了再看

作者: laopi   发布时间: 2009-05-20