エンティティの関連をモデリングする

http://code.google.com/appengine/articles/modeling.html

はじめに

たしかにGetting Started Guideは単純なAppEngineモデルのプロパティを埋めるのに必要なことを教えてくれるけど、もしデータストアに現実世界の何かを表現できるようになりたいと言うならそれだけじゃ足りない。ウェブアプリケーション開発の初心者であろうと、SQLデータベースに馴染んでいようと、この記事はAppEngineのデータ表現の次の領域へ進みたい全ての人のために書かれている。

エンティティに関連が必要なのはなぜか

ユーザーが連絡先を保存できるアドレス帳を持つ、かっこよくて新しいウェブアプリケーションを構築中だと考えよう。ユーザが保存する連絡先のために、相手の名前、誕生日(絶対忘れちゃいけない)、住所、電話番号、会社を取得したい。

ユーザが住所を追加したいと思うと、フォームに情報を入力し、次のような感じのモデルにその情報が保存される:

class Contact(db.Model):

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # Phone info.
  phone_number = db.PhoneNumberProperty()
 
  # Company info.
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

上出来だ。ユーザはすぐにこのアドレスブックを使い始め、データストアはすぐに埋まり始めるだろう。そしてこの新アプリケーションをデプロイしてしばらくすると、ユーザが電話番号をひとつしか持てないことを不満に思っていると知ることになる。ある人の家の番号だけじゃなく会社の番号も記録したいときはどうすればいいんだ?問題ないさ、と思い、君はデータ構造に会社の電話番号を付け加える。つまり次のようにだ:

# Phone info.
phone_number = db.PhoneNumberProperty()
work_phone_number = db.PhoneNumberProperty()

フォームに新しいフィールドを付け加えると君は仕事に戻る。アプリケーションを再びデプロイするとすぐにたくさんの不満を聞くことになるだろう。新しい電話番号フィールドが増えたことを知ると、さらにほかのフィールドも希望し始める。FAX番号がほしいという人もいれば、携帯番号がほしいという人もいる。携帯を複数持ってる人だっているだろう(いまどきの子供たちはホント大忙しだ)!FAXや携帯用のフィールドを一つ二つ追加することもできる。だけど携帯を3つ持ってたらどうする?10個だと?誰かが君が考えたこともないような電話を発明すると?

そのとき、モデルには関連が必要になる。

一対多

連絡先それぞれについて好きなだけたくさんの電話番号を設定できるようにするための答えはこうだ。そのためには電話番号それ自身をあらわすクラスと連絡先一つに複数の電話番号を関連付ける手段が必要になる。ReferencePropertyを使えば一対多関連のモデル化は簡単だ。新しいクラスは、たとえばこうなるだろう:

class Contact(db.Model):

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # 元のphone_numberプロパティは、暗黙的に作成される
  # 'phone_numbers'というプロパティと置き換えられる

  # Company info.
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

class PhoneNumber(db.Model):
  contact = db.ReferenceProperty(Contact,
                                 collection_name='phone_numbers')
  phone_type = db.StringProperty(
    choices=('home', 'work', 'fax', 'mobile', 'other'))
  number = db.PhoneNumberProperty()

この方法のキーになるのはcontactプロパティだ。ReferencePropertyとして定義することで、Contact型の値だけを代入できるプロパティが定義できる。参照型のプロパティを定義すると参照されるクラスに暗黙的なコレクションプロパティが作成される。デフォルトではそのコレクションは_setという名前になる。今回だとContact.phonenumber_setというプロパティなるだろう。ただ、この属性はphone_numbersと呼んだほうが分かりやすいと思うはずだ。ReferencePropertyのキーワードパラメータとしてcollection_nameを渡してやればデフォルトの名前を上書きできる。

連絡先と、電話番号を一つ関連付けるのは簡単だ。"Scott"という連絡先が家と携帯の二つの電話番号を持つとしよう。その連絡先は次のように作成できる:

scott = Contact(name='Scott')
scott.put()
PhoneNumber(contact=scott,
            phone_type='home',
            number='(650) 555 - 2200').put()
PhoneNumber(contact=scott,
            phone_type='mobile',
            number='(650) 555 - 2201').put()

ReferencePropertyがContactに特殊なプロパティを追加してくれているので、ある人物が与えられたときに関係するすべての電話番号は簡単に手に入る。ある人物のすべての番号を表示したければ、次のようにすればいい。

print 'Content-Type: text/html'
print
for phone in scott.phone_numbers: 
  print '%s: %s' % (phone.phone_type, phone.number)

この結果は次のようになる:

home: (650) 555 - 2200
mobile: (650) 555 - 2201

注: デフォルトではこの種の関連では順序は保持されないので、出力の順序は異なるかもしれない

phone_numbers仮想属性はQueryのインスタンスなので、Contactに関するコレクションをさらに絞り込んだりソートしたりできる。たとえば、家の番号だけがほしければ次のようにする:

scott.phone_numbers.filter('phone_type =', 'home')

Scottが電話をなくしたときは、単にレコードを削除すればいい。PhoneNumberインスタンスを削除するだけでもうクエリには現れなくなる。

jack.phone_numbers.filter('phone_type =', 'home').get().delete()

多対多

ユーザが連絡先をグループにまとめて管理できるようにしたいとする。グループとしては「友人」「同僚」「家族」などがあるだろう。ユーザはそれらのグループをまとめて何かの処理、たとえば友人すべてにハッカソンの招待状を送ったりとか、ができる。まずは簡単に次のようなGroupモデルを定義しよう:

class Group(db.Model):

  name = db.StringProperty()
  description = db.TextProperty()

Contactには新しくgroupという名前のReferencePropertyを追加することもできるが、そうすると連絡先はたった一つのグループにしか所属できなくなる。たとえば同僚であり友人であるような人もいるだろう。多対多関連を表現する方法が必要だ。

キーのリスト

とても簡単なやり方は、関連の一方がキーのリストを持つようにすることだ。

class Contact(db.Model):
  # User that owns this entry.
  owner = db.UserProperty()

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # Company info.
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

  # Group affiliation
  groups = db.ListProperty(db.Key)

グループにユーザーを追加・削除することはキーのリストを操作することになる。

friends = Group.gql("WHERE name = 'friends'").get()
mary = Contacts.gql("WHERE name = 'Mary'").get()
if friends.key() not in mary.groups:
  mary.groups.append(friends.key())
  mary.put()

グループのメンバー全員が見たいときも、簡単なクエリを実行するだけでいい。Groupエンティティにヘルパ関数を作ると便利だろう。

class Group(db.Model):
  name = db.StringProperty()
  description = db.TextProperty()

  @property
  def members(self):
    return Contact.gql("WHERE groups = :1", self.key())

このやり方で多対多関連を実装した場合は制限が少しある。まず、利用できるのがKeyオブジェクトだけなので、リストが保持されているコレクションの値は明示的に検索してやらないといけない。さらに重要な制限は、ListPropertyにあまり大きなリストを保持するのは避けたほうがいいということだ。つまりリストに入れるのは関連のうちより少ない数になると思われる方にしたほうがいい。今回の例ではContactをリストに入れたが、これは一人の人が余りにたくさんのグループに所属することはなさそうだけど、グループは何百ものメンバを持ちそうだからだ。

関連モデル

ユーザの一人がトップクラスのセールスウーマンである会社のチームの人たちを知ってるとしよう。彼女は同じ会社の情報を何度も何度も入力しなければいけなくてとても面倒くさいと思う。特定の会社の情報は一度だけ入力してそれぞれの人をそこに関連付けるようにはできないんだろうか?もし単純な話なら単にContactとCompanyに一対多の関連を持たすだけでいいが、話はそう簡単でもない。彼女の連絡先の何人かは契約社員で複数の会社に所属しており、それぞれで異なる肩書きを持っている。さてどうする?

ここで必要なのは関連自身についてなにか追加の情報を持てるような多対多関連だ。そのためには関連を表すモデルを追加すればいい:

class Contact(db.Model):
  # User that owns this entry.
  owner = db.UserProperty()

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # The original organization properties have been replaced by
  # an implicitly created property called 'companies'. 

  # Group affiliation
  groups = db.ListProperty(db.Key)

class Company(db.Model):
  name = db.StringProperty()
  description = db.StringProperty()
  company_address = db.PostalAddressProperty()

class ContactCompany(db.Model):
  contact = db.ReferenceProperty(Contact,
                                 required=True,
                                 collection_name='companies')
  company = db.ReferenceProperty(Company,
                                 required=True,
                                 collection_name='contacts')
  title = db.StringProperty()

誰かに会社情報を追加したければContactCompanyインスタンスを作成すればいい。

mary = Contacts.gql("name = 'Mary'").get()
google = Company.gql("name = 'Google'").get()
ContactCompany(contact=mary,
               company=google,
               title='Engineer').put()

この方法を使うと、関連に関する情報を保持できるようになっただけではなく、キーのリストを持つ方法と比べて、よりたくさんの関連を処理できるという利点もある。ただし、関連をトラバースするのによりたくさんのデータストアへのアクセスが発生することになるので気をつける必要がある。この方式の多対多関連を使うのはそれが本当に必要な場合だけにして、実際に利用する場合もアプリケーションのパフォーマンスには気を配ろう。

結論

App Engineでは簡単にデータストアエンティティ間の関連を作成して、現実世界のアイデアを表現できる。ある一個のエンティティが不定数の情報を持つ必要があるならReferencePropertyを使おう。ほかのインスタンスとお互いに共有されるたくさんの異なるオブジェクトが必要ならキーのリストを使おう。この二つのアプローチでアプリケーションで使用するモデルのほとんどをカバーできることに気づくだろう。