Edited at

ドキュメントDBとしてのRDBMS


はじめに

この記事はNTTテクノクロス Advent Calendar 2018の5日目の記事です🙌

NTTテクノクロス@yamamo-i です🤗

普段はApache IgniteやPostgreSQLの技術検証をしています💪

本日はRDBMSをドキュメントDBとして利用する技術について触れたいと思います📖

PostgreSQL🐘をメインにしていますが今日はMySQL🐬の話をします😇


TL;DR


  • MySQLのX Protocolを試してみました。


    • MySQLのX Pluginの構築とPythonでX DevAPIを使ってみました。



  • ドキュメントDBとしてMySQLのX Protocolも選択肢に入れてみてよいと思います。


ドキュメント(json)管理

最近はMongoDBなどのNoSQLデータベースがドキュメントのデータストアとして台頭しています。

しかし、RDBMSもドキュメント管理のインターフェースを用意しています。


RDBMSでのドキュメント管理


MySQL X Protocol

MySQLではX Protocolというドキュメント管理(json型)用の操作Protocolを提供しています。

https://dev.mysql.com/doc/refman/8.0/en/document-store.html

以下の図がX Protocolの概念になります。

X DevAPIを利用してPython, JavaScript, Node.js, ShellなどからX Protocolを利用できます。

X Pluginはドキュメント管理用のMySQL Serverのインターフェースを用意します。


x-protocol

引用元(https://dev.mysql.com/doc/internals/en/x-protocol.html)


MySQLの公式から提供されることで以下のことが期待できます。


  • トランザクション操作ができる

  • json型用のSQLをアプリケーションから隠蔽できる

本記事はその構築方法や操作方法を記述します。


X Pluginの構築

MySQLをデフォルトで起動してもX Protocol(33060ポート)はLISTENしません。

MySQLコミュニティからパッケージが提供されているので追加します。

今回はDocker(18.06.1-ce-mac73)を用いて構築しました。

Dockerfileは以下になります。

FROM mysql:8.0.13

EXPOSE 33060
RUN set -x \
&& apt update \
&& apt install -y wget net-tools libpython2.7 \
&& apt --fix-broken install \
&& (cd tmp && wget https://dev.mysql.com/get/Downloads/MySQL-Shell/mysql-shell_8.0.13-1debian9_amd64.deb && dpkg -i mysql-shell_8.0.13-1debian9_amd64.deb) \
&& (cd tmp && wget https://dev.mysql.com/get/Downloads/MySQL-Shell/mysql-shell-dbgsym_8.0.13-1debian9_amd64.deb && dpkg -i mysql-shell-dbgsym_8.0.13-1debian9_amd64.deb) \
&& rm /tmp/*.deb

ビルドと起動は以下のコマンドを発行します。

# Docker Imageの作成

$ docker build -t mysql:8.0.13-x .

# Docker Containerの起動
$ docker run --name mysql -d -p33060:33060 --env MYSQL_ROOT_PASSWORD='password' mysql:8.0.13-x

# 33060ポートがLISTENします
$ docker exec -it mysql netstat -tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp6 0 0 :::33060 :::* LISTEN
tcp6 0 0 :::3306 :::* LISTEN

# Pluginのmysqlxが有効化しています
$ docker exec -it mysql mysql -uroot -p -e"SHOW plugins"
Enter password:
+---------------------------------+----------+--------------------+---------+---------+
| Name | Status | Type | Library | License |
+---------------------------------+----------+--------------------+---------+---------+
.
(中略)
.
| mysqlx | ACTIVE | DAEMON | NULL | GPL |
.
+---------------------------------+----------+--------------------+---------+---------+

これでX Protocolが開いたMySQLが起動しました。


PythonによるX DevAPIの操作

Python(今回は3.7.1)からX DevAPIを操作するにはmysql-connector-pythonを使用します。

インストールはpipを利用します。

# 今回は最新版の8.0.13を使用しました

$ pip install mysql-connector-python==8.0.13

まずはコネクションを取得して操作を始めます。

# X DevAPI用のライブラリをインポート

import mysqlx

# MySQLのX Protocolを使用してコネクションを張る
session = mysqlx.get_session({'host': '127.0.0.1', 'port': 33060, 'user': 'root', 'password': 'password'})
# `mysql` スキーマに接続
schema = session.get_schema('mysql')

これでデータベースの操作ができるようになりました。


DDL(Data Definition Language)

X DevAPIからドキュメント用のテーブルを作成できます。

# テーブルの作成

schema.create_collection('my_collection')

作成したテーブルは一意キーの_idとjson管理用のdocの2カラムが定義されます。

このテーブルでドキュメントを管理します。

mysql> show create table my_collection\G

*************************** 1. row ***************************
Table: my_collection
Create Table: CREATE TABLE `my_collection` (
`doc` json DEFAULT NULL,
`_id` varbinary(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,_utf8mb4'$._id'))) STORED NOT NULL,
PRIMARY KEY (`_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)


DML(Data Manipulation Language)


INSERT

レコードの挿入はaddメソッドで操作します。

Pythonのdictをそのまま挿入できるので便利です。


insert_record.py

# 挿入レコードを用意

record = {"company": {
"NTT-TX": [
"Soft",
"AT",
"IT"
]
}}
# データ挿入処理
collection.add(record).execute();

挿入したレコードは以下の様になっています。

docカラムのjsonには自動で_id要素が付与されます。

mysql> select * from my_collection;

+--------------------------------------------------------------------------------------+------------------------------+
| doc | _id |
+--------------------------------------------------------------------------------------+------------------------------+
| {"_id": "00005c034a580000000000000001", "company": {"NTT-TX": ["Soft", "AT", "IT"]}} | 00005c034a580000000000000001 |
+--------------------------------------------------------------------------------------+------------------------------+
1 row in set (0.00 sec)

MySQLに発行されているSQL1は以下であり、_idカラムで管理しているidを自動でjsonに付与しています。

SELECT @@mysqlx_document_id_unique_prefix,@@auto_increment_offset,@@auto_increment_increment;

INSERT INTO `mysql`.`my_collection` (doc) VALUES (JSON_SET(JSON_OBJECT('company',JSON_OBJECT('NTT-TX',JSON_ARRAY('Soft','AT','IT'))), '$._id', '00005c034a580000000000000001'));

また_idをユーザーが指定したり、複数レコードの挿入もできます。


bulk_insert.py

# レコードの生成

record_list = [{"_id": _id, "age": _id * 5} for _id in range(10)]
# listでも複数引数でも挿入可能
collection.add(*record_list).execute();

mysql> select * from my_collection;

+-----------------------+-----+
| doc | _id |
+-----------------------+-----+
| {"_id": 0, "age": 0} | 0 |
| {"_id": 1, "age": 5} | 1 |
| {"_id": 2, "age": 10} | 2 |
| {"_id": 3, "age": 15} | 3 |
| {"_id": 4, "age": 20} | 4 |
| {"_id": 5, "age": 25} | 5 |
| {"_id": 6, "age": 30} | 6 |
| {"_id": 7, "age": 35} | 7 |
| {"_id": 8, "age": 40} | 8 |
| {"_id": 9, "age": 45} | 9 |
+-----------------------+-----+
10 rows in set (0.00 sec)

INSERT INTO `mysql`.`my_collection` (doc) VALUES (JSON_OBJECT('_id',0,'age',0)),(JSON_OBJECT('_id',1,'age',5)),(JSON_OBJECT('_id',2,'age',10)),(JSON_OBJECT('_id',3,'age',15)),(JSON_OBJECT('_id',4,'age',20)),(JSON_OBJECT('_id',5,'age',25)),(JSON_OBJECT('_id',6,'age',30)),(JSON_OBJECT('_id',7,'age',35)),(JSON_OBJECT('_id',8,'age',40)),(JSON_OBJECT('_id',9,'age',45));


READ

レコードの読み込みはfind, bind, limit で操作します。


select_records.py

record = collection.find("age >= :param").bind("param", 21).limit(2).execute()

for rec in record.fetch_all():
print(rec)

# 実行結果
# {"_id": 5, "age": 25}
# {"_id": 6, "age": 30}


発行されるSQLは以下になります。

SELECT doc FROM `mysql`.`my_collection` WHERE (JSON_EXTRACT(doc,'$.age') >= 21) LIMIT 0, 2;


UPDATE

レコードのUPDATEはmodify, patchメソッドで操作します。


update_records.py

collection.modify("age <= 21").patch('{"_is": "young"}').execute()

collection.modify("age > 21").patch('{"_is": "old"}').execute()

発行されるSQLとレコードは以下のようになります。

mysql> select * from my_collection;

+---------------------------------------+-----+
| doc | _id |
+---------------------------------------+-----+
| {"_id": 0, "_is": "young", "age": 0} | 0 |
| {"_id": 1, "_is": "young", "age": 5} | 1 |
| {"_id": 2, "_is": "young", "age": 10} | 2 |
| {"_id": 3, "_is": "young", "age": 15} | 3 |
| {"_id": 4, "_is": "young", "age": 20} | 4 |
| {"_id": 5, "_is": "old", "age": 25} | 5 |
| {"_id": 6, "_is": "old", "age": 30} | 6 |
| {"_id": 7, "_is": "old", "age": 35} | 7 |
| {"_id": 8, "_is": "old", "age": 40} | 8 |
| {"_id": 9, "_is": "old", "age": 45} | 9 |
+---------------------------------------+-----+
10 rows in set (0.00 sec)

UPDATE `mysql`.`my_collection` SET doc=JSON_SET(JSON_MERGE_PATCH(doc,JSON_OBJECT('_is','young')),'$._id',JSON_EXTRACT(`doc`,'$._id')) WHERE (JSON_EXTRACT(doc,'$.age') <= 21);

UPDATE `mysql`.`my_collection` SET doc=JSON_SET(JSON_MERGE_PATCH(doc,JSON_OBJECT('_is','old')),'$._id',JSON_EXTRACT(`doc`,'$._id')) WHERE (JSON_EXTRACT(doc,'$.age') > 21);


DELETE

レコードのDELETEはremoveメソッドで操作します。


delete_records.py

collection.remove("age >= 31").execute()


発行されるSQLとレコードは以下のようになります。

mysql> select * from my_collection;

+---------------------------------------+-----+
| doc | _id |
+---------------------------------------+-----+
| {"_id": 0, "_is": "young", "age": 0} | 0 |
| {"_id": 1, "_is": "young", "age": 5} | 1 |
| {"_id": 2, "_is": "young", "age": 10} | 2 |
| {"_id": 3, "_is": "young", "age": 15} | 3 |
| {"_id": 4, "_is": "young", "age": 20} | 4 |
| {"_id": 5, "_is": "old", "age": 25} | 5 |
| {"_id": 6, "_is": "old", "age": 30} | 6 |
+---------------------------------------+-----+
7 rows in set (0.00 sec)

DELETE FROM `mysql`.`my_collection` WHERE (JSON_EXTRACT(doc,'$.age') >= 31);


DCL(Data Control Language)

X DevAPIはMySQLがベースであるからこそトランザクション操作も実現できます。

トランザクションはsession.start_transaction, session.commit,session.rollbackメソッドで操作できます。

以下のようなコードで書けます。


transaction.py

# セッションの確立

session = mysqlx.get_session({'host': '127.0.0.1', 'port': 33060, 'user': 'root', 'password': 'root'})

try:
# テーブル情報の取得
schema = session.get_schema('mysql')
schema.create_collection('my_collection', True)
collection = schema.get_collection('my_collection')

# トランザクションの開始
session.start_transaction()
collection.add({"_id": 1, "NTT": "TX"}).execute()

# トランザクションのコミット
session.commit()
except Exception:
# 例外時はトランザクションをロールバック
session.rollback()
finally:
session.close()


上記のコードを実行した時、発行されるSQLは以下になります。

START TRANSACTION;

INSERT INTO `mysql`.`my_collection` (doc) VALUES (JSON_OBJECT('_id',1,'NTT','TX'));
COMMIT;

故意に例外を発生させるとROLLBACKが発行されます。

START TRANSACTION;

INSERT INTO `mysql`.`my_collection` (doc) VALUES (JSON_OBJECT('_id',1,'NTT','TX'));
ROLLBACK;


その他

MySQL X DevAPIは他にも以下の操作ができます。

RDBMSで一般的な操作がドキュメントDBとして実現されています。


まとめ

X Protocolを使ってMySQLをドキュメントDBとして操作してみました。

メリットは以下に感じました。


  • RDBMSなので使い慣れている

  • トランザクション機能が使える

  • json型のIFを隠蔽してくれる

  • MySQLが公式にリリースしている

いきなりドキュメントDBを導入するのではなくRDBMSの機能を使うのもよいのではないでしょうか?

明日からのNTTテクノクロス Advent Calendar 2018もお楽しみに🎉

明日の担当は @j-yama さんです🙏





  1. MySQLに発行されているSQLの調査はslow_query_logを有効かつlong_query_timeを0にして調査しました。