談談 MySQL 的 JSON 數據類型

本文做者李喆明,奇舞團前端開發工程師。前端

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_SETJSON_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_INSERTJSON_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() 不支持數字查找這個也是一個不小的坑了。

相關文章
相關標籤/搜索