在Google App Engine中使用数据库

标签:Google App Engine, Python

Google App Engine的教程终于来到了数据库部分。这是GAE最有用、最复杂,也是限制最多的地方。
阅读本文需要您懂一般的数据库使用。

废话少说,先给参考文档:
官方文档(英文):http://code.google.com/appengine/docs/python/datastore/
中文翻译(部分,版本较老,与官方文档不同步):
http://blog.csdn.net/lobby/category/400740.aspx
http://blog.csdn.net/jj_liuxin/archive/2008/12/28/3630281.aspx

注意:本文已很久未更新了,一切以Google App Engine官方文档为准,这里仅供参考。

一、概况
GAE的数据库叫作datastore,它与传统的关系数据库不同,可以认为它是一种分布式的对象数据库。它的底层是由Bigtable数据库搭建的。
这个数据库可以存储db.Model类的数据对象。
实际上,GAE的数据库模型很像Django。

二、实体和模型
当一个类继承了db.Model类时,它就可以作为一个数据模型,生成可存储在数据库中的数据对象。这个模型就相当于关系数据库的表。
每个模型都可以有很多属性,这些属性就定义了其中可存储的数据类型,它相当于表的字段。
每个对象在数据库中都被称为一个实体(entity)。这个实体就相当于表的记录。
与关系数据库不同的是,每个实体都可以有自己独特的属性,而且属性的类型也可以不同。

先来看个例子:
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()
  owner = db.UserProperty(required=True)
这就定义了一个Pet模型,我们可以用它来生成实体。
其中db.StringProperty和db.DateProperty等都是db.Property类的子类。当在模型定义中使用Property类来定义属性时,这些属性就是这个模型的固定属性了;它们的值会被检验,以确保能在数据库中正确存储。
实体也和一般的对象一样,可以有自己特有的属性(以及方法),但这些属性不会被存储到数据库中。

在实体上调用put方法,就能将其存入数据库。例如:
from google.appengine.api import users

pet = Pet(name="Fluffy",
          type="cat",
          owner=users.get_current_user())
# required=True的属性在构造时就必须赋值
pet.weight_in_pounds = 24
这里有个注意点,模块是会被缓存和重用的,所以不要在模型的配置中使用用户相关的数据作为属性的缺省值。举例来说,这个users.get_current_user()就不适合作为owner的缺省值,因为使用该模块的用户如果没显式对owner赋值的话,就可能会使用上个用户作为owner。更多信息可以看APP的缓存机制

此外,实体还可以拥有动态属性,这需要模型继承db.Expando类。
动态属性也可以存储在数据库中,但由于动态属性没有相应的模型属性定义,动态属性不会被校验。
当在动态属性上加过滤器(filter)来查询时,只会返回有该属性且属性类型相同的实体。例如:
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

#删除动态属性
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 中包含p1,但不含p2和p3

people = db.GqlQuery("SELECT * FROM Person WHERE favorite > :1",50)
# people 中不包含任何实体
再者,你还可以继承PolyModel类,以定义继承性的模型。
使用也和其他模型一样,例如:
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()

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

for contact in Contact.all():
  print 'Phone: %s\nAddress: %s\n\n'
        % (contact.phone,
           contact.address))
三、属性和类型
在定义模型时可以使用很多种属性,每个属性都能在Types and Property Classes文档里找到详细说明,这里只提注意点。

1.字符串
GAE支持最大长度为500字节的String类型,和最大100万字节的Text类型。前者支持索引,可以在搜索中使用过滤器;后者不行。它们的值可以是unicode,也可以是str,后者缺省被当成ascii编码类型。
此外还有非文本类型ByteString和Blob类型。大小和索引情况同上。Blob类型一般是用于存储二进制数据的。

2.列表
要让一个属性可以有多个值,则可以使用List和StringList类型。
需要注意的是,当在一个列表属性上使用过滤器时,将会对其中的成员进行比较,而不是整个列表进行比较。只要其中一个成员符合,就通过这个条件过滤。
# 获取所有包含6的实体(不是实体本身为6)
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers = 6")

# 获取所有至少包含一个小于10的元素的实体.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers < 10")
可以将一个空的list赋值给一个静态的ListProperty。这个值在datastore中并不存在,但模型实例表现为好像这个值是一个空的list。静态的ListProperty 不能够是 None值。
一个List动态属性的值不能是一个空的list。然而它可以是None,并可以删除。
如果对列表进行正序排序,用来排序的值是列表的最小元素;反之则用最大元素。因此很少对列表进行排序。

3.引用
引用类型是其他实体的key,它相当于关系数据库中的外键。引用的值虽然是key,但它可以自动解引用为实体,可以直接当成实体来使用。同时,实体也能自动引用,当成key来使用。
一个ReferenceProperty属性值可以像一个模型实体一样的使用。如果引用的实体在内存不存在,访问它时,将会自动从数据库里面取出相应实体。
当一个实体A有一个引用指向实体B,那么B就称为A的祖先。注意,如果删除B,A并不会被删除,关联关系也不会消失。但你可以取出A,检查其ReferenceProperty属性值是否为None。
另外,ReferenceProperty还有个反向引用(back-references)的特性。即B的secondmodel_set属性可以返回所有引用它的查询结果实体集(包含A在内)。
此外,当你需要在一个模型中使用多个引用属性时,需要显式地加上collection_name参数,避免往回引用时出错。
最后,自动引用和解引用、类型检查,以及反向引用,只有当使用ReferenceProperty时才有效,Expando动态属性或ListProperty等其他属性是没有这些机制的。

4.属性名
__*__(前后都为2个下划线)这种形式的属性名是被数据库保留的,应用程序不允许创建这种属性。
以一个下划线开头的属性名会被忽略,数据库不会存储这些数据,但你可以在程序中临时使用。
此外,由于Python API的限制,已经用作模型的方法的名字,也是不能用于属性名的。但数据库却允许这样做,只需要在属性的构造函数里增加name参数即可:
class MyModel(db.Model):
  obj_key = db.StringProperty(name="key")
四、创建、获取和删除数据
终于开始真正使用数据库了,其实用起来很简单,就和一般的对象差不多。

1.创建和更新:
调用一个模型的构造函数,即可创建这个类的对象。
更新则只需要修改对象的属性即可。
调用这个对象的put方法,或使用db的put方法,即可保存该对象到数据库。
例子:
pet = Pet(name="Fluffy",
          type="cat",
          owner=users.get_current_user())

pet.put()  #等于下面这句
db.put(pet)
2.查询
数据库可以查询一个模型类型的实体。一条查询可以用条件子句来过滤实体的属性值,也能返回经过排序的结果集,还可以通过祖先来限制查询结果的范围(其实就相当于inner join)。
Datastore API提供了2种查询方式:一种是通过调用Model类的all方法,查询所有该模型的对象,然后再调用filter、order和ancestor方法来限制和排序结果集;另一种是使用Gql查询。
先说前者,例子如下:
class Story(db.Model):
  title = db.StringProperty()
  date = db.DateTimeProperty()

query = Story.all()

query.filter('title =', 'Foo')
query.order('-date')
query.ancestor(key)

# 这些方法也可以链式调用
query.filter('title =', 'Foo').order('-date').ancestor(key)
再说后者,这个比前者多了个对结果集的个数限制以及偏移量指定。
它又有2种方式:
一种是使用GqlQuery类的构造函数来创建查询对象:
# 可用位置来绑定参数
query = db.GqlQuery("SELECT * FROM Story WHERE title = :1 "
                    "AND ANCESTOR IS :2 "
                    "ORDER BY date DESC",
                    'Foo', key)

# 也可用名字来绑定参数
query = db.GqlQuery("SELECT * FROM Story WHERE title = :title "
                    "AND ANCESTOR IS :parent "
                    "ORDER BY date DESC",
                    title='Foo', parent=key)

# 字符串、数字和Boolean值可以作为字面值(literal values)直接使用
query = db.GqlQuery("SELECT * FROM Story WHERE title = 'Foo' "
                    "AND ANCESTOR IS :parent "
                    "ORDER BY date DESC",
                    parent=key)
另一种是使用Model类的gql方法:
query = Story.gql("WHERE title = :title "
                  "AND ANCESTOR IS :parent "
                  "ORDER BY date DESC",
                  title='Foo', parent=key)
此外,还可用bind方法来重新绑定参数,以便重复使用一个查询对象。

3.执行查询并获取结果(集)
在创建查询对象时,应用程序并不会访问数据库。直到对结果集进行操作时,才会访问数据库。
获取结果集有2种方式:使用fetch方法,和使用迭代接口(iterator interface)。
fetch方法一次最多查询1000个结果,你也可以设置让其返回指定个数(不超过1000)的结果集。
此外,fetch还可以设置偏移量,即从第几个实体开始返回。但fetch查询的结果并不受偏移量限制,仅是只从偏移量个实体开始返回而已。所以假设偏移量为100,则最大只能返回900个实体。所以应该用过滤器来限制返回条数,多调用几次以得出全部结果。
这个限制非常讨厌,但GAE的数据库速度可以说是非常慢的,就算没这个限制,也会超出执行时间。
例子:
results = query.fetch(10)
for result in results:
  print "Title: " + result.title
如果是取实体的数目,可以用count方法。但它也是取出所有记录再统计数目,比关系数据库的count操作慢很多,而且也受1000条的影响。应该只在结果集很小,或设置了个数限制时使用。
迭代方式则没有查询结果的个数限制,因为它相当于一次获取一个实体,不过速度自然会比前者慢(访问数据库次数多很多)。受GAE的执行时间限制,实际上应该也不会超过1000条。
例子:
for result in query:
  print "Title: " + result.title
还可以使用db.getModel.get方法获取一个实体。
例子:
entity.put()
key = entity.key()

# ...

entity = db.get(key)
4.删除
你可以使用delete方法来删除实体。
例子:
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)
看上去是先取出来,再进行删除的,速度应该是很慢的。
此外,无法直接删除一个模型。

五、Key
每个实体都有一个唯一的key,用于标识它。一个key有3个组成部分:描述它和其他实体之间的父子关系的路径(path);实体类型(kind,即模型的类名);程序给实体设置的名字,或者数据库给实体生成的数字ID。
每个实体都有一个唯一的标识符。可以在程序中对其赋值,只需在构造时传给key_name参数一个字符串即可:
s = Story(key_name="xzy123")
如果没有指定key_name,数据库会给它生成一个数字ID。在一般情况下,这个ID会根据实体的创建时间而增长,但数据库并不保证它一定这样,且增长幅度可能不为1。我就曾看过2个实体创建时间和他们的ID大小不是相对的;也曾有过ID从几十突然增大到1001的情况;接着过了几天,ID又从几十开始增加了。这应该是因为同时有多个插入操作,数据库就往后跳跃了一个较大的尺度;等应用完成后,GAE会在空闲时寻找浪费的ID,继续在那插入。
注意:一旦建立,实体的名字或ID就不能更改。

六、实体组(EntityGroup)
每个实体都属于一个实体组。单个事务可以操作一组实体。实体组的关系告诉GAE在分布式网络同一部分储存几个实体。一个事务为一个实体组建立的数据库操作,要么全部成功,要么全部失败。
当应用程序将一个实体赋值为另一个实体的父亲,这个操作就把新的实体并到父亲的同一组。
没有父亲的实体是根实体。父子关系可以多级。从根节点开始的链称为实体的路径。这条路径上的实体都是它的祖先。实体创建时,父亲就指定了,并且不能被改变。
通过指定继承路径,你可以在不创建父亲的情况下,就创建一个带祖先路径的实体。为了实现它,你应该用类型(模型名)和key name创建一个祖先的key(它并不对应一个真正的实体),然后用它作为新实体的父亲。所有具备相同根祖先的实体都属于同一组。无论这个根祖先是否是一个真正的实体。

提示:
  • 只有需要事务的时候才使用实体组。其他实体间的关系请使用ReferenceProperty和key值,它们可以用于查询。
  • 实体组越多,根实体就越多,数据库就能更有效率地使用节点(更好地实现分布式),以提供更佳的更新和插入性能。多用户在试图同时更新同组的实体时,会导致一些事务的重新执行,还可能导致失败。不要把所有的实体放在同一个根下。
  • 定义实体组的一个较好的规则是,使它小到只对单个用户有价值,或者更小。
  • 实体组不会对查询有明显的影响。
七、索引
每条数据库的查询都要用到索引,如果没有相应的索引,查询就不会成功。这是GAE的数据库最大的限制,也是与其他数据库最大的不同,虽然是个很讨厌的限制,但也是为了避免全表扫描导致性能降低。
GAE使用index.yaml来定义索引。幸运的是,如果你是在开发服务器上使用,第一次查询时会自动帮你创建索引,你只需上传到Google的服务器就行了。

1.索引介绍
一次数据库查询可以指定结果集必须符合的条件,例如实体类型、属性值的范围、祖先,以及排列顺序。查询时会去查找有没有符合该条件的索引,只有当索引中定义了这些后,才能按索引进行相应的查询。
指定属性范围可以用以下操作符:<、<=、=、>、>=、!=和IN。
!=操作实际上是将<和>操作的结果集合并;IN操作实际上是转成其中所有元素的=操作的结果集合并,这可能造成很多次的数据库访问。

数据库通过以下步骤执行一次查询:
  1. 数据库识别符合实体类型、过滤器属性、过滤器操作符和排列顺序的索引。
  2. 数据库扫描索引,并找到第一条符合所有条件的实体。
  3. 数据库继续扫描索引,返回找到的每个实体,直到发现不符合条件的实体或索引结束。
索引表包含了所有使用了过滤器的属性和排列顺序。它的每行都以下列顺序排列:
  1. 祖先
  2. 使用了=或IN过滤器的属性
  3. 使用了不等于过滤器的属性
  4. 使用了排列顺序的属性
此外,GAE的数据库并不支持通配符查询,但可以通过这种方式来查找以某些字符开头的字符串:
db.GqlQuery("SELECT * FROM MyModel WHERE prop >= :1 AND prop < :2", "abc", u"abc" + u"\ufffd")
其中u"\ufffd"是unicode中可能出现的最大的字符,通过这种方式,就能查找abc开头的字符串。

要注意的是,在没有索引的属性上使用过滤器等是不会返回任何结果的,不在索引里的属性也是不会被返回的。所以如果想返回属性值为None的实体,你可以在定义数据模型时,为这个属性定义默认值(例如None)。
Text和Blob类型是没有索引的,也不能在它们上查询。

另外,属性值的排序是先按属性类型,再按属性的值。这就意味着整型一定排在浮点型前,浮点型又一定排在字符串前,即:37 < 36.5 < "36"。
如果这不是你所期望的,可以让其只能为相同类型。

2.使用index.yaml定义索引
GAE将为下列查询自动在index.yaml中创建索引:
  • 只使用了=、IN和祖先过滤器的查询
  • 只使用了不等于过滤器的查询
  • 只在一个属性上使用了一次排序的查询
其他的查询需要手动定义索引:
  • 有多个排序的查询
  • 在key上的降序排序查询
  • 在多个属性上同时使用了不等于和=或IN过滤器的查询
  • 同时使用了不等于和祖先过滤器的查询
不过在开发服务器上,当找不到对应索引时,它会自动创建一个索引,保证查询不会失败。
你可以在dev_appserver.py启动时加上--require_indexes参数,时它不会自动创建索引,以确保和生产服务器是相同的。
更多关于定义索引的信息,你可以查看配置索引的文档。

3.查询Key
你只需在查询中指定__key__为查询属性即可。
使用key可以顺序遍历一个模型,例如:
class MainHandler(webapp.RequestHandler):
  def get(self):
    query = Entity.gql('ORDER BY __key__')

    # 使用1个查询参数来记录最后一条应该查询的实体
    last_key_str = self.request.get('last')
    if last_key_str:
      last_key = db.Key(last_key_str)
      query = Entity.gql('WHERE __key__ > :1 ORDER BY __key__', last_key)

    # 如果一次查询20条,找找是否有第21条
    entities = query.fetch(21)
    new_last_key_str = None
    if len(entities) == 21:
      new_last_key_str = str(entities[19].key())
注意,如果你只需要查找一个特定key对应的实体,用db.get方法会更快。

3.查询限制
由于索引的限制,导致有以下的限制存在:
  • 在属性上使用过滤器和排序,则需要该属性存在。不存在该属性的实体不会被返回。
  • 没有过滤器会符合没有属性的实体。如果确实需要返回属性为None的实体,需要创建一个带None的过滤器。
  • 只能在一个属性上使用不等于过滤器。
    例如:
    SELECT * FROM Person WHERE birth_year >= :min
                           AND birth_year <= :max
    
    SELECT * FROM Person WHERE birth_year >= :min_year
                           AND height >= :min_height     # 这是错的
    
    SELECT * FROM Person WHERE last_name = :last_name
                           AND city = :city
                           AND birth_year >= :min_year
  • 如果在一个属性上使用不等于过滤器,那么进行排序时,它必须在其他属性前排序。
    例如:
    SELECT * FROM Person WHERE birth_year >= :min_year
                         ORDER BY last_name              # 错误
    
    SELECT * FROM Person WHERE birth_year >= :min_year
                         ORDER BY last_name, birth_year  # 错误
    
    SELECT * FROM Person WHERE birth_year >= :min_year
                         ORDER BY birth_year, last_name
  • 对列表属性排序很可能超出索引限制。
    由于列表的排序是基于其元素的,所以它索引是这样的:
    • 如果对列表进行升序排列,则按列表中的最小元素进行排列
    • 如果对列表进行降序排列,则按列表中的最大元素进行排列
    • 其他元素和列表大小不影响排序
    • 如果相同,则再使用key值进行排列
    当在一个实体上定义了多个列表的索引时,索引的数目将会急速增长。
    例如这个索引:
    indexes:
    - kind: MyModel
      properties:
      - name: x
      - name: y
    创建一个实体:
    class MyModel(db.Expando):
      pass
    
    e2 = MyModel()
    e2.x = ['red', 'blue']
    e2.y = [1, 2]
    e2.put()
    这会让数据库生成8条索引记录,即在x、y上各有顺序和倒序的2个,然后在x和y上又有4种顺序组合。
    当一个put操作需要作用在很多条索引上时,就可能超过限制,并抛出BadRequestError异常。
    解决这种情况,需要先将出错的索引从index.yaml中去掉,再执行appcfg.py vacuum_indexes,最后将移除的索引添加回来,并执行appcfg.py update_indexes
    为了避免它发生,最好少在列表属性上定义索引,并只给出一种排列顺序。

八、事务
为保证一系列数据库操作要么都执行成功,要么都不产生效果,我们需要用到事务。
我们将事物中的操作用db.run_in_transaction函数调用就行了。例如:
from google.appengine.ext import db

class Accumulator(db.Model):
  counter = db.IntegerProperty()

def increment_counter(key, amount):
  obj = db.get(key)
  obj.counter += amount
  obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)
如果执行成功,会进行提交;如果产生异常,则会回滚。如果产生的是Rollback异常,函数会返回None;其他异常则会向外抛出。

事务中还有些限制:
不能使用Query或GqlQuery查询,但可以用key来获取实体。
在一个事务中,不能对一个实体进行超过一次的创建或更新操作。

此外,如果同一时刻有多个事务对同一实体进行操作,可能会导致失败。这种情况下,事务会自动重试几次,如果仍失败,将会抛出TransactionFailedError异常。你可以用db.run_in_transaction_custom_retries函数来设置重试次数。

教程就到此为止了,顺便给一篇实体关系建模的文章:
英文版:http://code.google.com/appengine/articles/modeling.html
中文翻译:http://www.cnblogs.com/kuber/archive/2008/08/19/ModelingEntityRelationshipsInGAE.html

3条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?