2012-07-18

データモデルが変わるときのデプロイ

運用中のウェブサービスで、保存してあるデータのスキーマを変えたいときがあります。このとき、プログラムのコードとデータベースの間で互換性が保ちつつ、双方を更新する必要があります。Google App Engine を使ったサービスでのやったことのメモです。



プロパティの増減

最初、こんなデータがあったとします。

from google.appengine.ext import db

class User(db.Model):
    name = db.StringProperty()

これでたくさんのデータが保存されている状態になったとしましょう。ここで、プロパティが増えます。

class User(db.Model):
    name = db.StringProperty()
    birthday = db.DateProperty()

Google App Engine のデータストアには ALTER TABLE みたいなのがありません。が、こういう場合はデータストアに何もしなくていいです。birthday を指定せずに保存されたエンティティは birtyday に None が入っています。

プロパティが減る場合は、単純にモデルの定義からプロパティを消せばいいでしょう。データストアには残っているけれど、あっても害がないから。けど、後々、違う型で復活したりすると、よろしくない気がします。

クエリが増える

プロパティを変更したととき、あたらしいクエリを発行することになった場合には、 (1) インデックスの更新してから、(2) コードをデプロイします。

これまで使っていなかったけど、新たに以下のようなクエリをするようになったとしましょう。

User.all().query(birthday<xxx).query(name<yyy) 

dev_appserver をローカルで動かして、このコードを一度でも実行すると index.yaml が更新されます。以下のコマンドで App Engine のインデックスを更新します。

appcfg.py update_indexes .

ダッシュボードの Datastore Indexing のページを見ると、Building… と書かれている箇所があります。何度かリロードして、Ready になってから、appcfg.py update します。先にインデックスを更新しないと、アプリケーションコードがクエリしようとしたとき、「インデックスができてねーよ」エラーが出ます。


より複雑な変更


ほんとに困るのは、プロパティの型が変わってしまうとか、複数のプロパティでひとつの状態を表したくなるとかいう場合です。


class User(db.Model):
    name = db.StringProperty()
    birthday = db.StringProperty()  # DateProperty から StringProperty へ変更

このコードをデプロイして user.birthday にアクセスしたとき、以前の DateProperty で保存されていると、ここでエラーが出ます。だからといって、先にデータを書き換えてしまうと、現行コードでエラーが出ます。

書籍「継続的デリバリー」は、新旧どちらスキーマでも動くようにアプリケーションを変更したあと、データベースのスキーマを変更する、という方針を提案しています。 Google App Engine のデータストアであれば、新旧どちらのプロパティの型であってもいけるようなモデルに変更する、ということになります。

class User(db.Model):
    name = db.StringProperty()
    date_birthday = db.DateProperty(name='birthday')
    str_birthday = db.StringProperty(name='str_birthday')

    def get_birthday(self):
        if self.date_birthday:
            return self.date_birthday.strftime('%Y-%m-%d')
        else:
            return self.str_birthday

    def set_birthday(self, val):
        if isinstance(val, date):
            self.str_birthday = '%Y-%m-%d' % (val.year, val.month, val.day)
        elif isinstance(val, str):
            self.str_birthday = val
        else:
            raise TypeError
        self.date_birthday = None

    birthday = property(get_birthday, set_birthday)

ハンガリアンっぽくてキモいです。あと文字列に変換するところは、関数に切り出したほうがいいですね。で、これでデプロイします。データの個数が少なければ remote_api で、多ければ mapreduce のライブラリを使って、プロパティの型を変更します。すべてのデータが新しい型に対応したら、不要なコードを取り除いてデプロイします。

class User(db.Model):
    name = db.StringProperty()
    birthday = db.DateProperty(name='str_birthday')

と、偉そうに書いたものの、最後の書き換えをせずに残っている箇所もあります。