django-google-spanner==3.0.0でエミュレータを使おうとして挫折した話


Qiita Django Advent Calendar 2021 6日目は、DjangoのデータベースバックエンドにGoogle Cloud Spannerを使えるようにするdjango-google-spannerに関する話を書きます。

django-google-spannerの公式ドキュメントには本物のCloud Spannerインスタンスと接続する使い方しか書かれていませんが、この方法だとmigrateコマンドの実行にかなり時間がかかります(私が試したときは10分ぐらいかかりました)。 ローカルのエミュレータを使うようにすれば、もっと早くなって楽に開発できるのでは? と考えて、実際にやってみました。

結論から先に書くと、この試みはうまくいきませんでした… migrateコマンドの実行は高速化できるのですが、データの登録に失敗するケースがあります。

うまくいかなかったのですが、検証内容をここに公開します。 以下Cloud Spanner公式ドキュメントを参考に、django-google-spannerでエミュレータを使うサンプルアプリを作ってみます。

Cloud Spanner エミュレータの使用  |  Google Cloud

サンプルアプリで使っているPyhtonとライブラリのバージョンは以下のとおりです。

  • Python 3.10.0
  • Django==3.2.9
  • django-google-spanner==3.0.0

また、開発環境にGoogle Cloud SDKをインストールする必要があります。

最初にCloud Spannerエミュレータを使う準備を行います。 以下の環境変数を設定します(< >で囲った値は任意の文字列を入れてください)。

1
2
3
4
5
6
7
8
$ # エミュレータ上でのGoogle Cloudプロジェクト名
$ export GOOGLE_CLOUD_PROJECT="<your_project_name>"
$ # エミュレータ上でのCloud Spannerインスタンス名
$ export DJANGO_SPANNER_INSTANCE="<your_instance_name>"
$ # エミュレータ上でのCloud Spannerデータベース名
$ export DJANGO_DATABASE_NAME="<your_database_name>"
$ # エミュレータに繋ぐための設定
$ export SPANNER_EMULATOR_HOST=localhost:9010

gcloudにCloud Spannerエミュレータ用configを作成します。

1
2
3
4
$ gcloud config configurations create emulator
$ gcloud config set auth/disable_credentials true
$ gcloud config set project $GOOGLE_CLOUD_PROJECT
$ gcloud config set api_endpoint_overrides/spanner http://localhost:9020/

上記を実行すると、gcloudのconfigがemulatorに切り替わります。 Cloud Spannerエミュレータの使用を止めてデフォルトのconfigに戻したい場合はgcloud config configurations activate defaultを実行してください。

Cloud Spannerエミュレータを起動します。開発中はエミュレータのプロセスを立ち上げっぱなしにします。

1
$ gcloud emulators spanner start

Cloud Spannerエミュレータ上でインスタンスとデータベースを作成します。

1
2
3
4
$ gcloud spanner instances create $DJANGO_SPANNER_INSTANCE \
   --config=emulator-config --description="Test Instance" --nodes=1
$ gcloud spanner databases create $DJANGO_DATABASE_NAME \
   --instance $DJANGO_SPANNER_INSTANCE

これでCloud Spannerエミュレータが使えるようになりました。

次に、Djangoプロジェクトを作成します。

1
2
3
4
5
6
$ mkdir django_google_spanner_example
$ cd django_google_spanner_example
$ python3.10 -m venv .venv
$ source .venv/bin/activate
(.venv)$ pip install Django==3.2.9 django-google-spanner==3.0.0
(.venv)$ django-admin startproject django_google_spanner_example .

settings.pyは以下のように編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os

# (省略)
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_spanner',  # これを追記
]
# (省略)
DATABASES = {
    'default': {
        'ENGINE': 'django_spanner',
        'PROJECT': os.environ['GOOGLE_CLOUD_PROJECT'],
        'INSTANCE': os.environ['DJANGO_SPANNER_INSTANCE'],
        'NAME': os.environ['DJANGO_DATABASE_NAME'],
    }
}
# (省略)

これでDjangoアプリケーションからCloud Spannerエミュレータに繋げるようになりました。 migrateコマンドを実行してみましょう。1秒程度でテーブルの作成が完了するはずです。 見事、高速化に成功しましたね! と言いたいところですが…

createsuperuserで管理者ユーザーを作ろうとすると、以下のエラーが発生します。

1
2
3
4
5
6
7
8
9
(.venv)$ ./manage.py createsuperuser
Username (leave blank to use 'ryu22e'): example
Email address: test@example.com
Password:
Password (again):
Traceback (most recent call last):
(省略)
    raise ProgrammingError(e.details if hasattr(e, "details") else e)
django.db.utils.ProgrammingError: []

なお、このエラーはエミュレータを使っている場合のみ発生するもので、本物のCloud Spannerインスタンスでは普通に登録できます。

django-google-spannerのGitHub Issueに、このエラーに関する報告が挙がっていました。

Fix missing inferred types in insert statements · Issue #566 · googleapis/python-spanner-django

どうやら、Cloud Spannerエミュレータ上のテーブルにデータをinsertする際、デフォルト値nullのカラムに指定する値を省略すると発生するエラーのようです。DjangoのデフォルトのUserモデルはlast_loginフィールドがblank=True, null=Trueで、createsuperuserコマンド実行時にlast_loginの値を省略して登録しようとしてエラーになっています。

このエラーを回避する方法を色々考えてみましたが、私が思いつくのは以下の2つでした。

  • Cloud Spannerエミュレータを本物の挙動に合わせる
  • django-google-spannerが依存しているgoogle-cloud-spannerを変更して、デフォルト値nullのカラムに必ず値を指定する 1

自分でPRを作って華麗に解決してからこの記事を書けばかっこよかったのですが、ここで力尽きてしまいました…

django-google-spanner==3.0.0で開発する場合は、以下の選択肢しかないと思います。

  • 開発環境でも本物のCloud Spannerインスタンスを使う
  • 開発環境のみSQLiteを使う

ただし、SQLiteを使う場合、Cloud Spanner独自の制限を理解していないと、本番環境のみで起こるエラーの発生に繋がります。 たとえば、Cloud SpannerではALTER TABLE 〜 RENAMEをサポートしていません。ところが、Djangoは普通のRDBを想定した設計になっているため、モデル名やフィールド名を変更してmakemigrationsコマンドを実行すると、ALTER TABLE 〜 RENAMEを実行するマイグレーションファイルが作られます。この状態で本番環境でmigrateコマンドを実行すると、エラーが発生します。

Cloud Spannerを使うDjangoプロジェクトでは、モデル名やフィールド名の変更時に以下の手順でマイグレーションファイルを分けるしかなさそうです。

  1. 新しいモデル・フィールドを追加してmakemigrationsコマンドを実行
  2. --emptyオプション付きのmakemigrationsコマンドで空のマイグレーションファイルを作成
  3. 2.で作ったマイグレーションファイルにデータを移行するPythonコードを記述(こんな感じで
  4. 古いモデル・フィールドを削除してmakemigrationsコマンドを実行

Cloud Spanner独自の制限についての詳細は、以下ドキュメントを参照してください。

python-spanner-django / limitations.rst at main・googleapis / python-spanner-django


  1. このアプローチは既にPRが存在しますが、「エミュレータの場合のみ値を指定するコードにしたほうがいい」というレビューコメントがついていて、未対応のまま開発が止まっています(2021/12/06時点)。 https://github.com/googleapis/python-spanner/pull/200 ↩︎


comments powered by Disqus