AppEngineのdb.delete()の仕様が変わっていた件
高円寺なうでは、『流行のキーワード』という所謂バズワードを抽出する機能があるのですが、ある時からずっと内容が更新されていないことに先日気が付きました。
で、ログを見てみると以下のようなエラー。
04-30 01:59AM 09.291
Traceback (most recent call last):
File "****(一部略)", line 20, in reg
db.delete(data)
File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/db/__init__.py", line 1300, in delete
keys = [_coerce_to_key(v) for v in models]
File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/db/__init__.py", line 361, in _coerce_to_key
raise datastore_errors.BadArgumentError('Expected only one model or key')
BadArgumentError: Expected only one model or key
流行キーワード機能は定期的に集計処理を実行して、表示用データをエンティティとして登録しており、表示データ更新時には一旦すべてのキーワードデータを削除します。今回エラーで処理が落ちていたのがその削除処理の部分。
ログからは、google.appengine.ext.db.delete() 実行時のパラメータがおかしいと怒られているのですが、これまで数ヶ月間問題なく動いていた実装。仕様が変わったのかと思ってリリースノートを見てもそれらしい記述を見つけられず・・。
ただローカル環境のSDKを1.3.1→1.3.3 にバージョンアップしたところ同様のエラーが発生するようになったため、ここ最近のバージョンアップで仕様変更になったのは間違いなさそうです。
で、英語版と日本語版のAPIドキュメントを見比べていたら異なる箇所を見つけました。(日本語版ドキュメントは最新内容反映までにタイムラグがあるようです)
【英語版ドキュメント】
q = db.GqlQuery("SELECT * FROM Message WHERE create_date < :1", earliest_date) results = q.fetch(10) for result in results: result.delete() # or... q = db.GqlQuery("SELECT __key__ FROM Message WHERE create_date < :1", earliest_date) results = q.fetch(10) db.delete(results)
【日本語版ドキュメント】
q = db.GqlQuery("SELECT * FROM Message WHERE create_date < :1", earliest_date) results = q.fetch(10) for result in results: result.delete() # or... q = db.GqlQuery("SELECT * FROM Message WHERE create_date < :1", earliest_date) results = q.fetch(10) db.delete(results)
前半部分のモデルインスタンスの delete()メソッドについては特に変更はないようですが、後半の記述部分、つまりdb.delete() のパラメータとして削除対象のオブジェクトを渡す場合には、エンティティを丸ごと渡すのではなくて、キーを明示的に抽出してあげないといけなくなったようです。
英語版と日本語版の差異がどのタイミングで生じたかは不明ですが、ひとまずこの内容に従って実装を修正したところ、正常実行されるようになりました。
修正内容は以下のような感じ。
# エンティティを削除 - data = Model.all() + q = db.GqlQuery("SELECT __key__ FROM Model") + data = q.fetch(1000) db.delete(data)
まあ反省点としては、ログの監視とSDKのバージョンアップはこまめにしないとダメということですね。。不具合に気付かず放置してしまい申し訳ありませんでした。
追記
id:tagomorisより以下のご指摘をいただきました。ありがとうございます!
diffを見る限り元のコードだとdb.delete()に与えられていたのはQueryオブジェクト(Model.all()の結果はQueryオブジェクトで、Modelのリストではないです)なので、元々のdb.delete()には与えるべきでないものですね。仕様外動作を期待していたコードが、最近の修正で動かなくなった、と見るべきかと思います。(実際にext.db.delete()には修正が入っています。)
肝心のことを書き忘れました。現在のバージョン(1.3.3)でdb.delete()にモデルのインスタンスのリストを渡しても、Keyのリストを渡しても、どちらも問題なく動作しますよ。