Google App Engine:如何修改你的数据模型
导言
如果你有一个成功的GAE应用, 不可避免的你会要修改你的数据库架构. 本文通过一个小例子介绍了修改数据库架构的两个基本步骤:
- 更新数据模型类定义
- 更新Datastore中的已有数据实体(这一步并不是总是必要的, 下面会讲什么时候你需要这样做)。
开始之前
在更新你的数据模型时,你可能需要暂时禁止用户在你的应用中更新数据。 是否确实需要取决于你的应用, 但是在某些情况下, 暂时禁止用户输入会大大便于你更新已有数据。
更新你的数据模型
这里有一个例子,一个简单的图片:
classPicture(db.Model):
author = db.UserProperty()
png_data = db.BlobProperty()
name = db.StringProperty(default='') # 唯一的的图片名
我们来修改这个model, 为每个图片加上评分。为了保存评分, 我们保存用户评分的次数和评价得分值。更新这个数据模型很容易,我们只是增加两个新的属性:
classPicture(db.Model):
author = db.UserProperty()
png_data = db.BlobProperty()
name = db.StringProperty(default='') # Unique name.
num_votes = db.IntegerProperty(default=0)
avg_rating = db.FloatProperty(default=0)
现在所有保存到Datastore的新建实体将获得一个默认的评分为0 。请注意,Datastore中现有的数据并没有自动被修改, 所以他们不会有这些属性。
更新现有数据实体
App Engine datastore并不要求所有数据有相同的一组属性。 更新你的数据模型之后,现有的数据实体将继续没有这些属性。 在某些情况下,这就够了,你不需要做什么。 那什么时候你想更新现有的数据,使他们也有新的属性? 一种情况是当你想要对新的属性做查询。在我们这个Picture的例子中, 查询"最受欢迎"或者"最不受欢迎"图片不会返回更新之前的数据, 因为它们没有相应的评分属性。要解决此问题,我们需要更新Datastore中现有的数据实体。
从概念上来说,更新现有的数据实体很容易。你只需要创建request handler, 取出每个实体,设置新属性的值, 然后保存数据。 有两个我们必须要解决的问题:
- 查询返回数据集有1000条的上限。如果有多于1000条记录,需要多次查询以获得所有记录。
- GAE要求http request 在很短的时间内必须返回,否则request 会超时。如果有很多数据,request handler不能在一个请求中处理完所有数据。
- 读取一个数据实体
- 设置属性的值(如果属性有缺省值,会自动设置)
- 保存数据
- 用meta refresh标记,让浏览器访问更新下一个数据的URL
一句警告:写读取查询时,应避免使用OFFSET(它不适合大数据集)而是使用WHERE语句量限制返回的数据数量。如果你的数据已经有某种唯一值的属性,这很容易。 在这个例子中,图片名称(name)
是唯一的,所以我们对name
将使用WHERE语句。
代码如下:
# Request handler for the URL /update_datastore
def get(self):
name = self.request.get('name',None)
if name isNone:
# First request, just get the first name out of the datastore.
pic = models.Picture.gql('ORDER BY name DESC').get()
name = pic.name
q = models.Picture.gql('WHERE name <= :1 ORDER BY name DESC', name)
pics = q.fetch(limit=2)
current_pic = pics[0]
if len(pics)==2:
next_name = pics[1].name
next_url ='/update_datastore?name=%s'% urllib.quote(next_name)
else:
next_name ='FINISHED'
next_url ='/' # Finished processing, go back to main page.
# In this example, the default values of 0 for num_votes and avg_rating are
# acceptable, so we don't need to do anything other than call put().
current_pic.put()
context ={
'current_name': name,
'next_name': next_name,
'next_url': next_url,
}
self.response.out.write(template.render('update_datastore.html', context))
相应的template显示我们正在更新哪条记录, 并用meta refresh 自动转到下条记录:
<html>
<head>
<metahttp-equiv="refresh"content="0;url={{ next_url }}"/>
</head>
<body>
<h3>Update Datastore</h3>
<ul>
<li>Updated: {{ current_name }}</li>
<li>About to update: {{ next_name }}</li>
</ul>
</body>
</html>
如果你的数据中没有具有唯一值的属性,上面的例子不能直接使用(当某个属性值对应很多条记录时, 它会很慢) 。你需要扩展它以能够处理这种情况。但是概念是相同的,使用WHERE限制查询返回的数据集数目以分批更新数据,然后逐条保持。
从Datastore中移除已删除的属性
如果你从数据模型中移除了一个属性,你会发现现有的实体仍然有这个属性。它仍然会显示在管理控制台(admin console)中,并仍然存在于Datastore众。 要真正清理旧数据,你需要的遍历每条记录并逐条移除属性值。
- 确认你已从数据模型定义删除了属性。
- 如果你的数据模型类继承自db.model ,它改为继承自db.expando 。(db.model实例无法动态修改,这是我们在下一步要做的) 。
- 遍历现有的所有数据(如上文所述) 。 对每条数据,使用
delattr
删除属性,然后保存数据。 - 如果你的数据模型原本继承自db.model ,不要忘了改回去。
以后
这种多次request的处理方法现在是可行的。 不过现在我们正在做一些离线处理的解决方法。当这些成为可用的,可能会提供一个更合理的方式来修改你的数据,而且不用加重服务器的负担。你可以考虑订阅我的blog,跟踪最新的进展