Google App Engine Python2ランタイムとPython3ランタイムのDatastoreの相互運用性を検証する

Google App Engine ユーザの皆さん、こんにちは。[株式会社レヴィ](https://levii.co.jp)のゆみやまです。弊社でも App Engine を 2012年頃から利用していて、現在、Python2ランタイムからPython3ランタイムへの移行を進めています。

弊社のサービスでは、データベースに Cloud Datastore を利用していて、ランタイム移行を行ってもバックエンドのデータベースは共通のものを利用し続ける予定です。しかし、Python2ランタイムで利用できる App Engine SDK の提供する ndbライブラリと、 Python3ランタイムで利用できる Google Cloud SDK の提供する Datastore Client の間で相互に運用可能にするには幾つかの注意が必要です。

本記事では、App Engine の Python2 ランタイムと、 Python3 ランタイムの間で、相互に運用可能な Cloud Datastore の利用方法について整理します。

# 相互運用性のまとめ

 - Python2 ndb の提供するプロパティの中で、相互運用に注意が必要なプロパティ
    - StructuredProperty (プロパティ名の表現を工夫することで相互運用可能)
    - JsonProperty (JSON文字列表現のバイト列として相互運用可能)
    - 以下は未検証だが何らかの工夫で相互運用が可能かもしれない
        - PickleProperty (未検証)
        - DateProperty (未検証)
        - TimeProperty (未検証)
        - GeoProperty (未検証)
        - GenericProperty (未検証)
        - ComputedProperty (未検証)
 - Python3 Datastore Client で利用可能なプロパティの中で、相互運用に注意が必要なプロパティ
     - 埋め込みエンティティ
        - (Python2 ndb で読み出し・書き込みが行えないので相互にRead/Writeするとデータが破壊される)

# プリミティブなプロパティ値の相互運用性
Cloud Datastore で扱うエンティティの持つ、プリミティブなプロパティ値については、Python2ランタイムとPython3ランタイムの間で相互に運用可能です。具体的には、Python2ランタイムで利用可能な以下の型については、Python3ランタイムでも対応する値として読み出すことが可能です。

 - IntegerProperty
 - FloatProperty
 - BooleanProperty
 - StringProperty
 - TextProperty
 - BlobProperty
 - DateTimeProperty
 - KeyProperty

以下のような Python2 ndb を利用したコードで作成した Datastore エンティティは、 Python3 Datastore Client でも問題なく読み取り・書き込みを行うことができます。

```py
class PrimitiveProperties(ndb.Model):
    integer = ndb.IntegerProperty()
    float = ndb.FloatProperty()
    boolean = ndb.BooleanProperty()
    string = ndb.StringProperty()
    text = ndb.TextProperty()
    indexed_blob = ndb.BlobProperty(indexed=True)
    blob = ndb.BlobProperty()
    datetime = ndb.DateTimeProperty()
    datastore_key = ndb.KeyProperty()
    blobstore_key = ndb.BlobKeyProperty()
    created_at = ndb.DateTimeProperty(auto_now_add=True)
    updated_at = ndb.DateTimeProperty(auto_now=True)
    python2_updated_at = ndb.DateTimeProperty(repeated=True)
    python3_updated_at = ndb.DateTimeProperty(repeated=True)
```

以下の場合に相互運用性があることの確認は行っていません

 - プロパティのオプションに compressed=True を指定した場合の振る舞い
 - DateProperty, TimeProperty の振る舞い
 - GeoProperty の振る舞い

# Python2 の ndb で提供されるプリミティブ型の repeated=True オプションの相互運用性

Python2ランタイムの ndb ライブラリで提供される repeated=True オプションが付与されている場合、Python3ランタイムの Cloud Datastore クライアントでも配列として扱うことができます。

```py
class PrimitiveProperties(ndb.Model):
    # ...
    python2_updated_at = ndb.DateTimeProperty(repeated=True)
    python3_updated_at = ndb.DateTimeProperty(repeated=True)
```

Python3 Datastore Client では、単一のプロパティの配列に、異なる型の値を指定することができますが、それを Python2 ndb で扱うことができるかは確認していません。

```py
entity = datastore.Entity(key=datastore_client.key("MixedArray"))
entity.update({ "mixed_array": [ "string", 123, datetime.datetime.utcnow() ] })
datastore_client.put(entity)
```
# Python2 の ndb で提供される StructuredProperty の相互運用性
## repeated がない場合
Python2 で StructuredProperty を利用して保存したデータは、Python3 の Datastore Client では、保存対象のエンティティのプロパティ名と、StructuredPropertyになっているエンティティのプロパティ名をドットで繋いだ形で参照することができます。

```py
class Tag(ndb.Model):
    name = ndb.StringProperty()

class Parent(ndb.Model):
    tag = ndb.StructuredProperty(Tag)
    updated_at = ndb.DateTimeProperty(auto_now=True)

Parent(tag=Tag(name="new-tag"))
```

```py
# python3 datastore client で取得した結果
{
  "tag.name": "new-tag",
  "updated_at": datetime.datetime(2019, 11, 5, 11, 31, 29, 140885, tzinfo=<UTC>)
}
```
## repeated がある場合
StructuredProperty は repeated 修飾子をネストできないという制限があります。
Python2 の ndb ライブラリを利用して Book エンティティに対して複数の Tag を付与する場合には、以下のような構造として表現されます。

```py
# python2 ndb ライブラリでの表現
class Tag(ndb.Model):
    name = ndb.StringProperty()
    color = ndb.StringProperty()

class Book1(ndb.Model):
    tags = ndb.StructuredProperty(Tag, repeated=True)
    updated_at = ndb.DateTimeProperty(auto_now=True)

Book1(tags=[Tag(name="non-fiction", color="blue"), Tag(name="science-fiction", color="black")])
```

```py
# python3 datastore client で取得した結果
{
  "tags.name": ["non-fiction", "science-fiction"],
  "tags.color": ["blue", "black"],
  "updated_at": datetime.datetime(2019, 11, 5, 11, 31, 29, 140885, tzinfo=<UTC>)
}
```

プロパティ名の構造は repeated が無い場合と同様で、 repeated が指定されている場合には、その値が配列となります。
上述のような Tag という一塊として扱いたいプロパティが複数のプロパティに分割されて、それぞれの配列として表現されるので扱いづらさはありますが、、相互に運用可能のなので、良しとしましょう！

# Python2 の ndb で提供される JsonPropertyの相互運用性
Python2 ndb の JsonProperty として保存した値は、Python3 Datastore Client では JSON文字列表現のバイト列表現として参照することができます。

```py
# python2 ndb ライブラリでの表現
class JsonPropertyModel(ndb.Model):
    json = ndb.JsonProperty()

JsonPropertyModel({
    'key': 'string-value',
    'list': [1234567890, 3.141592, 'hello'],
    'dict': { 'key': 'nested-value' }
}).put()
```

```py
# python3 datastore client で取得した結果
print(entity)
# => {
#     'json': b'{"dict":{"key":"nested-value"},"list":[1234567890,3.141592,"hello"],"key":"string-value"}'
# }

# dict として取り出すには、以下のような操作をすればよい
json.loads(entity['json'].decode('utf-8'))
```

# Python3 Datastore Client の 埋め込みエンティティ (Embedded Entity) の相互運用性
Python3 Datastore Client では任意のオブジェクト構造をプロパティとして保存することができます（埋め込みエンティティ）。
このプロパティは、Python2 ndb ライブラリでは取り扱うことができないので、相互運用時には利用できません。

```py
# python3 datastore client で埋め込みエンティティを設定する
entity = datastore.Entity(key=datastore_client.key('Embedded', 'python3'))
entity.update({
    'embedded': {
        'embedded-key': 'embedded-value',
        'array': [
            123,
            'hello'
        ],
        'python3_updated_at': [datetime.datetime.utcnow()]
    }
})
```

```py
# python2 ndb側で取得した値
embedded = ndb.Key('Embedded', 'python3').get()
print(embedded)
# => Embedded(key=Key('Embedded', 'python3'), embedded='j\x04j\x00r\x00r"\x1a\x0cembedded-key \x00*\x10\x1a\x0eembedded-valuer\r\x1a\x05array \x01*\x02\x08{r\x12\x1a\x05array \x01*\x07\x1a\x05hellor#\x08\x07\x1a\x12python3_updated_at \x01*\t\x08\xa9\xc0\x8c\x9a\x89\xd3\xe5\x02\x82\x01\x00')
# ndb で取得したオブジェクトを保存しなおすと、データが破損してしまう
```

# 参考

 - 本記事の検証に用いたリポジトリ: [levii/datastore-interop](https://github.com/levii/datastore-interop)
 - [エンティティ プロパティ リファレンス  |  Python 2 の App Engine スタンダード環境  |  Google Cloud](https://cloud.google.com/appengine/docs/standard/python/ndb/entity-property-reference?hl=ja#)
 - [エンティティ、プロパティ、キー  |  Cloud Datastore ドキュメント  |  Google Cloud](https://cloud.google.com/datastore/docs/concepts/entities?hl=ja#)