正在加载...

Google AppEnigne 实体关系建模

2009年05月07日 20:45

实体关系建模

Rafe Kaplan, Software Engineer
2008 6月

导言

“快速入门”告诉了你怎么使用一个简单的AppEngine数据模型, 怎么使用属性.但是如果你想在datastore中建立起真实世界的映射比这复杂的多. 不管你是一个web应用开发新手还是已经习惯使用SQL数据库, 本文会深入介绍在AppEngine中数据建模

为什么我需要实体关系?

设想你在创建一个很酷的web应用, 其中包括了一个地址簿, 让用户存储他们的联系人. 对每个联系人你想保持姓名, 生日(这可不能忘记!), 地址, 电话号码和工作的公司.

当用户想增加一个地址时, 他们在一个表单中输入, 然后表单在一个如下的模块中保存地址信息:

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()

挺不错, 用户马上开始使用他们的地址簿, 开始输入联系人. 没有多久, 你听说用户对一个联系人只能有一个电话号码不满意. 如果想保持某个人的家里电话和工作电话怎么办? 没有问题, 你想, 只要在Contact中加入一个工作电话号码就成. 你把数据结构改成如下:

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

加入新的字段, 修改后的表单马上投入了使用. 上线后没有多久, 你收到不少新的抱怨. 当用户们看到新的电话号码字段后, 开始要求再增加几个. 有些想加传真号码, 有些想加手机, 甚至有些用户要求加入不止一个手机号码(现在的男孩子们够忙的)! 你可能会为传真加一个字段, 然后是手机, 也许是两个手机号码,J但如果用户要三个手机号码呢? 如果要十个呢? 如果有一天一种你从来没有想到过的号码被发明出来了呢?

你的数据模型需要使用关系了.

One to Many一对多

答案是允许用户为联系人添加任意多个电话号码. 为此, 你需要把电话号码单独放在一个类里面, 并且想办法把多个电话号码与一个联系人关联起来. 使用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属性. 因为contact属性是ReferenceProperty, 你只能把Contact类型赋值给这个属性. 每当你定于了一个ReferenceProperty属性,会在被引用的类上自动创建一个collection属性. 这个collection缺省名字是<类名>_set. 比如说在这个例子中, 缺省名是Contact.phonenumber_set. 但是如果我们叫它phone_numbers可能会更直观些. 使用关键字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上创建的collection属性, 要得到一个联系人的所有电话号码就很容易了. 如果你要打印出某个人的所有电话号码,你可以这样做:

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, 意味著你可以用它来进一步缩小结果集, 排序. 比如说, 如果你只想要返回家庭号码, 你可以这样做

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

当Scott弄丢了手机, 非常容易删除那条记录. 只要把对应的实例删除就可以了:

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

Many to Many多对多

你想要让用户能够对联系人分组. 可能会有类似”朋友”, “同事”和”家庭”这样的分组. 这样用户可以进行批量操作, 如给所有朋友发送” hack-a-thon”活动的邀请. 我们这样定义一个分组:

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

你可以在Contact中定义一个叫group的新ReferenceProperty. 但是这样就只允许一个联系人属于一个分组. 举例来说, 某个人把他的一些同事也放在朋友组. 你需要一种方法来表示多对多的关系.

List of Keys

一种很简单的办法是在关系的其中一方创建一个keys的列表

   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)

将某个用户从group中增加和删除相当于对keys列表操作:

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

要得到一个组的所有成员, 你可以执行一个简单的查询. 在Group类中加入一个helper函数:

class Group(db.Model):
  name = db.StringProperty()
  description = db.TextProperty()
@property
  def members(self):
    return Contact.gql("WHERE groups = :1", self.key())

这种方式实现多对多关系有一些限制. 首先在使用列表一方你只保存了keys,当你需要返回数据时, 必需你自己手工根据keys读取数据. 另外一个更重要的是你要避免在list中保存太多的keys. 这意味着你应该在list项比较少一方放你的list. 在这个例子中, 一个group会有很多个成员, 但是一个人不大可能属于很多个group, 所以我们在Contact中保存groups list, 而不是相反.

关系模型

你的一个用户是个顶级销售. 在一个公司她(对, 她)认识很多人. 她觉得要不断地输入同一个公司的信息很繁琐. 难道不能输入一次公司信息然后把它和人关联起来吗? 如果就这么简单的话, 就只需要在联系人和公司之间建立一对多的关系, 但是实际情况有点复杂. 有些联系人是Contractor(合同工?), 不只在一个公司工作, 也不只一个头衔. 现在怎么办?

你需要建立多对多关系, 而且还需要关于关系的信息. 为此你可以用一个专门的数据模型来描述这种关系

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 = Contact.gql("name = 'Mary'").get()
google = Company.gql("name = 'Google'").get()
ContactCompany(contact=mary,
               company=google,
               title='Engineer').put()

除了可以保存关于关系的信息, 比起列表这种实现方式还有一个优点, 可以维护很大的关系集合. 然而, 你要非常小心, 逐个遍历集合中的数据会多次访问datastore. 只有当你确实需要的时候才使用这种多对多实现, 而且要注意应用程序的性能.

结论

AppEngine中能很容易地创建实体关系, 来描述真实世界的事物和想法. 使用ReferenceProperty当你需要关联某种类型的多个实例到一个实体. 使用keys列表当你允许不同的对象互相之间关联(?这句很难翻). 在大多数情况下, 你会发现这两种方式就能满足你的需要.

相关阅读:



我要留言

麻烦,计算一下:10+8

sidebartop

博客公告

sidebarbottom
sidebartop

分类杂谈

sidebarbottom
sidebartop
sidebarbottom
sidebartop
sidebarbottom
sidebartop

最近评论

  • 美国优洛: 想不到现在世界上还有这种地方!...
  • cracode: 是IE反钓鱼网站验证...
  • baizhongliang: 站长你好,很喜欢你的网站,看到贵站的网站...
  • baizhongliang: 站长你好,很喜欢你的网站,看到贵站的网站...
  • baizhongliang: 站长你好,很喜欢你的网站,看到贵站的网站...
sidebarbottom
sidebartop
sidebarbottom
sidebartop

标签

友情链接

sidebarbottom