03-ORM 篇:Django ORM 基础

发布日期:2024-12-15 修改时间:2024-12-15 阅读所需:11 分钟
django
python
web
backend

在大多数编程语言社区中,Web 框架是最为百花齐放的一个赛道。Django 中的大多数功能都可以在 Python 社区中找到其他替代的 Web 框架,但唯独有一个功能特性是这些框架所不具备的,有人从 Django 切换到其他 Python Web 框架时还会对这个功能心心念念,甚至一度希望 Django 能将这个功能独立出来成库以便能和其他框架结合使用。

这个功能就是 Django ORM

什么是 ORM?ORM 即 Object-Relational Mapping 的缩写,简单来说我们就是将数据库里面的表映射成编程语言里面的对象结构,那么通过操作对象来完成数据库操作,减少我们在某个编程语言语法和 SQL 语法之间来回切换,而专注于逻辑开发即可。

Diagram应用代码(Python)Django ORM数据库(SQL)User.objects.filter(username='john_doe')User.objects.filter(username='john_doe')SELECT * FROM user WHERE username = 'john_doe'SELECT * FROM user WHERE username = 'john_doe'对象操作SQL转换

当然如果你已经是 Python Web 开发者,那么你就或多或少都将 SQLAlchemy 库作为你操作 ORM 的首选。不过 SQLAlchemy 很强大,但文档的复杂度比较高;而 Django ORM 在易用和功能上做到了一个很好的平衡,尽管它的功能不一定有 SQLAlchemy 那样丰富,但它较为完善的文档和无缝衔接的功能可以覆盖 80% 在模型层操作的场景。

由于 Django ORM 内容较多,因此将按拆分成多个篇章进行介绍。

使用 Django ORM

Django ORM 与 Django 框架相绑定,因此我们在安装 Django 时就已经是开箱即用。默认情况下 Django 将为我们使用 SQLite 数据库,并且都写在了 DATABASES 这一配置中。在项目的 settings.py 文件中你便可以看到:

djangotutor/settings.py
...
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
...

Django 支持在项目中多个数据库,倘若在使用 ORM 时我们没有特别指定那么将会使用 default 所表示的数据库。

还记得我们之前通过 manage.pystartapp 命令创建了一个 myapp 应用吗?现在我们可以到当中的 models.py 文件去自定义你的模型了。假设我们要设计一个书籍相关的业务,那么我们就将在这里面得到如下的模型:

myapp/models.py
from django.core.validators import MinValueValidator
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=100)
address = models.CharField(max_length=200)
city = models.CharField(max_length=100)
state_province = models.CharField(max_length=50)
country = models.CharField(max_length=50)
website = models.URLField(null=True, blank=True)
def __str__(self):
return self.name
class Author(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
bio = models.TextField(blank=True)
email = models.EmailField(unique=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class Book(models.Model):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, related_name="books")
pages = models.IntegerField(validators=[MinValueValidator(1)])
price = models.DecimalField(
max_digits=10, decimal_places=2, validators=[MinValueValidator(0)]
)
publisher = models.ForeignKey(
Publisher, on_delete=models.CASCADE, related_name="books"
)
publication_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
def __str__(self):
return self.title

要使用 Django ORM 就得基于 django.db.models.Model 类去定义模型,它将与数据库的同名表建立起映射关系,其中类名就是表名,而当中的属性字段就是表字段,你可以在当中设置对应的数据库字段约束或信息等。

后续所有要对数据库表进行改动的地方都会在你定义模型的地方进行改动,但改动完我们还不能直接就开始在代码层进行使用,因为我们还需要将其与数据库同步,这时候我们就需要 makemigrationsmigrate 这两个命令了。但在执行前请记得将 myapp 添加到 INSTALLED_APPS 中,以便 Django 能够找到要同步的模型信息。

ORM 迁移与同步

在第一章中我们知道了怎么用 Django 命令之后,现在就直接在 manage.py 所在路径下执行 makemigrations 命令,它会为我们在对应应用目录下生成一个 migrations 文件夹,里面包含了所有的迁移记录,以便你在开发阶段可以随时回退到上一次迁移的状态。但请在迁移时注意备份你的数据。

Terminal window
uv run manage.py makemigrations
Migrations for 'myapp':
myapp/migrations/0001_initial.py
+ Create model Author
+ Create model Publisher
+ Create model Book

现在可以看到终端上输出了迁移文件的内容,它包含了我们在 models.py 中定义的模型信息,并且每个迁移文件也都是一个合法的 Python 文件,大多数时候我们不需要手动修改当中的内容:

myapp/migrations/0001_initial.py
# Generated by Django 5.1.1 on 2024-10-06 07:37
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Author',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('bio', models.TextField(blank=True)),
('email', models.EmailField(max_length=254, unique=True)),
],
),
migrations.CreateModel(
name='Publisher',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('address', models.CharField(max_length=200)),
('city', models.CharField(max_length=100)),
('state_province', models.CharField(max_length=50)),
('country', models.CharField(max_length=50)),
('website', models.URLField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Book',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('pages', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
('price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
('publication_date', models.DateField()),
('isbn', models.CharField(max_length=13, unique=True)),
('authors', models.ManyToManyField(related_name='books', to='myapp.author')),
('publisher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='books', to='myapp.publisher')),
],
),
]

现在我们进一步就可以执行 migrate 命令了,它会自动将迁移文件同步到数据库中。需要注意的是,由于 Django 内置了一些组件应用,比如管理后台和用户鉴权等,因此在我们初次同步时也会连同它们的模型信息一起同步到数据库中。

Terminal window
uv run manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, myapp, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying myapp.0001_initial... OK
Applying sessions.0001_initial... OK

每次当我们对模型有修改时都必然会执行 makemigrationsmigrate 两个命令,就好比使用 Git 时的 git addgit commit 操作一样。

当然,Django 也会将每次迁移的记录都保存在数据库中,因此你可以通过 showmigrations 命令来查看已经执行过的迁移记录。

与现有数据库同步

一般来说手动编写 ORM 映射通常是我们在开始新项目时才会这么做,但倘若你已经是有其他项目共用的数据库,而刚好你又想在 Django 中使用这些表并开发相关的业务逻辑,那么你可以使用 inspectdb 命令,Django 会自动将数据库中的表拉取并形成相应的 ORM 模型。当然最好需要指定导出到哪个文件中。

Terminal window
uv run manage.py startapp legacy
uv run manage.py inspectdb --database=legacy user_action_logs user_points > legacy/models.py

和自己手动编写 ORM 模型类不同的是,使用了 inspectdb 的模型不会被 Django 管理,因为默认它们都被设置了 managed=False 的属性,因此不论你对其如何进行修改删除或创建都不会同步到数据库中。

from django.db import models
class UserActionLog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.CharField(max_length=100)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
managed = False
db_table = 'user_action_logs'

相信你注意到了上面示意的模型类中还包含了一个 Meta 类,这是 Django 特殊的元数据管理方式,在后续的章节中我们会对其展开,现在先暂时按下不表。

在交互式解释器中使用 ORM

有时为了开发调试,我们也可以使用 Django 提供的 shell 命令来进入到交互式的 Python 解释器界面,在当中直接就基于 ORM 进行操作,比如对数据进行增删改查,Django 会自动为我们导入模块,我们只需要像正常使用 Python 代码那样即可:

Terminal window
uv run manage.py shell
Python 3.12.5 (main, Aug 6 2024, 19:08:49) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.models import Author, Book, Publisher
>>> Author.objects.all()
<QuerySet [<Author: Author 1 Last Name 1>, <Author: Author 2 Last Name 2>, <Author: Author 3 Last Name 3>]>
>>> Book.objects.get(pk=2)
<Book: Book 1>
>>> Publisher.objects.filter(name__endswith='1')
<QuerySet [<Publisher: Publisher 1>]>