3.2.3 DatabaseWrapper类的实战案例

接下来介绍Python交互模式下DatabaseWrapper类的实战案例,这里采用先分析结果后执行验证的顺序进行讲解。此次演示只是牛刀小试,后面的章节将在本节的基础上详细分析ORM框架的操作原理。

创建DatabaseWrapper对象并调用connect()函数

前文在演示ConnectionHandler类的使用时,得到了一个字典结果:

接下来,使用保存了数据库信息的字典数据初始化DatabaseWrapper类,并比较该类中通过connections属性和MySQLdb.connect()方法得到的连接对象。从前面的源码分析中可知,它们都属于同一个类:

继续调用mysql_version()方法。注意,由于该方法前面加了@cached_property装饰器,所以只需用访问属性的方式即可调用。那么,wrapper.mysql_version的结果应该是什么呢?从字面的含义很容易猜出应该是该数据库的版本信息。mysql_version()方法最终解析的是mysql_server_info()方法的结果。而mysql_server_info()方法的逻辑非常简单:先获取游标,再执行SELECT VERSION()方法,最后通过游标的fetchone()方法获取第1条结果,同时取得结果的第1个元素。下面使用MySQLdb模块进行测试:

上面演示的是mysql_server_info()方法的模拟结果。接下来看看在mysql_version()方法中对该结果的加工操作。接着上面的交互模式,继续执行如下操作:

从上面的代码可以看到,wrapper.mysql_version的结果应该为(5,7,18),是由MySQL的三个版本号组成的元组:

接下来继续分析cursor的来龙去脉,它是mysqlclient模块中的游标对象吗?分析一下源码就清楚了:

注意,在上面的代码中,使用数字1~5简单标记了从temporary_connection()方法开始的代码执行顺序。

上面的逻辑关系比较清楚,最终在mysql_server_info()方法中得到的cursor并不是mysqlclient模块中的游标对象,而是由Django封装的CursorWrapper对象(和前面提到的CursorWrapper对象不同,前者是在django/db/utils.py中定义的),该对象是在django/db/backends/utils.py中定义的:

这里封装的CursorWrapper类在实例化时需要传入两个参数:cursor(mysqlclient模块中的游标对象)和db(BaseDatabaseWrapper对象及其子类对象)。该类完全兼容mysqlclient模块中游标类的所有属性与方法,其中,execute()方法和executemany()方法会分别调用self.cursor的execute()方法和executemany()方法并返回结果。而对于其他属性和方法,如fetchone()方法等,则是通过魔法函数__getattr__()获取的。通过阅读该魔法函数的源码可知,对于非WRAP_ERROR_ATTRS集合中的属性值,直接通过getattr()方法获取self.cursor中对应的属性值即可。如果想要获取['fetchone','fetchmany','fetchall','nextset']这些属性,就需要调用self.db.wrap_database_errors(cursor_attr)。注意,此时cursor_attr已经通过getattr()方法从self.cursor中获取对应的属性值了。继续追踪self.db中的wrap_database_errors()方法,它的定义在BaseDatabaseWrapper类中:

继续追踪DatabaseErrorWrapper类的实现,它位于django/db/utils.py文件中:

由上面两处源码可知,wrap_database_errors()方法返回的是DatabaseErrorWrapper对象,因此self.db.wrap_database_errors(cursor_attr)实际上调用的是DatabaseErrorWrapper类中的魔法函数__call__(),而该魔法函数其实就是返回func方法的一个封装形式,使得原方法在with self语句下执行并返回相应的结果。以获取fetchone属性值为例,在魔法函数__call__()中,传入的func参数正是mysqlclient模块中游标类的fetchone方法,这里得到的是inner方法。inner方法只是对传入的func方法进行了封装,最终调用执行的仍然是mysqlclient模块中的fetchone方法。

因此,整个在DatabaseWrapper类中得到的cursor与mysqlclient模块中的游标对象相比有两处升级(调用cursor()方法得到的cursor值):

◎ 用django/db/mysql/base.py中的CursorWrapper对象封装mysqlclient模块中的游标对象。这个对应封装的方法为DatabaseWrapper对象中的create_cursor()方法。

◎ 假设上一步得到的是x_cursor,调用父类中的_prepare_cursor()方法继续处理x_cursor,在该方法中继续调用make_cursor()方法,最终使用在django/db/backends/utils.py中定义的CursorWrapper类进一步封装x_cursor。

接下来在Python命令行中进行相关类的操作,以验证上面的分析结果:

注意,在上面的演示代码中有两个地方需要注意。

(1)使用python manage.py shell方式迚入交互模式,可以直接通过导入connections模块得到DatabaseWrapper对象,不用像前文那样麻烦,需要导入环境变量等。

(2)有些人在执行wrapper.cursor()语句后得到的可能是CursorDebugWrapper对象,这是因为first_django项目的settings.py文件中的DEBUG被设置为True。如果将其设置为False后再次执行上面的语句,就可以得到CursorWrapper对象了。通过源码可知,CursorDebugWrapper类继承了CursorWrapper类,并重写了execute()方法和executemany()方法。这两个方法主要是记录并打印方法的执行时间,以便调试。

上面介绍的是针对MySQL的底层原理。除MySQL外,还可以选择Oracle、PostgreSQL、SQLite3等数据库。通过在项目的settings.py文件中选择不同的数据库ENGINE,就可以和前文分析的一样,借助对应的Python模块封装一层,提供统一对外的DatabaseWrapper类及方法了。这样就形成了Django的一大特色:支持多种数据库。这样的编程模式在Python中十分常见,Ansible源码和Scrapy源码均是如此。

现在回到最开始追踪源码的部分,即ConnectionHandler类的魔法函数__getitem__()中。在上一个案例中,由于在Django项目中设置的数据库是MySQL,因此魔法函数__getitem__()返回的conn其实是django.db.backends.mysql.base.DatabaseWrapper对象,借助这个对象可以完成很多操作,和使用mysqlclient模块一样:

至此,关于ORM框架的核心部分就分析完了。Django为MySQL数据库封装了mysqlclient模块,升级了相应的游标类,并提供了统一的对外操作接口,而对数据库的操作最后都通过调用mysqlclient模块中的相关类与方法完成。有兴趣的读者可以根据各自熟悉的数据库分析Django底层的封装代码,此处不再赘述。