Django 模型和迁移
在我的前两篇文章中,我研究了用 Python 编写的 Django Web 应用程序框架。Django 的文档将其描述为一个 MTV 框架,其中 MTV 代表模型(Model)、模板(Template)和视图(View)。
当一个请求进入 Django 应用程序时,应用程序的 URL 模式决定将调用哪个视图方法。正如我在之前的文章中提到的,视图方法可以直接将内容返回给用户,或者发送模板的内容。模板通常不仅包含 HTML,还包含 Django 特有的指令,这些指令允许您传递变量值、执行循环和有条件地显示文本。
您可以使用视图和模板创建许多有趣的 Web 应用程序。然而,大多数 Web 应用程序也使用数据库,并且在许多情况下,这意味着关系数据库。实际上,很少有 Web 应用程序不使用某种数据库。
多年来,Web 应用程序通常直接与数据库对话,通过文本字符串发送 SQL。因此,您会说类似这样的话:
s = "SELECT first_name, last_name FROM Users where id = 1"
然后,您将通过数据库客户端库将该 SQL 发送到服务器,并使用该库检索结果。虽然这种方法确实允许您直接利用 SQL 的强大功能,但这意味着您的应用程序代码现在包含带有另一种语言的文本字符串。这种(例如)Python 和 SQL 的混合可能会变得难以维护和使用。此外,在 Python 中,您习惯于使用对象、属性和方法。为什么您不能以这种方式访问数据库呢?
当然,答案是您不能,因为关系数据库最终确实需要接收 SQL 才能正常运行。因此,许多程序使用 ORM(对象关系映射器),它将方法调用和对象属性转换为 SQL。在 Python 世界中,有一个成熟的 ORM 称为 SQLAlchemy。然而,Django 选择使用自己的 ORM,您可以使用它来定义数据库表,以及在这些表中插入、更新和检索信息。
因此,在本文中,我将介绍如何在 Django 中创建模型,如何基于这些模型定义创建和应用迁移,以及如何从 Django 应用程序中与模型进行交互。
模型Django 世界中的“模型”是一个 Python 类,它代表数据库中的一个表。如果您正在创建一个预约日历,您的数据库可能至少有两个不同的表:人员和预约。为了在 Django 中表示这些,您需要创建两个 Python 类:Person 和 Appointment。这些模型中的每一个都在应用程序内的 models.py 文件中定义。
在这里,有必要指出,模型是特定于某个 Django 应用程序的。每个 Django 项目包含一个或多个应用程序,并且假定您可以在不同的项目中重用应用程序。
在我为本文创建的 Django 项目(“atfproject”)中,我有一个单一的应用程序(“atfapp”)。因此,我可以在 atfproject/atfapp/models.py 中定义我的模型类。默认情况下,该文件包含一行:
from django.db import models
鉴于创建预约日历的示例,让我们首先定义您的 Appointment 模型:
from django.db import models
class Appointment(models.Model):
starts_at = models.DateTimeField()
ends_at = models.DateTimeField()
meeting_with = models.TextField()
notes = models.TextField()
def __str__(self):
return "{} - {}: Meeting with {} ({})".format(self.starts_at,
self.ends_at,
self.meeting_with,
self.notes)
请注意,在 Django 模型中,您使用称为描述符的 Python 对象将列定义为类属性。描述符允许您使用属性(例如 appointment.starts_at),但对于在后台触发的方法。在数据库模型的情况下,Django 使用描述符来检索、保存、更新和删除数据库中的数据。
上面代码中唯一实际的实例方法是 __str__,每个 Python 对象都可以使用它来定义它如何转换为字符串。Django 使用 __str__ 方法来呈现您的模型。
Django 提供了大量的字段类型,您可以在模型中使用,这些字段类型(在很大程度上)与大多数流行数据库中可用的列类型相匹配。例如,上述模型使用了两个 DateTimeFields 和两个 TextFields。您可以想象,这些字段映射到 SQL 中的 DATETIME 和 TEXT 列。这些字段定义不仅决定了数据库中定义的列类型,还决定了 Django 的管理界面和表单允许用户输入数据的方式。除了 TextField 之外,您还可以使用 BooleanFields、EmailFields(用于电子邮件地址)、FileFields(用于上传文件),甚至 GenericIPAddressField 等。
除了选择适合您数据的字段类型之外,您还可以传递一个或多个选项来修改字段的行为。例如,DateField 和 DateTimeField 允许您传递“auto_now”关键字参数。如果传递并设置为 True,Django 将自动将字段设置为存储新记录时的当前时间。这不一定是您始终想要的行为,但它经常需要,以至于 Django 提供了它。对于其他字段也是如此——它们提供了您可能并不总是需要的选项,但这些选项确实可以派上用场。
迁移所以,现在您有了一个模型!您如何开始使用它呢?好吧,首先您需要以某种方式将您的模型转换为数据库可以使用的 SQL。这意味着,在继续进行之前,您需要告诉 Django 您正在使用哪个数据库。这在您的项目的配置文件中完成;在我的情况下,这将是 atfproject/atfproject/settings.py。该文件定义了 Django 中使用的许多变量。其中之一是 DATABASES,这是一个字典,用于定义项目中使用的数据库。(是的,可以使用多个数据库,尽管我不确定这通常是否是一个好主意。)
默认情况下,DATABASES 的定义是:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
换句话说,Django 在开箱即用的情况下,被定义为使用 SQLite。SQLite 对于大多数用途来说是一个很棒的数据库,但对于将为公众服务的真正的、生产就绪的数据库应用程序来说,它的功能严重不足。对于这种情况,您需要更强大的东西,例如我最喜欢的数据库 PostgreSQL。然而,对于这里的小实验的目的,您可以使用 SQLite。
SQLite 的许多优点之一是它为每个数据库使用一个文件;如果文件存在,SQLite 会从那里读取数据。如果文件尚不存在,则在首次使用时创建该文件。因此,通过使用 SQLite,您可以避免任何配置。
但是,您仍然需要以某种方式将您的 Python 代码转换为 SQLite 可以使用的 SQL 定义。这是通过“迁移”完成的。
现在,如果您来自 Ruby on Rails 的世界,您会熟悉迁移的想法——它们描述了对数据库所做的更改,以便您可以轻松地从旧版本的数据库移动到新版本。我记得没有迁移的日子,它们明显没有那么令人愉快——它们的发明确实使 Web 开发变得更容易。
迁移是 Django 世界的后来者。长期以来一直有外部库,例如 South,但 Django 本身中的迁移是相对较新的。Rails 用户可能会惊讶地发现,在 Django 中,开发人员不会直接创建迁移。相反,您告诉 Django 检查您的模型定义,将这些定义与数据库的当前状态进行比较,然后生成适当的迁移。
鉴于我刚刚创建了一个模型,我回到项目的根目录,然后执行:
django-admin.py makemigrations
此命令在项目的根目录中执行,它告诉 Django 查看“atfapp”应用程序,将其模型与数据库进行比较,然后生成迁移。
现在,如果您在此处遇到错误(我经常遇到!),您应该仔细检查以确保您的应用程序已添加到项目中。仅仅将您的应用程序放在 Django 项目的目录中是不够的。您还必须将其添加到 INSTALLED_APPS,这是项目 settings.py 中的一个元组。例如,在我的情况下,定义如下所示:
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'atfapp'
)
我在我的系统上运行 makemigrations
的输出如下所示:
Migrations for 'atfapp':
0001_initial.py:
- Create model Appointment
换句话说,Django 现在已经描述了数据库的当前状态(其中“Appointment”不存在)与最终状态(其中将有一个“Appointment”表)之间的差异。如果您有兴趣查看此迁移的外观,您可以随时查看 atfapp/migrations 目录,您将在其中看到 Python 代码。
我没有说过迁移将用 SQL 描述所需的数据库更新吗?是的,但描述最初是用 Python 编写的。至少在理论上,如果您想迁移到不同的数据库服务器,这将允许您这样做。
现在您有了迁移,是时候应用它们了。在项目的根目录中,我现在输入:
django-admin.py migrate
然后看到:
Operations to perform:
Apply all migrations: admin, contenttypes, auth, atfapp, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying atfapp.0001_initial... OK
Applying sessions.0001_initial... OK
上面显示“atfapp”的初始迁移已运行。但是所有这些其他迁移来自哪里呢?答案很简单。Django 的用户模型和其他内置模型也使用迁移进行描述,因此,如果我的 Django 项目中尚未发生这种情况,它们将与我的迁移一起应用。
您可能已经注意到,每个迁移都给出了一个编号。这允许 Django 跟踪迁移的历史记录,并在必要时应用多个迁移。您可以创建一个迁移,然后创建一个新迁移,然后将它们一起应用(如果您想保持更改分开)。
或者,也许更实际的是,您可以与其他人一起在一个项目上工作,每个人都在更新数据库。他们每个人都可以创建自己的迁移,并将它们提交到共享的 Git 存储库中。如果您从 Git 检索最新更改,您将获得来自同事的所有迁移,然后可以将它们应用于您的应用程序。
进一步迁移假设您修改了您的模型。您如何创建和应用新的迁移?答案实际上相当简单。修改模型并要求 Django 创建适当的迁移。然后,您可以运行新创建的迁移。
因此,让我们向 Appointment 模型添加一个新字段“minutes”,以记录会议期间发生的事情。我在模型中添加了一行,这样文件现在看起来像这样:
from django.db import models
class Appointment(models.Model):
starts_at = models.DateTimeField()
ends_at = models.DateTimeField()
meeting_with = models.TextField()
notes = models.TextField()
minutes = models.TextField() # New line here!
def __str__(self):
return "{} - {}: Meeting with {} ({})".format(self.starts_at,
self.ends_at,
self.meeting_with,
self.notes)
现在我再次运行 makemigrations
,但这一次,Django 将模型的当前定义与数据库的当前状态进行比较。对于 Django 来说,处理起来似乎是理所当然的,而且应该是这样,但有一件事除外:Django 默认将列定义为禁止 NULL 值。如果我添加不允许 NULL 值的“minutes”列,那么对于现有行,我将遇到麻烦。因此,Django 会询问我是要选择一个默认值来放入此字段,还是我更愿意在迁移开始之前停止迁移并调整我的定义。
我喜欢迁移的一件事是它们可以帮助您避免像这样的愚蠢错误。我将选择第一个选项,指示“whatever”是(非常有用)默认值。完成此操作后,Django 完成迁移的定义并将其写入磁盘。现在我可以再次应用待处理的迁移:
django-admin.py migrate
我看到:
Operations to perform:
Apply all migrations: admin, contenttypes, auth, atfapp, sessions
Running migrations:
Applying atfapp.0002_appointment_minutes... OK
果然,新的迁移已被应用!
当然,Django 本可以猜测我的意图。但是,在这种情况下和大多数其他情况下,Django 都遵循 Python 的经验法则,即最好是显式的而不是隐式的,并避免猜测。
结论Django 的模型允许您以独立于数据库的方式创建各种不同的字段。此外,Django 在数据库的不同版本之间创建迁移,使得在项目向前推进时,即使有多个开发人员在处理它,也可以轻松迭代数据库定义。
在我的下一篇文章中,我计划研究如何从您的 Django 应用程序中使用您已定义的模型。