Python Web 开发之 Django Models 详解

Django 是由 Python 语言编写的基于 MVC(即 Model View Controller)架构的 Web 开发框架。
其架构中的模型(Model)主要负责处理 Web 应用的数据逻辑部分,包括定义数据存储单位(即数据库表)的字段属性和行为、与数据库交互以及其他相关联的操作。
通常一个模型映射于一个特定的数据库表。

Django 中的模型有以下几个基本属性:

  • 每个模型都是继承自 django.db.models.Model 类的子类
  • 模型类的属性分别对应于与之相关联的数据表中的字段
  • Django 会自动生成用于访问数据库的 API

一、基本使用

项目初始化

在开始编写 Web 应用代码之前,需要先使用如下命令初始化一个 Django 项目并创建应用:

1
2
3
$ django-admin startproject myproject
$ cd myproject
$ python manage.py startapp myapp

最终生成的项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
myproject
├─manage.py

├─myapp
│ ├─admin.py
│ ├─apps.py
│ ├─models.py
│ ├─tests.py
│ ├─views.py
│ └─migrations

└─myproject
├─settings.py
├─urls.py
└─wsgi.py

定义模型

用于定义模型的代码通常保存在 myproject/myapp/models.py 文件中。
下面的代码即定义了一个简单的 Person 模型:

1
2
3
4
5
from django.db import models

class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)

其中的 first_namelast_name 两个类属性即对应于数据库表的两个字段。
Person 模型会以如下的 SQL 语句创建与之关联的数据库表(id 字段默认会自动添加):

1
2
3
4
5
CREATE TABLE myapp_person (
"id" serial NOT NULL PRIMARY KEY,
"first_name" varchar(30) NOT NULL,
"last_name" varchar(30) NOT NULL
);

为了使模型生效,还需要将 myapp 包含进 settings.py 配置文件中的 INSTALLED_APPS,编辑 myproject/myproject/settings.py 文件,内容如下:

1
2
3
4
5
INSTALLED_APPS = [
#...
'myapp',
#...
]

之后即可使用 python manage.py makemigrations myapp 创建数据库迁移文件;
再运行 python manage.py migrate 命令将模型中定义的表结构迁移至数据库中。

Django Shell 测试

完成数据库迁移后,可使用 python manage.py shell 命令进入 Django Shell 交互式命令行,通过 Django 提供的模型 API 进行测试(插入数据):

1
2
3
4
5
6
7
>>> from myapp.models import Person
>>> john = Person(first_name='John', last_name='Smith')
>>> john.save()
>>> Person.objects.all()
<QuerySet [<Person: Person object (1)>]>
>>> john.first_name
'John'

访问 sqlite3 数据库查询最终结果,John Smith 已添加至数据表中:

1
2
3
4
5
6
7
>>> import sqlite3
>>> conn = sqlite3.connect('db.sqlite3')
>>> cursor = conn.cursor()
>>> cursor.execute('select * from myapp_person')
<sqlite3.Cursor object at 0x0000022C36ADBD50>
>>> print(cursor.fetchone())
(1, 'John', 'Smith')

二、字段(Field)

模型中最重要的也是唯一必须存在的项目就是字段,它由模型类的属性定义,用来表述与模型相关联的数据表的结构。

字段的类型与选项

模型中的每个字段都是 django.db.models.Field 类的实例,对应于数据库表中的列。
Django 内置了大量的字段类型,如 CharFieldTextFieldDateTimeField 等。具体可查看 模型字段参考

每个字段都可以接收特定的字段相关的参数,比如 CharField 需要传入 max_length 用于定义 VARCHAR 类型的字符长度。

此外还有一些通用的可选的字段选项。如:

  • null:如为 True,则 Django 会将空值在数据库中存为 NULL。该选项默认为 False。
  • blank:如为 True,则该字段允许为空。与 null 选项不同,blank 是与表单验证相关的,而 null 是数据库相关的。
  • default:用于设置字段的默认值。
  • primary_key:用于设置模型的主键。如未指定任何字段为主键,则 Django 会自动添加 IntegerField 字段作为主键。
  • unique:设置字段的值是否允许重复。

PS:默认情况下,Django 会给每个模型添加如下字段
id = models.AutoField(primary_key=True)
作为为自增的主键。如果想覆盖此默认行为,直接手动指定其他字段为主键(primary_key=True)即可。

关系

额,关系型数据库的强大之处即在于各数据库表之间的相互关联。Django 支持定义三种最常见的数据库关系:多对一、多对多和一对一。

可以通过 django.db.models.ForeignKey 创建多对一关系,只需要像定义其他字段那样将它作为类属性引入即可。如:

1
2
3
4
5
6
7
8
9
from django.db import models

class Manufacturer(models.Model):
name = models.CharField(max_length=20)
location = models.CharField(max_length=40)

class Car(models.Model):
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
price = models.IntegerField()

运行 python manage.py makemigrations 命令创建数据库迁移文件:

1
2
3
4
5
$ python manage.py makemigrations myapp
Migrations for 'myapp':
myapp/migrations/0001_initial.py
- Create model Manufacturer
- Create model Car

使用 python manage.py sqlmigrate myapp 0001 命令查看具体会执行哪些 SQL 语句(基于 sqlite3):

1
2
3
4
5
6
7
8
9
10
11
12
$ python manage.py sqlmigrate myapp 0001
BEGIN;
--
-- Create model Manufacturer
--
CREATE TABLE "myapp_manufacturer" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(20) NOT NULL, "location" varchar(40) NOT NULL);
--
-- Create model Car
--
CREATE TABLE "myapp_car" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "price" integer NOT NULL, "manufacturer_id" integer NOT NULL REFERENCES "myapp_manufacturer" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "myapp_car_manufacturer_id_2be676ab" ON "myapp_car" ("manufacturer_id");
COMMIT;

多对多和一对一的数据库关系则分别可以使用 ManyToManyFieldOneToOneField 定义。

三、模型的属性与方法

Meta 选项

模型的 Meta 选项在模型类的定义中是可选的,它基本上包含了除字段以外的所有内容。比如数据纪录的顺序(ordering)、关联的数据库表的名称(db_table)和索引(indexes)等。

Django 模型支持的所有 Meta 选项可以参考 Model Meta options

示例代码:

1
2
3
4
5
6
7
8
from django.db import models

class Ox(models.Model):
horn_length = models.IntegerField()

class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"

自定义模型方法

在模型中创建自定义方法可以为模型对象添加个性化的“底层”功能。参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
from django.db import models

class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
birth_date = models.DateField()

@property
def full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)

此时的 Person 模型除了可以从数据库中读取和写入数据等基本功能外,还可以通过它调用自定义的 full_name 方法完成额外的需求(返回全名)。

覆盖默认的模型方法

有些情况下,还可以通过修改模型内置的方法,改变模型与数据库的具体交互方式。尤其是 save() (向数据库中存入数据)和 delete() (从数据库中删除纪录)等方法。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.db import models

class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)

def save(self, *args, **kwargs):
self.first_name = self.first_name.capitalize()
self.last_name = self.last_name.capitalize()
super().save(*args, **kwargs)

@property
def full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)

重新迁移数据库,进入 Django Shell 测试,结果如下:

1
2
3
4
5
6
7
>>> from myapp.models import Person
>>> john = Person(first_name='john', last_name='smith')
>>> john.save()
>>> john
<Person: Person object (2)>
>>> john.full_name
'John Smith'

四、数据库操作

一旦创建了数据模型,Django 即会自动生成与数据库交互的 API 供用户创建、获取、更新和删除数据对象。

此处先创建如下的模型文件供测试使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django.db import models

class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()

def __str__(self):
return self.name

class Author(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()

def __str__(self):
return self.name

class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=255)
body_text = models.TextField()
pub_date = models.DateField()
mod_date = models.DateField()
authors = models.ManyToManyField(Author)
n_comments = models.IntegerField()
n_pingbacks = models.IntegerField()
rating = models.IntegerField()

def __str__(self):
return self.headline

Insert

可以通过实例化模型类创建一个数据对象,并调用其 save() 方法将对应的记录插入(执行 INSERT SQL 语句)到数据库表中。

1
2
3
4
5
6
7
8
9
>>> from myapp.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news')
>>> b.save()
>>> b.name = 'Beatles Blog All'
>>> b.save()
>>> b
<Blog: Beatles Blog All>
>>> b.tagline
'All the latest Beatles news'

插入 ForeignKey 与 ManyToManyField
更新 ForeignKey 与操作普通字段的方式相同,将正确类型的对象赋值给对应字段并调用 save() 方法即可。如:

1
2
3
4
5
>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

更新 ManyToManyField 的方式稍有不同,需要使用 add() 方法:

1
2
3
>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

获取数据

从数据库中获取数据会生成一个 QuerySet 对象,它代表从数据库中取出的数据对象的集合。QuerySet 等同于数据库中的 SELECT 语句,它可以有零个或者多个 filterfilter 对应于数据库中的筛选条件如 WHERELIMIT 等。

获取单个对象
>>> one_entry = Entry.objects.get(pk=1)

PS:pkprimary key

获取所有对象
>>> all_entries = Entry.objects.all()

应用筛选器
>>> entry = Entry.objects.filter(pub_date__year=2006)

Limiting
>>> entries = Entry.objects.all()[:5]

排序
>>> entry = Entry.objects.order_by('headline')[0]

字段查询

字段查询对应于 SQL 中的 WHERE 语句,可以通过向 QuerySet 对象的方法 filter()exclude()get() 中传入特定的参数来实现。
基本的查询参数语法如下:field__lookuptype=value

如:>>> Entry.objects.filter(pub_date__lte='2006-01-01')
等同于:SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

其中 lteless or equal(小于等于)。其他类似的 lookuptype 还包括 gt(大于)、gte(大于等于)、lt(小于)、exactiexact(忽略大小写)、startswithistartswithendswithiendswithcontainsrange(指定范围)、regex(正则表达式)、iregex 等。

以下为一些常见的使用示例:

  • exact:>>> Entry.objects.get(headline__exact="Cat bites dog")
    等于 SELECT ... WHERE headline = 'Cat bites dog';

  • iexact:>>> Blog.objects.get(name__iexact="beatles blog")
    等于 SELECT ... WHERE name ILIKE 'beatles blog';

  • startswith:>>> Entry.objects.filter(headline__startswith='Lennon')
    等于 SELECT ... WHERE headline LIKE 'Lennon%';

  • contains:>>> Entry.objects.get(headline__contains='Lennon')
    等于 SELECT ... WHERE headline LIKE '%Lennon%';

  • in:>>> Entry.objects.filter(id__in=[1, 3, 4])
    等于 SELECT ... WHERE id IN (1, 3, 4);

  • range:

    1
    2
    3
    4
    import datetime
    start_date = datetime.date(2005, 1, 1)
    end_date = datetime.date(2005, 3, 31)
    Entry.objects.filter(pub_date__range=(start_date, end_date))

等于 SELECT ... WHERE pub_date BETWEEN '2005-01-01' and '2005-03-31';

Manager

Manager 是提供给 Django 模型,用于做数据库查询操作的接口。Django 项目中的每一个模型都需要至少包含一个 Manager 对象。

默认情况下,Django 会在每一个模型类中添加一个名为 objects 的 Manager 。通过将 models.Manager() 赋值给除 objects 以外的类属性,可以覆盖此默认行为:

1
2
3
4
5
from django.db import models

class Person(models.Model):
#...
people = models.Manager()

此时 Person.objects.all() 查询语句会报出 AttributeError 错误,而 Person.people.all() 则返回所有的 Person 对象。

自定义 Manager

自定义的 Manager 方法可以向模型中添加表级别的查询功能。与之对应的纪录级别的功能则需要使用模型方法

如:

1
2
3
4
5
6
7
8
9
10
11
12
# First, define the Manager subclass.
class DahlBookManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(author='Roald Dahl')

# Then hook it into the Book model explicitly.
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=50)

objects = models.Manager() # The default manager.
dahl_objects = DahlBookManager() # The Dahl-specific manager.

以上面的模型为例,Book.objects.all() 会返回数据库中所有的书籍信息,而 Book.dahl_objects.all() 则会返回所有作者为 Roald Dahl 的书籍。
Django 允许向模型中添加任意数量的 Manager() 实例,因此可以用来为模型定义一些通用的筛选器。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AuthorManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(role='A')

class EditorManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(role='E')

class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
role = models.CharField(max_length=1, choices=[('A', _('Author')), ('E', _('Editor'))])
people = models.Manager()
authors = AuthorManager()
editors = EditorManager()

Person.people.all()Person.authors.all()Person.editors.all() 都可以作为从模型中获取数据的接口,且 authorseditors 已预先根据 role 对数据进行了筛选。

参考资料

Django 2.2 官方文档