django模型_更好的Django模型

微生善
2023-12-01

django模型

在Django中,大多数与数据库的交互都是通过其对象关系映射器(Object-Relational Mapper,ORM)进行的,该功能是Django与其他最新的Web框架(如Rails)共享的功能。 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支持的数据库列表,请参阅参考资料

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

清单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)

接下来,使用更改后的模型来创建一些分配了Format实例的示例文档。

清单6.创建一些具有指定格式的文档
# First create a series of Format objects \
	and save # them to the database
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)

在此示例中,查询的含义非常清楚,但是在成熟的应用程序中,您可能需要对结果集施加更多限制。 您可能希望将列表限制为仅标记为公开的文档或不超过30天的文档。 如果必须从应用程序中的多个位置调用此查询,则使所有过滤子句保持同步可能会成为真正的维护难题和错误源。

这是定制经理可以提供帮助的地方。 自定义管理器提供了定义无限数量的“固定”查询的功能-与内置管理器方法类似,例如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.提供每种格式类型方法的Custom Manager类
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的子类get_by_format ,所有默认方法,例如all() ,都可以使用。 是否包括默认管理器以及自定义管理器是个人喜好。
  • 也可以将新经理直接分配给objects 。 唯一的缺点是如果您随后要覆盖初始QuerySet本身。 这样,您的新objects将具有其他开发人员可能无法预期的意外行为。
  • 您需要在定义模型类之前在models.py中定义管理器类,否则该类将对Django不可用。 这类似于对ForeignKey类引用的限制。
  • 我可以简单地用一个带有参数的方法来实现DocumentManager ,例如with_format(format_name) 。 通常,我更喜欢具有冗长的方法名称但不带参数的管理器方法。
  • 可以为一个类分配的自定义管理器的数量没有技术限制,但是您可能不需要一个或两个以上。

使用新的管理器方法非常简单。

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装饰器,该装饰器在类上生成将类本身作为第一个参数的方法。 如果您想对类对象本身进行一些自省而不实例化,这可能会很有用。

Django V1.1中的汇总查询

在V1.1中,Django的ORM包含强大的查询方法,这些查询方法提供以前只能通过原始SQL获得的功能。 对于Python开发人员而言,他们对SQL持谨慎态度,对于希望保持Django应用程序在多个数据库引擎之间都可使用的人来说,这真是福音。

在当今的社交媒体应用程序中,不仅通过严格的字段(如字母顺序或创建日期)来排序项目,而且还通过动态数据来排序项目,这是非常普遍的。 例如,在示例应用程序中,您可能希望根据在每个文档上发表的评论的数量按受欢迎程度列出文档。 在Django V1.1之前,您只能通过编写一些自定义SQL代码,创建不可移植的存储过程或(最糟糕的是)编写一些效率低下的面向对象查询来执行此操作。 另一种方法是定义一个虚拟数据库字段,该字段将包含想要的值(例如,Comment行的数量)进行计数,并通过覆盖文档的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()的值Comment.document.related_name() ,该字段名称位于聚合方法的名称之前。 该代码调用django.db.models.Count ,这是聚合库中可用的简单数学函数之一。 (有关完整列表,请参阅参考资料 。)

Document.objects.annotate(Count('comments'))是一个QuerySet在该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风格的网站中一样,基于分配给注释的评级来聚合文档。 有关聚合的更多信息,请参阅参考资料

结论

对对象关系映射器的一种指控是,它们提取了太多的数据库引擎,以致于无法使用它们编写高效,可扩展的应用程序。 对于某些类型的应用程序-具有数百万次访问权限和高度相关的模型的应用程序-这种主张通常是正确的。

绝大多数Web应用程序从来没有达到如此庞大的受众,也没有达到如此复杂的水平。 ORM旨在快速启动项目,并帮助开发人员跳入数据库驱动的项目,而无需对SQL有深入的了解。 随着您的网站变得越来越大和越来越流行,您肯定需要按照本文第一部分中所述审核性能。 最终,您可能需要开始用原始SQL或存储过程替换ORM驱动的代码。

令人高兴的是,像Django这样的易于使用的ORM的功能正在不断发展。 Django V1.1的聚合库是向前迈出的重要一步,它可以高效地生成查询,同时仍提供熟悉的面向对象语法。 为了获得更大的灵活性,Python开发人员还应该考虑SQLAlchemy,尤其是对于不依赖Django的Python Web应用程序。


翻译自: https://www.ibm.com/developerworks/opensource/library/os-django-models/index.html

django模型

 类似资料: