JAVA前线
欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习
【资料图】
假设系统用户一共有三种角色:普通用户、管理员、超级管理员,现在需要设计一张用户角色表记录这类信息。我们不难设计出如下方案:
id | name | super | admin | normal |
---|---|---|---|---|
101 | 用户一 | 1 | 0 | 0 |
102 | 用户二 | 0 | 1 | 0 |
103 | 用户三 | 0 | 0 | 1 |
104 | 用户四 | 1 | 1 | 1 |
用户一具有超级管理员角色,用户二具有管理员角色,用户三具有普通用户角色,用户四同时具有三种角色。
2 发现问题如果新增加一种角色呢?可以新增一个字段:
id | name | super | admin | normal | new |
---|---|---|---|---|---|
101 | 用户一 | 1 | 0 | 0 | 0 |
102 | 用户二 | 0 | 1 | 0 | 0 |
103 | 用户三 | 0 | 0 | 1 | 0 |
104 | 用户四 | 1 | 1 | 1 | 0 |
按照上述一个字段表示一种角色设计表,功能没有问题,优点是容易理解结构清晰,但是我们想一想有没有什么问题?笔者遇到过如下问题:
在复杂业务环境一份数据可能会使用在不同场景,例如上述数据存储在MySQL数据库,这一份数据还会被用在如下场景:
检索数据需要同步一份到ES使用此表通过Flink计算业务指标订阅此表Binlog消息进行业务处理如果表结构发生变化,数据源之间需要重新对接,业务方也要进行代码修改,这样开发成本非常高。有没有办法避免此类问题?
3 解决方案我们可以使用位图法,同一个字段可以表示多个业务含义。首先设计如下数据表,userFlag字段暂时不填:
id | name | user_flag |
---|---|---|
101 | 用户一 | 暂时不填 |
102 | 用户二 | 暂时不填 |
103 | 用户三 | 暂时不填 |
104 | 用户四 | 暂时不填 |
位图每一个bit表示一种角色:
使用位图法表示如下数据:
id | name | super | admin | normal |
---|---|---|---|---|
101 | 用户一 | 1 | 0 | 0 |
102 | 用户二 | 0 | 1 | 0 |
103 | 用户三 | 0 | 0 | 1 |
104 | 用户四 | 1 | 1 | 1 |
用户一位图如下,十进制数值等于4:
用户二位图如下,十进制数值等于2:
用户三位图如下,十进制数值等于1:
用户四位图如下,十进制数值等于7:
现在可以填写数据表第三列:
id | name | user_flag |
---|---|---|
101 | 用户一 | 4 |
102 | 用户二 | 2 |
103 | 用户三 | 1 |
104 | 用户四 | 7 |
本文结合mongodb实现思路有两种:
方案一:取出二进制字段在应用层运算方案二:在数据层直接运算二进制字段4.1 用户实体用户实体对应数据表user:
@Document(collection="user")publicclassUser{@Id@Field("_id")privateStringid;@Field("userId")privateStringuserId;@Field("role")privateLongrole;}4.2 用户角色
定义枚举时不要直接定义为1、2、4这类数字,应该采用位移方式进行定义,这样使用者可以明白设计者的意图。
publicenumUserRoleEnum{//1->00000001NORMAL(1L<<0,"普通用户"),//2->00000010MANAGER(1L<<1,"管理员"),//4->00000100SUPER(1L<<2,"超级管理员"),;privateLongcode;privateStringdescription;privateUserRoleEnum(Longcode,Stringdescription){this.code=code;this.description=description;}//新增角色->位或操作//oldRole->00000001->普通用户角色//addRole->00000010->新增管理员角色//newRole->00000011->具有普通用户和管理员角色publicstaticLongaddRole(LongoldRole,LongaddRole){returnoldRole|addRole;}//删除角色->异或操作//oldRole->00000011->普通用户和管理员角色//delRole->00000010->删除管理员角色//newRole->00000001->普通用户角色publicstaticLongremoveRole(LongoldRole,LongdelRole){returnoldRole^delRole;}//是否具有某种角色->位与操作//allRole->00000011->普通用户和管理员角色//qryRole->00000001->查询是否具有管理员角色//resRole->00000001->具有管理员角色publicstaticbooleanhasRole(Longrole,LongqueryRole){LongresRole=(role&queryRole);returnqueryRole==resRole;}}4.3 数据准备
新增用户一到用户四:
db.user.insertMany([{"userId":"user1","role":NumberLong(4)},{"userId":"user2","role":NumberLong(2)},{"userId":"user3","role":NumberLong(1)},{"userId":"user4","role":NumberLong(7)}])4.4 应用层运算
应用层运算有三个关键步骤:
查询用户角色内存计算新角色更新数据库@ServicepublicclassUserBizService{@ResourceprivateMongoTemplatemongoTemplate;//查询用户publicUsergetUser(StringuserId){Queryquery=newQuery();Criteriacriteria=Criteria.where("userId").is(userId);query.addCriteria(criteria);Useruser=mongoTemplate.findOne(query,User.class);returnuser;}//新增角色publicbooleanaddRole(StringuserId,LongaddRole){//查询用户角色Useruser=getUser(userId);//计算新角色LongfinalRole=UserRoleEnum.addRole(user.getRole(),addRole);//更新数据库Queryquery=newQuery();Criteriacriteria=Criteria.where("userId").is(userId);query.addCriteria(criteria);Updateupdate=newUpdate();update.set("role",finalRole);mongoTemplate.updateFirst(query,update,User.class);returntrue;}//删除角色publicbooleanremoveRole(StringuserId,LongdelRole){//查询用户角色Useruser=getUser(userId);//计算新角色LongfinalRole=UserRoleEnum.removeRole(user.getRole(),delRole);//更新数据库Queryquery=newQuery();Criteriacriteria=Criteria.where("userId").is(userId);query.addCriteria(criteria);Updateupdate=newUpdate();update.set("role",finalRole);mongoTemplate.updateFirst(query,update,User.class);returntrue;}//查询用户是否具有某种角色publicbooleanqueryRole(StringuserId,LongqueryRole){//查询用户角色Useruser=getUser(userId);//计算是否具有某种角色returnUserRoleEnum.hasRole(user.getRole(),queryRole);}}4.5 数据层运算4.5.1 位运算操作符(1) 查询操作符
操作符 | 含义 |
---|---|
bitsAllClear | 指定二进制位全为0 |
bitsAllSet | 指定二进制位全为1 |
bitsAnyClear | 任意指定二进制位为0 |
bitsAnySet | 任意指定二进制位为1 |
下列语句可以查出用户四:
0-2三个位置全部等于1db.user.find({"role":{$bitsAllSet:[0,1,2]}})0-2任意一个位置等于1
db.user.find({"role":{$bitsAnySet:[0,1,2]}})3-7位置全部等于0
db.user.find({"role":{$bitsAllClear:[3,4,5,6,7]}})3-7位置任意等于0
db.user.find({"role":{$bitsAnyClear:[3,4,5,6,7]}})(2) 计算操作符
操作符 | 含义 | 操作 |
---|---|---|
and | 位与 | 查询角色 |
or | 位或 | 新增角色 |
xor | 位异或 | 删除角色 |
db.user.update({"userId":"user3"},{$bit:{"role":{or:NumberLong(4)}}})user4删除普通用户角色
db.user.update({"userId":"user4"},{$bit:{"role":{xor:NumberLong(1)}}})4.5.2 代码实例
@ServicepublicclassUserBizService{/**新增角色*/publicbooleanaddRoleBit(StringuserId,LongaddRole){Queryquery=newQuery();Criteriacriteria=Criteria.where("userId").is(userId);query.addCriteria(criteria);Updateupdate=newUpdate();update.bitwise("role").or(addRole);mongoTemplate.updateFirst(query,update,User.class);returntrue;}/***删除角色*/publicbooleanremoveRoleBit(StringuserId,LongaddRole){Queryquery=newQuery();Criteriacriteria=Criteria.where("userId").is(userId);query.addCriteria(criteria);Updateupdate=newUpdate();update.bitwise("role").xor(addRole);mongoTemplate.updateFirst(query,update,User.class);returntrue;}/***查询rolePosition位置全部等于0的用户**表示不具有rolePositions中所有角色的用户*/publicList5 文章总结queryRoleAllClear(List rolePositions){Criteriacriteria=Criteria.where("role").bits().allClear(rolePositions);List users=mongoTemplate.query(User.class).matching(criteria).all();returnusers;}/***查询rolePosition位置任一等于0的用户**表示不具有rolePositions中任一角色的用户*/publicList queryRoleAnyClear(List rolePositions){Criteriacriteria=Criteria.where("role").bits().anyClear(rolePositions);List users=mongoTemplate.query(User.class).matching(criteria).all();returnusers;}/***查询rolePosition位置全部等于1的用户**表示具有rolePositions中所有角色的用户*/publicList queryRoleAllSet(List rolePositions){Criteriacriteria=Criteria.where("role").bits().allSet(rolePositions);List users=mongoTemplate.query(User.class).matching(criteria).all();returnusers;}/***查询rolePosition位置任一等于1的用户**表示具有rolePositions中任一角色的用户*/publicList queryRoleAnySet(List rolePositions){Criteriacriteria=Criteria.where("role").bits().anySet(rolePositions);List users=mongoTemplate.query(User.class).matching(criteria).all();returnusers;}}
本文我们从一个简单案例开始,分析了直接新增字段的优缺点。新增字段方案遇到最多问题就是在复杂业务场景中,需要新增数据对接工作量,增加了开发维护成本。
我们又介绍了位图法,一个字段就可以表示多种业务含义,减少了字段冗余,节省了对接开发成本。同时位图法增加了代码理解成本,数据库字段含义不直观,需要进行转义,大家可以根据业务需求场景选择。
JAVA前线
欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习