您的位置:首页 > 编程语言 > Go语言

Django 模型高级部分及聚集查询

2010-02-22 14:27 337 查看
Liza Daly, 软件工程师和所有人, Threepress Consulting Inc.

2009 年 6 月 18 日

花 5 分钟学习 wiki,然后进入实际的应用程序编程,Django 程序员很容易创建让人迷惑、难于维护或低效的模型类。在本文中,了解如何避免一些常见的查询错误、如何使用模型管理器来封装复杂查询以及如何充分利用 Django V1.1 强大的聚集特性。
在 Django 内,与数据库的大多数交互都通过对象关系映射器(ORM),这个特性是 Django 与其他最新的 Web 框架(比如 Rails)所共有的。ORM 越来越受开发人员欢迎,因为 ORM 能够自动化与数据库的很多常见交互,而且会使用为人熟知的面向对象方式,而不是 SQL 语句。

Django 程序员可能会选择绕过原生 ORM,而选择流行的
SQLAlchemy
包,虽然
SQLAlchemy
十分强大,但是却较难使用,而且需要更多的代码行。虽然有些 Django 应用程序是使用
SQLAlchemy
而非原生 ORM 开发的,但是 Django 最吸引人的一些特性,比如其自动生成的管理界面,都要求使用 ORM。


常用缩写词

API:应用程序编程接口(Application programming interface)

HTML:超文本标记语言(Hypertext Markup Language)

RDBMS:关系数据库管理系统(Relational database management system)

SQL:结构化查询语言(Structured Query Language)

本文着重阐释了 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 支持的数据库的完整列表,请参见 参考资料

避免 ORM 查询生成中的常见陷阱

Django 的设计支持敏捷开发的风格,因此能快速进行原型化和实验。在开始阶段,最好不要过于担心性能,而是要关注可读性和实现的简便性。

有时,发现性能问题并不需要太长时间。通常在初次用实际数据试用应用程序时,很容易发现性能问题。有时,若只包含几个测试的测试套件的执行时间超过了 5 分钟的界限,这就表明存在性能问题。有时,应用程序运行过慢,也表示性能问题的存在。所幸的是,现在已经有了一些很容易识别的模式,这些模式亦很容易修复。清单 1(应用程序的 models.py 文件)和 清单 2 给出了一个很常见的例子。

清单 1. 示例应用程序的基本模型:models.py

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()


有关代码示例

Django 提供了一种很方便的捷径来在独立的代码内设置一个工作环境:运行
python manage.py shell
。本文中所有的代码示例都预先假设工作环境均是以这种方式调用的。

在 Django lingo 内,本文中作了如下假设:

Django 项目的名称是 better_models。

better_models 项目包含一个应用程序,名为 examples。

示例应用程序所模拟的是一个类似 blog 的基本文档系统,这些文档可能没有任何注释,也可能有注释。

清单 2 显示了如何以一种低效的方式访问清单 1 中所设置的那些模型。

清单 2. 非常慢地访问那些模型

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', ...)

(所生成的实际查询语法将会随数据库引擎而变化。)

清单 3. 用来获得条目列表的快速查询

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 中的代码的一种可能方式,这种方式是程序员很有可能选择采用的,因为他们没有意识到这是个陷阱。

清单 4. 一个会降低数据库使用效率的常见模型

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
,这个模型描述了此系统内文档的格式。下面是一个示例。

清单 5. 为示例添加一个模型

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)


数据库更新的最佳实践

任何时候在向 models.py 中添加表或列时,都需要重新同步相关数据库。以下是数据库更新的几个最佳实践:

在开发的早期,只使用内存数据库,如 sqlite3,并采用数据库的固有功能来自动加载示例内容。内存数据库对单一用户速度很快,并能大量减少在传统的 RDBMS(如 MySQL)中丢弃或重建表时的等待时间。

采用测试驱动的开发方式。Django 的测试框架每次都会从头重建数据库,所以表总是最新的。将这个功能与 sqlite3 内存数据库相结合就可以使测试更快。

试试众多管理数据库同步的 Django 附加软件。虽然我已经有很多
django-evolution
程序包的经验,但除此之外还有很多其他的程序包值得一试。关于
django-evolution
的更多信息,请参见 参考资料

若在开发或测试中选择了使用 sqlite3,请确保对产品数据库执行最终的集成测试。在大多数情况下,Django 的 ORM 可以帮助消除 RDBMS 引擎间的差异,但我们并不保证所有的行为都是一样的。

接下来,用这个变更过的模型创建一些示例文档,这些文档均已分配了
Format
实例。

清单 6. 创建具有指定格式的文档

# 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
示例。

清单 7. 能为每个格式类型提供方法的定制管理器类

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
管理器只是 Django
models.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)的一贯精神。

覆盖定制管理器所返回的初始 QuerySet

另一个适用于管理器类的编码模式可以不涉及任何定制方法。例如,您不必去定义一个只返回 HTML 格式文档的新方法,相反,您可以定义一个完全 运行在该限制集之上的定制管理器,如下面的示例所示。

清单 8. 针对 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()
方法,然后才能在该结果之上再应用其他的查询方法,如下所示。

清单 9. 使用这个定制格式管理器

# 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
修饰符把它声明为一个静态方法,如下所示。

清单 10. 作为模型类上的静态方法添加一个实用函数

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),这一点会很有用。






Django V1.1 中的聚集查询

在 2009 年 4 月发布的 V1.1 中,Django 的 ORM 包括了很多功能强大的查询方法,这些方法所提供的功能以前只有通过原始的 SQL 才可用。对于对 SQL 心存戒心的 Python 开发人员 — 以及任何希望他/她的 Django 应用程序能跨多个数据库引擎可用的人而言,这的确是个福音。

在当今根据需求而不断调整而成的应用程序中,通常不仅需要能依常规的字段,如字母顺序或创建日期,来对项目进行排序,还需要按其他某种动态数据对项目进行排序。例如,在示例应用程序中,您可能需要按受欢迎程度对文档进行排序,也就是基于每个文档的注释的数量进行排列。在 Django V1.1 发布之前,往往需要编写一些定制 SQL 代码,才能实现这个功能,结果,所创建的存储过程不可移植,或 — 最糟的 — 编写的面向对象的查询十分低效。另一种方法就是定义一个 dummy 数据库字段,其中包含用来计数的理想值(例如,注释行的数量)并通过覆盖文档的
save()
方法手动更新它。

Django 聚合排除了所有上述需求。现在仅用一个
QuerySet
方法(
annotate()
)就可以实现对文档按注释的数量进行排序。清单 11 提供了一个示例。

清单 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 抽取所有相关数据,然后再手动地对结果集进行计算,使用聚合的处理过程显然更高效。

Django V1.1 中的其他聚合类型

新的聚合库不仅可以返回更复杂的结果集,还可以返回直接从数据库抽取出的非
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 应用程序。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: