Python 数据建模

注意强烈建议构建新应用的开发者使用 NDB 客户端库,它与 DB 客户端库相比具有多项优势,例如可通过 Memcache API 进行自动实体缓存。如果您当前使用的是较早的 DB 客户端库,请参阅 DB 到 NDB 的迁移指南

概览

数据存储区实体有一个键和一组属性。应用使用数据库存储区 API 定义数据模型,并创建要存储为实体的模型的实例。模型可为 API 所创建的实体提供通用结构,并可以定义用于验证属性值的规则。

Model 类

Model 类

应用描述它与模型配合使用的数据的类型。模型是继承自 Model 类的 Python 类。模型类定义了数据存储区实体的新种类,以及该种类将采用的属性。种类名称由继承自 db.Model

Model 属性 (property) 是使用模型类中的类特性 (attribute) 定义的。每个类特性 (attribute) 均是 Property 类的一个子类实例,通常是某个提供的 Property 类。Property 实例保留了属性的配置,如实例是否必须要有该属性才有效,或用于实例的默认值(如果未提供)。

from google.appengine.ext import db

class Pet(db.Model):
    name = db.StringProperty(required=True)
    type = db.StringProperty(required=True, choices=set(["cat", "dog", "bird"]))
    birthdate = db.DateProperty()
    weight_in_pounds = db.IntegerProperty()
    spayed_or_neutered = db.BooleanProperty()

在 API 中,某个定义的实体种类的实体用对应的模型类的实例表示。应用可以通过调用类的构造函数来创建新实体。应用使用实例的属性来访问和控制实体的属性。模型实例构造函数接受属性的初始值作为关键字参数。

from google.appengine.api import users

pet = Pet(name="Fluffy",
          type="cat")
pet.weight_in_pounds = 24

注意:Model 类的特性 (attribute) 是模型属性 (property) 的配置,其值为 Property 实例。模型实例的属性是实际属性值,其值为 Property 类接受的类型

模型类会使用 Property 实例来验证分配给模型实例属性的值。当第一次构建模型实例时以及为实例属性分配新值时,都会进行属性值验证。这确保了属性绝不会有无效值。

由于在构建实例时要进行验证,因此配置为必需属性的任何属性都必须在构造函数中进行初始化。在本示例中,nametype 是必需值,因此它们的初始值是在构造函数中指定的。weight_in_pounds 不是模型所必需的,所以它在开始时未被分配值,到后面才分配了值。

在第一次“放置”使用构造函数创建的模型实例之前,数据存储区中没有该实例。

注意:如同所有 Python 类属性一样,模型属性配置是在第一次导入脚本或模块时初始化的。由于 App Engine 会缓存在两次请求之间导入的模块,因此模块配置可能在某个用户的请求期间进行初始化,并在另一个用户的请求期间重复使用。请勿用请求或当前用户特定的数据初始化模型属性配置(例如默认值)。请参阅应用缓存了解详情。

Expando 类

使用 Model 类定义的模型建立一组固定的属性,该类的每个实例都必须具有这些属性(可能有默认值)。这是一种非常有用的对数据对象进行建模的方法,但是数据存储区并不要求指定类型的每个实体都有相同的属性集。

有时,实体有一些非必要属性(例如其他同类实体的属性),其实是非常有用的。在 Datastore API 中这种实体由“expando”模型表示。Expando 模型类是 Expando 父类的子类。分配给 Expando 模型实例属性的任何值都将成为数据存储区实体的属性,并使用该属性的名称。这些属性称为动态属性。类属性中使用 Property 类实例定义的属性为固定属性

Expando 模型可以拥有固定属性和动态属性。模型类仅使用固定属性的 Property 配置对象来设置类属性。应用为动态属性分配值时将创建动态属性。

class Person(db.Expando):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    hobbies = db.StringListProperty()

p = Person(first_name="Albert", last_name="Johnson")
p.hobbies = ["chess", "travel"]

p.chess_elo_rating = 1350

p.travel_countries_visited = ["Spain", "Italy", "USA", "Brazil"]
p.travel_trip_count = 13

由于动态属性没有模型属性定义,所以动态属性未经验证。任何动态属性的值可以为任何 Datastore 基类型,其中包括 None。两个同类实体的相同动态属性可以有不同的值类型,并且可以有一个不设置属性,而另一个设置。

与固定属性不同,动态属性无需存在。值为 None 的动态属性与不存在的动态属性不同。如果 Expando 模型实例没有某一属性的特性,则对应的数据实体将没有该属性。您可以通过删除该特性来删除动态属性。

名称以下划线 (_) 开头的属性不会保存到数据存储区实体中。这样一来,您便可以在模型实例上存储值以供临时内部使用,而不会影响与实体一起保存的数据。

注意:静态属性将始终保存到 Datastore 实体中,无论该属性是 Expando、Model,还是以下划线 (_) 开头。

del p.chess_elo_rating

如果某个查询在过滤条件中使用了动态属性,那么它将只返回其属性值与查询中使用的值的类型相同的实体。同样,该查询只返回设置了该属性的实体。

p1 = Person()
p1.favorite = 42
p1.put()

p2 = Person()
p2.favorite = "blue"
p2.put()

p3 = Person()
p3.put()

people = db.GqlQuery("SELECT * FROM Person WHERE favorite < :1", 50)
# people has p1, but not p2 or p3

people = db.GqlQuery("SELECT * FROM Person WHERE favorite > :1", 50)
# people has no results

注意:上面的示例跨多个实体组使用查询,这样可能会返回过时的结果。如需获得高度一致的结果,请在实体组中使用祖先查询

Expando 类是 Model 类的子类,且继承了它的所有方法。

PolyModel 类

Python API 包含另一个用于数据建模的类,您可以通过它定义类的层次结构,并执行可以返回指定类或其任一子类的实体的查询。此类模型和查询称为“多态”,因为它们允许一个类的多个实例作为父类查询的结果。

下面的示例定义了 Contact 类,以及子类 PersonCompany

from google.appengine.ext import db
from google.appengine.ext.db import polymodel

class Contact(polymodel.PolyModel):
    phone_number = db.PhoneNumberProperty()
    address = db.PostalAddressProperty()

class Person(Contact):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    mobile_number = db.PhoneNumberProperty()

class Company(Contact):
    name = db.StringProperty()
    fax_number = db.PhoneNumberProperty()

此模型确保所有 Person 实体和所有 Company 实体都具有 phone_numberaddress 属性,并且查询 Contact 实体可以返回 PersonCompany 实体。只有 Person 实体具有 mobile_number 属性。

子类可以与任何其他模型类一样实例化:

p = Person(phone_number='1-206-555-9234',
           address='123 First Ave., Seattle, WA, 98101',
           first_name='Alfred',
           last_name='Smith',
           mobile_number='1-206-555-0117')
p.put()

c = Company(phone_number='1-503-555-9123',
            address='P.O. Box 98765, Salem, OR, 97301',
            name='Data Solutions, LLC',
            fax_number='1-503-555-6622')
c.put()

查询 Contact 实体可以返回 ContactPersonCompany 的实例。以下代码将打印以上所创建的两个实体的信息:

for contact in Contact.all():
    print 'Phone: %s\nAddress: %s\n\n' % (contact.phone_number,
                                          contact.address)

查询 Company 实体只能返回 Company 的实例:

for company in Company.all()
    # ...

目前,多态模型不应直接传递给 Query 类构造函数。相反,它应使用 all() 方法,如上面示例所示。

如需详细了解如何使用多态模型及其实现方式,请参阅 PolyModel 类

Property 类和类型

数据存储区支持一组固定的实体属性的值类型,包括 Unicode 字符串、整数、浮点数、日期、实体键、字节字符串 (blob) 和各种 GData 类型。每个数据存储区值类型都有 google.appengine.ext.db 模块提供的对应 Property 类。

类型和 Property 类描述了所有受支持的值类型及其对应的 Property 类。下面介绍几种特殊的值类型。

字符串和 Blob

数据存储区支持两种用于存储文本的值类型:长度最多为 1500 个字节的短文本字符串和长度最多为 1 兆字节的长文本字符串。短字符串编入索引,可以在查询过滤条件和排序顺序中使用。长字符串不编入索引,不能在过滤条件或排序顺序中使用。

短字符串值可以是 unicode 值或 str 值。如果值是 str,则假设编码为 'ascii'。如需为 str 值指定一种不同的编码,可以用 unicode()类型的构造函数将其转换为 unicode 值,这种类型的构造函数会使用 str 和编码名称作为参数。短字符串可通过 StringProperty 类进行建模。

class MyModel(db.Model):
    string = db.StringProperty()

obj = MyModel()

# Python Unicode literal syntax fully describes characters in a text string.
obj.string = u"kittens"

# unicode() converts a byte string to a Unicode string using the named codec.
obj.string = unicode("kittens", "latin-1")

# A byte string is assumed to be text encoded as ASCII (the 'ascii' codec).
obj.string = "kittens"

# Short string properties can be used in query filters.
results = db.GqlQuery("SELECT * FROM MyModel WHERE string = :1", u"kittens")

长字符串值由 db.Text 实例表示。其构造函数采用 unicode 值或 str 值,并可能会使用 str 中使用的编码名称。长字符串可通过 TextProperty 类进行建模。

class MyModel(db.Model):
    text = db.TextProperty()

obj = MyModel()

# Text() can take a Unicode string.
obj.text = u"lots of kittens"

# Text() can take a byte string and the name of an encoding.
obj.text = db.Text("lots of kittens", "latin-1")

# If no encoding is specified, a byte string is assumed to be ASCII text.
obj.text = "lots of kittens"

# Text properties can store large values.
obj.text = db.Text(open("a_tale_of_two_cities.txt").read(), "utf-8")

数据存储区还支持两种类似类型的非文本字节字符串:db.ByteStringdb.Blob。这些值是原始字节的字符串,不作为编码文本(如 UTF-8)进行处理。

db.StringProperty 值类似,db.ByteString 值也会编入索引。与 db.TextProperty 属性类似,db.ByteString 的值限制为 1500 个字节。ByteString 实例代表短字节字符串,并使用 str 值作为构造函数的参数。字节字符串通过 ByteStringProperty 类进行建模。

db.Text 类似,db.Blob 值最大可达一兆字节,但不会编入索引,并且不能在查询过滤器或排序顺序中使用。db.Blob 类使用一个 str 值作为其构造函数的参数,或者您可以直接分配该值。Blob 通过 BlobProperty 类进行建模。

class MyModel(db.Model):
    blob = db.BlobProperty()

obj = MyModel()

obj.blob = open("image.png").read()

列表

属性可以有多个值,在数据存储区 API 中表示为 Python list。列表可以包含数据存储区支持的任何值类型的值。一个列表属性甚至可以有不同类型的值。

由于顺序通常会得到保留,因此当查询和 get() 返回实体时,列表属性值的顺序与存储时的顺序相同。有一个例外:BlobText 值将移到列表的末尾;不过,它们仍保留着相对于彼此的原来顺序。

ListProperty 类对列表进行建模,并强制列表中的所有值都为给定类型。为方便起见,该库还提供了 StringListProperty(类似于 ListProperty(basestring))。

class MyModel(db.Model):
    numbers = db.ListProperty(long)

obj = MyModel()
obj.numbers = [2, 4, 6, 8, 10]

obj.numbers = ["hello"]  # ERROR: MyModel.numbers must be a list of longs.

列表属性上的查询使用过滤条件逐个测试列表中的每个值。仅当列表中的某个值符合该属性的所有过滤条件时,该实体才会与查询匹配。如需了解详情,请参阅数据存储区查询页面。

# Get all entities where numbers contains a 6.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers = 6")

# Get all entities where numbers contains at least one element less than 10.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers < 10")

查询过滤条件仅对列表成员执行操作。无法在一个查询过滤条件中测试两个列表的相似之处。

数据存储区在内部将列表属性值表示为该属性的多个值。如果列表属性值为空列表,则该属性在数据存储区中没有表示。对于静态属性(具有 ListProperty)和动态属性,数据存储区 API 处理这种情况的方式不同:

  • 可以为静态 ListProperty 分配空列表作为值。该属性不存在于数据存储区中,但是模型实例的行为就像值是空列表一样。静态 ListProperty 不能None 值。
  • 无法为具有 list 值的动态属性分配空列表值。但是,它可以有 None 值,并可以删除(使用 del)。

ListProperty 模型测试添加到列表中的值是否为正确类型,如果类型不正确,则会引发 BadValueError。即使在检索先前存储的实体并将其加载到模型中时,也会进行此测试(可能会失败)。由于 str 值在存储前转换为 unicode 值(作为 ASCII 文本),因此 ListProperty(str) 会视为 ListProperty(basestring),即接受 strunicode 值的 Python 数据类型。您还可以使用 StringListProperty() 实现此目的。

如需存储非文本字节字符串,请使用 db.Blob 值。当存储和检索 blob 字符串的字节时,将保留这些字节。您可以将形式为 Blob 列表的属性声明为 ListProperty(db.Blob)

列表属性可以采用不同寻常的方式与排序顺序进行交互;请参阅 Datastore 查询页面了解详情。

引用

一个属性值可以包含另一个实体的键。该值是 Key 实例。

ReferenceProperty 类对键值进行建模,并强制所有值都引用给定种类的实体。为方便起见,该库还提供了 SelfReferenceProperty,相当于 ReferenceProperty,它引用具有该属性的实体的同一种类实体。

为 ReferenceProperty 属性分配模型实例时,系统将自动使用其键作为值。

class FirstModel(db.Model):
    prop = db.IntegerProperty()

class SecondModel(db.Model):
    reference = db.ReferenceProperty(FirstModel)

obj1 = FirstModel()
obj1.prop = 42
obj1.put()

obj2 = SecondModel()

# A reference value is the key of another entity.
obj2.reference = obj1.key()

# Assigning a model instance to a property uses the entity's key as the value.
obj2.reference = obj1
obj2.put()

ReferenceProperty 属性值可以用作引用实体的模型实例。如果引用的实体不在内存中,则使用该属性作为实例时,系统将自动从数据存储区中提取实体。ReferenceProperty 还存储了一个键,但使用该属性会导致载入相关实体。

obj2.reference.prop = 999
obj2.reference.put()

results = db.GqlQuery("SELECT * FROM SecondModel")
another_obj = results.fetch(1)[0]
v = another_obj.reference.prop

如果某个键指向一个不存在的实体,访问该属性将引发错误。如果某个应用程序预计引用可能无效,则可以使用 try/except 块测试对象是否存在:

try:
  obj1 = obj2.reference
except db.ReferencePropertyResolveError:
  # Referenced entity was deleted or never existed.

ReferenceProperty 有另外一个便利的功能:反向引用。当模型具有另一个模型的 ReferenceProperty 时,每个引用的实体都会获得一个值为 Query 的属性,该属性会返回引用它的第一个模型的所有实体。

# To fetch and iterate over every SecondModel entity that refers to the
# FirstModel instance obj1:
for obj in obj1.secondmodel_set:
    # ...

反向引用属性的名称默认为 modelname_set(其中模型类的名称采用小写字母,“_set”添加到末尾),可通过 ReferenceProperty 构造函数的 collection_name 参数进行调整。

如果有多个引用同一模型类的 ReferenceProperty 值,反向引用属性的默认结构将引发错误:

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class raises a DuplicatePropertyError with the message
# "Class Firstmodel already has property secondmodel_set"
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel)
    reference_two = db.ReferenceProperty(FirstModel)

如需避免这种错误,您必须明确地设置 collection_name 参数:

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class runs fine
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_one_set")
    reference_two = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_two_set")

模型实例的自动引用和取消引用、类型检查和反向引用仅在使用 ReferenceProperty 模型属性类时可用。存储为 Expando 动态属性值或 ListProperty 值的键不具有这些特性。