本文做者李喆明,奇舞团前端开发工程师。前端
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()
不支持数字查找这个也是一个不小的坑了。