《Django3.0
文档》笔记:Models
模型类
模型可以准确且唯一地描述数据,其包含了存储数据的重要字段。每个模型都是一个Python
的类,这些模型类继承自django.db.models.Model
,模型类的每个属性都相当于数据库表的一个字段。一般来说,每个模型类映射一张数据库表,每个模型类的实例代表数据库表的一行记录,每个字段映射为数据库表的一列。通过模型类和Django
提供的一套数据库抽象API
,可以创建、检索、更新和删除对象。
from django.db import models
class Person(models.Model):
first_name = models.charField(max_length=30)
last_name = models.CharField(max_length=30)
样例代码定义了一个Person
模型类,包含first_name
和last_name
两个字段。根据该模型类可以创建一个如下的数据库表:
CREATE TABLE myapp_person(
'id' serial NOT NULL PRIMARY KEY,
'first_name' varchar(30) NOT NULL,
'last_name' varchar(30) NOT NULL
);
字段
模型中最重要且唯一必要的是数据库的字段定义。字段再类属性中定义。定义字段名时应小心避免使用与模型API
冲突的名称,如clean
、save
、delete
等。
from django.db import models
class Musician(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
instrument = models.CharField(max_length=100)
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_data = models.DataField()
num_stars = models.IntegerField()
模型类中每一个字段都应该是某个Field
字段的实例,Django
利用这些字段类来实现以下功能:
- 字段类型用以指定数据库数据类型(如:
INTEGER
,VARCHAR
,TEXT
)。 - 在渲染表单字段时默认使用的
HTML
视图(如:<input type='text'>
,<select>
)。 - 基本的有效性验证功能,用于
Django
后台和自动生成表单。
每一种字段都需要指定一些特定的参数,例如,CharField
(以及它的子类)需要接收一个max_length
参数,用于指定数据库存储VARCHAR
数据时用的字节数。
一些可选的参数是通用的,可以用于任何字段类型,下面介绍一些经常用到的通用参数:
null
如果设置为True
,当该字段为空时,Django
会将数据库中该字段设置为NULL
,默认为False
。
blank
如果设置为True
,则该字段允许为空,默认为False
。(注意:该选项与null
不同,null
选项仅仅是数据库层面的设置,然而blank
是涉及表单验证方面。如果一个字段设置为blank=True
,在进行表单验证时,接收的数据该字段值允许为空,而设置为blank=False
时,不允许为空。
choices
一系列二元组,用作此字段的选项。如果提供了二元组,默认表单小部件是一个选择框,而不是标准文本字段,并将限制给出的选项。一个选项列表:
YEAR_IN_SCHOOL_CHOICES = [
('FR', 'Freshman'),
('SO', 'Sophomore'),
('JR', 'Junior'),
('SR', 'Senior'),
('GR', 'Graduate'),
]
每个二元组的第一个值会存储在数据库中,而第二个值将只会用于在表单中显示。对于一个模型类的实例,要获取该字段二元组中相对应的第二个值,使用get_Foo_display()
方法。例如:
from django.db import models
class Person(models.Model):
SHIRT_SIZES = (
('S', 'Small'),
('M', 'Medium'),
('L', 'Large'),
)
name = models.CharField(max_length=60)
shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES)
>>> p = Person(name="Fred Flintstone", shirt_size="L")
>>> p.save()
>>> p.shirt_size
'L'
>>> p.get_shirt_size_display()
'Large'
也可以使用枚举类以简洁的方式来定义choices
:
from django.db import models
class Runner(models.Model):
MedalType = models.TextChoices('MedalType', 'GOLD SILVER BRONZE')
name = models.CharField(max_length=60)
medal = models.CharField(blank=True, choices=MedalType.choices, max_length=10)
default
该字段的默认值,可以是一个值或者是个可调用的对象,如果是个可调用对象,每次实例化模型时都会调用该对象。
primary_key
如果设置为True
,将该字段设置为该模式的主键。在一个模型类中,如果没有对任何一个字段设置primary_key=True
选项,Django
会自动添加一个IntegerField
字段,并设置为主键。因此可以不手动设置主键,也可以手动重写Django
默认设置的主键。
主键字段是只读的,如果想修改一个模型类实例的主键并保存,等同于创建一个新的模型类实例。例如:
from django.db import models
class Fruit(models.Model):
name = models.CharField(max_length=100, primary_key=True)
>>> fruit = Fruit.objects.create(name='Apple')
>>> fruit.name = 'Pear'
>>> fruit.save()
>>> Fruit.objects.values_list('name', flat=True)
<QuerySet ['Apple', 'Pear']>
unique
如果设置为True
,这个字段的值必须在整个表中保持唯一。
verbose_name
除了ForeignKey
,ManyToManyField
和OneToOneField
,任何字段类型都接收一个可选的位置参数verbose_name
,如果未指定该参数值,Django
会自动使用字段的属性名作为该参数值,并且把下划线转换为空格。
在该例中,备注名为person's first name
:
first_name = models.CharField("person's first name", max_length=30)
在该例中,备注名为first name
:
first_name = models.CharField(max_length=30)
ForeignKey
,ManyToManyField
和OneToOneField
接收的第一个参数为模型的类名,后面可以添加一个verbose_name
参数:
poll = models.ForeignKey(
poll,
on_delete=models.CASCADE,
verbose_name = "related place",
)
sites = models.ManyToManyField(Site, verbose_name="list of sites")
place = models.OneToOneField(
Place,
on_delete=models.CASCADE,
verbose_name="related place",
)
惯例是不将verbose_name
的首字母大写,必要时Django
会自动把首字母转换为大写。
关联
显然,关系型数据库的强大之处在于各表之间的关联关系。Django
提供了定义三种最常见的数据库关联关系的方法:多对一,多对多,一对一。
多对一关联
定义一个多对一的关联关系,使用django.db.models.ForeignKey
类,和其他Field
字段类型一样,只需要在模型类中添加一个值为关联模型类的属性。
ForeignKey
类需要添加一个位置参数,即想要关联的模型类名。
例如,一个Car
模型类有一个制造者Manufacturer
,一个Manufacturer
制造许多辆车,但是每辆车都仅有一个制造者,那么使用下面的方法定义这个关系:
from django.db import models
class Manufacturer(models.Model):
# ...
pass
class Car(models.Model):
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
# ...
建议设置ForeignKey
字段名为想要关联的模型类名,也可以自定义关联字段名,例如:
class Car(models.Model):
company_that_makes_it = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
)
多对多关联
定义一个多对多的关联关系,使用django.db.models.ForeignKey
类,和其他Field
字段类型一样,只需要在模型类中添加一个值为关联模型类的属性。
ManyToManyField
类需要添加一个位置参数,即想要关联的模型类名。
例如,如果Pizza
含有多种Topping
(配料),一种Topping
可能存在于多个Pizza
中,并且每个Pizza
含有多种Topping
,那么可以表示这种关系:
from django.db import models
class Topping(models.Model):
# ...
pass
class Pizza(models.Model):
# ...
toppings = models.ManyToManyField(Topping)
建议设置ManyToManyField
字段名为一个复数名词(如toppings
),表示所要关联的模型类对象的集合。
注意:对于多对多关联关系的两个模型类,可以在任何一个模型类中添加ManyToManyField
字段,但只能选择一个模型类设置该字段,即不能同时在两模型类中添加该字段。一般情况下,应该把ManyToManyField
实例放到需要在表单中被编辑的对象中。在之前的例子中,toppings
被放在Pizza
当中(而不是Topping
中有指向Pizza
的ManyToManyField
实例),因为相较于配料被放在不同的披萨当中,披萨当中有很多种配料更加符合常理。按照先前说的,在编辑Pizza
的表单时用户可以选择多种配料。
中介模型
如果只是想要一个类似于记录披萨和配料关系之间混合和搭配的多对多关系,标准的ManyToManyField
就够用了,但是有时可能需要将数据与两个模型之间的关系相关联。
举例来讲,考虑一个需要跟踪音乐人属于哪个音乐组的应用程序,在人和他们所在的组之间有一个多对多关系,可以使用ManyToManyField
来表示这个关系。然而,如果想要记录更多的信息在这样的关联表当中,比如想要记录某人是何时加入一个组的。对于这些情况,Django
允许指定用于控制多对多关系的模型类:可以在中介模型当中添加额外的字段。在实例化ManyToManyField
的时候使用through
参数指定多对多关系使用哪个中介模型。举例代码如下:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
通过中介模型完成ManyToManyField
后,可以通过实例化中介模型来创建多对多关系:
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles, date_joined=date(1962, 8, 16), invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles, date_joined=date(1960, 8, 1), invite_reason="Wanted to from a band.")
>>> beatles.member.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
如果自定义中介模型没有强制(model1, model2)
对的唯一性,调用remove()
方法会将相关的实例都删除,调用clear()
方法则会删除所有的中介模型实例。
>>> Membership.objects.create(person=ringo, group=beatles, date_joined=date(1968, 9, 4), invite_reason="You've been gone for a month and we miss you.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
>>> # This deletes both of the intermediate model instances for Ringo Starr
>>> beatles.members.remove(ringo)
>>> beatles.members.all()
<QuerySet [<Person: Paul McCartney>]>
>>> # Beatles have broken up
>>> beatles.members.clear()
>>> # Note that this deletes the intermediate model instances
>>> Membership.objects.all()
<QuerySet []>
一旦建立了自定义多对多关联关系,就可以执行查询操作,和一般的多对多关联关系一样,可以使用多对多关联模型的属性来查询:
# Find all the groups with a member whose name starts with 'Paul'
>>> Group.objects.filter(members__name__startswith='Paul')
<QuerySet [<Group: The Beatles>]>
# Find all the members of The Beatles that joined after 1 Jan 1961
>>> Person.objects.filter(group__name='The Beatles', membership__date__joined__gt=date(1961,1,1))
<QuerySet [<Person: Ringo Starr]>
>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'
>>> ringos_membership = ringo.membership_set.get(group=beatles)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'
一对一关联
使用OneToOneField
来定义一对一关系,就像使用其他类型的Field
一样:在模型属性中包含它。当一个对象以某种方式“继承”另一个对象时,这对该对象的主键非常有用。OneToOneField
需要一个位置参数:与模型相关联的类。
例如,当建立一个有关“位置”信息的数据库时,可能会包含通常的地址,电话等字段。接着,如果想要接着建立一个关于“餐厅”的数据库,除了将位置数据库当中的字段复制到Restaurant
模型类,也可以将一个指向Place
模型类的OneToOneField
放到Restaurant
当中(因为“餐厅”是一个“地点”),事实上,在处理这样的情况下最好使用模型继承,它隐含的包括了一个一对一关系。和ForeignKey
一样,可以创建自关联关系也可以创建与尚未定义的模型的关系。
OneToOneField
字段还可以接收一个可选的parent_link
参数。OneToOneField
类通常自动的成为模型的主键,这条规则现在不再使用了(然而可以通过手动指定primary_key
参数)。因此,可以在单个模型当中指定多个OneToOneField
字段。
字段命名限制
Django
对模型类的字段名有一些限制:
1.一个字段的名称不能是Python
保留字,因为这会导致Python
语法错误。比如:
class Example(models.Model):
pass = models.IntegerField() # 'pass' is a reserved word!
2.一个字段名称不能包含连续的多个下划线,原因在于Django
查询语法的工作方式。比如:
class Example(models.Model):
foo__bar = models.IntegerField() # 'foo__bar has two underscores!'
3.字段名不能以下划线结尾,原因同上。
SQL
保留字,例如join
,where
或select
,是可以被用在模型字段名当中的,因为Django
在对底层的SQL
查询当中清洗了所有的数据库表名和字段名,通过使用特定数据库引擎的引用语法。
Meta
选项
使用内部Meta
类来给模型赋予元数据,就像:
from django.db import models
class 0x(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
模型的元数据即“所有不是字段的东西”,比如排序选项(oedering
),数据库表名(db_table
),或是阅读友好的单复数名(verbose_name
和verbose_name_plural
)。这些都不是必须的,并且在模型当中添加Meta
类也是完全可选的。
模型属性
objects
模型当中最重要的属性是Manager
,它默认是Django
模型和数据库查询操作之间的接口,并且它被用作从数据库中获取实例对象,如果没有指定自定义的Manager
,默认名称是objects
。Manager
只能通过模型类来访问,不能通过模型实例来访问。
模型方法
在模型方法中添加自定义方法会给你的对象提供自定义的“行级”操作能力,与之对应的是类Manager
的方法意在提供“表级”的操作,模型方法应该在某个对象实例上生效。这是一个将相关逻辑代码放在一个地方的技巧——模型。比如,该模型有一些自定义方法:
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()
def baby_boomer_status(self):
"Returns the person's baby-boomer status."
import datetime
if self.birth_date < datetime.date(1945, 8, 1):
return "Pre-boomer"
elif self.birth_date < datetime.date(1965, 1, 1):
return "Baby-boomer"
else:
return "Post-boomer"
@property
def full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)
例子中的最后一个方法是property
,下面介绍两个最可能期望复写的:
__str__()
一个Python
的“魔法方法”,返回值友好地展示了一个对象。Python
和Django
在要将模型实例展示为纯文本时调用。最有可能的应用场景是交互式控制台或后台。
get_absolute_url()
该方法告诉Django
如何计算一个对象的URL
。Django
在后台接口使用此方法,或任意时间它需要计算一个对象的URL
。任何需要一个唯一URL
的对象需要定义此方法。
模型继承
模型继承在Django
中与普通类继承在Python
中的工作方式几乎完全相同,但也应该遵循本页开头的内容。这意味着其基类应该继承自django.db.models.Model
。
你只需要决定父类模型是否需要拥有它们的权利(拥有它们的数据表),或者父类仅作为承载仅子类中可见的公共信息的载体。
Django
有三种可用的继承风格。
- 常见情况下,仅将父类用于子类公共信息的载体,因为你不会想在每个子类中把这些代码都敲代一遍。这样的父类永远都不会单独使用,所以抽象基类是你需要的。
- 若继承了一个模型(可能来源于其它应用),且想要每个模型都有对应的数据表,请使用多表继承。
- 最后,若只想修改模型的
Python
级行为,而不是以任何形式修改模型字段,代理模型是最佳选择。
抽象基类
抽象基类在你要将公共信息放入很多模型时会很有用。编写你的基类,并在Meta
类中填入abstract=True
。该模型将不会创建任何数据表。当其用作其他模型类的基类时,它的字段会自动添加至子类。举个例子:
from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
class Students(CommonInfo):
home_group = models.CharField(max_length=5)
Student
模型拥有3个字段:name
,age
和home_group
。CommonInfo
模型不能用作普通的Django
模型,因为它是一个抽象基类。它不会生成数据表,也没有管理器,也不能被实例化和保存。从抽象基类继承来的字段可以被其他字段或值重写,或用None
删除。对很多用户来说,这种继承可能就是你想要的,它提供了一种在Python
级抽出公共信息的方法,但仍会在子类模型中创建数据表。
Meta继承
当一个抽象基类被建立,Django
将所有你在基类中申明的Meta
内部类以属性的形式提供。若子类未定义自己的Meta
类,他将会继承父类的Meta
类,当然,子类也可直接继承父类的Meta
,比如:
from django.db import models
class CommonInfo(models.Model):
# ...
class Meta:
abstract = True
ordering = ['name']
class Student(CommonInfo):
# ...
class Meta(CommonInfo.Meta):
db_table = 'student_info'
Django
在安装Meta
属性前,对抽象基类的Meta
做了一个调整——设置abstract=False
。这意味着抽象基类的子类不会自动地变成抽象类。当然,你可以继承一个抽象基类创建另一个抽象基类,只需记住显式地设置abstract=True
。抽象基类的某些Meta
属性对子类是没用的,比如包含db_table
意味着所有的子类(你并未在子类中指定他们的Meta
)会使用同一张数据表,这肯定不是我们想要的。
related_name
和related_query_name
若你在外键或者多对多字段使用了related_name
或related_query_name
,你必须为该字段提供一个独一无二的反向名字和查询名字。这在抽象基类中一般会引发问题,因为基类中的字段都被子类继承且保持同样的值(包括related_name
和related_query_name
)。为了解决此问题,当你在抽象基类中(也只能是在抽象基类中)使用了related_name
和related_query_name
,部分值需要包含'%(app_lable)s'
和'%(class)s'
。
'%(class)s'
用使用了该字段的子类的小写类名替换。'%(app_label)s'
用小写的包含子类的应用名替换。每个安装的应用名必须是唯一的,应用内的每个模型类名也必须是唯一的。因此,替换后的名字也是唯一的。、
举个例子,有个应有common/models.py
:
from django.db import models
class Base(models.Model):
m2m = models.ManyToManyField(
OtherModel,
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss",
)
class Meta:
abstract = True
class ChildA(Base):
pass
class ChildB(Base):
pass
附带另一个应用rare/models.py
:
from common.models import Base
class ChildB(Base):
pass
common.ChildA.m2m
字段的反转名是 common_childa_related
,反转查询名是common_childas
。 common.ChildB.m2m
字段的反转名是common_childb_related
, 反转查询名是 common_childbs
。 rare.ChildB.m2m
字段的反转名是rare_childb_related
,反转查询名是 rare_childbs
。这决定于你如何使用 '%(class)s'
和 '%(app_label)s'
构建关联名字和关联查询名。但是,若你忘了使用它们,Django
会在你执行系统检查(或运行 migrate
)时抛出错误。
如果你未指定抽象基类中的related_name
) 属性,默认的反转名会是子类名,后接 '_set'
。这名字看起来就像你在子类中定义的一样。比如,在上述代码中,若省略了 related_name
) 属性, ChildA
的 m2m
字段的反转名会是 childa_set
, ChildB
的是 childb_set
。
多表继承
Django
支持的第二种模型继承方式是层次结构中的每个模型都是一个单独的模型。某个模型都指向分离的数据表,且可被独立查询和创建。继承关系介绍了子类和父类之间的连接(通过一个自动创建的 OneToOneField
)。比如:
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=True)
Place
的所有字段均在Restaurant
中可用,虽然数据分别存在不同的表中,所以,以下操作均可:
>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")
若有一个Place
同时也是Restaurant
,你可以通过小写的模型名将Place
对象转为Restaurant
对象。
>>> p = Place.objects.get(id=12)
# If p is a Restaurant object, this will give the child class:
>>> p.restaurant
<Restaurant: ...>
然而,若上述例子中的 p
不是 一个 Restaurant
(它仅是个 Place
对象或是其它类的父类),指向 p.restaurant
会抛出一个 Restaurant.DoesNotExist
异常。
Restaurant
中自动创建的连接至 Place
的 OneToOneField
看起来像这样:
place_ptr = models.OneToOneField(
Place, on_delete=models.CASCADE,
parent_link=True,
primary_key=True,
)
你可以在 Restaurant
中重写该字段,通过申明你自己的 OneToOneField
,并设置 parent_link=True
。
Meta
和多表继承
多表继承情况下,子类不会继承父类的 Meta
。所以的 Meta
类选项已被应用至父类,在子类中再次应用会导致行为冲突(与抽象基类中应用场景对比,这种情况下,基类并不存在)。故,子类模型无法访问父类的 Meta
类。不过,有限的几种情况下:若子类未指定 ordering
属性或 get_latest_by
属性,子类会从父类继承这些。
如果父类有排序,而你并不期望子类有排序,你可以显示的禁止它:
class ChildModel(ParentModel):
# ...
class Meta:
# Remove parent's ordering effect
ordering = []
继承与反向关系
由于多表继承使用隐式的 OneToOneField
连接子类和父类,所以直接从父类访问子类是可能的,就像上述例子展示的那样。然而,使用的名字是 ForeignKey
和ManyToManyField
关系的默认值。如果你在继承父类模型的子类中添加了这些关联,你必须指定 related_name
属性。假如你忘了,Django
会抛出一个合法性错误。
比如,让我们用上面的 Place
类创建另一个子类,包含一个 ManyToManyField
:
class Supplier(Place):
customers = models.ManyToManyField(Place)
这会导致以下错误:
Reverse query name for 'Supplier.customers' clashes with reverse query
name for 'Supplier.place_ptr'.
HINT: Add or change a related_name argument to the definition for
'Supplier.customers' or 'Supplier.place_ptr'.
将 related_name
像下面这样加至 customers
字段能解决此错误: models.ManyToManyField(Place, related_name='provider')
。
代理模型
使用多表继承时,每个子类模型都会创建一张新表。这一般是期望的行为,因为子类需要一个地方存储基类中不存在的额外数据字段。不过,有时候你只想修改模型的Python
级行为——可能是修改默认管理器,或添加一个方法。
这是代理模型继承的目的:为原模型创建一个代理。你可以创建、删除和更新代理模型的实例,所有的数据都会存储的像你使用原模型(未代理的)一样。不同点是你可以修改代理默认的模型排序和默认管理器,而不需要修改原模型。
代理模型就像普通模型一样申明。你需要告诉Django
这是一个代理模型,通过将Meta
类的proxy
属性设置为True
。
例如,假设你想为Person
模型添加一个方法,你可以这么做:
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
class MyPerson(Person):
class Meta:
proxy = True
def do_something(self):
# ...
pass
MyPerson
类与父类 Person
操作同一张数据表。特别提醒, Person
的实例能通过 MyPerson
访问,反之亦然。
>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>
你也可以用代理模型定义模型的另一种不同的默认排序方法。你也许不期望总对 Persion
进行排序,但是在使用代理时,总是依据 'last_name'
属性进行排序:
class OrderedPerson(Person):
class Meta:
ordering = ["last_name"]
proxy = True
现在,普通的 Person
查询结果不会被排序,但 OrderdPerson
查询会按 last_name
排序。