25
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

NTTテクノクロスAdvent Calendar 2018

Day 5

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

Last updated at Posted at 2018-12-04

はじめに

この記事は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にして調査しました。

25
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?