![深入理解Django:框架内幕与实现原理](https://wfqqreader-1252317822.image.myqcloud.com/cover/14/43738014/b_43738014.jpg)
3.2.3 DatabaseWrapper类的实战案例
接下来介绍Python交互模式下DatabaseWrapper类的实战案例,这里采用先分析结果后执行验证的顺序进行讲解。此次演示只是牛刀小试,后面的章节将在本节的基础上详细分析ORM框架的操作原理。
创建DatabaseWrapper对象并调用connect()函数
前文在演示ConnectionHandler类的使用时,得到了一个字典结果:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_122_1.jpg?sign=1739255699-rqp7ixPgf5tqDZj9eiogDB6HelQmh2Gy-0-c67657d839c97597b6212774e0d3aa17)
接下来,使用保存了数据库信息的字典数据初始化DatabaseWrapper类,并比较该类中通过connections属性和MySQLdb.connect()方法得到的连接对象。从前面的源码分析中可知,它们都属于同一个类:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_122_2.jpg?sign=1739255699-gcRZgh8RpHXVi3kO8AOUjP7n8r0AHbHk-0-291c226f5410b0867372ff5d1c259f13)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_123_1.jpg?sign=1739255699-TPe6KGMusJ6HMj25UpjwZenBv5eZHUP9-0-272d9acefefc433ae82c027dc772c640)
继续调用mysql_version()方法。注意,由于该方法前面加了@cached_property装饰器,所以只需用访问属性的方式即可调用。那么,wrapper.mysql_version的结果应该是什么呢?从字面的含义很容易猜出应该是该数据库的版本信息。mysql_version()方法最终解析的是mysql_server_info()方法的结果。而mysql_server_info()方法的逻辑非常简单:先获取游标,再执行SELECT VERSION()方法,最后通过游标的fetchone()方法获取第1条结果,同时取得结果的第1个元素。下面使用MySQLdb模块进行测试:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_123_2.jpg?sign=1739255699-iITJq9srRC4EwE9ff4yOQJQBZaB9R05L-0-f757ef268ba9e6db5dc18b899860de99)
上面演示的是mysql_server_info()方法的模拟结果。接下来看看在mysql_version()方法中对该结果的加工操作。接着上面的交互模式,继续执行如下操作:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_124_1.jpg?sign=1739255699-2pgvcfKYGpbq4LE4wtr5gIHmHpbfIrEm-0-43c18ee1d5b20cba173b2cdeaad5a65c)
从上面的代码可以看到,wrapper.mysql_version的结果应该为(5,7,18),是由MySQL的三个版本号组成的元组:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_124_2.jpg?sign=1739255699-dsgpeGTutLiCioUfe7UFrKJjyQpo0zTC-0-ed549ab5e19c1f4bda2e3a5405d49fea)
接下来继续分析cursor的来龙去脉,它是mysqlclient模块中的游标对象吗?分析一下源码就清楚了:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_124_3.jpg?sign=1739255699-qAFLWvG6LlEzCRZoV04KVQqO0jDHZ71i-0-e58907432738fa80edc6940f6b23aa84)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_125_1.jpg?sign=1739255699-2QepROoKyA8lQmRi6amMLiwtD3zZEMYt-0-cae75f04ba4a2c72a538826dac3defd2)
注意,在上面的代码中,使用数字1~5简单标记了从temporary_connection()方法开始的代码执行顺序。
上面的逻辑关系比较清楚,最终在mysql_server_info()方法中得到的cursor并不是mysqlclient模块中的游标对象,而是由Django封装的CursorWrapper对象(和前面提到的CursorWrapper对象不同,前者是在django/db/utils.py中定义的),该对象是在django/db/backends/utils.py中定义的:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_125_2.jpg?sign=1739255699-4y9vFrkgvecJgc6DzqqwYHrODmeLZ5dI-0-02b30cb3a5c8d8956e3d9c2dfa0b7c36)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_126_1.jpg?sign=1739255699-nkCJsPx2fmfa48oS7yziVzD6ZVt7tWke-0-840e7b9a5526da512620eb4ca7f6a887)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_127_1.jpg?sign=1739255699-ChCJ00FNlhQQVvMcXNofDuMrS1jOvdaO-0-ff5e8d4995202230f320ed3715c868e6)
这里封装的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类中:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_127_2.jpg?sign=1739255699-6xvcQz7emqYYs7WRCD7084vD2jO5mqhq-0-074f94664a14f0bb257bb7f1b9fdbbe8)
继续追踪DatabaseErrorWrapper类的实现,它位于django/db/utils.py文件中:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_127_3.jpg?sign=1739255699-vCmSvxLrmZ8i8fxqAQENF761JGPWl4N6-0-01033827715776f30fef5a1ef6ac892f)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_128_1.jpg?sign=1739255699-HpVPXJUjttNuS5s805btn8ymubfkWd54-0-126071afb0bd43ca6593b9845ef87416)
由上面两处源码可知,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命令行中进行相关类的操作,以验证上面的分析结果:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_129_1.jpg?sign=1739255699-XAdl2ZTmecrDu31JUWkgajn3P1ZnuGqg-0-8ab10f65d5b3521f48db56749bb2ef8e)
注意,在上面的演示代码中有两个地方需要注意。
(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模块一样:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_130_1.jpg?sign=1739255699-zPAnf1NRuZVevWsVgQt4d9djWAKXGmIE-0-4bee5ecab1041f3d63e86758d3fc7399)
至此,关于ORM框架的核心部分就分析完了。Django为MySQL数据库封装了mysqlclient模块,升级了相应的游标类,并提供了统一的对外操作接口,而对数据库的操作最后都通过调用mysqlclient模块中的相关类与方法完成。有兴趣的读者可以根据各自熟悉的数据库分析Django底层的封装代码,此处不再赘述。