the get_or_insert method on Google App Engine

Google App Engine (GAE) であるものを作っています.
それにしてもよくつまづきます.

例えばある記事をデータストアに保存するとき,以下のようなモデルを使うとします.

class Entry(db.Model):
    link = db.LinkProperty()
    title = db.StringProperty()
    body = db.TextProperty()

linkはその記事のURLで,titleはその記事のタイトル,bodyはその記事の内容です.
記事の内容,つまりbodyを得るためにはurlopen*, requestsモジュールなどを使ってURLを開く必要があります.
この処理は時間がかかりますよね.
今回は,
link, titleは事前にわかっており,linkを使ってbodyを取得し,最後にデータストアに格納する際の処理について書きたいと思います.

さて,いろいろな記事があるわけですが,それぞれの記事に固有な(ユニークな)属性はなんでしょうか.
僕はURLを使いたいと思います.
URLがかぶっている記事なんて,存在しないと思うからです.
というわけで,ある一つの記事を格納する際に,既にデータストアにあるかどうかをチェックしたいと思います.
これにより,既にある記事の内容をもう一度読み込む必要はなくなります.
いろいろ調べたのですが,db.Model.get_or_insertというクラスメソッドが使えそうだな,という印象を受けました.
get_or_insertメソッドは以下のような形をしております.
get_or_insert(key_name, **kwds)
このkey_nameに記事のURLを指定してやるというわけです.
そうすると,あるURL uをkey_name=uとして指定してやると,既にデータストアにある場合はそのオブジェクトが,無い場合は新しいオブジェクトを作成し,それを格納してくれる,と思っています.
そこで,以下のようにしてみました.

a_entry = Entry.get_or_insert(key_name=entry.link,
                              title=entry.title,
                              link=entry.link,
                              body=unicode(f(entry.link), "utf-8"))

ただし,entry.linkはその記事のURLで,entry.titleはその記事のタイトル,関数fはURLを開き,HTMLを除去したテキストを返す関数です.
関数fは重い処理なので,できるだけ呼びたくありません.
これで,既にデータストアにある記事は関数fを呼ぶ必要はなくなると思っていました.
しかし,残念ながら呼んでしまうのです.
そこで以下のようにしてみました.

a_entry = Entry.get_or_insert(key_name=entry.link,
                              title=entry.title,
                              link=entry.link)
if a_entry.body is None:
    a_entry.body = unicode(f(entry.link), "utf-8")

つまり,bodyが無い場合だけ,関数fを呼ぶようにしました.そうすると,bodyがデータストアに格納されません.
これは,putすれば解決するかな,と思い,以下のように修正.

  a_entry = Entry.get_or_insert(key_name=entry.link,
                                title=entry.title,
                                link=entry.link)
  if a_entry.body is None:
      a_entry.body = unicode(f(entry.link), "utf-8")
   a_entry.put()

うまく動きました!感動です.
しかし,コードが汚いですね.
要は,getされたのか,insertされたのか知りたいわけです.
調べると,良い記事がありましたので紹介させて頂きます.ここの4です.

ここにあるクラスメソッドを使わせていただきます.モデルを変更.

class Entry(db.Model):
    link = db.LinkProperty()
    title = db.StringProperty()
    body = db.TextProperty()

    @classmethod
    def insert_or_fail(cls, key_name, **kwds):
        def txn():
            entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
            if entity is None:
                entity = cls(key_name=key_name, **kwds)
                entity.put()
                return entity
            return None
        return run_in_transaction(txn)

そして,こう呼び出します.

a_entry = Entry.insert_or_fail(key_name=entry.link)
if a_entry is not None:
    a_entry.link = entry.link
    a_entry.title = entry.title
    a_entry.body = unicode(extract(entry.link), "utf-8")
    a_entry.put()

insert_or_failはinsertされたら(データストアにその記事がないなら)そのオブジェクトを返し,それ以外はNoneを返します.
Noneならば何もしなくていいわけですが,insertされたなら,各プロパティに値を入れて再度putしてやります.
これでコードが少しきれいになったでしょうか.しかし,別に最初のコードでよかったかもしれませんね.

さて,続きを書こう.
GAEには悩まされているので,またいろいろポストすると思われます!

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中