MySQL中的JSON
Posted Valineliu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL中的JSON相关的知识,希望对你有一定的参考价值。
从5.7.8开始,mysql开始支持JSON
类型,用于存储JSON数据。
JSON
类型的加入模糊了关系型数据库与NoSQL之间的界限,给日常开发也带来了很大的便利。
这篇文章主要介绍一下MySQL中JSON类型的使用,主要参考MySQL手册:https://dev.mysql.com/doc/refman/8.0/en/
1. 为什么要用JSON
自从MySQL添加对JSON的支持之后,一些表结构变更的操作就变得简单了一些。
1.1 JSON的使用场景
虽然关系型数据库一直很有效,但是面对需求的不断变化,文档型数据库更加灵活方便。
MySQL支持JSON之后,模糊了关系型与文档型数据库之间的界限。
在开发过程中经常会遇见下面几种情况:
- 表中仅仅小部分数据需要新添加的字段;
- 当这个新添加的字段很有可能只是临时使用后续会废弃的时候;
- 当后面还不知道要新添加什么字段但大概率要添加的时候。
这些时候,使用一个JSON进行存储比较合适,不用更改表结构,非常方便。
1.2 字符串还是JSON类型
在还不支持JSON的MySQL 5.7版本之前,没有选择只能使用一个字符串类型存储JSON数据了。
但是如果数据库支持JSON
类型,那么就还是使用JSON
类型吧。
JSON
类型相比与使用字符串存储JSON数据有如下的好处:
- 可以对存储的JSON数据自动校验,不合法的JSON数据插入时会报错;
- 优化的存储结构。
JSON
类型将数据转化为内部结构进行存储,使得可以对JSON
类型数据进行搜索与局部变更;而对于字符串来说,需要全部取出来再更新。
2. JSON的增删改查
这里将简单介绍一下JSON
类型的使用,主要是增删改查等操作。
MySQL中使用utf8mb4
字符集以及utf8mb4_bin
字符序来处理JSON中的字符串,因此JSON中的字符串时大小写敏感的。
2.1 创建JSON列
创建一个JSON
类型的列很简单:
CREATE TABLE videos (
id int NOT NULL AUTO_INCREMENT,
ext json NOT NULL,
PRIMARY KEY (id)
);
我们构建了一个表videos
,里面有一个JSON
类型的ext
字段,用于存储一些扩展数据。
2.2 插入JSON值
和其它类型一样,使用INSERT
来插入数据:
INSERT INTO videos
VALUES (1, '"vid": "vid1", "title": "title1", "tags": ["news", "china"], "logo": true'),
(2, '"vid": "vid2", "tags":[], "title": "title2", "logo": false'),
(3, '"vid": "vid3", "title": "title3"');
来看一下现在表里的数据:
mysql> select * from videos;
+----+-----------------------------------------------------------------------------+
| id | ext |
+----+-----------------------------------------------------------------------------+
| 1 | "vid": "vid1", "logo": true, "tags": ["news", "china"], "title": "title1" |
| 2 | "vid": "vid2", "logo": false, "tags": [], "title": "title2" |
| 3 | "vid": "vid3", "title": "title3" |
+----+-----------------------------------------------------------------------------+
每一个ext
都是一个JSON数据。
2.3 校验JSON
使用JSON
类型的一个好处就是MySQL可以自动检查数据的有效性,避免插入非法的JSON数据。
2.3.1 JSON合法性校验
首先需要校验一个值是否是一个合法的JSON,否则插入会失败:
mysql> insert into videos values (1, '');
ERROR 3140 (22032): Invalid JSON text: "Missing a name for object member." at position 1 in value for column 'videos.ext'.
同时还可以使用JSON_VALID()
函数查看一个JSON值是否合法:
mysql> select json_valid('');
+-----------------+
| json_valid('') |
+-----------------+
| 0 |
+-----------------+
mysql> select json_valid('"vid": "vid1"');
+-------------------------------+
| json_valid('"vid": "vid1"') |
+-------------------------------+
| 1 |
+-------------------------------+
2.3.2 JSON模式校验
如果更进一步,除了值是否是合法JSON外,还需要校验模式,比如JSON值要包含某些字段等。
这时可以定义一个模式(schema),然后使用JSON_SCHEMA_VALID()
或JSON_SCHEMA_VALIDATION_REPORT()
函数来校验。
JSON_SCHEMA_VALID()
和JSON_SCHEMA_VALIDATION_REPORT()
两个函数是8.0.17版本引入的,5.7版本还没有。
定义一个模式:
"id": "schema_for_videos",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for the table videos",
"type": "object",
"properties":
"vid":
"type": "string"
,
"tags":
"type": "array"
,
"logo":
"type": "boolean"
,
"title":
"type": "string"
,
"required": ["title", "tags"]
字段含义:
- id: 模式的唯一ID;
- $schema: JSON模式校验的标准,应该是这个值保持不变;
- description: 模式的描述;
- type: 根元素的类型,MySQL中JSON的根元素还可以是数组(array);
- properties: JSON元素的列表,每一个元素都应该描述出来,里面列出了对应的类型;
- required: 必要的元素。
在MySQL中定义一个变量:
mysql> set @schema = '"id":"schema_for_videos","$schema":"http://json-schema.org/draft-04/schema#","description":"Schema for the table videos","type":"object","properties":"title":"type":"string","tags":"type":"array","required":["title","tags"]';
Query OK, 0 rows affected (0.04 sec)
这样就可以使用JSON_SCHEMA_VALID()
或JSON_SCHEMA_VALIDATION_REPORT()
校验一个JSON是否满足要求了:
mysql> select json_schema_valid(@schema, '"title": "", "vid": "", "logo": false, "tags": []') as 'valid?';
+--------+
| valid? |
+--------+
| 1 |
+--------+
mysql> select json_schema_validation_report(@schema, '"title": "", "vid": "", "logo": false, "tags": []') as 'valid?';
+-----------------+
| valid? |
+-----------------+
| "valid": true |
+-----------------+
JSON_SCHEMA_VALID()
和JSON_SCHEMA_VALIDATION_REPORT()
的区别就是后者可以给出不满足要求的地方:
mysql> select json_schema_valid(@schema, '"vid": "", "logo": false, "tags": []') as 'valid?';
+--------+
| valid? |
+--------+
| 0 |
+--------+
mysql> select json_schema_validation_report(@schema, '"vid": "", "logo": false, "tags": []') as 'valid?'\\G
*************************** 1. row ***************************
valid?: "valid": false, "reason": "The JSON document location '#' failed requirement 'required' at JSON Schema location '#'", "schema-location": "#", "document-location": "#", "schema-failed-keyword": "required"
当然,这两个函数的第二个参数要是一个合法的JSON,不然MySQL会报错:
mysql> select json_schema_valid(@schema, '') as 'valid?';ERROR 3141 (22032): Invalid JSON text in argument 2 to function json_schema_valid: "Missing a name for object member." at position 1.
我们还可以将这个模式添加到表的定义上,这样插入数据就可以使用这个模式进行校验了:
ALTER TABLE videos
ADD CONSTRAINT CHECK (JSON_SCHEMA_VALID('"id":"schema_for_videos","$schema":"http://json-schema.org/draft-04/schema#","description":"Schema for the table videos","type":"object","properties":"vid":"type":"string","tags":"type":"array","logo":"type":"bool","title":"type":"string","required":["title","tags"]', ext));
当然如果表里已经有数据了且不符合这个校验模式,MySQL会报错:
ERROR 3819 (HY000): Check constraint 'videos_chk_1' is violated.
应该修改原来的数据以满足要求后再添加校验。
添加之后,新增的数据就会进行校验:
mysql> INSERT INTO videos VALUES (1, '"vid": "vid1", "title": "title1", "tags": ["news", "china"], "logo": true');Query OK, 1 row affected (0.04 sec)
mysql> INSERT INTO videos VALUES (2, '"vid": "vid2", "title": "title2"');ERROR 3819 (HY000): Check constraint 'videos_chk_1' is violated.
2.4 JSON的格式化
使用JSON_PRETTY()
函数进行美化输出:
mysql> select json_pretty(ext) from videos\\G
*************************** 1. row ***************************
json_pretty(ext):
"vid": "vid1",
"logo": true,
"tags": [
"news",
"china"
],
"title": "title1"
2.5 获取JSON元素
JSON
字段优于JSON字符串的一点就是JSON
字段可以直接获取内部的元素而不用获取整个文档。
MySQL中支持使用JSON_EXTRACT()
函数以及->
,->>
操作符来获取JSON内部的元素:
mysql> select json_extract('"a": 9, "b":[1,2,3]', '$.a') as a;
+------+
| a |
+------+
| 9 |
+------+
1 row in set (0.04 sec)
mysql> select json_extract('"a": 9, "b":[1,2,3]', '$.b') as b;
+-----------+
| b |
+-----------+
| [1, 2, 3] |
+-----------+
1 row in set (0.04 sec)
mysql> select json_extract('"a": 9, "b":[1,2,3]', '$.b[1]') as 'b[1]';
+------+
| b[1] |
+------+
| 2 |
+------+
1 row in set (0.04 sec)
使用->
:
mysql> select * from videos;
+----+-----------------------------------------------------------------------------+
| id | ext |
+----+-----------------------------------------------------------------------------+
| 1 | "vid": "vid1", "logo": true, "tags": ["news", "china"], "title": "title1" |
| 2 | "vid": "vid2", "logo": false, "tags": [], "title": "title2" |
| 3 | "vid": "vid3", "logo": false, "tags": ["food"], "title": "title3" |
+----+-----------------------------------------------------------------------------+
3 rows in set (0.04 sec)
mysql> select ext->'$.title' from videos;
+----------------+
| ext->'$.title' |
+----------------+
| "title1" |
| "title2" |
| "title3" |
+----------------+
3 rows in set (0.04 sec)
->
就是JSON_EXTRACT()
函数的别名。
使用JSON_UNQUOTE()
函数去掉引号:
mysql> select json_unquote(ext->'$.title') from videos;
+------------------------------+
| json_unquote(ext->'$.title') |
+------------------------------+
| title1 |
| title2 |
| title3 |
+------------------------------+
3 rows in set (0.04 sec)
还可以使用->>
达到同样的效果(->>
就是JSON_UNQUOTE(JSON_EXTRACT(...))
的别名):
mysql> select ext->>'$.title' from videos;
+-----------------+
| ext->>'$.title' |
+-----------------+
| title1 |
| title2 |
| title3 |
+-----------------+
3 rows in set (0.04 sec)
2.6 JSONPath
在获取JSON元素的过程中,我们使用了类似$.title
,$.b[1]
这样的结构来指定元素,这些就是JSONPath。
JSONPath使用$
符号表示整个JSON文档,后面可以跟着不同的符号表示不同的元素:
- 一个点号(
.
)加上key,可以获取指定key的值; [N]
获取数组中下标为N
的元素(0开始);[N to M]
数组元素还可以指定开头结尾(都包含);[last]
last表示数组中的最后一个元素;[*]
获取数组中的所有元素;prefix**suffix
获取所有prefix
开头suffix
结尾的JSONPath。
以下面的JSON为例:
"a": "a_value",
"b": [1, 2, 3, 4, 5],
"c": true,
"d":
"a": "inner_a",
"b": [11, 22, "inner_b"]
'$'
得到整个文档;'$.a'
就是"a_value"
;'$.b'
就是[1, 2, 3, 4, 5]
;'$.b[*]'
等同于'$.b'
;'$.b[2]'
得到数组b
中的第三个元素3
;'$.d.a'
得到的就是"inner_a"
;'$.d.b[2]'
得到的就是"inner_b"
;'$.b[1 to 2]'
返回[2, 3]
;'$.b[last]'
返回5
;'$.b[last-2 to last-1]'
返回[3, 4]
;'$**.a'
返回的是所有以a
结尾的元素组成的数组["a_value", "inner_a"]
;'$**.b'
就是数组的数组了[[1, 2, 3, 4, 5], [11, 22, "inner_b"]]
。
JSONPath并不仅仅可以用来获取JSON内的元素,涉及到JSON值增删改查的函数基本上都需要一个JSONPath作为参数来指定要操作的元素。
2.7 搜索JSON元素
JSON
类型的另一个优势就是可以进行搜索。
搜索可以使用JSON_SEARCH()
函数,返回匹配的JSONPath。
JSON_SEARCH()函数原型如下:
JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
其中前三个是必须参数:
json_doc
: 一个有效的JSON文档;one_or_all
: 字符串,必须是'one'
或'all'
,用于指定匹配返回的个数,如果是'one'
的话只返回匹配的第一个,否则全部返回;search_str
: 就是需要搜索的值,目前只支持字符串搜索,同时还可以添加%
或_
来模糊匹配;
后两个是可选参数:
escape_char
: 转义字符,默认是\\
;如果不指定或为NULL
的话,也是\\
;否则,这个参数只能为空(此时还是\\
)或者一个字符(指定多个会报错);path
: 指定了开始搜索的位置,如果没有的话就是整个文档。
接下来以下面这个JSON文档为例看一下如何进行搜索:
"a": "a_value",
"b": ["1", "2", "3", "4", "5"],
"c": true,
"d":
"a": "a_value",
"b": ["1", "2", "bvalue"]
json_search(@j, 'one', 'a_value')
返回"$.a"
;json_search(@j, 'all', 'a_value')
返回["$.a", "$.d.a"]
;json_search(@j, 'all', '1')
返回["$.b[0]", "$.d.b[0]"]
;json_search(@j, 'all', '%_value')
返回["$.a", "$.d.a", "$.d.b[2]"]
;json_search(@j, 'all', '%\\_value')
返回["$.a", "$.d.a"]
,注意和上一个的区别;json_search(@j, 'all', '%|_value', '|')
指定转义符,返回["$.a", "$.d.a"]
;json_search(@j, 'all', '%|_value', '|', '$.a')
指定了开始搜索的位置,返回"$.a"
,没有匹配$.d.a
;
接下来,我们就可以在WHERE
中使用JSON_SEARCH()
了。
还是之前的videos
表:
mysql> select * from videos;
+----+-----------------------------------------------------------------------------+
| id | ext |
+----+-----------------------------------------------------------------------------+
| 1 | "vid": "vid1", "logo": true, "tags": ["news", "china"], "title": "title1" |
| 2 | "vid": "vid2", "logo": false, "tags": [], "title": "title2" |
| 3 | "vid": "vid3", "logo": false, "tags": ["food"], "title": "title3" |
+----+-----------------------------------------------------------------------------+
3 rows in set (0.04 sec)
mysql> select * from videos where json_search(ext, 'all', 'title2');
+----+---------------------------------------------------------------+
| id | ext |
+----+---------------------------------------------------------------+
| 2 | "vid": "vid2", "logo": false, "tags": [], "title": "title2" |
+----+---------------------------------------------------------------+
1 row in set, 1 warning (0.04 sec)
mysql> select * from videos where json_search(ext, 'all', 'food', '', '$.tags');
+----+---------------------------------------------------------------------+
| id | ext |
+----+---------------------------------------------------------------------+
| 3 | "vid": "vid3", "logo": false, "tags": ["food"], "title": "title3" |
+----+---------------------------------------------------------------------+
1 row in set, 1 warning (0.04 sec)
还可以使用->
操作符来搜索:
mysql> select ext from videos where ext->'$.logo' = true;
+------------------------------------------------------------------------------------------------+
| ext |
+------------------------------------------------------------------------------------------------+
| "vid": "vid1", "logo": true, "tags": ["news", "china"], "title": "title1", "protected": true |
+------------------------------------------------------------------------------------------------+
1 row in set (0.04 sec)
2.8 JSON中插入新元素
MySQL中有几个函数可以支持向JSON中新增元素:
JSON_INSERT()
JSON_ARRAY_APPEND()
JSON_ARRAY_INSERT()
这几个函数支持就地更新,而不是取出JSON文档更改后全量覆盖。
使用JSON_INSERT()
函数新增元素:
update videos set ext = json_insert(ext, '$.protected', true);
如果要增加的元素已经有了的话,则没有变化。
JSON_ARRAY_APPEND()
函数可以向数组中追加元素:
update videos set ext = json_array_append(ext, '$.tags', 'tech') where json_search(ext, 'all', 'title2', '', '$.title');
这里同时使用了JSON_SEARCH()
进行匹配。
JSON_ARRAY_INSERT()
函数可以在数组的指定位置中添加元素:
update videos set ext=json_array_insert(ext, '$.tags[1]', 'beijing') where ext->'$.vid' = 'vid1';
结果:
mysql> select ext from videos where ext->'$.vid' = 'vid1';
+-----------------------------------------------------------------------------------------------------------+
| ext |
+-----------------------------------------------------------------------------------------------------------+
| "vid": "vid1", "logo": true, "tags": ["news", "beijing", "china"], "title": "title1", "protected": true |
+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.04 sec)
2.9 更新JSON元素
使用JSON_REPLACE()或JSON_SET()函数来更新JSON中的元素。
JSON_REPLACE()函数可以用来更新元素的值:
update videos set ext = json_replace(ext, '$.protected', false) where ext->'$.vid' = 'vid1';
不过如果JSON中没有要更新的key,那么就什么也不做。
JSON_SET()除了可以更新元素的值之外,如果指定的元素不存在,还可以添加:
update videos set ext = json_set(ext, '$.size', 100) where ext->'$.vid' = 'vid1';
2.10 删除JSON元素
使用JSON_REMOVE()
函数可以删除JSON中的元素:
update videos set ext = json_remove(ext, '$.size') where ext->'$.vid' = 'vid1';
update videos set ext = json_remove(ext, '$.tags[1]') where ext->'$.vid' = 'vid1';
JSON_REMOVE()
函数可以指定多个JSONPath来删除多个元素,这时MySQL是从左到右一个个删除的。
这样即使是相同的JSONPath但是顺序不一样,结果就会不一样:
mysql> select json_remove('"a": [1,2,3,4,5]', '$.a[2]', '$.a[3]');
+-------------------------------------------------------+
| json_remove('"a": [1,2,3,4,5]', '$.a[2]', '$.a[3]') |
+-------------------------------------------------------+
| "a": [1, 2, 4] |
+-------------------------------------------------------+
1 row in set (0.04 sec)
mysql> select json_remove('"a": [1,2,3,4,5]', '$.a[3]', '$.a[2]');
+-------------------------------------------------------+
| json_remove('"a": [1,2,3,4,5]', '$.a[3]', '$.a[2]') |
+-------------------------------------------------------+
| "a": [1, 2, 5] |
+-------------------------------------------------------+
1 row in set (0.04 sec)
2.11 JSON合并
MySQL中支持将两个JSON文档合并成一个文档。可以通过下面的两个函数来完成:
JSON_MERGE_PATCH()
:相当于第二个参数更新第一个参数;JSON_MERGE_PRESERVE()
:尽可能地保留两个参数的元素。
这两个函数有很大的不同,使用的时候一定要注意。
2.11.1 JSON_MERGE_PATCH
函数接收至少两个参数,如果多于两个参数的话,那么就前两个合并的结果与后一个进行合并。
下面假设有两个参数进行讨论,多于两个的也是类似的。
如果有一个参数是NULL
,那么结果就是NULL
:
mysql> select json_merge_patch('"a": 1, "b": [1,2]', null);
+------------------------------------------------+
| json_merge_patch('"a": 1, "b": [1,2]', null) |
+------------------------------------------------+
| NULL |
+------------------------------------------------+
1 row in set (0.04 sec)
mysql> select json_merge_patch(null, '"b": null');
+---------------------------------------+
| json_merge_patch(null, '"b": null') |
+---------------------------------------+
| NULL |
+---------------------------------------+
1 row in set (0.04 sec)
如果第一个参数不是object,那么结果就相当于一个空的object和第二个参数合并,其实就是第二个参数:
mysql> select json_merge_patch('', '"a": "a"');
+--------------------------------------+
| json_merge_patch('', '"a": "a"') |
+--------------------------------------+
| "a": "a" |
+--------------------------------------+
1 row MySQL数据类型 - JSON数据类型
在 Flutter 的 Ferry Graphql 中序列化标量 JSON 以实现灵活的查询
Apollo Graphql 自定义标量 JSON - 抱怨“TypeError:type.getFields 不是函数”