本文做者李喆明,奇舞團前端開發工程師。前端
MySQL 5.7 增長了 JSON 數據類型的支持,在以前若是要存儲 JSON 類型的數據的話咱們只能本身作 JSON.stringify()
和 JSON.parse()
的操做,並且沒辦法針對 JSON 內的數據進行查詢操做,全部的操做必須讀取出來 parse 以後進行,很是的麻煩。原生的 JSON 數據類型支持以後,咱們就能夠直接對 JSON 進行數據查詢和修改等操做了,較以前會方便很是多。mysql
爲了方便演示我先建立一個 user
表,其中 info
字段用來存儲用戶的基礎信息。要將字段定義成 JSON 類型數據很是簡單,直接字段名後接 JSON
便可。sql
CREATE TABLE user (
id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30) NOT NULL,
info JSON
);
複製代碼
表建立成功以後咱們就按照經典的 CRUD 數據操做來說講怎麼進行 JSON 數據類型的操做。數據庫
添加數據這塊是比較簡單,不過須要理解 MySQL 對 JSON 的存儲本質上仍是字符串的存儲操做。只是當定義爲 JSON 類型以後內部會對數據再進行一些索引的建立方便後續的操做而已。因此添加 JSON 數據的時候須要使用字符串包裝。json
mysql> INSERT INTO user (`name`, `info`) VALUES('lilei', '{"sex": "male", "age": 18, "hobby": ["basketball", "football"], "score": [85, 90, 100]}');
Query OK, 1 row affected (0.00 sec)
複製代碼
除了本身拼 JSON 以外,你還能夠調用 MySQL 的 JSON 建立函數進行建立。數組
JSON_OBJECT
:快速建立 JSON 對象,奇數列爲 key,偶數列爲 value,使用方法 JSON_OBJECT(key,value,key1,value1)
JSON_ARRAY
:快速建立 JSON 數組,使用方法 JSON_ARRAY(item0, item1, item2)
mysql> INSERT INTO user (`name`, `info`) VALUES('hanmeimei', JSON_OBJECT(
-> 'sex', 'female',
-> 'age', 18,
-> 'hobby', JSON_ARRAY('badminton', 'sing'),
-> 'score', JSON_ARRAY(90, 95, 100)
-> ));
Query OK, 1 row affected (0.00 sec)
複製代碼
不過對於 JavaScript 工程師來講不論是使用字符串來寫仍是使用自帶函數來建立 JSON 都是很是麻煩的一件事,遠沒有 JS 原生對象來的好用。因此在 think-model
模塊中咱們增長了 JSON 數據類型的數據自動進行 JSON.stringify()
的支持,因此直接傳入 JS 對象數據便可。markdown
因爲數據的自動序列化和解析是根據字段類型來作的,爲了避免影響已運行的項目,須要在模塊中配置 jsonFormat: true
才能開啓這項功能。async
//adapter.js
const MySQL = require('think-model-mysql');
exports.model = {
type: 'mysql',
mysql: {
handle: MySQL,
...
jsonFormat: true
}
};
複製代碼
//user.js
module.exports = class extends think.Controller {
async indexAction() {
const userId = await this.model('user').add({
name: 'lilei',
info: {
sex: 'male',
age: 16,
hobby: ['basketball', 'football'],
score: [85, 90, 100]
}
});
return this.success(userId);
}
}
複製代碼
下面讓咱們來看看最終存儲到數據庫中的數據是什麼樣的函數
mysql> SELECT * FROM `user`;
+----+-----------+-----------------------------------------------------------------------------------------+
| id | name | info |
+----+-----------+-----------------------------------------------------------------------------------------+
| 1 | lilei | {"age": 18, "sex": "male", "hobby": ["basketball", "football"], "score": [85, 90, 100]} |
| 2 | hanmeimei | {"age": 18, "sex": "female", "hobby": ["badminton", "sing"], "score": [90, 95, 100]} |
+----+-----------+-----------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
複製代碼
爲了更好的支持 JSON 數據的操做,MySQL 提供了一些 JSON 數據操做類的方法。和查詢操做相關的方法主要以下:oop
JSON_EXTRACT()
:根據 Path 獲取部分 JSON 數據,使用方法 JSON_EXTRACT(json_doc, path[, path] ...)
->
:JSON_EXTRACT()
的等價寫法->>
:JSON_EXTRACT()
和 JSON_UNQUOTE()
的等價寫法JSON_CONTAINS()
:查詢 JSON 數據是否在指定 Path 包含指定的數據,包含則返回1,不然返回0。使用方法 JSON_CONTAINS(json_doc, val[, path])
JSON_CONTAINS_PATH()
:查詢是否存在指定路徑,存在則返回1,不然返回0。one_or_all
只能取值 "one" 或 "all",one 表示只要有一個存在便可,all 表示全部的都存在才行。使用方法 JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)
JSON_KEYS()
:獲取 JSON 數據在指定路徑下的全部鍵值。使用方法 JSON_KEYS(json_doc[, path])
,相似 JavaScript 中的 Object.keys()
方法。JSON_SEARCH()
:查詢包含指定字符串的 Paths,並做爲一個 JSON Array 返回。查詢的字符串能夠用 LIKE 裏的 '%' 或 '_' 匹配。使用方法 JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
,相似 JavaScript 中的 findIndex()
操做。咱們在這裏不對每一個方法進行逐個的舉例描述,僅提出一些場景舉例應該怎麼操做。
舉這個例子就是想告訴下你們怎麼獲取 JSON 數據中的部份內容,並按照正常的表字段進行返回。這塊可使用 JSON_EXTRACT
或者等價的 ->
操做均可以。其中根據例子能夠看到 sex
返回的數據都帶有引號,這個時候可使用 JSON_UNQUOTE()
或者直接使用 ->>
就能夠把引號去掉了。
mysql> SELECT `name`, JSON_EXTRACT(`info`, '$.age') as `age`, `info`->'$.sex' as sex FROM `user`;
+-----------+------+----------+
| name | age | sex |
+-----------+------+----------+
| lilei | 18 | "male" |
| hanmeimei | 16 | "female" |
+-----------+------+----------+
2 rows in set (0.00 sec)
複製代碼
這裏咱們第一次接觸到了 Path 的寫法,MySQL 經過這種字符串的 Path 描述幫助咱們映射到對應的數據。和 JavaScript 中對象的操做比較相似,經過 .
獲取下一級的屬性,經過 []
獲取數組元素。
不同的地方在於須要經過 $
表示自己,這個也比較好理解。另外就是可使用 *
和 **
兩個通配符,好比 .*
表示當前層級的全部成員的值,[*]
則表示當前數組中全部成員值。**
相似 LIKE 同樣能夠接前綴和後綴,好比 a**b
表示的是以 a 開頭,b結尾的路徑。
路徑的寫法很是簡單,後面的內容裏也會出現。上面的這個查詢對應在 think-model
的寫法爲
//user.js
module.exports = class extends think.Controller {
async indexAction() {
const userModel = this.model('user');
const field = "name, JSON_EXTRACT(info, '$.age') AS age, info->'$.sex' as sex";
const users = await userModel.field(field).where('1=1').select();
return this.success(users);
}
}
複製代碼
mysql> SELECT `name` FROM `user` WHERE JSON_CONTAINS(`info`, '"male"', '$.sex') AND JSON_SEARCH(`info`, 'one', 'basketball', null, '$.hobby');
+-------+
| name |
+-------+
| lilei |
+-------+
1 row in set, 1 warning (0.00 sec)
複製代碼
這個例子就是簡單的告訴你們怎麼對屬性和數組進行查詢搜索。其中須要注意的是 JSON_CONTAINS()
查詢字符串因爲不帶類型轉換的問題字符串須要使用加上 ""
包裹查詢,或者使用 JSON_QUOTE('male')
也能夠。
若是你使用的是 MySQL 8 的話,也可使用新增的 JSON_VALUE()
來代替 JSON_CONTAINS()
,新方法的好處是會帶類型轉換,避免剛纔雙引號的尷尬問題。不須要返回的路徑的話,JSON_SEARCH()
在這裏也可使用新增的 MEMBER OF
或者 JSON_OVERLAPS()
方法替換。
mysql> SELECT `name` FROM `user` WHERE JSON_VALUE(`info`, '$.sex') = 'male' AND 'basketball' MEMBER OF(JSON_VALUE(`info`, '$.hobby'));
+-------+
| name |
+-------+
| lilei |
+-------+
1 row in set (0.00 sec)
mysql> SELECT `name` FROM `user` WHERE JSON_VALUE(`info`, '$.sex') = 'male' AND JSON_OVERLAPS(JSON_VALUE(`info`, '$.hobby'), JSON_QUOTE('basketball'));
+-------+
| name |
+-------+
| lilei |
+-------+
1 row in set (0.00 sec)
複製代碼
上面的這個查詢對應在 think-model
的寫法爲
//user.js
module.exports = class extends think.Controller {
async indexAction() {
const userModel = this.model('user');
const where = {
_string: [
"JSON_CONTAINS(info, '\"male\"', '$.sex')",
"JSON_SEARCH(info, 'one', 'basketball', null, '$.hobby')"
]
};
const where1 = {
_string: [
"JSON_VALUE(`info`, '$.sex') = 'male'",
"'basketball' MEMBER OF (JSON_VALUE(`info`, '$.hobby'))"
]
};
const where2 = {
_string: [
"JSON_VALUE(`info`, '$.sex') = 'male'",
"JSON_OVERLAPS(JSON_VALUE(`info`, '$.hobby'), JSON_QUOTE('basketball'))"
]
}
const users = await userModel.field('name').where(where).select();
return this.success(users);
}
}
複製代碼
MySQL 提供的 JSON 操做函數中,和修改操做相關的方法主要以下:
JSON_APPEND/JSON_ARRAY_APPEND
:這兩個名字是同一個功能的兩種叫法,MySQL 5.7 的時候爲 JSON_APPEND
,MySQL 8 更新爲 JSON_ARRAY_APPEND
,而且以前的名字被廢棄。該方法如同字面意思,給數組添加值。使用方法 JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)
JSON_ARRAY_INSERT
:給數組添加值,區別於 JSON_ARRAY_APPEND()
它能夠在指定位置插值。使用方法 JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)
JSON_INSERT/JSON_REPLACE/JSON_SET
:以上三個方法都是對 JSON 插入數據的,他們的使用方法都爲 JSON_[INSERT|REPLACE|SET](json_doc, path, val[, path, val] ...)
,不過在插入原則上存在一些差異。
JSON_INSERT
:當路徑不存在才插入JSON_REPLACE
:當路徑存在才替換JSON_SET
:無論路徑是否存在JSON_REMOVE
:移除指定路徑的數據。使用方法 JSON_REMOVE(json_doc, path[, path] ...)
因爲 JSON_INSERT
, JSON_REPLACE
, JSON_SET
和 JSON_REMOVE
幾個方法支持屬性和數組的操做,因此前兩個 JSON_ARRAY
方法用的會稍微少一點。下面咱們根據以前的數據繼續舉幾個實例看看。
mysql> UPDATE `user` SET `info` = JSON_REPLACE(`info`, '$.age', 20) WHERE `name` = 'lilei';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT JSON_VALUE(`info`, '$.age') as age FROM `user` WHERE `name` = 'lilei';
+------+
| age |
+------+
| 20 |
+------+
1 row in set (0.00 sec)
複製代碼
JSON_INSERT
和 JSON_SET
的例子也是相似,這裏就很少作演示了。對應到 think-model
中的話,須要使用 EXP 條件表達式處理,對應的寫法爲
//user.js
module.exports = class extends think.Controller {
async indexAction() {
const userModel = this.model('user');
await userModel.where({name: 'lilei'}).update({
info: ['exp', "JSON_REPLACE(info, '$.age', 20)"]
});
return this.success();
}
}
複製代碼
mysql> UPDATE `user` SET `info` = JSON_ARRAY_APPEND(`info`, '$.hobby', 'badminton') WHERE `name` = 'lilei';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT JSON_VALUE(`info`, '$.hobby') as hobby FROM `user` WHERE `name` = 'lilei';
+-----------------------------------------+
| hobby |
+-----------------------------------------+
| ["basketball", "football", "badminton"] |
+-----------------------------------------+
1 row in set (0.00 sec)
複製代碼
JSON_ARRAY_APPEND
在對數組進行操做的時候仍是要比 JSON_INSERT
之類的方便的,起碼你不須要知道數組的長度。對應到 think-model
的寫法爲
//user.js
module.exports = class extends think.Controller {
async indexAction() {
const userModel = this.model('user');
await userModel.where({name: 'lilei'}).update({
info: ['exp', "JSON_ARRAY_APPEND(info, '$.hobby', 'badminton')"]
});
return this.success();
}
}
複製代碼
mysql> UPDATE `user` SET `info` = JSON_REMOVE(`info`, '$.score[0]') WHERE `name` = 'lilei';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT `name`, JSON_VALUE(`info`, '$.score') as score FROM `user` WHERE `name` = 'lilei';
+-------+-----------+
| name | score |
+-------+-----------+
| lilei | [90, 100] |
+-------+-----------+
1 row in set (0.00 sec)
複製代碼
刪除這塊和以前修改操做相似,沒有什麼太多須要說的。可是對數組進行操做不少時候咱們可能就是想刪值,可是殊不知道這個值的 Path 是什麼。這個時候就須要利用以前講到的 JSON_SEARCH()
方法,它是根據值去查找路徑的。好比說咱們要刪除 lilei 興趣中的 badminton 選項能夠這麼寫。
mysql> UPDATE `user` SET `info` = JSON_REMOVE(`info`, JSON_UNQUOTE(JSON_SEARCH(`info`, 'one', 'badminton'))) WHERE `name` = 'lilei';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT JSON_VALUE(`info`, '$.hobby') as hobby FROM `user` WHERE `name` = 'lilei';
+----------------------------+
| hobby |
+----------------------------+
| ["basketball", "football"] |
+----------------------------+
1 row in set (0.00 sec)
複製代碼
這裏須要注意因爲 JSON_SEARCH
不會作類型轉換,因此匹配出來的路徑字符串須要進行 JSON_UNQUOTE()
操做。另外還有很是重要的一點是 JSON_SEARCH
沒法對數值類型數據進行查找,也不知道這個是 Bug 仍是 Feature。這也是爲何我沒有使用 score
來進行舉例而是換成了 hobby
的緣由。若是數值類型的話目前只能取出來在代碼中處理了。
mysql> SELECT JSON_VALUE(`info`, '$.score') FROM `user` WHERE `name` = 'lilei';
+-------------------------------+
| JSON_VALUE(`info`, '$.score') |
+-------------------------------+
| [90, 100] |
+-------------------------------+
1 row in set (0.00 sec)
mysql> SELECT JSON_SEARCH(`info`, 'one', 90, null, '$.score') FROM `user` WHERE `name` = 'lilei';
+-------------------------------------------------+
| JSON_SEARCH(`info`, 'one', 90, null, '$.score') |
+-------------------------------------------------+
| NULL |
+-------------------------------------------------+
1 row in set (0.00 sec)
複製代碼
以上對應到 think-model
的寫法爲
//user.js
module.exports = class extends think.Controller {
async indexAction() {
const userModel = this.model('user');
// 刪除分數
await userModel.where({name: 'lilei'}).update({
info: ['exp', "JSON_REMOVE(info, '$.score[0]')"]
});
// 刪除興趣
await userModel.where({name: 'lilei'}).update({
info: ['exp', "JSON_REMOVE(`info`, JSON_UNQUOTE(JSON_SEARCH(`info`, 'one', 'badminton')))"]
});
return this.success();
}
}
複製代碼
因爲最近有一個需求,有一堆數據,要記錄這堆數據的排序狀況,方便根據排序進行輸出。通常狀況下確定是給每條數據增長一個 order
字段來記錄該條數據的排序狀況。可是因爲有着批量操做,在這種時候使用單字段去存儲會顯得特別麻煩。在服務端同事的建議下,我採起了使用 JSON 字段存儲數組的狀況來解決這個問題。
也由於這樣瞭解了一下 MySQL 對 JSON 的支持狀況,同時將 think-model
作了一些優化,對 JSON 數據類型增長了支持。因爲大部分 JSON 操做須要經過內置的函數來操做,這個自己是能夠經過 EXP 條件表達式來完成的。因此只須要對 JSON 數據的添加和查詢作好優化就能夠了。
總體來看,配合提供的 JSON 操做函數,MySQL 對 JSON 的支持完成一些平常的需求仍是沒有問題的。除了做爲 WHERE 條件以及查詢字段以外,其它的 ORDER
, GROUP
, JOIN
等操做也都是支持 JSON
數據的。
不過對比 MongoDB 這種天生支持 JSON 的話,在操做性上仍是要麻煩許多。特別是在類型轉換這塊,使用一段時間後發現很是容易掉坑。何時會帶引號,何時會不帶引號,何時須要引號,何時不須要引號,這些都容易讓新手發憷。另外 JSON_SEARCH()
不支持數字查找這個也是一個不小的坑了。