MySQL から Datastore へマイグレーションする際の Key 設計例

**Google Cloud Platform Advent Calendar 2017 11日目**を担当する avvmoto です。よろしくお願いします。


この記事では、MySQL 等から Datastore にマイグレーションするときに必要となる、 Key 設計についての考慮点を解説します。


# はじめに

GAE 等 GCP の強力な機能を用いるため、 MySQL 等 RDBMS から Datastore へマイグレーションしたくなる時があると思います。
Datastore には強力な機能が備わっていますが、独特の特徴があるので、それらを考慮して Key 設計する必要があります。

今回は、以下を実現することにします。

1.  マイグレート元のIDを引き継ぐ
2. 新規 Entity の ID の発行には、Datastore 付属の automatically assigned ID を用いる
  - つまり、アプリ開発者が ID の衝突を防ぐ仕組みを実装せずに済み、楽をします
3. パフォーマンス悪化となる、ホットスポット問題を発生させない


これを実現するときの考慮点と、実現例をご紹介します。

#  Key 設計指針

## Key の型・値

Key に用いられる型は二種類あります。 [^1]

* int
  * Cloud Datastore が自動でアサインするか、 Allocate した上でその範囲内の値を用いる
* string
  * 利用者が任意のキーを指定する



今回はマイグレート元の ID を引き継ぐことを考えているため、 string の Key を用いることにします。
単純に int の ID を string に変換すると、つまり例えば元の ID が `1` だった場合に Key として string の `"1"` を用いる場合、いわゆるホットスポット [^2] の問題が発生し、パフォーマンスに問題が出ます。

そのため、連番の int のID に対し、適度に分散されたstringが得られるような、何らかの1:1となり、しかも可逆な変換をした上で、それを Key として用いると良いでしょう。
この変換のアルゴリズムには、例えば Blowfish [^3] [^4] などが利用できます。


既存のレコードをマイグレーションするときの Key このように設定できます。Datastore 移行後に新規に作成する Entity の Key については、次の章で説明します。

![a1.png](https://qiita-image-store.s3.amazonaws.com/0/25630/54a7846a-2392-1748-b07b-82b556d40a2c.png)




[^4]: 仮に golang を用いる場合、 Bigint は他言語に比べて遅いと言われますが、 Blowfish は Bigint を用いない点も適切です。

[^3]: https://ja.wikipedia.org/wiki/Blowfish

[^2]: https://cloud.google.com/datastore/docs/best-practices?hl=ja#keys


[^1]: https://cloud.google.com/datastore/docs/concepts/entities#assigning_identifiers


## ID の Allocate
### 新規 Entity の作成

Key に string を用いた場合、既存の衝突しないことは自分で担保しないとなりません。[^1]
int の Key を一つ `AllocateID` で Allocate し、それを Blowfish 等で変換した string を用いることで、既存レコードの ID と衝突しない string を得ることができます。
ただし、事前に以下のように、既存レコードの ID を Allocate しておく必要があります。


なお、今回の例だとEntity の Key には string を用いていますが、int の Key を Allocate することに特に問題はないようです。

![a2.png](https://qiita-image-store.s3.amazonaws.com/0/25630/599f2ec1-036b-c6d4-acbc-682389a989b2.png)

### 既存レコード ID の Allocate

既存レコードの ID が連番で採番されていて、最大値は `N` であるとします。この場合、 Datastore の自動アサインで、 1 ~ N の範囲が用いられないように、事前にその範囲を Allocate しておく必要があります。
この Allocate は、必ず新規 Entity を作成する前に行いましょう。
この Allocate には [allocateIdRange](https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService#allocateIdRange-com.google.appengine.api.datastore.KeyRange-) [^5] を用いましょう。

なお上記ドキュメントには、 [DatastoreService.KeyRangeState] (https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService.KeyRangeState) に応じて、Allocate しても他の Entity を上書きする場合があると書いてあります。
しかし新規の(つまりまだ Entity の作成されていない) Kind に対して、初回の `allocateRangeId()` で Allocate した領域に対しては、上書きされる事はありません。詳しくは[補足:KeyRangeState](#補足keyrangestate) をご覧ください。




[^5]: なお golang にはこれの相当物は、今のところありません。 https://github.com/golang/appengine/issues/97

### Enity Group の場合

Enity Group の場合は注意が必要です。子となる Entity の Key は、親 Entity の Key を含めたものとなります。 そのため、親 Entity 毎に、子となる Kind で ID の Allocate が必要となります。


# まとめ

MySQL 等から Datastore へマイグレーションするときの考慮点や、設計例をご紹介しました。
実際に Key を設計するときは、今回の考慮点以外にもビジネスロジックから Strong Consistency や ユニーク制約が必要になり、より多くの考慮点が発生すると思います。
そのため、ケースバイケースで Key 設計をすることになると思います。

あやまりや不正確な点がありましたら、ご教授のほどよろしくお願いします。


# 補足:KeyRangeState

[allocateIdRange](https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService#allocateIdRange-com.google.appengine.api.datastore.KeyRange-) によると、

> writing entities with manually assigned keys in this range may overwrite existing entities (or new entities written by a separate request) depending on the DatastoreService.KeyRangeState returned. 

とあります。つまり allocateIdRange で取得した ID を手動で明示的に Key に指定して Entity を作成すると、 [DatastoreService.KeyRangeState] (https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService.KeyRangeState) によっては、既存の Entity を上書きする場合があります。

[DatastoreService.KeyRangeState] (https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService.KeyRangeState) を見てみましょう。

| KeyRangeState | 訳 |
|---------------|--------|
| COLLISION     | 衝突   |
| CONTENTION    | 競合   |
| EMPTY         | 空     |


`COLLISION` はわかりやすいです。
> Indicates that entities with keys inside the given KeyRange already exist and writing to this range will overwrite those entities.

つまり、 Allocate した `KeyRange` の中に、既に Entity が存在している状態を指します。


`Empty` は、
> Indicates the given KeyRange is empty and the datastore's automatic ID allocator will not assign keys in this range to new entities.

つまり、 Allocate した `KeyRange` の中に、Entity が存在しない状態です。


`CONTENTION` はややこしいです。
> Indicates the given KeyRange is empty but the datastore's automatic ID allocator may assign new entities keys in this range.

つまり、Allocate した `KeyRange` の中に Entities は存在しないものの、自動 ID アロケーターがその範囲内に新しい Entity の Key を Allcoate する場合がある状態です。


この状態は、内部実装的に、 Key の Allocate 済みの範囲を、領域として保存していないことに由来します。[^6] [^7]
つまり、その値以下は自動アロケーターが発番に用いないという最大値を持っているのです。仮にこの値を「Allocate済みの最大値」 と呼びましょう。
`KeyRange` を指定して `allocateIdRange()` で Allocate するということは、対象の Kind の「Allocate済みの最大値」 を `KeyRange` の最大値に設定することである、と言い換えることができます。

さて、 `CONTENTION` についてです。 Allocate しようとした `KeyRange` の中に、「Allocate済みの最大値」がある場合もありえます。
この状態が `CONTENTION` となります。


最終的な結論として、今回気にしていたEntity の無い空の Kind への `allocateIdRange()` についていえば、 任意の `1 ~ N ` の `allocateIdRange()` が成功し、自動 ID アロケーターは以後この範囲を発番に用いない、といえます。



[^6]: この仕様を表現しているドキュメントは見つかりませんでしたが、私が Google Cloud Platform Support に問い合わせた結果、そうだということでした。なお公開されていない仕様なので、将来的に変更される可能性があります。しかし仮に将来仕様が変わったとしても、既に Allocate 済みの領域は、ずっと Allocate 済み扱いされることを期待していいと思います。
[^7]: このあたりの仕様は、 Datastore のクライアントライブラリーが、裏側の Datastore API へ送っている [リクエストパラメーター](https://github.com/greghaskins/googleappengine/blob/e78ea5c1ef55564b776ff155630202afb2d4ecfa/java/src/main/com/google/appengine/api/datastore/DatastoreServiceImpl.java#L164) からも察することができます。 



# 参考資料

* https://ja.wikipedia.org/wiki/Blowfish
* https://cloud.google.com/datastore/docs/best-practices?hl=ja#keys
* https://cloud.google.com/datastore/docs/concepts/entities#assigning_identifiers
* https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService#allocateIdRange-com.google.appengine.api.datastore.KeyRange-
* https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/DatastoreService.KeyRangeState

# 脚注