让 MariaDB 理解 MySQL JSON

对于希望升级到 MariaDB 的 MySQL 用户来说是个好消息:MariaDB 10.5.7 及更高版本可以理解之前不兼容的 MySQL JSON 字段了!这篇博客文章解释了如何安装和使用它们。

介绍 MYSQL_JSON 插件

从 MariaDB 10.5.7(提交 f6549e)开始,通过加载 MYSQL_JSON 数据类型插件,可以从包含 JSON 的 MySQL 5.7 表进行升级。

MariaDB 和 MySQL 的 JSON 格式不一样。在 MySQL 中,JSON 类型是原生类型,而在 MariaDB 中,JSON 只是 LONGTEXT 的别名。这在实践中意味着 JSON 格式的数据彼此不兼容。从本博客的角度来看,哪种格式更好并不重要。本博客旨在说明如何解决这个问题,通过轻松将 MySQL JSON 字段移植到 MariaDB JSON 字段。

Monty 建议为 MariaDB 提供一个开放的机制,以便在 MariaDB 领域中理解 MySQL JSON。 

为了实现这种提议的架构,我们为 10.5 版本引入了一个插件,它将负责所有繁重的工作。

在这篇博客中,我们将探讨在 MYSQL_JSON 插件出现之前的问题状态,以及 MYSQL_JSON 在使用生成的 MySQL JSON 数据的实际示例中的用法。我们将了解如何在 MySQL 中创建 JSON 数据,如何使用生成的数据以及如何手动安装 MYSQL_JSON 插件,以及如何使用配置文件加载插件。之后,我们将执行一个不可逆的操作,修改原始的 MySQL JSON 数据并将其转换为 MariaDB 特有的格式。

在 MySQL 5.7 中创建 JSON 数据

mysql> select @@version;
+--------------+
| @@version    |
+--------------+
| 5.7.28 |
+--------------+
1 row in set (0,00 sec)

mysql> create table mysql_json (t json) engine=myisam;
Query OK, 0 rows affected (0,00 sec)

mysql> show create table mysql_json\G
*************************** 1. row ***************************
       Table: mysql_json
Create Table: CREATE TABLE `mysql_json` (
  `t` json DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0,01 sec

mysql> insert into mysql_json values 
('{
    "country":"Bosnia and Herzegovina",
    "city":["Zenica", "Sarajevo", "Tuzla", "Mostar", "Banja Luka"],
    "faculty":["Faculty of Mechanical Engineering in Zenica", "Faculty of Electrical Engineering Sarajevo", "Faculty of Electrical Engineering Tuzla"],
    "team": ["Čelik", "Željezničar", "Sarajevo", "Sloboda", "Velež", "Zrinjski", "Borac"]
   }');

mysql> select * from mysql_json\G
*************************** 1. row ***************************
t: {"city": ["Zenica", "Sarajevo", "Tuzla", "Mostar", "Banja Luka"], "team": ["Čelik", "Željezničar", "Sarajevo", "Sloboda", "Velež", "Zrinjski", "Borac"], "country": "Bosnia and Herzegovina", "faculty": ["Faculty of Mechanical Engineering in Zenica", "Faculty of Electrical Engineering Sarajevo", "Faculty of Electrical Engineering Tuzla"]}
1 row in set (0,00 sec)

在 MYSQL_JSON 插件出现之前读取 MySQL 5.7 JSON

在 10.5.7 之前,MariaDB 在处理 MySQL JSON 数据类型时,会在“.frm”文件中返回错误的信息,如下所示

MariaDB [test]> show create table mysql_json;
ERROR 1033 (HY000): Incorrect information in file: './test/mysql_json.frm'
MariaDB [test]> select * from mysql_json;
ERROR 1033 (HY000): Incorrect information in file: './test/mysql_json.frm'

安装 MYSQL_JSON 插件

现在,让我们使用从 MySQL 生成的数据目录(“/tmp/mysql-datadir”),并通过 CLI 和配置文件为 MYSQL_JSON 插件使用一些系统变量/命令行 (CLI) 选项来启动 MariaDB。

使用 CLI 安装插件

$ mysqld --no-defaults --datadir=/tmp/mysql-datadir --lc-messages_dir=./sql/share --plugin-dir=<path to directory containing type_mysql_json.so/dll> --plugin-maturity=alpha

Version: '10.5.7-MariaDB'  socket: '/tmp/mysql.sock'  port: 3306  Source distribution

由于我们将使用名为“MYSQL_JSON”的插件,其共享库“type_mysql_json.so”位于“plugin/type_mysql_json”目录下(从源代码编译时),我们应该为系统变量 plugin_dir (作为插件固有的安全特性)指定插件所在的路径。 由于我们的插件目前处于 alpha 成熟度级别,我们必须将 MariaDB plugin_maturity 服务器系统变量至少设置为此级别才能安装我们的插件。在上面的例子中,我们也可以使用 plugin_load_add mysqld 选项指定要加载插件库中的哪个插件(如下面使用配置文件所示),但在此示例中,我们将手动进行(使用 INSTALL PLUGIN 语句)。

让我们在测试数据上启动 mysql 客户端,看看我们的 mysql 表中发生了什么

MariaDB [(none)]> select @@version;
+----------------------+
| @@version            |
+----------------------+
| 10.5.7-MariaDB |
+----------------------+
1 row in set (0.000 sec)

MariaDB [(none)]> select @@plugin_dir;
+--------------------------------------------------------+
| @@plugin_dir                                           |
+--------------------------------------------------------+
|<build_dir>/plugin/type_mysql_json/ |
+--------------------------------------------------------+
1 row in set (0.000 sec)

MariaDB [(none)]> select @@plugin_maturity;
+-------------------+
| @@plugin_maturity |
+-------------------+
| alpha             |
+-------------------+
1 row in set (0.000 sec)

MariaDB [test]> show create table mysql_json;
ERROR 4161 (HY000): Unknown data type: 'MYSQL_JSON'

MariaDB [test]> select * from mysql_json;
ERROR 4161 (HY000): Unknown data type: 'MYSQL_JSON'

MariaDB [test]> select * from information_schema.plugins where plugin_name="mysql_json";
Empty set (0.002 sec)

MariaDB [test]> select * from mysql.plugin;
Empty set (0.001 sec)

在带有 JSON 列的表上运行 show create tableselect 语句会导致“Unknown data type: MYSQL_JSON”错误消息。

我们知道,原因在于缺少插件。

查看 information_schema (IS) 的 plugins 表或执行 show plugins  语句时,不会显示任何插件。现在,让我们安装并验证我们的插件(使用 information_schema.plugins show plugins 语句)。由于我们是使用 INSTALL SONAME 语句手动安装插件,插件的验证也会在 mysql.plugin 表中可见,而通过配置文件加载插件时则不会。

MariaDB [(none)]> install soname 'type_mysql_json';
Query OK, 0 rows affected (0.002 sec)

MariaDB [(none)]> select * from information_schema.plugins where plugin_name="mysql_json"\G
*************************** 1. row ***************************
           PLUGIN_NAME: MYSQL_JSON
        PLUGIN_VERSION: 0.1
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: DATA TYPE
   PLUGIN_TYPE_VERSION: 100507.0
        PLUGIN_LIBRARY: type_mysql_json.so
PLUGIN_LIBRARY_VERSION: 1.14
         PLUGIN_AUTHOR: Anel Husaković, Vicențiu Ciorbaru
    PLUGIN_DESCRIPTION: Data type MYSQL_JSON
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
       PLUGIN_MATURITY: Alpha
   PLUGIN_AUTH_VERSION: 0.1
1 row in set (0.002 sec)

MariaDB [test]> select * from mysql.plugin;
+------------+--------------------+
| name       | dl                 |
+------------+--------------------+
| MYSQL_JSON | type_mysql_json.so |
+------------+--------------------+
1 row in set (0.001 sec)

使用配置文件加载插件

为了跳过上述手动配置插件的步骤,可以在配置文件中使用以下内容在服务器启动时加载插件。请注意,在这种情况下,mysql.plugin 将是空的

plugin_dir=<path to directory containing type_mysql_json.so/dll> 
plugin_load_add=mysql_json=type_mysql_json
plugin_maturity=alpha

使用插件时获得的描述性错误

现在,如果我们尝试执行 show create table select 该表,我们会得到一条有意义的消息,两种情况下的消息相同

MariaDB [test]> show create table mysql_json;
ERROR 1707 (HY000): Table rebuild required. Please do "ALTER TABLE `test.mysql_json` FORCE" or dump/reload to fix it!
MariaDB [test]> select * from mysql_json;
ERROR 1707 (HY000): Table rebuild required. Please do "ALTER TABLE `test.mysql_json` FORCE" or dump/reload to fix it!

错误消息显示了两种可能的修复方法

– 使用带有 FORCE 的 ALTER TABLE 进行修复

– 或使用 mysqldump 进行修复。
调用 mysqlcheck 时也会出现相同的消息

$ mysqlcheck -c test mysql_json 

Failed to SHOW CREATE TABLE `mysql_json`
Error: Table rebuild required. Please do "ALTER TABLE `test.mysql_json` FORCE" or dump/reload to fix it

修复错误的方法

ALTER TABLE FORCE

让我们按照错误消息的建议进行操作

MariaDB [test]> ALTER TABLE test.mysql_json FORCE;
Query OK, 1 row affected (0.011 sec)               
Records: 1  Duplicates: 0  Warnings: 0

MariaDB [test]> show create table mysql_json\G
*************************** 1. row ***************************
       Table: mysql_json
Create Table: CREATE TABLE `mysql_json` (
  `t` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.000 sec)

MariaDB [test]> select * from mysql_json\G
*************************** 1. row ***************************
t: {"city": ["Zenica", "Sarajevo", "Tuzla", "Mostar", "Banja Luka"], "team": ["Čelik", "Željezničar", "Sarajevo", "Sloboda", "Velež", "Zrinjski", "Borac"], "country": "Bosnia and Herzegovina", "faculty": ["Faculty of Mechanical Engineering in Zenica", "Faculty of Electrical Engineering Sarajevo", "Faculty of Electrical Engineering Tuzla"]}
1 row in set (0.000 sec)

可以看到,来自 MySQL 的数据已成功转换为 MariaDB,并且客户端能够读取有效数据。

MySQL mysqldump

启动 MySQL 服务器并调用 mysqldump 客户端

$ mysqldump test>mysql_json_dump.sql

使用空的数据目录启动 MariaDB 服务器并启动 mysql 客户端

MariaDB [test]> show tables;
Empty set (0.000 sec)

在 MariaDB 中恢复原始 MySQL 数据

$ mysql test<mysql_json_dump.sql 

验证

$ mysql test -e "show create table mysql_json\G select * from mysql_json\G"
*************************** 1. row ***************************
       Table: mysql_json
Create Table: CREATE TABLE `mysql_json` (
  `t` json /* MySQL 5.7 */ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
*************************** 1. row ***************************
t: {"city": ["Zenica", "Sarajevo", "Tuzla", "Mostar", "Banja Luka"], "team": ["Čelik", "Željezničar", "Sarajevo", "Sloboda", "Velež", "Zrinjski", "Borac"], "country": "Bosnia and Herzegovina", "faculty": ["Faculty of Mechanical Engineering in Zenica", "Faculty of Electrical Engineering Sarajevo", "Faculty of Electrical Engineering Tuzla"]}

运行 mysql_upgrade force

ALTER FORCE 相同的结果可以通过 mysql_upgrade 实现(下面的示例对 3 个 MySQL 表执行升级)

$ mysql_upgrade test mysql_json
...
test.mysql_json                                    Needs upgrade
test.mysql_json_innodb
error    : Table rebuild required. Please do "ALTER TABLE `mysql_json_innodb` FORCE" or dump/reload to fix it!
test.tt
error    : Table rebuild required. Please do "ALTER TABLE `tt` FORCE" or dump/reload to fix it!

Repairing tables
test.mysql_json                                    OK
`test`.`mysql_json_innodb`                         FIXED
`test`.`tt`                                        FIXED

探索 JSON 函数

在 MariaDB 中获得带有 JSON 列的表后,我们可以使用一组 JSON 函数来处理 JSON 字段。

让我们创建一个临时(会话)表作为工作表,并使用存储在基础表中的所有数据。

MariaDB [test]> create temporary table my_temp like mysql_json;
Query OK, 0 rows affected (0.000 sec)

MariaDB [test]> insert into my_temp(t) select * from mysql_json;
Query OK, 1 row affected (0.000 sec)
Records: 1  Duplicates: 0  Warnings: 0

现在让我们看看 JSON_DETAILED 函数的实际应用,它是一种漂亮的展示 JSON 结构的方式

MariaDB [test]> select json_detailed(t) from my_temp\G
*************************** 1. row ***************************
json_detailed(t): {
    "city": 
    [
        "Zenica",
        "Sarajevo",
        "Tuzla",
        "Mostar",
        "Banja Luka"
    ],
    "team": 
    [
        "Čelik",
        "Željezničar",
        "Sarajevo",
        "Sloboda",
        "Velež",
        "Zrinjski",
        "Borac"
    ],
    "country": "Bosnia and Herzegovina",
    "faculty": 
    [
        "Faculty of Mechanical Engineering in Zenica",
        "Faculty of Electrical Engineering Sarajevo",
        "Faculty of Electrical Engineering Tuzla"
    ]
}
1 row in set (0.000 sec)

让我们看看如何使用 JSON_EXTRACT 函数来使用一些数据,通过文档路径获取数据并对结果集应用索引

MariaDB [test]> select json_extract(t,'$.country'), json_extract(t,'$.city') from my_temp\G
*************************** 1. row ***************************
json_extract(t,'$.country'): "Bosnia and Herzegovina"
   json_extract(t,'$.city'): ["Zenica", "Sarajevo", "Tuzla", "Mostar", "Banja Luka"]

MariaDB [test]> select json_extract(t,'$.city[0]') from my_temp\G
*************************** 1. row ***************************
json_extract(t,'$.city[0]'): "Zenica"

让我们尝试插入一个包含有效 JSON 对象的新行,然后使用 JSON_CONTAINS 函数来获取该值

MariaDB [test]> insert into my_temp values ('{"key1":"value1"}');
Query OK, 1 row affected (0.000 sec)
MariaDB [test]> select * from my_temp where json_contains(t, '"value1"', '$.key1');
+-------------------+
| t                 |
+-------------------+
| {"key1":"value1"} |
+-------------------+
1 row in set (0.000 sec)

让我们尝试插入一个包含无效 JSON 对象的新行,然后使用 JSON_TYPE JSON_KEYS 函数检查我们插入了什么

MariaDB [test]> insert into my_temp values ('{"key-wrong":}');
Query OK, 1 row affected (0.000 sec)

MariaDB [test]> select json_type(t) from my_temp;
+--------------+
| json_type(t) |
+--------------+
| OBJECT       |
| OBJECT       |
| OBJECT       |
+--------------+
3 rows in set (0.000 sec)

MariaDB [test]> select json_keys(t) from my_temp;
+----------------------------------------+
| json_keys(t)                           |
+----------------------------------------+
| ["city", "team", "country", "faculty"] |
| ["key1"]                               |
| NULL                                   |
+----------------------------------------+
3 rows in set, 1 warning (0.000 sec)

为了防止插入错误的 JSON 数据,我们可以对 JSON 字段使用 CHECK 约束,这意味着会应用 JSON_VALID 函数。

让我们首先尝试在已经存在污染数据的 数据集上使用 CHECK 约束,如上面的示例所示。

MariaDB [test]> alter table my_temp add check(json_valid(t));
ERROR 4025 (23000): CONSTRAINT `CONSTRAINT_1` failed for `test`.`my_temp`

正如预期的那样,该语句会失败,因为隐含的约束被违反了。为了解决这个问题,我们必须删除 JSON_KEY 为 NULL 的最后一行,因为我们知道它是错误的行。之后,再次使用 CHECK 约束修改表并验证结果。

MariaDB [test]> delete from my_temp where json_keys(t) is NULL;
Query OK, 1 row affected, 1 warning (0.000 sec)

MariaDB [test]> alter table my_temp add constraint c1 check(json_valid(t));
Query OK, 2 rows affected (0.001 sec)              
Records: 2  Duplicates: 0  Warnings: 0

MariaDB [test]> show create table my_temp\G
*************************** 1. row ***************************
       Table: my_temp
Create Table: CREATE TEMPORARY TABLE `my_temp` (
  `t` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
  CONSTRAINT `c1` CHECK (json_valid(`t`))
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.000 sec)

如果我们使用的是基础表而不是临时表,CHECK 约束将在 INFORMATION_SCHEMA.CHECK_CONSTRAINTS 表中可见。

现在如果我们尝试插入错误的数据,CHECK 约束将被激活

MariaDB [test]> insert into my_temp values ('{"key-wrong":}');
ERROR 4025 (23000): CONSTRAINT `c1` failed for `test`.`my_temp`

请注意,在 MariaDB 表中添加 JSON 字段时,会自动创建一个带有 JSON_VALID 的 CHECK 约束。

MariaDB [test]> alter table x add column (j json);
Query OK, 0 rows affected (0.017 sec)

MariaDB [test]> show create table x\G
*************************** 1. row ***************************
       Table: x
Create Table: CREATE TABLE `x` (
  `x` int(11) DEFAULT NULL,
  `j` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`j`))
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.000 sec)

有更多演示示例,请参考 视频教程

结论

展示了使用 JSON 类型的新机制的实际示例。

这种机制的目的是为 MariaDB 用户提供数据一致性的选项,并减少 MySQL 和 MariaDB 领域之间 JSON 令人困惑的状态差异。

另请参阅