Google App Engine中使用数据库

http://blog.sina.com.cn/s/blog_53a802e90100n5id.html

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

一、概况

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

二、实体和模型

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

先来看个例子:

复制内容到剪贴板

代码:

fromgoogle.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方法,就能将其存入数据库。例如:

复制内容到剪贴板

代码:

fromgoogle.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)来查询时,只会返回有该属性且属性类型相同的实体。例如:

复制内容到剪贴板

代码:

classPerson(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类,以定义继承性的模型。

使用也和其他模型一样,例如:

复制内容到剪贴板

代码:

fromgoogle.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='123First 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='DataSolutions, 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 andProperty 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个下划线)这种形式的属性名是被数据库保留的,应用程序不允许创建这种属性。

以一个下划线开头的属性名会被忽略,数据库不会存储这些数据,但你可以在程序中临时使用。

此外,由于PythonAPI的限制,已经用作模型的方法的名字,也是不能用于属性名的。但数据库却允许这样做,只需要在属性的构造函数里增加name参数即可:

复制内容到剪贴板

代码:

classMyModel(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.查询

数据库可以查询一个模型类型的实体。一条查询可以用条件子句来过滤实体的属性值,也能返回经过排序的结果集,还可以通过祖先来限制查询结果的范围(其实就相当于innerjoin)。

DatastoreAPI提供了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 "
              "ANDANCESTOR IS :parent "
              "ORDER BYdate 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条。

例子:

复制内容到剪贴板

代码:

forresult 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在分布式网络同一部分储存几个实体。一个事务为一个实体组建立的数据库操作,要么全部成功,要么全部失败。

当应用程序将一个实体赋值为另一个实体的父亲,这个操作就把新的实体并到父亲的同一组。

没有父亲的实体是根实体。父子关系可以多级。从根节点开始的链称为实体的路径。这条路径上的实体都是它的祖先。实体创建时,父亲就指定了,并且不能被改变。

通过指定继承路径,你可以在不创建父亲的情况下,就创建一个带祖先路径的实体。为了实现它,你应该用类型(模型名)和keyname创建一个祖先的key(它并不对应一个真正的实体),然后用它作为新实体的父亲。所有具备相同根祖先的实体都属于同一组。无论这个根祖先是否是一个真正的实体。


提示:

  • 只有需要事务的时候才使用实体组。其他实体间的关系请使用ReferenceProperty和key值,它们可以用于查询。

  • 实体组越多,根实体就越多,数据库就能更有效率地使用节点(更好地实现分布式),以提供更佳的更新和插入性能。多用户在试图同时更新同组的实体时,会导致一些事务的重新执行,还可能导致失败。不要把所有的实体放在同一个根下。

  • 定义实体组的一个较好的规则是,使它小到只对单个用户有价值,或者更小。

  • 实体组不会对查询有明显的影响。


七、索引

每条数据库的查询都要用到索引,如果没有相应的索引,查询就不会成功。这是GAE的数据库最大的限制,也是与其他数据库最大的不同,虽然是个很讨厌的限制,但也是为了避免全表扫描导致性能降低。

GAE使用index.yaml来定义索引。幸运的是,如果你是在开发服务器上使用,第一次查询时会自动帮你创建索引,你只需上传到Google的服务器就行了。


1.索引介绍

一次数据库查询可以指定结果集必须符合的条件,例如实体类型、属性值的范围、祖先,以及排列顺序。查询时会去查找有没有符合该条件的索引,只有当索引中定义了这些后,才能按索引进行相应的查询。

指定属性范围可以用以下操作符:<、<=、=、>、>=、!=和IN。

!=操作实际上是将<和>操作的结果集合并;IN操作实际上是转成其中所有元素的=操作的结果集合并,这可能造成很多次的数据库访问。


数据库通过以下步骤执行一次查询:

  • 数据库识别符合实体类型、过滤器属性、过滤器操作符和排列顺序的索引。

  • 数据库扫描索引,并找到第一条符合所有条件的实体。

  • 数据库继续扫描索引,返回找到的每个实体,直到发现不符合条件的实体或索引结束。


索引表包含了所有使用了过滤器的属性和排列顺序。它的每行都以下列顺序排列:

  • 祖先

  • 使用了=或IN过滤器的属性

  • 使用了不等于过滤器的属性

  • 使用了排列顺序的属性


此外,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可以顺序遍历一个模型,例如:

复制内容到剪贴板

代码:

classMainHandler(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
                     ANDbirth_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
                     ANDbirth_year >= :min_year

  • 如果在一个属性上使用不等于过滤器,那么进行排序时,它必须在其他属性前排序。

    例如:

    复制内容到剪贴板

    代码:

    SELECT* FROM Person WHERE birth_year >=:min_year
                    ORDER BYlast_name           #错误

    SELECT * FROM Person WHERE birth_year >=:min_year
                    ORDER BYlast_name, birth_year  #错误

    SELECT * FROM Person WHERE birth_year >=:min_year
                    ORDER BYbirth_year, last_name

  • 对列表属性排序很可能超出索引限制。

    由于列表的排序是基于其元素的,所以它索引是这样的:

    • 如果对列表进行升序排列,则按列表中的最小元素进行排列

    • 如果对列表进行降序排列,则按列表中的最大元素进行排列

    • 其他元素和列表大小不影响排序

    • 如果相同,则再使用key值进行排列


    当在一个实体上定义了多个列表的索引时,索引的数目将会急速增长。

    例如这个索引:

    复制内容到剪贴板

    代码:

    indexes:
    - kind: MyModel
      properties:
      - name: x
      - name: y

    创建一个实体:

    复制内容到剪贴板

    代码:

    classMyModel(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.pyvacuum_indexes,最后将移除的索引添加回来,并执行appcfg.pyupdate_indexes

    为了避免它发生,最好少在列表属性上定义索引,并只给出一种排列顺序。


八、事务

为保证一系列数据库操作要么都执行成功,要么都不产生效果,我们需要用到事务。

我们将事物中的操作用db.run_in_transaction函数调用就行了。例如:

复制内容到剪贴板

代码:

fromgoogle.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函数来设置重试次数。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值