Django Models

作者:Reuven Lerner

在我的上一篇文章中,我继续研究了 Django Web 框架,展示了如何创建和修改模型。正如您所看到的,Django 希望您使用 Python 代码来描述您的模型。然后,模型描述被转换为 SQL,并与可能存在的模型的任何先前版本进行比较。然后,Django 创建一个“迁移”,这是一个描述如何从一个版本的模型定义移动到下一个版本的文件。迁移是一个非常棒的工具,它允许开发人员以定义的块向前(和向后)移动他们的数据库。迁移使与他人协作和升级现有应用程序变得更加容易。

问题是,迁移与您想要运行的日常应用程序几乎没有关系。它们对于创建和维护应用程序的模型很有用,但在您的应用程序中,您将需要使用模型本身。

因此,在本文中,我将介绍 Django 的 ORM(对象关系映射器)。您将看到 Django 如何允许您执行应用程序中所需和期望的所有传统 CRUD(创建-读取-更新-删除)操作,以便您可以使用数据库来驱动您的 Web 应用程序。

为了本文的目的,我将使用我在上个月的文章中创建的“atfapp”项目中的“atfapp”应用程序。预约日历的模型在 atfapp/models.py 中定义如下


class Appointment(models.Model):
    starts_at = models.DateTimeField()
    ends_at = models.DateTimeField()
    meeting_with = models.TextField()
    notes = models.TextField()
    minutes = models.TextField()
    def __str__(self):
        return "{} - {}: Meeting with {} 
           ↪({})".format(self.starts_at,
                                self.ends_at,
                                self.meeting_with,
                                self.notes)

如您所见,上面的模型有四个字段,指示会议开始时间、结束时间、与谁会面以及会议开始前的备注。前两个字段被定义为 Django 中的 DateTime 字段,它被转换为数据库中的 SQL TIMESTAMP 时间。

创建一个新的预约

开始使用 Django 模型的最简单和最好的方法是使用 Django 交互式 shell——即 Django 环境中的 Python 交互式 shell。在您的项目中,只需键入


django-admin shell

您将被置于交互式 Python 解释器中——或者如果您安装了 IPython,则在 IPython 中。此时,您可以开始与您的项目及其各种应用程序进行交互。为了使用您的 Appointment 对象,您需要导入它。因此,我做的第一件事是写


from atfapp.models import Appointment

这告诉 Django 我想进入“atfapp”包——由于 Django 应用程序是 Python 包,这意味着“atfapp”子目录——然后从 models.py 模块导入“Appointment”类。

要记住的重要事情是,Django 模型只是一个 Python 类。ORM 魔力发生是因为您的类继承自 models.Model,以及您用于定义数据库中列的类属性。您越了解 Python 对象,您就会越熟悉 Django 模型。

如果您想创建一个新的预约对象,您可以像通常对 Python 对象所做的那样做


>>> a = Appointment()

果然,如果您询问“a”本身,它会告诉您


>>> type(a)
atfapp.models.Appointment

您可能尝试做的第一件事是将您的新预约保存到数据库。您可以使用“save”方法来做到这一点


>>> a.save()

但是,如果您尝试这样做,您会很快发现您会得到一个异常——一个 IntegrityError,异常的名称是这样,它看起来像这样


    IntegrityError: NOT NULL constraint failed:
atfapp_appointment.starts_at

在这里,Django 混合了 Python 和 SQL 来告诉您哪里出错了。您定义的模型需要一个 starts_at 列,该列被转换为数据库中的 NOT NULL 约束。因为您没有为您的预约对象定义 starts_at 值,所以您的数据无法存储在数据库中。

实际上,如果您只是获取对象的打印表示,您会看到情况就是这样


>>> a
<Appointment: None - None: Meeting with  ()>

上面的输出来自 __str__ 实例方法,您可以看到该方法在上面定义。新对象对于 starts_atends_atmeeting_with 具有 None 值。请注意,对于 meeting_with 和 notes,您没有 None 值。那是因为前者被定义为 DateTimeField,而后者被定义为 TextField

默认情况下,Django 模型被定义为它们在数据库中的列是 NOT NULL。我认为这是一件好事。NULL 值会导致各种问题,最好明确地命名它们。如果您希望字段允许 NULL 值,您需要传递 null=True 选项,如


starts_at = models.DateTimeField(null=True)

但是,我对开始时间和结束时间的 NULL 值不感兴趣。因此,如果您想存储您的预约,您需要提供一些值。您可以通过分配给相关字段来做到这一点


>>> from datetime import datetime
>>> a.starts_at = datetime.now()
>>> a.ends_at = datetime(2015, 4, 28, 6,43)

一旦您完成此操作,您就可以保存它


>>> a.save()

创建模型的另一种方法是在创建时传递参数


>>> b = Appointment(starts_at=datetime.now(),
        ends_at=datetime.now(),
        meeting_with='VIP', notes='Do not be late')
读取您的预约

现在您有两个预约,让我们尝试将它们读回来,看看您可以对它们做什么。对您在数据库中创建的对象的访问是通过“objects”属性完成的,在 Django 中称为“管理器”。objects 上的“all”方法会将您的所有对象返回给您


>>> len(Appointment.objects.all())
2

您可以使用您的列名作为每个对象上的属性


>>> for a in Appointment.objects.all():
    print "{}: {}".format(a.starts_at, a.notes)

2015-04-28 05:59:21.316011+00:00:
2015-04-28 07:14:07.872681+00:00: Do not be late

Appointment.objects.all() 返回一个在 Django 中称为 QuerySet 的对象。正如您在上面看到的,QuerySet 是可迭代的。而且,如果您在它上面调用 len(),或者即使您要求它的表示(例如,在 Python shell 中),您也会看到它显示为列表。因此,您可能会认为您在这里谈论的是列表,这可能意味着使用大量的内存。

但是,Django 开发人员在这方面非常聪明,QuerySet 实际上是一个迭代器——这意味着它会尽力不一次将大量记录检索到内存中,而是使用“延迟加载”来等待直到真正需要信息时。实际上,仅创建 QuerySet 对数据库没有影响;只有当您实际尝试使用 QuerySet 的对象时,查询才会运行。

能够取回所有记录很好,但更实用和更重要的是能够选择单个记录,然后对它们进行排序。

为此,您可以将“filter”方法应用于您的管理器


>>> for a in Appointment.objects.filter(meeting_with='VIP'):
        print a.starts_at

现在您知道与 VIP 的预约何时开始。但是,如果您想搜索一系列事物,例如自 2015 年 1 月 1 日以来的所有预约,该怎么办?

Django 提供了许多执行此类比较的特殊方法。对于您在模型中定义的每个字段,Django 定义了 __lt__lte__gt__gte 方法,您可以使用这些方法来过滤查询集。例如,要查找自 2015 年 1 月 1 日以来的所有预约,您可以说


>>> Appointment.objects.filter(starts_at__gte=datetime(2015,1,1))

如您所见,由于您有一个 starts_at 字段名称,Django 接受一个 starts_at__gte 关键字,该关键字被转换为适当的运算符。如果您传递多个关键字,Django 将在底层 SQL 中将它们与 AND 组合起来。

QuerySet 也可以以更复杂的方式进行过滤。例如,您可能想将字段与 NULL 进行比较。在这种情况下,您不能在 SQL 中使用 = 运算符,而必须使用 IS 运算符。因此,您可能想使用类似这样的东西


>>> Appointment.objects.filter(notes__exact=None)

请注意,__exact 知道根据是否给定 None(转换为 SQL 的 NULL)或其他值来应用适当的比较。

您可以询问字段是否包含字符串


>>> Appointment.objects.filter(meeting_with__contains='VIP')

如果您不关心大小写敏感性,您可以使用 icontains 代替


>>> Appointment.objects.filter(meeting_with__icontains='VIP')

不要犯在您要搜索的字符串的前后添加 % 字符的错误。Django 会为您做到这一点,将 icontains 过滤器参数转换为 SQL ILIKE 查询。

您甚至可以在 QuerySet 上使用切片表示法,以获得 OFFSETLIMIT 的效果。但是,重要的是要记住,在许多数据库中,使用 OFFSETLIMIT 可能会导致性能问题。

Django 默认情况下定义了一个“id”字段,该字段表示存储的每个记录的数字主键。如果您知道 ID,您可以基于该 ID 进行搜索,使用 get 方法


>>> Appointment.objects.get(pk=2)

如果存在具有此主键的记录,则会返回该记录。否则,您将收到一个 DoesNotExist 异常。

最后,您还可以使用 order_by 方法对返回的记录进行排序。例如


>>> Appointment.objects.filter
↪(starts_at__gte=datetime(2015,1,1)).order_by('id')

如果您想反转排序,该怎么办?只需在列名前面加上一个 - 符号


>>> Appointment.objects.filter
↪(starts_at__gte=datetime(2015,1,1)).order_by('-id')

如果您想按列的组合(升序或降序)排序,您可以将多个参数传递给 order_by

Django 的 QuerySet 的一个很好的特性是,每次调用 filterorder_by 都会返回一个新的 QuerySet 对象。这样,您可以一次性或增量地调用 filter。此外,您可以创建一个 QuerySet,然后将其用作进一步 QuerySet 的基础,每个 QuerySet 将独立执行(必要时)其查询。

创建动态查询的一个大问题是 SQL 注入——用户可以通过操纵,强制执行他们自己的 SQL,而不是您预期的 SQL。使用 Django 的 QuerySet 基本上消除了这种威胁,因为它会检查并适当地引用它接收到的任何参数,然后再将它们的值传递给 SQL。真的,现在没有理由让 SQL 注入成为问题——在尝试绕过 Django 的安全措施之前,请三思(或三思)。

更新和删除

更新 Django 模型的字段非常容易。修改一个或多个属性,就像您对任何其他 Python 对象所做的那样,然后保存更新的对象。在这里,我从数据库中加载第一个(未排序的)记录,然后再更新它


>>> a = Appointment.objects.first()
>>> a.notes = 'blah blah'
>>> a.save()

请注意,如果您更改了“id”属性然后保存您的对象,您最终将在数据库中创建一个新记录!当然,您不应该更改任何事件中对象的“id”,但现在您也可以认为自己被警告了。

要删除对象,只需在实例上使用 delete 方法。例如


>>> len(Appointment.objects.all())
2

>>> a = Appointment.objects.first()
>>> a.delete()

>>> len(Appointment.objects.all())
>>> 1

正如您在上面的示例中看到的,我发现我的数据库中总共有两条记录。我加载第一条记录,然后删除它。在调用之后——无需保存或以其他方式批准此操作——您可以看到该记录已被删除。

结论

在我的下一篇文章中,我将完成关于 Django 的系列文章,讨论您可以在不同模型之间建立的不同类型的关系。我将研究一对一、一对多和多对多关系,以及 Django 如何让您表达和处理每种关系。

资源

Django 的主要站点是 http://DjangoProject.com,它有大量的优秀文档,包括教程。有几页专门介绍 QuerySet 以及如何创建和操作它们。

关于 Python(Django 在其中实现)的信息,请访问 https://pythonlang.cn

Reuven M. Lerner,一位资深的 Web 开发人员,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并发布了面向程序员的免费每周新闻通讯,网址为 http://lerner.co.il/newsletter。Reuven 的 Twitter 账号是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论