MySQL Connector/J 8.0.15 で X DevAPI を使ってドキュメントデータベース操作をしてみる

MySQL 8.0 では、

 - JSON 機能
 - X DevAPI

を活用すればドキュメントデータベース（ドキュメントストア）としても使えるよ、ということなので、遅ればせながら Java（8）環境で MySQL Connector/J を使って試してみました。

---

**【参考】**

 - **[X DevAPI User Guide](https://dev.mysql.com/doc/x-devapi-userguide/en/)**
 - **[MySQL Connector/J X DevAPI Reference](https://dev.mysql.com/doc/dev/connector-j/8.0/)**

---

# 準備

以下を同一のマシン（Windows PC でも Linux 仮想マシンでも OK）にインストールします。

 - MySQL Community Server 8.0.15
 - Java SE 8 JDK / JRE（とりあえず OpenJDK ベースものなら大丈夫かと）
 - MySQL Connector/J 8.0.15
 - テストコード（後述）

※テストコードから MySQL Connector/J にパスを通した状態で .jar 化するなどしてください。面倒なのでわたしは Windows 10 に MySQL Community Server をインストールし、Maven で MySQL Connector/J を組み込んで Eclipse 上でコードを実行しました。 

次に、MySQL（Community）Server 上にテスト用のユーザを登録しておきます（コマンドラインからでも MySQL Workbench からでも OK）。

 - ユーザ名は`testuser@localhost`
 - パスワードは`T35_U53r`
 - SELECT, INSERT, UPDATE, DELETE 権限
 - 認証は`caching_sha2_password`でも`mysql_native_password`でも OK

最後に、`CREATE DATABASE`などで DB（スキーマ）`test_db`を作成して準備完了です。

# テストコードの説明

↓が簡単なドキュメント処理をするテストコードです。

```java:テストコード
package site.hmatsu47.DocDbTest;

import java.util.List;

import com.mysql.cj.xdevapi.Collection;
import com.mysql.cj.xdevapi.DbDoc;
import com.mysql.cj.xdevapi.DocResult;
import com.mysql.cj.xdevapi.Schema;
import com.mysql.cj.xdevapi.Session;
import com.mysql.cj.xdevapi.SessionFactory;


public class Main {

	public static void main(String args[]) {
		// サーバに接続
		Session session = new SessionFactory().getSession("mysqlx://localhost:33060/test_db?user=testuser&password=T35_U53r");

		// DBに接続
		Schema db = session.getSchema("test_db");

		// コレクション'test_collection'を作成
		Collection col = db.createCollection("test_collection", true);

		// コレクションにドキュメントを追加
		col.add("{\"id\":1, \"name\":\"さくらばさん\", \"Program\":\"Java Champion\"}")
		        .execute();
		col.add("{\"id\":2, \"name\":\"よくさん\", \"Program\":\"Oracle ACE\"}")
		        .execute();
		col.add("{\"id\":3, \"name\":\"せろさん\", \"Program\":[\"Java Champion\",\"Oracle Groundbreaker Ambassador\"]}")
		        .execute();
		col.add("{\"id\":4, \"name\":\"とみたさん\", \"Program\":\"Oracle ACE Associate\"}")
		        .execute();
		col.add("{\"id\":5, \"name\":\"みたにさん\", \"Program\":\"Oracle ACE\"}")
		        .execute();

		// 本題とは関係ないのですが、OracleのACE・Groundbreaker・Java ChampionのDBにじゅくちょーさんが見当たらないのです…

		// コレクションの「id」列にインデックスを追加
		col.createIndex("id_index", "{\"fields\": [{\"field\": \"$.id\", \"type\": \"INT\"}]}");

		// コレクションから「Program LIKE '%Oracle%'」を探して表示
		searchProgram(col, "Oracle");

		System.out.println();

		// コレクションから「Program LIKE '%Java%'」を探して表示
		searchProgram(col, "Java");

		System.out.println();

		// コレクションから「id=2」を探して表示
		searchId(col, 2);

		System.out.println();

		// コレクションから「id=4」を探して表示
		searchId(col, 4);

		// コレクションを削除
		db.dropCollection("test_collection");
	}

	// コレクションから対象ドキュメントの「Program」を文字列検索して表示する
	private static void searchProgram(Collection col, String keyword) {

		System.out.println("Search: " + keyword);
		DocResult docs = col.find("Program like :program")
		        .bind("program", "%" + keyword + "%").execute();

		// 結果を取得して表示
		List<DbDoc> docl = docs.fetchAll();
		docl.forEach(doc -> System.out.println(doc.toFormattedString()));
	}

	// コレクションから対象ドキュメントの「id」を数値検索して表示する
	private static void searchId(Collection col, long value) {

		System.out.println("Search: " + value);
		DocResult docs = col.find("id = :id")
		        .bind("id", value).execute();

		// 結果を取得して表示
		System.out.println(docs.fetchOne().toFormattedString());
	}
}
```

`new SessionFactory().getSession()`で引数に`URL`を指定して DB サーバに接続し、`session.getSchema();`で引数にスキーマ（DB）名を指定して DB に接続しています。

なお、`URL`はその名の通り`URL`ですので、パスワードに記号を使っている場合は適宜エスケープ処理をしてください（わたしはうっかりミスをして小一時間悩みました）。

その後、順に

 - ドキュメントコレクションの新規作成（同名のものが既に存在するときはそのまま使う）
 - ドキュメントコレクションにドキュメントを追加（計 5 行）
 - インデックス作成
 - 文字列`LIKE`検索
 - 数値検索（インデックス使用を意図したもの）
 - ドキュメントコレクションの削除

を行っています。

見ての通り、文字列`LIKE`検索では、1 つのドキュメントに同じ名前の要素を複数持つ場合でも正しく検索されていることがわかります。

実行途中にテーブル設計を見るとこうなります（`SHOW CREATE TABLE test_db.test_collection;`）

```sql:test_collectionのテーブル設計
CREATE TABLE `test_collection` (
  `doc` json DEFAULT NULL,
  `_id` varbinary(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,_utf8mb4'$._id'))) STORED NOT NULL,
  `$ix_i_C6EF652A87142734264FBA60F41C59108CF4D8CA` int(11) GENERATED ALWAYS AS (json_extract(`doc`,_utf8mb4'$.id')) VIRTUAL,
  PRIMARY KEY (`_id`),
  KEY `id_index` (`$ix_i_C6EF652A87142734264FBA60F41C59108CF4D8CA`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
```

また、実行結果はこうなります。

```json-doc:実行結果
Search: Oracle
{
"Program" : "Oracle ACE",
"_id" : "00005c8ded80000000000000006b",
"id" : 2,
"name" : "よくさん"
}
{
"Program" : ["Java Champion", "Oracle Groundbreaker Ambassador"],
"_id" : "00005c8ded80000000000000006c",
"id" : 3,
"name" : "せろさん"
}
{
"Program" : "Oracle ACE Associate",
"_id" : "00005c8ded80000000000000006d",
"id" : 4,
"name" : "とみたさん"
}
{
"Program" : "Oracle ACE",
"_id" : "00005c8ded80000000000000006e",
"id" : 5,
"name" : "みたにさん"
}

Search: Java
{
"Program" : "Java Champion",
"_id" : "00005c8ded80000000000000006a",
"id" : 1,
"name" : "さくらばさん"
}
{
"Program" : ["Java Champion", "Oracle Groundbreaker Ambassador"],
"_id" : "00005c8ded80000000000000006c",
"id" : 3,
"name" : "せろさん"
}

Search: 2
{
"Program" : "Oracle ACE",
"_id" : "00005c8ded80000000000000006b",
"id" : 2,
"name" : "よくさん"
}

Search: 4
{
"Program" : "Oracle ACE Associate",
"_id" : "00005c8ded80000000000000006d",
"id" : 4,
"name" : "とみたさん"
}
```

# その他

X DevAPI には、ほかにも

 - 非同期処理
 - リレーショナルな DB スキーマに対する CRUD 等の操作
 - トランザクション処理

など、いくつかの機能があります。

---

 - **[Qiitaに投稿したMySQL 8.0関連記事](https://qiita.com/hmatsu47/items/040d65d118d0ecec6381)**