使用二進制的方式來表示數據狀態(支持無順序狀態)
文章目錄
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