谈谈 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() 不支持数字查找这个也是一个不小的坑了。

相关文章
相关标签/搜索