用一張表來存儲數據狀態,並且可以進行多狀態精確查詢;使用二進制來表示數據狀態,並且是可以無順序的狀態;解決使用中間表來存儲數據的多狀態;數據狀態還可以這麼玩;

使用二進制的方式來表示數據狀態(支持無順序狀態)

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 的交集,如圖所示
在這裏插入圖片描述
該運算的算法是,將指定數 & 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.工具類【★】

/**
 * 二進制轉換工具
 *
 * @author Chimm Huang
 * @author [email protected]
 * @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.聯繫作者

書寫的能力還需要鍛鍊,我個人會經常分享一些知識,不論是否深奧,分享這些東西,一個原因是想分享,二個原因也是爲了鍛鍊自己的書寫水平,革命還尚未成功,我還需更加努力。
email[email protected]
微信905369866

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章