用一张表来存储数据状态,并且可以进行多状态精确查询;使用二进制来表示数据状态,并且是可以无顺序的状态;解决使用中间表来存储数据的多状态;数据状态还可以这么玩;

使用二进制的方式来表示数据状态(支持无顺序状态)

[toc]

1. 背景介绍

我将分享一个案例,引发思考。该方案拥有多种解决方案,所以各有优势,也各有缺点,读者自行思考,自行选择解决方案,我主要想给大家分享 “使用二进制的方式表示数据状态” 这一类解决方案。下面,我将提出一个案例,大家可以想一下可以用何种方式来解决这个问题。

2. 通过一个案例引发思考

我们在大学实习的时候,肯定是需要签订《三方协议》的,该协议涉及三方,即:“学校”、“自己”、“实习公司”,我们将这三方假设为三个用户。

在这里插入图片描述

我们在签订三方协议的时候,是没有签订顺序的,谁都可以先盖章签字。那么,在这种背景下,我们可以如何设计我们的系统,来表示这种无顺序的状态呢?

2.1 当签章有顺序时,我们是如何设计的?

如果《三方协议》的签订是有顺序的,假设顺序为 “自己” --> “实习公司” --> “学校”。那么我们会如何设计?

其实,一张表即可完成设计

| contract_id | sign_status |

| :-- | :-- |

| 1 | 0 |

contract_id :三方协议合同id

sign_status:合同签订状态:0-未签订、1-学生已签订、2-实习公司已签订、3-学校已签订

当状态为 3 的时候,即可认定为该合同已签订完毕。这就是有顺序的设计方式。

但是我们生活中的签订,却是无顺序的。

2.2 当签章顺序无法控制时,我们是如何设计的?

由于我们无法控制谁先签,谁后签,所以原本的设计方案就不可行了。

使用二进制表示状态,是什么意思?

我们有 3 个用户,对应二进制 000 ,若有一方签订了,则将属于他的那个 0 设置为 1 ,即可表示该用户已签订。假设二进制的 3 个 0 ,从左到右分别对应:学校、实习公司、学生。此时的二进制状态为 000 ,数据库存储的十进制状态为 0 ,代表未签订状态。

| contract_id | sign_status |

| :-- | :-- |

| 1 | 0 |

contract_id :三方协议合同id

sign_status:合同签订状态:000-未签订、001-学生已签订、010-实习公司已签订、100-学校已签订

在这里插入图片描述

接着,假设学生第一个签章了,那么就将属于学生的那个 0 ,改为 1 ,此时的二进制状态为 001,对应数据库十进制状态为 1 ,代表学生已签章。如图所示:

| contract_id | sign_status |

| :-- | :-- |

| 1 | 1 |

在这里插入图片描述

接着,学校开始签章,二进制状态变为 101 ,对应数据库存储的十进制状态为 5

| contract_id | sign_status |

| :-- | :-- |

| 1 | 5 |

在这里插入图片描述

最后是实习公司签章:

| contract_id | sign_status |

| :-- | :-- |

| 1 | 7 |

在这里插入图片描述

可以看出,当二进制状态为 111 ,即十进制为 7 的时候,表示《三方协议》已签章完毕。

3. 无顺序状态改变的问题解决了,那么我们如何进行搜索呢?

3.1 案例介绍

同样的,我介绍一个案例供大家思考。

我们现在有一个系统,是录入新闻信息,然后用户可以在前台根据分类查看新闻。假设前台的新闻分类有:0-全部、1-股票、2-理财、3-黄金、4-基金。我们在后台新增的新闻,他的分类可以是多选的,如 ["黄金","基金"],在这种情况下,前台用户可以在 全部、黄金、基金 这 3 个分类下面找到该新闻。

3.2 未使用二进制状态的数据库设计

一般这种情况,我们会使用一张中间表,来存储新闻类别(也可以使用字符串字段来表示,如"1,2,3,4",但是我个人认为这样做不优雅。假设,我们类别多了,我需要搜索分类id为 2 的新闻信息,如果使用 contrains('2') 关键字,请问,是否会搜索出 分类id=12 的分类信息。当然,我只是举了这一个例子,无论采用何种方法,我认为使用字符串表示状态,在进行搜索的时候都不够优雅)

在这里插入图片描述

3.3 使用二进制状态的数据库设计

如果前端嫌二进制处理麻烦,我们可以使用 null-全部、1-股票、2-理财、3-黄金、4-基金 ,来对接前端,使用二进制状态对接数据库。

我们可以参照上面《三方协议》的案例,使用二进制来表示新闻的状态

| news_id| type|

| :-- | :-- |

| 1 | 7 |

news_id :新闻id

type:新闻类型:0001-股票、0010-理财、0100-黄金、1000-基金

当新闻即为黄金分类,又为基金分类的时候,他的二进制状态为:1100,对应十进制:12

在这里插入图片描述

1. 我们如何将前端传递的 [1,2,3,4] 对应数据库的表现形式?【★】

前端传递的参数:null-全部、1-股票、2-理财、3-黄金、4-基金

数据库保存的状态:0001-股票、0010-理财、0100-黄金、1000-基金

我们可以看出,前端传递的数字,其实就是对应数据库二进制状态从右开始数,第n个1的位置,因此,我们需要写一个工具类,使他们可以进行相互转换,即:

[1,2,3,4] ⇒ 15

7 ⇒ [1,2,3]

工具类请点击查看:2.工具类。下面我将介绍工具类的实现原理,感兴趣的朋友可以看一看。

1.1 位偏移运算(<<)

将 [1,2,3,4] 转为 1111 即 15

如果不懂该运算的朋友,可以去网上搜一下左偏移、右偏移。我简单介绍下。


1 << 1 = 2 (0001 向左偏移1位 = 0010。对应十进制 2)

2 << 1 = 4 (0010 向左偏移1位 = 0100。对应十进制 4)

2 << 2 = 8 (0010 向左偏移2位 = 1000。对应十进制 8)

3 << 1 = 6 (0011 向左偏移1位 = 0110。对应十进制 6)

右偏移同理


1 >> 1 = 0 (0001 向右偏移1位 = 0000。对应十进制 0)

2 >> 1 = 1 (0010 向右偏移1位 = 0001。对应十进制 1)

2 >> 2 = 0 (0010 向右偏移2位 = 0000。对应十进制 0)

3 >> 1 = 1 (0011 向右偏移1位 = 0001。对应十进制 1)


我们假设想要查看3-黄金分类,那么我们就需要将 3 转换为二进制数 0100,对应十进制为 4

我们假设想要查看3-黄金 或者 4-基金的分类,那么我们就需要将 [3,4] 转换为二进制数 1100,对应十进制为 12

代码实现如下


    /**

    * 将数组里面的数字对应至二进制 1 的位置(从右开始数)

    * 如:[1,3] 代表,二进制数中,第1的和第三的位置为 1,其他位置为 0,即:0101

    * 则 [1,3] 将会被转换为十进制数字:5

    *

    * @param numberList 数字列表

    * @return 二进制填充 1 后对应的十进制

    */

    public static int convert2Binary(List<Integer> numberList) {

        int number = 0;

        for (Integer cursor : numberList) {

            if (cursor == 0)

                number += 0;

            number += 1 << (cursor - 1);

        }

        return number;

    }

1.2 按位与运算(&)来进行搜索

将 12 转为 [3,4]

将前端传递的 [1,2,3,4] 转换为对应的二进制数字后,此时我们又需要用到 &运算,不懂的朋友可以去搜一下,我简单概括下,其实就是取两个数的二进制 1 的交集,如图所示

[图片上传失败...(image-1c9d6c-1588229414034)]

该运算的算法是,将指定数 & 1,从二进制数的最右边开始,若结果为 1 ,则表示该位置有值,记录下当前的位置,该位置即是对应前端的 [1,2,3,4] 的值。如图所示:

在这里插入图片描述

此时,我们便实现了互转,代码如下:


/**

* 获取 二进制 中,出现 1 的位置(从右开始数)

* 如:3 对应的二进制为 : 0011

* 则,该方法返回 [1,2]

*

* @param number 十进制数

* @return 出现 1 数字的位置

*/

public static List<Integer> find1Cursor(int number) {

    if (number < 0)

        return new ArrayList<>();

    List<Integer> cursorList = new ArrayList<>();

    int cursor = 0;

    if (number == 0) {

        cursorList.add(cursor);

        return cursorList;

    }

    while (true) {

        if (number == 0)

            break;

        //移动坐标

        ++cursor;

        //如果低位二进制有 1 值,则将坐标保存到数组中

        if ((number & 1) == 1) {

            cursorList.add(cursor);

        }

        number >>= 1;

    }

    return cursorList;

}

2.工具类【★】

<span id="title-1"></span>


/**

* 二进制转换工具

*

* @author Chimm Huang

* @author chimmhuang@163.com

* @date 2020/3/12

*/

public class BinaryUtil {

    private BinaryUtil() { }

    /**

    * 获取 二进制 中,出现 1 的位置(从右开始数)

    * 如:3 对应的二进制为 : 0011

    * 则,该方法返回 [1,2]

    *

    * @param number 十进制数

    * @return 出现 1 数字的位置

    */

    public static List<Integer> find1Cursor(int number) {

        if (number < 0)

            return new ArrayList<>();

        List<Integer> cursorList = new ArrayList<>();

        int cursor = 0;

        if (number == 0) {

            cursorList.add(cursor);

            return cursorList;

        }

        while (true) {

            if (number == 0)

                break;

            //移动坐标

            ++cursor;

            //如果低位二进制有 1 值,则将坐标保存到数组中

            if ((number & 1) == 1) {

                cursorList.add(cursor);

            }

            number >>= 1;

        }

        return cursorList;

    }

    /**

    * 将数组里面的数字对应至二进制 1 的位置(从右开始数)

    * 如:[1,3] 代表,二进制数中,第1的和第三的位置为 1,其他位置为 0,即:0101

    * 则 [1,3] 将会被转换为十进制数字:5

    *

    * @param numberList 数字列表

    * @return 二进制填充 1 后对应的十进制

    */

    public static int convert2Binary(List<Integer> numberList) {

        int number = 0;

        for (Integer cursor : numberList) {

            if (cursor == 0)

                number += 0;

            number += 1 << (cursor - 1);

        }

        return number;

    }

}

3.4 数据库的sql进行分类查询

在数据库中,我们使用&运算,数据库会返回非0的数据,

假设我们要查询 [3,4] 的分类,我们的 sql 如下:


SELECT * FROM `news` WHERE news_type & 12

只要具备 3-黄金4-基金 分类的新闻,就可以被数据库查询出来。

通用mapper可以自己定义criteria添加查询条件,如:


// 设置查询条件

Example example = Example.builder(News.class)

        .build();

example.createCriteria()

        .andCondition("news_type &", type);

4. 优缺点

优点:少建立一张表,少一次多表查询
缺点:可读性太差

5.联系作者

书写的能力还需要锻炼,我个人会经常分享一些知识,不论是否深奥,分享这些东西,一个原因是想分享,二个原因也是为了锻炼自己的书写水平,革命还尚未成功,我还需更加努力。

emailchimmhuang@163.com

微信905369866

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343