在Tornado中使用Django的ORM的注意事项
1. 如何在Django外使用它的ORM
Django 的 ORM 虽然功能和性能都不怎么样,但重在简单方便,更重要的是,已经熟悉使用它了。
要在 Django 外使用它的 ORM ,最简单的办法,就是设置一个 DJANGO_SETTINGS_MODULE 的环境变量就可以了:
import os os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
当然,如果你要是喜欢折腾,也可以从 django.conf 中把 settings 引入,然后使用 configure 方法去配置它的 DATABASES。
2. Django环境和Tornado的不同之处
能正确使用 Django 的 API 不代表就万事大吉,它的 ORM 是给自己量身订做的,换到另外一个环境中去,适配的问题只能我们自己解决。
Django 是传统的 Web 运行方式,一个请求过来,就开一个新的“进程/线程”去处理它,在数据库的连接处理上:
- 在需要进行数据库操作时,才去处理与数据库的连接。
- 因为是传统的 Web 运行方式,所以,每个请求都会使用单独的数据库连接。
- 连接一旦创建,那么直到当前请求结束,此连接才会关闭。此请求中的接下来的数据库操作会共用连接。
- 每次数据库操作时,都会去获取一次 cursor ,此时,会判断连接的可用性。所以,不用担心所谓的“8小时问题”,对于 MySQL 来说,它使用 ping() 方法判断连接的可用性。
- 请求处理完时, Django 会确保连接关闭。
搞清楚 Django 对数据库连接的处理方式,就好办了。
Tornado 的运行方式是单进程的,同时, Tornado 中的 请求 显然与 Django 中的 请求 不是一个东西,那么 Django 的 ORM 机制,不会知道 Tornado 中一个请求什么时候结束。而 Tornado 又是单进程方式,所以,如果你什么都不做,那么结果就是, Tornado 打开一个与数据库的连接,永远不关闭,请求之间也共用这个连接。
看到这里,可以看到,在 Tornado 中要做一个连接池是多少简单的事,当然,除非和数据库的交互支持异步调用的方式,否则永远只能使用一个数据库连接,没连接池什么事。
3. Tornado中要做的事
如果你什么也不做,初看起来,数据库工作得很好, Tornado 自己的数据库封装中面临的“8小时问题”对 Django 的 ORM 来说是不存的,因为它每次获取 cursor
时都会判断连接的可用性。
不过你需要解决的另外一个问题是事务提交。
Tornado 在自己的 MySQL 封装中,使用了 connect.autocommit(True),而 Django 的 ORM 默认是没有打开 autocommit
的。所以,你会看到的一个现象就是,对于 InnoDB 引擎, Tornado 跑起来之后,另外一个客户端对数据库的修改(比如添加了一个记录),在 Tornado 之中是不可见的。你要么打开 autocommit ,要么手动 commit ,我觉得手动 commit 比较好,在每次请求结束时:
# -*- coding: utf-8 -*- import os os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' from django.db import connections class BaseHandler(tornado.web.RequestHandler): def finish(self, *args, **kargs): [c.connection and c.connection.commit() for c in connections.all()]
另外要注意的一点是,在 Tornado 中最好不要使用 Django 的 @transaction.commit_on_success 这类全局性的东西,除非你真的清楚它对整个处理流程有怎样的影响。
4. 2012-7-15更新
在 finish() 中直接 commit() 有一个问题——虽然 Django 自己在执行数据库操作时会确保数据库的连接已经成功建立,但是,在调用 finish() 之前可能根本就没有任何的数据库操作。这时直接使用 commit() 时可能连接已经失效了(但是连接它还存在,即不为 None),于是就会抛出异常。当然,如果之前没有任何的数据库操作, commit() 本身也是不需要的。我们应该把代码作一些修改:
# -*- coding: utf-8 -*- import os os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' from django.db import connections class BaseHandler(tornado.web.RequestHandler): def finish(self, *args, **kargs): for c in connections.all(): try: c._commit() except: pass
5. 2012-8-15更新
对于 InnoDB 而言, Select 这类读操作也是受事务隔离影响的(参考:http://www.cnblogs.com/shiyangxt/archive/2009/02/11/1388519.html)。简单说,另外的事务已经 commit 的数据,对处于旧事务中的 Select 也是不可见的。
这里有一个很大的影响,就是前面我们在 finish() 中确保执行了 commit(),通常情况下,一个新的请求过来,是处于一个全新的事务当中。但是,如果在 Tornado 当中有一些 callback ,在新请求过来之前就访问了数据库,即打开了一个新的事务,而这些内部的 callback 又不会去调用 finish() 。于是,新的请求就可能处于一个‘旧’的事务当中,这时做 Select 就可能找不到一些数据。所以,为了确保新的请求处于新的事务当中,以保证 Select 操作的正确,我们在 initialize() 也需要做一遍 commit():
def initialize(self): for c in connections.all(): try: c._commit() except: pass
这样我们也只能保证“请求”的事务是新的。对于其它非请求时的数据库访问,比如写的一些定时器类的功能,在查库之前,也需要做一遍 commit() 以保证从数据库取出来的数据是有效的。