キャッチアップMySQL8.0　MySQLでNoSQLを使ってみる

#はじめに
最近仕事ばかりしていたら技術の進歩に置いていかれてしまった感が否めない今日この頃です。
とはいえ世の中は進んでいくので、1周遅れてでも追いかけなければなりませんね。

ということで、今回はMySQL8.0のNoSQLにキャッチアップしていきます。

#What's New MySQL8.0?
Newって言っても2年前のことですけどねー。
https://mysqlserverteam.com/whats-new-in-mysql-8-0-generally-available/
公式で公開されているポイントは８項目あって、データベースエンジニアの方なんかにとっては
「postgresのwindow関数がMySQLにもやってきた！」というのもニュースらしいのですが、
「MySQLでNoSQLも使える！」というのがMySQL8.0の大きなテーマだったようです。
[NoSQL + SQL = MySQL](https://twitter.com/lefred/status/989967294607458305/photo/1)

ということでMySQL8.0の公式ドキュメントに従って
どんな感じでNoSQLが使えるのか実際に触ってみます。
https://dev.mysql.com/doc/refman/8.0/en/document-store.html

ちなみに作業環境はMacです。

#環境を作る
ドキュメントの最初のページにざっくり仕組みが書いてあります。
"MySQL as a document store"というのがMySQL流NoSQLのキャッチフレーズらしいですね。
この"MySQL as a document store"を使うためには
・SQLサーバーに"X plugin"が必要
・SQLクライアントに"X Protocol"が必要
だけど
・どっちもMySQL8.0をインストールすれば入ってる
とのことです。

早速、Dockerコンテナでサーバーを立てます。
ちょうどいい記事があったので、こちらの手順を参考にさせてもらいました。
https://qiita.com/ucan-lab/items/b094dbfc12ac1cbee8cb

以下、実際に行った手順。

```shell:shell
# 適当に作業ディレクトリを決めます
WORKSPACE=/Users/<your-name>/sandbox
# ベースディレクトリにディレクトリを作ります
cd $WORDSPACE
mkdir mysql8-trial
cd mysql8-trial

# 適当なエディタでdocker-composeを作る
nano docker-compose.yml
```

```docker-compose.yml
version: "3"
services:
  db:
    image: mysql:8.0
    volumes:
      - db-store:/var/lib/mysql
      - ./logs:/var/log/mysql
      - ./my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_DATABASE=${DB_NAME}
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_ROOT_PASSWORD=${DB_PASS}
      - TZ=${TZ}
    ports:
      - ${DB_PORT}:3306
volumes:
  db-store:
```
試験的に作るだけだから名前付きボリューム(db-store)はいらなかったかも。
元記事はテストサーバーにtmpfsを使ってますね。Dockerの知識もキャッチアップしなければ。

```Shell
# Dockerプロセスに渡す環境変数を作成
nano .env
```

```txt:.env
DB_NAME=testrdb
DB_USER=mysql
DB_PASS=mysql
DB_PORT=13306
TZ=Asia/Tokyo
```

```shell:shell
# Docker環境に投げ込むMySQLの設定ファイルを作成
nano my.cnf
```

```txt:my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_bin

default-time-zone = SYSTEM
log_timestamps = SYSTEM

default-authentication-plugin = mysql_native_password

log-error = /var/log/mysql/mysql-error.log

slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0

general_log = 1
general_log_file = /var/log/mysql/mysql-query.log

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4
```

※my.cnfは一応文字コードとか怖いから丸パクりしましたけど、見た感じざっと触って試す分には飛ばしてもいいかも？

```shell:shell
#とりあえず環境を立ててみる
docker-compose up
```

```shell:shell
#環境に入ってみる
docker-compose exec db bash

#インストールチェック
mysql -V
# mysql  Ver 8.0.23 for Linux on x86_64 (MySQL Community Server - GPL)

#接続チェック
mysql -u root -p
# .envで設定したパスワード = mysql を入力

show databases;
# testrdb があることをチェック
# MySQLから抜ける
\q

# Dockerから抜ける
exit
```

とりあえずMySQLサーバーがうまく動いていることは確認ができたので、今度はDockerの外からアクセス。
クライアントだけならローカルに最新版を入れてしまいます。私の場合、昔インストールしたクライアントが入っていたんですが多分

```shell:shell
brew install mysql-client
```

で行けると思います。
インストールしたら接続してみます。docker-compose.ymlでDockerの3306番ポートと13306番ポートが繋がる様になっているので

```shell:shell
mysql -h localhost -u root -p --port 13306 testrdb 
```

あれ？繋がりませんね。
mysqlはホスト名がlocalhost(省略した場合も同じ)の時はポートにつながずUnixソケットを探してしまうらしいです。
https://dev.mysql.com/doc/refman/5.6/ja/connecting.html
その場合はIPで指定すれば良いとのこと。

```shell:shell
mysql -h 127.0.0.1 -u root -p --port 13306 testrdb 
```

これで繋りました。さて、そろそろ公式ドキュメントの方に戻って次のページに進みます。
https://dev.mysql.com/doc/refman/8.0/en/document-store-interfaces.html

通常のクライアントとは別に、MySQL Shellのインストールを勧められています。とりあえずインストールしてみましょう。
以下の手順に従ってファイルをダウンロードします。Macの場合はpkgファイルをダウンロードして実行。
https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-install.html

Macにインストールがブロックされる場合、ctrl+クリック→「実行」で。

```shell:shell
#MySQL Shellのインストールチェック
mysqlsh --version
# mysqlsh   Ver 8.0.23 for macos10.14 on x86_64 - for MySQL 8.0.23 (MySQL Community Server (GPL))

# 接続してみる
mysqlsh root@127.0.0.1:13306
```

繋がりましたね。ドキュメントを読み進めていきます。
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-shell.html

> Tip
> If you connect to the instance using classic MySQL protocol, for example by using the default port of 3306 instead of the mysqlx_port, you cannot use the
> Document Store functionality shown in this tutorial. For example the db global object is not populated.
> To use the Document Store, always connect using XX Protocol.

接続したところで残念なお知らせですが、3306番ポートで繋いだ場合
チュートリアルで紹介する機能は使えないので、代わりにmysqlx_port(33060)で繋ぐように書いてあります。
なぜ先に言わないのか。

Dockerを止めて設定を書き直し、再度立ち上げます。Dockerを立ち上げたshellでctrl+Cを入力してコンテナを停止させ、 

```yml:docker-compose.yml
version: "3"
services:
  db:
    image: mysql:8.0
    volumes:
      - db-store:/var/lib/mysql
      - ./logs:/var/log/mysql
      - ./my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_DATABASE=${DB_NAME}
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_ROOT_PASSWORD=${DB_PASS}
      - TZ=${TZ}
    ports:
      - ${DB_PORT}:3306
      - ${XDB_PORT}:33060 # 追加
volumes:
  db-store:
```

```txt:.env
DB_NAME=testrdb
DB_USER=mysql
DB_PASS=mysql
DB_PORT=13306
XDB_PORT=33061 # 追加
TZ=Asia/Tokyo
```

設定ファイルを変更して

```shell:shell
#環境を再構築して動かす
docker-compose up

#mysqlx_portに接続、先ほどDockerの33060番ポートと繋いだローカルの33061番に繋ぐ
mysqlsh root@127.0.0.1:33061
```

さて、これで環境作成は完了です。

#サンプルデータを読み込む

早速、先ほど接続した状態のMysql Shellを操作していきたいところですが、
サンプル用に"world_x"というデータセットが用意されているのでダウンロードしてデータベースに取り込みます。
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-download.html
ここのworld_x-db.zipをクリックしてしてダウンロードして

```shell:shell
cd $WORDSPACE
# ダウンロードフォルダからファイルを移動します
mv /Users/your-name/Downloads/world_x-db.zip ./
# 展開します
unzip world_x-db.zip

# 大体どんなデータが入っているのかチェック
less world_x-db/world_x-db.sql
```

```sql:world_x-db.sql
--- (略)

CREATE TABLE `city` (
  `ID` int NOT NULL AUTO_INCREMENT,
  `Name` char(35) NOT NULL DEFAULT '',
  `CountryCode` char(3) NOT NULL DEFAULT '',
  `District` char(20) NOT NULL DEFAULT '',
  `Info` json DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

--- (略)

```

なるほど、なんとなく察しがついてきました。JSON型のカラムがNoSQLの話と関係してそうです。
この"city"を操作するのがチュートリアルになりそうです。とりあえずsqlを読み込ませましょう。MySQL Shellでの操作です。

```mysql:MySQLShell

#sqlモードに切り替え
\sql

#sqlファイルの実行
\source /Users/your-name/sandbox/mysql8-trial/world_x-db/world_x.sql
# Switching to SQL mode... Commands end with ;

#jsモードに戻す
\js
# Switching to JavaScript mode...

#使用するスキーマをworld_xに切り替え
\use world_x
# Default schema `world_x` accessible through db.

#データベースの確認
db
# <Schema:world_x>
```

最後のコマンドはもう次のページ
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-documents-collections.html
に入ってしまいましたが、これでサンプルデータの読み込みは完了です。

#MySQL Shellを操作してみる

公式ドキュメントに従って、別のコマンドを叩いてみます。

```mysql:MySQLShell
db.getCollections()
# [
#     <Collection:countryinfo>
# ]
```

あれ？さっきsqlファイルでチェックした"city"とは別の名前が出てきました。公式の進め方からは脱線しますが

```mysql:MySQLShell
# sqlモードに切り替え
\sql

#使用するスキーマをworld_xに切り替え
use world_x;

#テーブル一覧
SHOW TABLES;
# +-------------------+
# | Tables_in_world_x |
# +-------------------+
# | city              |
# | country           |
# | countryinfo       |
# | countrylanguage   |
# +-------------------+
```

4つテーブルが入っていたみたいです。もう少し調べます。

```mysql:MySQLShell
#テーブルの中身を確認
SHOW COLUMNS FROM city;
# +-------------+----------+------+-----+---------+----------------+
# | Field       | Type     | Null | Key | Default | Extra          |
# +-------------+----------+------+-----+---------+----------------+
# | ID          | int      | NO   | PRI | NULL    | auto_increment |
# | Name        | char(35) | NO   |     |         |                |
# | CountryCode | char(3)  | NO   |     |         |                |
# | District    | char(20) | NO   |     |         |                |
# | Info        | json     | YES  |     | NULL    |                |
# +-------------+----------+------+-----+---------+----------------+

SHOW COLUMNS FROM countryinfo;
# +--------------+---------------+------+-----+---------+-------------------+
# | Field        | Type          | Null | Key | Default | Extra             |
# +--------------+---------------+------+-----+---------+-------------------+
# | doc          | json          | YES  |     | NULL    |                   |
# | _id          | varbinary(32) | NO   | PRI | NULL    | STORED GENERATED  |
# | _json_schema | json          | YES  |     | NULL    | VIRTUAL GENERATED |
# +--------------+---------------+------+-----+---------+-------------------+
```

どうやら、Mysql Shellはテーブルの構造を見てcollectionとそれ以外（普通のテーブル？）に分けて見ていることが分かります。
ドキュメントに戻ります。基本的な操作として挙げられているのは
db.name.add()
db.name.find()
db.name.modify()
db.name.remove()
の4つです、とりあえずfindから使ってみます。

```mysql:MySQLShell
db.countryinfo.find()
# 全239件のJSONが出力される

db.countryinfo.find("Name = 'Australia'")
# {
#     "GNP": 351182,
#     "_id": "00005de917d8000000000000000e",
#     "Code": "AUS",
#     "Name": "Australia",
#     "IndepYear": 1901,
#     "geography": {
#         "Region": "Australia and New Zealand",
#         "Continent": "Oceania",
#         "SurfaceArea": 7741220
#     },
#     "government": {
#         "HeadOfState": "Elizabeth II",
#         "GovernmentForm": "Constitutional Monarchy, Federation"
#     },
#     "demographics": {
#         "Population": 18886000,
#         "LifeExpectancy": 79.80000305175781
#     }
# }
1 document in set (0.0113 sec)
```

おお、アプリケーション開発のフレームワークで使う見慣れたクエリ操作に近いですね。
以下、ほぼほぼ公式の通り。

__add()__
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-collections-add.html
addは完全にワンパターンで、引数にJSON文字列を渡してやるとINSERTが発生する様子。

__find()__
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-documents-find.html

* 全検索: find()
* キーの値で一致検索: find("Name = 'Australia'")
* キーの値で大小検索: find("GNP > 500000")
* アンド検索: find("GNP > 500000 and demographics.Population < 100000000")
* 複雑な条件も: find("GNP*1000000/demographics.Population > 30000")
* 変数のバインド: find("Name = :country").bind("country", "Italy")
* 出力キーの指定: find().fields(["GNP", "Name"])
* 出力キーと出力形式を指定: find().fields(mysqlx.expr('{"Name": upper(Name), "GNPPerCapita": GNP*1000000/demographics.Population}'))
* リミット: find().limit(5)
* 昇順ソート: find().sort(["IndepYear asc"])
* 降順ソート: find().sort(["IndepYear desc"])
* リミット&オフセット: find().sort(["IndepYear desc"]).limit(8).skip(1)

__modify()__
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-documents-modify.html

* 検索してキーを指定して値を上書き: modify("Code = 'SEA'").set("demographics", {"LifeExpectancy": 78, "Population": 28})
* 検索してキーを削除: modify("Name = 'Sealand'").unset("GNP")
* 全てのデータにキーを追加する: modify("true")db.set("Airports", [])
* 配列の中に値を追加する: modify("Name = 'France'").arrayAppend("$.Airports", "ORY")
* 位置を指定して配列の中に値を追加する: modify("Name = 'France'").arrayInsert("$.Airports[0]", "CDG")
* 配列から値を取り除く: db.countryinfo.modify("Name = 'France'").arrayDelete("$.Airports[1]")

__remove()__
https://dev.mysql.com/doc/refman/8.0/en/mysql-shell-tutorial-javascript-documents-remove.html

* 検索して削除: remove("Code = 'SEA'")
* 全て削除: remove("true")
* 最初の1つを削除: remove("true").limit(1)
* ソートして最初の1つを削除: remove("true").sort(["Name desc"]).limit(1)

これだけあれば簡単なNoSQLの構成なら十分に設計に組み込めそうな感じがしますね。
と、ちょっとだけ盛り上がってきた感じですが、これって実際の開発で使えるんでしょうか？

#X DevAPIが使える開発ツールは？

ここまで試してきたMySQL Shellのコマンドが使えるのはドキュメントの最初の方で書いてある通り
X DevAPIに対応したクライアント、つまり
MySQL Shell (which provides implementations of X DevAPI in JavaScript and Python)
Connector/C++
Connector/J
Connector/Node.js
Connector/NET
Connector/Python
に限られているというのが現状みたいですね。うーん？

#SQL JSON関数

X DevAPI自体はまだちょっと使える状態ではないかと思うのですが、APIなので、実際に発行されるSQLが別にあるはず
とりあえず使えるか使えないかはさておき、どんなSQL文が発行されているのかをみてみたいと思います。

ログを確認しながらMySQL ShellのコマンドとSQLを見比べてみます。

```shell:shell
cd $WORDSPACE
less +F logs/mysql-query.log
# この状態でMySQL Shellを操作して見比べる
```

まずは単純な検索から

```mysql:MySQLShell
db.countryinfo.find("Name = 'Australia'")
```

```log:logs/mysql-query.log
SELECT doc FROM `world_x`.`countryinfo` WHERE (JSON_EXTRACT(doc,'$.Name') = 'Australia')
```
JSON_EXTRACTというのがJSON型カラムを捜索するSQL関数として用意されているんですね。思ったより複雑ではなさそう。

```mysql:MySQLShell
db.countryinfo.find("Name is not NULL").sort(["IndepYear"]).limit(5)
```

```log:logs/mysql-query.log
SELECT doc FROM `world_x`.`countryinfo` WHERE (NOT (JSON_EXTRACT(doc,'$.Name') IS NULL)) 
ORDER BY JSON_EXTRACT(doc,'$.IndepYear') LIMIT 0, 5
```
基本的にJSONから値を取り出す場所以外は一般的なSQLと変わらない感じです。

```mysql:MySQLShell
# 変更をかける
db.countryinfo.modify("Code = 'SEA'").set("demographics", {"LifeExpectancy": 78, "Population": 28})
```

```log:logs/mysql-query.log
UPDATE `world_x`.`countryinfo` SET doc=JSON_SET(JSON_SET(doc,'$.demographics',
JSON_OBJECT('LifeExpectancy',78,'Population',28)),'$._id',JSON_EXTRACT(`doc`,'$._id')) 
WHERE (JSON_EXTRACT(doc,'$.Code') = 'SEA')
```

何となく様子がわかってきました。
基本的にはJSON操作に関係する部分がこのJSON~~みたいな関数に変換されている以外は普通のSQLと変わりません。
この関数を使いこなすことができればそこまで難しくなさそうな感じがしますね。リファレンスこちら。
https://dev.mysql.com/doc/refman/8.0/en/json-function-reference.html
JSON操作用の関数はSQL:2016の規格で定められているそうなので、
少なくとも将来的に消えて無くなるようなものではなさそうで安心です。（Wikipedia調べ）
https://en.wikipedia.org/wiki/SQL:2016

#JSON型をカラムを使ったNoSQLを開発に使うかどうか？

この記事を書いている時点ではNoSQLを使うアーキテクチャの記事はMongoDBかFireBase利用したものがほとんどでした。
NoSQLを採用するケース自体少ない上に、特化したデータベースがある中で使う意味が薄いと言われるとそんな気もします。

ただ、確かにRBBかNoSQLDBのどちらにするか？その上でどのDBを使うかという話だとそうなりますが、
1つトランザクションの中で2つRDBもNoSQLDBも操作することができるって結構便利な気がします。

あとは実装難易度とかメンテナンス性がどの程度なのかということですが
Stack Overflowで誰かが書いてたソースですが、railsでも

```model.rb
Model.where("json->'$.id' = :id and json->'$.type' = :type", id: 10, type: "mos")
# json->'$.id' は JSON_EXTRACT(json, '$.id') と同値
```
と、そこまで可読性を落とさず書けるようなので、気が向いたら使ってみようかなあと思うくらいには有りな気がします。
__#個人の感想です__
__#知らんけど__