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)。这个实体就相当于表的记录。
与关系数据库不同的是,每个实体都可以有自己独特的属性,而且属性的类型也可以不同。
先来看个例子:
这就定义了一个Pet模型,我们可以用它来生成实体。
其中db.StringProperty和db.DateProperty等都是db.Property类的子类。当在模型定义中使用Property类来定义属性时,这些属性就是这个模型的固定属性了;它们的值会被检验,以确保能在数据库中正确存储。
实体也和一般的对象一样,可以有自己特有的属性(以及方法),但这些属性不会被存储到数据库中。
在实体上调用put方法,就能将其存入数据库。例如:
这里有个注意点,模块是会被缓存和重用的,所以不要在模型的配置中使用用户相关的数据作为属性的缺省值。举例来说,这个users.get_current_user()就不适合作为owner的缺省值,因为使用该模块的用户如果没显式对owner赋值的话,就可能会使用上个用户作为owner。更多信息可以看APP的缓存机制。
此外,实体还可以拥有动态属性,这需要模型继承db.Expando类。
动态属性也可以存储在数据库中,但由于动态属性没有相应的模型属性定义,动态属性不会被校验。
当在动态属性上加过滤器(filter)来查询时,只会返回有该属性且属性类型相同的实体。例如:
再者,你还可以继承PolyModel类,以定义继承性的模型。
使用也和其他模型一样,例如:
三、属性和类型
在定义模型时可以使用很多种属性,每个属性都能在Types andProperty Classes文档里找到详细说明,这里只提注意点。
1.字符串
GAE支持最大长度为500字节的String类型,和最大100万字节的Text类型。前者支持索引,可以在搜索中使用过滤器;后者不行。它们的值可以是unicode,也可以是str,后者缺省被当成ascii编码类型。
此外还有非文本类型ByteString和Blob类型。大小和索引情况同上。Blob类型一般是用于存储二进制数据的。
2.列表
要让一个属性可以有多个值,则可以使用List和StringList类型。
需要注意的是,当在一个列表属性上使用过滤器时,将会对其中的成员进行比较,而不是整个列表进行比较。只要其中一个成员符合,就通过这个条件过滤。
可以将一个空的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参数即可:
四、创建、获取和删除数据
终于开始真正使用数据库了,其实用起来很简单,就和一般的对象差不多。
1.创建和更新:
调用一个模型的构造函数,即可创建这个类的对象。
更新则只需要修改对象的属性即可。
调用这个对象的put方法,或使用db的put方法,即可保存该对象到数据库。
例子:
2.查询
数据库可以查询一个模型类型的实体。一条查询可以用条件子句来过滤实体的属性值,也能返回经过排序的结果集,还可以通过祖先来限制查询结果的范围(其实就相当于innerjoin)。
DatastoreAPI提供了2种查询方式:一种是通过调用Model类的all方法,查询所有该模型的对象,然后再调用filter、order和ancestor方法来限制和排序结果集;另一种是使用Gql查询。
先说前者,例子如下:
再说后者,这个比前者多了个对结果集的个数限制以及偏移量指定。
它又有2种方式:
一种是使用GqlQuery类的构造函数来创建查询对象:
另一种是使用Model类的gql方法:
此外,还可用bind方法来重新绑定参数,以便重复使用一个查询对象。
3.执行查询并获取结果(集)
在创建查询对象时,应用程序并不会访问数据库。直到对结果集进行操作时,才会访问数据库。
获取结果集有2种方式:使用fetch方法,和使用迭代接口(iterator interface)。
fetch方法一次最多查询1000个结果,你也可以设置让其返回指定个数(不超过1000)的结果集。
此外,fetch还可以设置偏移量,即从第几个实体开始返回。但fetch查询的结果并不受偏移量限制,仅是只从偏移量个实体开始返回而已。所以假设偏移量为100,则最大只能返回900个实体。所以应该用过滤器来限制返回条数,多调用几次以得出全部结果。
这个限制非常讨厌,但GAE的数据库速度可以说是非常慢的,就算没这个限制,也会超出执行时间。
例子:
如果是取实体的数目,可以用count方法。但它也是取出所有记录再统计数目,比关系数据库的count操作慢很多,而且也受1000条的影响。应该只在结果集很小,或设置了个数限制时使用。
迭代方式则没有查询结果的个数限制,因为它相当于一次获取一个实体,不过速度自然会比前者慢(访问数据库次数多很多)。受GAE的执行时间限制,实际上应该也不会超过1000条。
例子:
还可以使用db.get或Model.get方法获取一个实体。
例子:
4.删除
你可以使用delete方法来删除实体。
例子:
看上去是先取出来,再进行删除的,速度应该是很慢的。
此外,无法直接删除一个模型。
五、Key
每个实体都有一个唯一的key,用于标识它。一个key有3个组成部分:描述它和其他实体之间的父子关系的路径(path);实体类型(kind,即模型的类名);程序给实体设置的名字,或者数据库给实体生成的数字ID。
每个实体都有一个唯一的标识符。可以在程序中对其赋值,只需在构造时传给key_name参数一个字符串即可:
如果没有指定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的数据库并不支持通配符查询,但可以通过这种方式来查找以某些字符开头的字符串:
其中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可以顺序遍历一个模型,例如:
注意,如果你只需要查找一个特定key对应的实体,用db.get方法会更快。
3.查询限制
由于索引的限制,导致有以下的限制存在:
-
在属性上使用过滤器和排序,则需要该属性存在。不存在该属性的实体不会被返回。
-
没有过滤器会符合没有属性的实体。如果确实需要返回属性为None的实体,需要创建一个带None的过滤器。
-
只能在一个属性上使用不等于过滤器。
例如:
-
如果在一个属性上使用不等于过滤器,那么进行排序时,它必须在其他属性前排序。
例如:
-
对列表属性排序很可能超出索引限制。
由于列表的排序是基于其元素的,所以它索引是这样的:
-
如果对列表进行升序排列,则按列表中的最小元素进行排列
-
如果对列表进行降序排列,则按列表中的最大元素进行排列
-
其他元素和列表大小不影响排序
-
如果相同,则再使用key值进行排列
当在一个实体上定义了多个列表的索引时,索引的数目将会急速增长。例如这个索引:
创建一个实体:
这会让数据库生成8条索引记录,即在x、y上各有顺序和倒序的2个,然后在x和y上又有4种顺序组合。
当一个put操作需要作用在很多条索引上时,就可能超过限制,并抛出BadRequestError异常。
解决这种情况,需要先将出错的索引从index.yaml中去掉,再执行appcfg.pyvacuum_indexes,最后将移除的索引添加回来,并执行appcfg.pyupdate_indexes。
为了避免它发生,最好少在列表属性上定义索引,并只给出一种排列顺序。
-
八、事务
为保证一系列数据库操作要么都执行成功,要么都不产生效果,我们需要用到事务。
我们将事物中的操作用db.run_in_transaction函数调用就行了。例如:
如果执行成功,会进行提交;如果产生异常,则会回滚。如果产生的是Rollback异常,函数会返回None;其他异常则会向外抛出。
事务中还有些限制:
不能使用Query或GqlQuery查询,但可以用key来获取实体。
在一个事务中,不能对一个实体进行超过一次的创建或更新操作。
此外,如果同一时刻有多个事务对同一实体进行操作,可能会导致失败。这种情况下,事务会自动重试几次,如果仍失败,将会抛出TransactionFailedError异常。你可以用db.run_in_transaction_custom_retries函数来设置重试次数。