花 5 分钟学习 wiki,然后进入实际的应用程序编程,Django 程序员很容易创建让人迷惑、难于维护或低效的模型类。在本文中,了解如何避免一些常见的查询错误、如何使用模型管理器来封装复杂查询以及如何充分利用 Django V1.1 强大的聚集特性。
在 Django 内,与数据库的大多数交互都通过对象关系映射器(ORM),这个特性是 Django 与其他最新的 Web 框架(比如 Rails)所共有的。ORM 越来越受开发人员欢迎,因为 ORM 能够自动化与数据库的很多常见交互,而且会使用为人熟知的面向对象方式,而不是 SQL 语句。
Django 程序员可能会选择绕过原生 ORM,而选择流行的 SQLAlchemy
包,虽然 SQLAlchemy
十分强大,但是却较难使用,而且需要更多的代码行。虽然有些 Django 应用程序是使用 SQLAlchemy
而非原生 ORM 开发的,但是 Django 最吸引人的一些特性,比如其自动生成的管理界面,都要求使用 ORM。
|
本文着重阐释了 Django ORM 的一些不为人熟知的特性,此外,本文还为 SQLAlchemy
的用户提供了一些有关低效查询生成的告诫,这对其编码很有帮助。
本文中使用的软件版本包括:
- Django V1.0.2(第 1 和第 2 部分)
- Django V1.1 alpha(第 3 部分)
- sqlite3
- Python V2.4-2.6(Django 尚不支持 Python V3)
- IPython(针对示例输出)
Django ORM 支持很多数据库后端,但 sqlite3 最易于安装,并且常常与操作系统捆绑。本文中的例子应该能与任何后端协作。要想获得 Django 支持的数据库的完整列表,请参见 参考资料 。
Django 的设计支持敏捷开发的风格,因此能快速进行原型化和实验。在开始阶段,最好不要过于担心性能,而是要关注可读性和实现的简便性。
有 时,发现性能问题并不需要太长时间。通常在初次用实际数据试用应用程序时,很容易发现性能问题。有时,若只包含几个测试的测试套件的执行时间超过了 5 分钟的界限,这就表明存在性能问题。有时,应用程序运行过慢,也表示性能问题的存在。所幸的是,现在已经有了一些很容易识别的模式,这些模式亦很容易修 复。清单 1(应用程序的 models.py 文件)和 清单 2 给出了一个很常见的例子。
from django.db import models # Some kind of document, like a blog post or a wiki page class Document(models.Model): name = models.CharField(max_length=255) # A user-generated comment, such as found on a site like # Digg or Reddit class Comment(models.Model): document = models.ForeignKey(Document, related_name='comments') content = models.TextField() |
|
清单 2 显示了如何以一种低效的方式访问清单 1 中所设置的那些模型。
from examples.model import * import uuid # First create a lot of documents and assign them random names for i in range(0, 10000): Document.objects.create(name=str(uuid.uuid4())) # Get a selection of names back to be looked up later names = Document.objects.values_list('name', flat=True)[0:5000] # The really slow way to get back a list of Documents that # match these names documents = [] for name in names: documents.append(Document.objects.get(name=name)) |
这虽然是一个人为的示例,却展示了一种非常常见的用例:给定一列标识符,从数据库获得对应于这些标识符的所有项目。
当使用内存中的 sqlite3 时,上述示例代码的运行时间为 65 秒。如果是一个独立于文件系统的数据库,运行所花时间可能更长。不过,清单 3 中也有针对这个运行缓慢的查询的一个补丁。与针对每个名称值发出多个数据库查询相反,使用 fieldname__in
操作符来生成一个 SQL 查询,如下所示:
SELECT * FROM model WHERE fieldname IN ('1', '2', ...) |
(所生成的实际查询语法将会随数据库引擎而变化。)
from examples import models
import uuid
for i in range(0, 10000):
Document.objects.create(name=str(uuid.uuid4()))
names = Document.objects.values_list('name', flat=True)[0:5000]
documents = list(Document.objects.filter(name__in=names))
|
上述代码在 3 秒内即可执行。请注意此代码会将查询结果强制转型为一个列表,以强制对此查询求值。由于 Django 查询会被延迟求值,因此,简单的分配查询结果并不会引起对数据库的任何访问,亦使对比无效。
习 惯于编写原始 SQL 的数据库大师们会觉得本例十分直白,但是很多 Python 程序员并不具有数据库背景。有时,程序员的开发习惯往往有悖于效率。清单 4 给出了改进清单 2 中的代码的一种可能方式,这种方式是程序员很有可能选择采用的,因为他们没有意识到这是个陷阱。
for name in names: documents.append(get_document_by_name(name)) def get_document_by_name(name): return Document.objects.get(name=name)) |
表面上看,创建一个用来从数据库检索文档的单独方法似乎是个 不错的主意。但是这里还有其他一些工作要做,例如在返回前向模型中添加数据。请注意,对于这个模型,进行重构形成独立的方法看起来像是对代码的改进。在开 发之初就编写单元测试并包括进一些针对大型数据集的测试可以帮助我们识别重构所导致的性能骤降。
|
|
所有 Django 的开发人员都使用内置 Manager
类:表单 Model.objects.*
的所有方法,都会调用此类。这个基础 Manager
类自动可用,并且提供常用的一些能够返回 QuerySets
的方法(例如,all()
)、返回值的方法(例如,count()
)及返回 Model
实例的方法(例如, get_or_create()
)。
我们鼓励 Django 的开发人员覆盖这个基础 Manager
类。为了说明此特性的用处,我们对这个示例应用程序进行了扩展,为它添加了一个新模型 Format
,这个模型描述了此系统内文档的格式。下面是一个示例。
from django.db import models
class Document(models.Model):
name = models.CharField(max_length=255)
format = models.ForeignKey('Format')
class Comment(models.Model):
document = models.ForeignKey(Document, related_name='comments')
content = models.TextField()
class Format(models.Model):
type = models.CharField(choices=( ('Text file', 'text'),
('ePub ebook', 'epub'),
('HTML file', 'html')),
max_length=10)
|
|
接下来,用这个变更过的模型创建一些示例文档,这些文档均已分配了 Format
实例。
# First create a series of Format objects and save them to the database format_text = Format.objects.create(type='text') format_epub = Format.objects.create(type='epub') format_html = Format.objects.create(type='html') # Create a few documents in various formats for i in range(0, 10): Document.objects.create(name='My text document', format=format_text) Document.objects.create(name='My epub document', format=format_epub) Document.objects.create(name='My HTML document', format=format_html) |
假设这个应用程序提供了一种方法来按格式对文档进行首次过滤,然后再按其他字段(如标题)对 QuerySet
进行过滤。那么一个只返回文本文档的示例查询就可以是:Document.objects.filter(format=format_text)
。
在这个示例中,查询的含义很清楚,但在一个成熟的应用程序中,往往还需要对结果集应用更多的限制。比如,只想让结果集中出现标记了 public 的那些文档或是那些 30 天以内的文档。若需要从应用程序中的多个位置调用这个查询,那么要让所有这些过滤子句保持同步将是一件很头疼的事,并且会引发很多的 bug。
这时就需要借助定制管理器。定制管理器提供了定义无限量封装(canned)查询的能力 — 这一点类似于内置管理器方法,例如 latest()
(它仅返回给定模型的一个最新实例)或 distinct()
(它在所生成的查询中发出一个 SELECT DISTINCT
子句)。这些查询可以减少在应用程序中需要复制的代码量,管理器则提高了可读性。在实际使用中,相信您一定不会愿意阅读如下所示的内容:
Documents.objects.filter(format=format_text,publish_on__week_day=todays_week_day, is_public=True).distinct().order_by(date_added).reverse() |
而会觉得下面的代码对于您或是新的开发人员更好理解:
Documents.home_page.all() |
创建一个定制管理器非常简单。清单 7 给出了 get_by_format
示例。
from django.db import models
class DocumentManager(models.Manager):
# The model class for this manager is always available as
# self.model, but in this example we are only relying on the
# filter() method inherited from models.Manager.
def text_format(self):
return self.filter(format__type='text')
def epub_format(self):
return self.filter(format__type='epub')
def html_format(self):
return self.filter(format__type='html')
class Document(models.Model):
name = models.CharField(max_length=255)
format = models.ForeignKey('Format')
# The new model manager
get_by_format = DocumentManager()
# The default model manager now needs to be explicitly defined
objects = models.Manager()
class Comment(models.Model):
document = models.ForeignKey(Document, related_name='comments')
content = models.TextField()
class Format(models.Model):
type = models.CharField(choices=( ('Text file', 'text'),
('ePub ebook', 'epub'),
('HTML file', 'html')),
max_length=10)
def __unicode__(self):
return self.type
|
关于这个代码的一些解释:
- 如果您定义一个定制管理器,那么 Django 将会自动删除默认管理器。但我更倾向于同时保留默认管理器和定制管理器,以便其他开发人员(或我自已)仍可继续使用
objects
,并且它仍会严格 地如我们预期的那样工作。然而,由于我的这个新get_by_format
管理器只是 Djangomodels.Manager
的一个子类,因此,所有的默认方法,比如all()
,对于它来说都是可用的。是否在包括定制管理器的同时还包括默认管理器,这就取决于您的个人喜好了。 - 将新管理器直接指定给
objects
也是可以的。惟一的缺点就是在想要覆盖初始的QuerySet itself
的时候,新的objects
就会有一个出乎其他开发人员意料之外的行为。 - 在定义模型类之前,需要在 models.py 内先定义管理器类,否则 Django 将不能用这个类。这与对
ForeignKey
类引用的限制很相似。 - 我本可以简单地用一个能接受参数的方法(如
with_format(format_name)
)实现DocumentManager
。但通常我更倾向于使用管理器方法,这些方法的名字虽然有些长,但它们均不接受参数。 - 对于可以指定给某个类的定制管理器的数量通常没有技术上的限制,但有一到两个就已经可以满足您的需要了。
使用新的管理器方法非常简单。
In [1]: [d.format for d in Document.get_by_format.text_format()][0] Out[1]: <Format: text> In [2]: [d.format for d in Document.get_by_format.epub_format()][0] Out[2]: <Format: epub> In [3]: [d.format for d in Document.get_by_format.html_format()][0] Out[3]: <Format: html> |
现在,有一个方便的位置可以用来放置与这些查询相关的任何功 能,并且还可以在不打乱代码的情况下应用额外的限制。将这种功能放入 models.py 而不是将它胡乱地丢入视图或模板标记也符合 Django model-view-controller(MVC)的一贯精神。
另一个适用于管理器类的编码模式可以不涉及任何定制方法。例如,您不必去定义一个只返回 HTML 格式文档的新方法,相反,您可以定义一个完全 运行在该限制集之上的定制管理器,如下面的示例所示。
class HTMLManager(models.Manager):
def get_query_set(self):
return super(HTMLManager, self).get_query_set().filter(format__type='html')
class Document(models.Model):
name = models.CharField(max_length=255)
format = models.ForeignKey('Format')
html = HTMLManager()
get_by_format = DocumentManager()
objects = models.Manager()
|
get_query_set()
方法继承自 models.Manager
,并在本示例中被覆盖以接受这个基础查询(与 all()
所生成的相同),并为其应用了一个额外的过滤。添加到这个管理器的所有后续方法都要首先调用 get_query_set()
方法,然后才能在该结果之上再应用其他的查询方法,如下所示。
# Our HTML query returns the same number of results as the manager # which explicitly filters the result set. In [1]: Document.html.all().count() Out[1]: 10 In [2]: Document.get_by_format.html_format().count() Out[2]: 10 # In fact we can prove that they return exactly the same results In [3]: [d.id for d in Document.get_by_format.html_format()] == [d.id for d in Document.html.all()] Out[3]: True # It is not longer possible to operate on the unfiltered # query in HTMLManager() In [4]: Document.html.filter(format__type='epub') Out[4]: [] |
若在您数据的子集上需要进行很多操作同时还希望减少代码量及需要生成的查询的复杂性时,可以考虑使用这个基于类的方法来过滤查询。
为管理器所能添加的方法的类型是没有限制的。如前面所示,方法可以返回 QuerySet
,也可以返回相关模型类的实例(比如 self.model
)。
在有些情况下,您可能希望执行一些与模型相关的操作,但又不能返回实例或 QuerySets
。Django 文档指出所有非模型类实例上的方法都应在管理器中,但还有一个可能性就是使用 Python 类和静态方法。
如下所示是一个实用方法的简单示例,这个方法与 Format
类有关,与具体某个实例无关。
# Return the canonical name for a format extension based on some # common values that might be seen "in the wild" def check_extension(extension): if extension == 'text' or extension == 'txt' or extension == '.csv': return 'text' if extension.lower() == 'epub' or extension == 'zip': return 'epub' if 'htm' in extension: return 'html' raise Exception('Did not get known extension') |
上述代码并不接受或返回 Format
类的实例,所以把它作为实例方法并不恰当。也可以把它添加给 FormatManager
,但是由于它根本不能访问数据库,所以把它放在那里也不太合适。
一个解决办法就是把它添加给 Format
类并用 @staticmethod
修饰符把它声明为一个静态方法,如下所示。
class Format(models.Model): type = models.CharField(choices=( ('Text file', 'text'), ('ePub ebook', 'epub'), ('HTML file', 'html')), max_length=10) @staticmethod def check_extension(extension): if extension == 'text' or extension == 'txt' or extension == '.csv': return 'text' if extension.lower() == 'epub' or extension == 'zip': return 'epub' if 'htm' in extension: return 'html' raise Exception('Did not get known extension') def __unicode__(self): return self.type |
这个方法可被称为 Format.check_extension(extension)
,它既不需要 Format
实例,也不需要创建一个管理器。
Python 还提供了 @classmethod
修饰符,它能基于类生成方法,并且第一个参数就是类本身。如果想要在不实例化的情况下执行类对象本身上的某种自查(introspection),这一点会很有用。
|
|
在 2009 年 4 月发布的 V1.1 中,Django 的 ORM 包括了很多功能强大的查询方法,这些方法所提供的功能以前只有通过原始的 SQL 才可用。对于对 SQL 心存戒心的 Python 开发人员 — 以及任何希望他/她的 Django 应用程序能跨多个数据库引擎可用的人而言,这的确是个福音。
在当今根据需求而不断调整而成的应用程序中,通常不仅需要能依常规的 字段,如字母顺序或创建日期,来对项目进行排序,还需要按其他某种动态数据对项目进行排序。例如,在示例应用程序中,您可能需要按受欢迎程度对文档进行排 序,也就是基于每个文档的注释的数量进行排列。在 Django V1.1 发布之前,往往需要编写一些定制 SQL 代码,才能实现这个功能,结果,所创建的存储过程不可移植,或 — 最糟的 — 编写的面向对象的查询十分低效。另一种方法就是定义一个 dummy 数据库字段,其中包含用来计数的理想值(例如,注释行的数量)并通过覆盖文档的 save()
方法手动更新它。
Django 聚合排除了所有上述需求。现在仅用一个 QuerySet
方法(annotate()
)就可以实现对文档按注释的数量进行排序。清单 11 提供了一个示例。
from django.db.models import Count # Create some sample Documents unpopular = Document.objects.create(name='Unpopular document', format=format_html) popular = Document.objects.create(name='Popular document', format=format_html) # Assign more comments to "popular" than to "unpopular" for i in range(0,10): Comment.objects.create(document=popular) for i in range(0,5): Comment.objects.create(document=unpopular) # If we return results in the order they were created (id order, by default), we get # the "unpopular" document first. In [1]: Document.objects.all() Out[1]: [<Document: Unpopular document>, <Document: Popular document>] # If we instead annotate the result set with the total number of # comments on each Document and then order by that computed value, we # get the "popular" document first. In [2]: Document.objects.annotate(Count('comments')).order_by('-comments__count') Out[2]: [<Document: Popular document>, <Document: Unpopular document>] |
annotate() QuerySet
方法自身并不执行任何聚合。相反,它可以指示 Django 将所传递的表达式的值指定给结果集中的一个伪列。默认情况下,这个列的名称将是所提供的字段名(这里就是 Comment.document.related_name()
的值)。上述代码调用了 django.db.models.Count
,这只是聚合库中诸多可用简单数学函数中的一个。(要获得完整清单,参见 参考资料 。)
Document.objects.annotate(Count('comments'))
的结果是一个 QuerySet
,并向其添加了一个新属性 — comments__count
—。如果想覆盖那个默认名称,可以将新的名称做为一个关键字参数传递。
Document.objects.annotate(popularity=Count('comments')) |
现在,这个中间 QuerySet
包含了与每个文件相关联的所有注释的计数值,我们就可以按这个字段进行排序了。由于我们希望把拥有最多注释的文档显示排在第一个,所以我们采用了降序,比如 .order_by('-comments__count')
。
使用聚合不仅减少编写代码的量,而且还可以确保这些操作能被快速地完成,因为它们是依靠数据库引擎来完成这些数学计算的。比起先通过 ORM 抽取所有相关数据,然后再手动地对结果集进行计算,使用聚合的处理过程显然更高效。
新的聚合库不仅可以返回更复杂的结果集,还可以返回直接从数据库抽取出的非 QuerySet
结果。例如,要获得数据库中所有文档的注释数量的平均值,可以使用下面的代码:
In [1]: from django.db.models import Avg In [2]: Document.objects.aggregate(Avg('comments')) Out[2]: {'comments__avg': 8.0} |
可以将聚合应用到过滤过的或未经过滤的查询,由 annotate
生成的列也可以像普通字段那样被过滤。还可以将聚合方法跨连接应用。例如,可以基于注释的级别(比如在一个 Slashdot 风格的站点中)聚合文档。要获得更多关于聚合的信息,请参见 参考资料 。
|
|
对于对象关系型映射器的一种指责是它们抽象掉了太多数据库引擎,以至于用它们编写可伸缩的高效应用程序不太可能。对于某些类型的应用程序 — 拥有数百万的访问量和高度关联的模型的应用程序,这个结论常常是正确的。
绝 大多数应用程序从未有过这么大的访问量,也达不到那么复杂的水平。然而,ORM 是被设计用来快速启动项目并帮助开发人员在对 SQL 没有深入了解的情况下就可以开发基于数据库的项目。虽然您的 Web 站点越来越大、越来越受欢迎,如我们在本文第一部分所描述的那样,您仍然需要对其性能进行审核。最终,您可能需要用原始 SQL 或存储过程来替代基于 ORM 的代码。
所幸,像 Django ORM 这样的简单易用的 ORM 的功能在不断发展。Django V1.1 聚合库是一个很大的进步,在提供熟悉的面向对象语法的同时,还提供了高效的查询生成。要想获得更多的灵活性,Python 开发人员还应该关注一下 SQLAlchemy,特别是对于那些不依赖于 Django 的 Python Web 应用程序。
学习
- Django Query API :查阅这个 Django Query API 的完整指南。
- 了解 Django V1.1 有何新特性。
- 查阅受 Django 支持的 数据库引擎 的完整列表。自 Django V1.0 发布之后,开始可以定义新的外部引擎。
- 访问 Django V1.1 聚会库 获得完整参考。
- 要收听针对软件开发人员的有趣访谈和讨论,请查看 developerWorks podcasts 。
- 随时关注 developerWorks 技术活动 和网络广播 。
- 查阅最近将在全球举办的面向 IBM 开源开发人员的研讨会、交易展览、网络广播和其他 活动 。
- 访问 developerWorks 开源专区 获得丰富的 how-to 信息、工具和项目更新,帮助您用开源技术进行开发,并与 IBM 产品结合使用。
- 通过免费的 developerWorks 演示中心 观看并了解 IBM 及开源技术和产品功能。
获得产品和技术
- 了解可与 Django ORM 媲美的一个功能强大的替代:SQLAlchemy 。此工具适合于大型的应用程序。
- SQLite V3 :自 V2.5,Python 开始捆绑对 SQLite V3 的支持,无需其他驱动程序。先前的 Python 版本需要您直接下载 pysqlite 。
- 使用可直接下载获得的 IBM 试用软件 改进您的下一个开源开发项目。
- 下载 IBM 产品评估版 或 IBM SOA Sandbox for Reuse ,尝试使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。
讨论
- 参与 developerWorks blogs 并加入 developerWorks 社区。
Liza Daly 是一名专门开发出版业应用程序的软件工程师。她曾经担任 Oxford University Press、O'Reilly Media 以及其他出版商的大型在线产品的主要开发人员。目前她是一名独立顾问,并且是 Threepress 的创建者,这是一个开发电子书应用程序的开源项目。 |