在Google App Engine中使用数据库
2009 2 18 02:16 PM 6168次查看
分类:Google App Engine 标签:Google App Engine, Python
阅读本文需要您懂一般的数据库使用。
废话少说,先给参考文档:
官方文档(英文):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.get或Model.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操作实际上是转成其中所有元素的=操作的结果集合并,这可能造成很多次的数据库访问。
数据库通过以下步骤执行一次查询:
- 数据库识别符合实体类型、过滤器属性、过滤器操作符和排列顺序的索引。
- 数据库扫描索引,并找到第一条符合所有条件的实体。
- 数据库继续扫描索引,返回找到的每个实体,直到发现不符合条件的实体或索引结束。
- 祖先
- 使用了=或IN过滤器的属性
- 使用了不等于过滤器的属性
- 使用了排列顺序的属性
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
这会让数据库生成8条索引记录,即在x、y上各有顺序和倒序的2个,然后在x和y上又有4种顺序组合。class MyModel(db.Expando): pass e2 = MyModel() e2.x = ['red', 'blue'] e2.y = [1, 2] e2.put()
当一个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
向下滚动可载入更多评论,或者点这里禁止自动加载。