EmbulkでMongoDB→MySQLへデータ移行してみた

本記事は[ZOZOテクノロジーズ アドベントカレンダー#2](https://qiita.com/advent-calendar/2020/zozo_tech2)の24日目の記事です。

# 概要
ZOZOTOWNのリプレイスでは Embulk を使いDBのデータ移行を行っております。
[Embulk](https://github.com/embulk/embulk) とはデータ転送のためのツールで、[plugin](https://plugins.embulk.org/) (input、output、filter) を組み合わせて様々なデータを意のままに転送することができます。 

今回、MongoDB → MySQL へのデータ移行を行ったのですが、
あまり情報が出回ってなかったので参考になればと思い、手順を紹介したいと思います。

# TL;DR;
* [embulk-input-mongodb](https://github.com/embulk/embulk-input-mongodb)、[embulk-output-mysql](https://github.com/embulk/embulk-output-jdbc/tree/master/embulk-output-mysql)
を使う
* filter を使いデータ加工を行う必要がある
  * カラム展開は [expand_json](https://github.com/civitaspo/embulk-filter-expand_json) を使う
  * NULLデータの default 値は [column](https://github.com/sonots/embulk-filter-column) で設定

# 移行要件
下記の要件でデータ移行を行いました。

* MongoDB 2.6 → mysql 5.7 へデータ移行
* MongoDB のデータはフィールド数がまちまち
* MySQLで NOT NULL としたいカラムに対し、MongoDB で Key が存在しないデータの場合デフォルト値を指定して移行

# 検証用環境構築
簡単に動作確認ができるよう、docker-compose で検証用環境をサクッと作ってしまいましょう。
手順だけ確認したいよ、という方はこちらは読み飛ばして下さい。

```docker-compose.yml
version: "3.7"
networks:
  backend:
services:
  mongodb:
    image: mongo:2.6
    container_name: mongodb
    networks:
      - backend
    ports:
      - 27018:27017
  mysql:
    image: mysql:5.7
    container_name: mysql
    environment:
      - MYSQL_DATABASE=test
      - MYSQL_ROOT_PASSWORD=hogehoge
    networks:
      - backend
    ports:
      - 3307:3306
```

上記 `docker-compose.yml` を作成後、下記コマンドを実行ください。

```
$ docker-compose up -d
$ docker-compose exec mongodb mongo test
(mongo)> db.Users.insert([
{"id": 1, "name": "hoge", "email": "hoge@example.com", "age": 20},
{"id": 2, "name": "fuga", "email": "fuga@example.com", "age": 25},
{"id": 3, "name": "foo", "age": 30}
])
```

下記の様なデータが格納されていれば準備 ok です。

```
> db.Users.find()
{ "_id" : ObjectId("5fe2679a62a2dec6c0f0751b"), "id" : 1, "name" : "hoge", "email" : "hoge@example.com", "age" : 20 }
{ "_id" : ObjectId("5fe2679a62a2dec6c0f0751c"), "id" : 2, "name" : "fuga", "email" : "fuga@example.com", "age" : 25 }
{ "_id" : ObjectId("5fe2679a62a2dec6c0f0751d"), "id" : 3, "name" : "foo", "age" : 30 }
```

# MongoDB → MySQL データ移行
## input-mongodb、output-mysql plugin
input : [embulk-input-mongodb](https://github.com/embulk/embulk-input-mongodb)
output : [embulk-output-mysql](https://github.com/embulk/embulk-output-jdbc/tree/master/embulk-output-mysql)
を使うことで、MongoDB → MySQL へデータ移行ができます。

Embulk の YAML は下記の様になります。

```config.yml
in:
  type: mongodb
  hosts:
  - {host: 127.0.0.1, port: 27018}
  database: test
  collection: "Users"
out:
  type: mysql
  host: 127.0.0.1
  port: 3307
  user: root
  password: hogehoge
  database: test
  table: users
  mode: replace
```

`embulk run config.yml` を実行すると MySQL users テーブルへ下記データが移行出来ます。

```
$ mysql -u root -phogehoge -P 3307 -h 127.0.0.1 test -e "select * from users;"
+-------------------------------------------------------------------------------------------------+
| record                                                                                          |
+-------------------------------------------------------------------------------------------------+
| {"_id":"5fe2679a62a2dec6c0f0751b","id":1.0,"name":"hoge","email":"hoge@example.com","age":20.0} |
| {"_id":"5fe2679a62a2dec6c0f0751c","id":2.0,"name":"fuga","email":"fuga@example.com","age":25.0} |
| {"_id":"5fe2679a62a2dec6c0f0751d","id":3.0,"name":"foo","age":30.0}                             |
+-------------------------------------------------------------------------------------------------+
```

MongoDB のデータを MySQL へ移行できました。
しかし、record カラムに json 構造がそのままデータが入っている状態です。
そこで、フィルタを使ってデータの加工を行っていきます。

## expand_json filter を使い json をカラムに展開
filter : [expand_json](https://github.com/civitaspo/embulk-filter-expand_json)
を使います。

embulk-input-mongodb では、デフォルトで[output されるカラム名は `record` となる](https://github.com/embulk/embulk-input-mongodb#configuration)ため、`json_column_name` を `record` とします。


```config.yml
in:
  type: mongodb
  hosts:
  - {host: 127.0.0.1, port: 27018}
  database: test
  collection: "Users"
filters:
  - type: expand_json
    json_column_name: record
    expanded_columns:
      - {name: id, type: long}
      - {name: name, type: string}
      - {name: email, type: string}
      - {name: age, type: long}
out:
  type: mysql
  host: 127.0.0.1
  port: 3307
  user: root
  password: hogehoge
  database: test
  table: users
  mode: replace
```

`embulk run config.yml` を実行すると MySQL users テーブルへ下記データが移行出来ます。

```
$ mysql -u root -phogehoge -P 3307 -h 127.0.0.1 test -e "select * from users;"
+------+------+------------------+------+
| id   | name | email            | age  |
+------+------+------------------+------+
|    1 | hoge | hoge@example.com |   20 |
|    2 | fuga | fuga@example.com |   25 |
|    3 | foo  | NULL             |   30 |
+------+------+------------------+------+
```

json をカラムに展開しつつデータ移行できました。
MongoDB で email の Key を持たないデータは、email が NULL となっています。
email カラムを NOT NULL としたい場合この状態は望ましくありません。
こちらもフィルターを使うことで、NULLではなく `''(空)` など default 値を設定することが出来ます。

## column filter を使い default 値を指定
filter : [column](https://github.com/sonots/embulk-filter-column)
を使います。

```config.yml
in:
  type: mongodb
  hosts:
  - {host: 127.0.0.1, port: 27018}
  database: test
  collection: "Users"
filters:
  - type: expand_json
    json_column_name: record
    expanded_columns:
      - {name: id, type: long}
      - {name: name, type: string}
      - {name: email, type: string}
      - {name: age, type: long}
  - type: column
    columns:
      - {name: id}
      - {name: name}
      - {name: email, default: ''}
      - {name: age}
out:
  type: mysql
  host: 127.0.0.1
  port: 3307
  user: root
  password: hogehoge
  database: test
  table: users
  mode: replace
```

`embulk run config.yml` を実行すると MySQL users テーブルへ下記データが移行出来ます。

```
+------+------+------------------+------+
| id   | name | email            | age  |
+------+------+------------------+------+
|    1 | hoge | hoge@example.com |   20 |
|    2 | fuga | fuga@example.com |   25 |
|    3 | foo  |                  |   30 |
+------+------+------------------+------+
```

email カラムを `''(空)` としてデータ移行できました。
以上で要件通りデータの移行が行えました。

# まとめ
[embulk-input-mongodb](https://github.com/embulk/embulk-input-mongodb) に filter を組み合わせることで、要件通り MongoDB → MySQL のデータ移行ができました。

Embulk には bult-in を含め多数の [plugin](https://plugins.embulk.org/) が存在しますので、様々な要件に柔軟に対応することができます。
(弊社では [Sonots](https://qiita.com/sonots)、[Civitaspo](https://qiita.com/Civitaspo) などが様々な plugin を公開しており、今回使用した filter もお二人作のものです。)

データ移行を伴うリプレイス時など、
ご興味ありましたら是非 Embulk を使い簡単にデータ移行を行ってみて下さい。

ご一読頂きありがとうございました。