Model模型
Model (模型) 简而言之即数据模型。模型不是数据本身(比如数据库里的数据),而是抽象的描述数据的构成和逻辑关系。每个Django model实际上是个类,继承了models.Model。每个Model应该包括属性,关系(比如单对单,单对多和多对多)和方法。当定义好Model模型后,Django的接口会自动在数据库生成相应的数据表(table)。这样就不用自己用SQL语言创建表格或在数据库里操作创建表格了。
案例:书与出版社的实际案例。出版社有名字和地址。书有名字,描述和添加日期。利用ForeignKey定义出版社与书单对多的关系,因为一个出版社可以出版很多书。定义了如下模型
# django_foundation_pro/foundation_app/models.py
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=30)
address = models.CharField()
def __str__(self):
return self.name
class Book(models.Model):
name = models.CharField(max_length=30)
description = models.TextField(blank=True, null=True)
publisher = ForeignKey(Publisher)
add_date = models.DateField()
def __str__(self):
return self.name
模型创建好后,当运行python manage.py migrate 创建表格的时候会遇到错误,错误原因如下:
- CharField里的max_length选项没有定义
- ForeignKey(Publisher)里的on_delete选项有没有定义
所以当定义Django模型Model的时候,一定要十分清楚两件事:
- 这个Field是否有必选项, 比如CharField的max_length和ForeignKey的on_delete选项是必须要设置的。
- 这个Field是否必需(blank = True or False),是否可以为空 (null = True or False)。这关系到数据的完整性。
其实在上述案例中还有一个隐藏的错误,即TextField(blank = True, null = True)。blank = True 表示字段不是必需的,在客户端不是必填选项。null = True表示这个字段可以存储为null空值。但是Django对于空白的CharField和TextField永远不会存为null空值,而是存储空白字符串'',所以正确的做法是设置default=''。
下表才是一个比较正确的Django模型(Model):
# django_foundation_pro/foundation_app/models.py
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
name = models.CharField(max_length=30)
description = models.TextField(blank=True, default="")
# blank = True 表示字段不是必需的,在客户端不是必填选项
# null = True 表示这个字段可以存储为null空值。
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
add_date = models.DateField(auto_now_add=True)
def __str__(self):
return self.name
修改模型后,运行python manage.py makemigrations和python manage.py migrate这两个命令,前者检查模型有无变化,后者将变化迁移至数据表,Django会在数据库(默认sqlite)中生成或变更由appname_modelname组成的数据表,本例两张数据表分别为foundation_app_publisher和foundation_app_book。
模型类属性命名限制
参考:https://docs.djangoproject.com/en/4.1/topics/db/models/
- 不能是python的保留关键字。
- 不允许使用连续的下划线,这是由django的查询方式决定的。例如:b__title = models.CharField(max_length=20)就不行。报错信息:foundation_app.DemoModel.b__title: (fields.E002) Field names must not contain "__".
- 定义属性时需要指定字段类型,通过字段类型的参数指定选项,语法如下:
属性名=models.字段类型(选项)
例如
btitle = models.CharField(max_length=20)
模型的组成
一个标准的Django模型分别由模型字段、META选项和方法三部分组成。Django官方编码规范建议按如下方式排列:
- 定义的模型字段:包括基础字段和关系字段
- 自定义的Manager方法:改变模型
- class Meta选项: 包括排序、索引等等(可选)。
- def __str__():定义单个模型实例对象的名字(可选)。
- def save():重写save方法(可选)。
- def get_absolute_url():为单个模型实例对象生成独一无二的url(可选)
- 其它自定义的方法。
Django Model的字段(Field)以及可选项和必选项
models.Model提供的常用模型字段包括基础字段和关系字段。
参考:https://docs.djangoproject.com/en/4.1/ref/models/fields/
字段选项(字段参数)
参考:https://docs.djangoproject.com/en/4.1/topics/db/models/#field-options
通过选项实现对字段的约束,选项如下:
- default 默认值。设置默认值。
- primary_key 若为True,则该字段会成为模型的主键字段,默认值是False,一般作为AutoField的选项使用。
- unique 如果为True, 这个字段在表中必须有唯一值,默认值是False。
- db_index 若值为True, 则在表中会为此字段创建索引(相当于目录),默认值是False。
- db_column 字段的名称,如果未指定,则使用属性的名称。
- null 如果为True,表示允许为空,默认值是False。
- blank
如果为True,则该字段允许为空白,默认值是False。(一般是后台管理处输入是否可为空格等)
【对比】:null是数据库范畴的概念,blank是后台管理页面表单验证范畴的。
【经验】:当修改模型类之后,如果添加的选项不影响表的结构,则不需要重新做迁移,商品的选项中default和blank不影响表结构。
- choices
一个2元组的序列,用作此字段的选项。如果给定了这个选项,默认的表单小部件将是一个选择框,而不是标准文本字段,并将选择限制为给定的选项。
示例如下:
YEAR_IN_SCHOOL_CHOICES = [
('FR', 'Freshman'),
('SO', 'Sophomore'),
('JR', 'Junior'),
('SR', 'Senior'),
('GR', 'Graduate'),
]
例1:btitle = models.CharField(max_length=20,unique=True) #该字段不能重复
例2:btitle = models.CharField(max_length=20,db_column='title') #自定义表字段的名称为title
from django.db import models
# 设计和表对应的类,模型类
# 图书类
class BookInfo(models.Model):
# 图书名称,CharField说明是一个字符串,max_length指定字符串的最大长度
btitle = models.CharField(max_length=20,unique=True) #该字段不能重复
# 出版日期,DateField说明是一个日期类型
bpub_date = models.DateField()
# 整型,阅读量
bread = models.IntegerField(default=0)
# 整型,评论量
bcomment = models.IntegerField(default=0)
# 布尔类型,删除标记
is_delete = models.BooleanField(default=False)
def __str__(self): #重定义系统的str方法,让它返回对应图书的名字
return self.btitle
class HeroInfo(models.Model):
# 英雄名称
hname = models.CharField(max_length=20)
# 性别,BooleanField说明是bool类型,default指定默认值,False代表男
hgender = models.BooleanField(default=False)
# 备注
hcomment = models.CharField(max_length=128)
# 关系属性 hbook,建立图书类和英雄人物类之间的一对多关系
# 关系属性对应的表的字段名格式: 关系属性名_id
hbook = models.ForeignKey('BookInfo', on_delete=models.CASCADE) #对应BookInfo表的主键ID
# 删除标记
is_delete = models.BooleanField(default=False)
def __str__(self): #返回英雄名
return self.hname
基础字段
AutoField () 自动增长的IntegerField,通常不用指定,不指定时Django会自动创建属性名为id的自动增长属性。 |
CharField() 字符字段
|
TextField() 适合大量文本字段
|
DateField() and DateTimeField() and TimeField()日期与时间字段
|
EmailField() 邮件字段
|
IntegerField() 整数 SlugField() URLField() BooleanField()布尔字段
|
FileField(upload_to=None, max_length=100) - 文件字段
|
ImageField(upload_to=None, height_field=None, width_field=None, max_length=100,)
|
ForeignKey(to, on_delete, **options) - 单对多关系
如下示例: staff_member = models.ForeignKey( User, on_delete=models.CASCADE, limit_choices_to={'is_staff': True}, )
|
ManyToManyField(to, **options) - 多对多关系
|
NullBooleanField 支持Null、True、False三种值。 |
DecimalField(max_digits=None, decimal_places=None) 十进制浮点数。
|
FloatField 浮点数。参数同上(精确度比上一个低) |
关系字段
OneToOneField(to, on_delete=xxx, options) - 单对单关系
- to必需指向其他模型,比如 Book or ‘self’。
- 必需指定on_delete选项(删除选项): i.e, “on_delete = models.CASCADE” or “on_delete = models.SET_NULL”。
- 可以设置 “related_name = xxx” 便于反向查询。
ForeignKey(to, on_delete=xxx, options) - 单对多关系
- to必需指向其他模型,比如 Book or ‘self’ .
- 必需指定on_delete选项(删除选项): i.e, “on_delete = models.CASCADE” or “on_delete = models.SET_NULL” .
- 可以设置”default = xxx” or “null = True” ;
- 如果有必要,可以设置 “limit_choices_to = “,
- 可以设置 “related_name = xxx” 便于反向查询。
ManyToManyField(to, options) - 多对多关系
- to 必需指向其他模型,比如 User or ‘self’ .
- 设置 “symmetrical = False “ 表示多对多关系不是对称的,比如A关注B不代表B关注A
- 设置 “through = 'intermediary model' “ 如果需要建立中间模型来搜集更多信息。
- 可以设置 “related_name = xxx” 便于反向查询。
示例:一个人加入多个组,一个组包含多个人,需要额外的中间模型记录加入日期和理由。
# django_foundation_pro/foundation_app/models.py
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)
对于OneToOneField和ForeignKey, on_delete选项和related_name是两个非常重要的设置,前者决定了关联外键删除方式,后者决定了模型反向查询的名字。
on_delete删除选项
Django提供了如下几种关联外键删除选项, 可以根据实际需求使用。
- CASCADE:级联删除。当删除publisher记录时,与之关联的所有 book 都会被删除。
- PROTECT: 保护模式。如果有外键关联,就不允许删除,删除的时候会抛出ProtectedError错误,除非先把关联了外键的记录删除掉。例如想要删除publisher,那要把所有关联了该publisher的book全部删除才可能删publisher。
- SET_NULL: 置空模式。删除的时候,外键字段会被设置为空。删除publisher后,book 记录里面的publisher_id 就置为null了。
- SET_DEFAULT: 置默认值,删除的时候,外键字段设置为默认值。
- SET(): 自定义一个值。
- DO_NOTHING:什么也不做。删除不报任何错,外键值依然保留,但是无法用这个外键去做查询。
related_name选项
related_name用于设置模型反向查询的名字,非常有用。在Publisher和Book模型里,可以通过book.publisher获取每本书的出版商信息,这是因为Book模型里有publisher这个字段。但是Publisher模型里并没有book这个字段,那么如何通过出版商反查其出版的所有书籍信息呢?
Django对于关联字段默认使用模型名_set进行反查,即通过publisher.book_set.all查询。但是book_set并不是一个很友好的名字,更希望通过publisher.books获取一个出版社已出版的所有书籍信息,这时就要修改的模型了,将related_name设为books, 如下所示:
# django_foundation_pro/foundation_app/models.py
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=50)
# address = models.CharField() # CharFields must define a 'max_length' attribute.
address = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
name = models.CharField(max_length=30)
description = models.TextField(blank=True, default="")
# blank = True 表示字段不是必需的,在客户端不是必填选项
# null = True 表示这个字段可以存储为null空值。
# publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
# 将related_name设置为books
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, related_name='books')
add_date = models.DateField(auto_now_add=True)
def __str__(self):
return self.name
再来对比一下如何通过publisher查询其出版的所有书籍,觉得哪个更好呢?
- 设置related_name前:publisher.book_set.all
- 设置related_name后:publisher.books.all
常见的Django Model META类选项
Django Model自带的META有很多的选项,都非常有用,如下:
from django.db import models
class DemoModel(models.Model):
class Meta:
# 按priority降序, order_date升序排列
get_latest_by = ['-priority', 'order_name']
# 自定义数据库里的表名字
db_table = 'custom_table'
# 自定义按哪个字段排序,-代表逆序
ordering = ['pub_date']
# 定义APP的标签
app_label = 'foundation_app' # 表示本模型所属的APP:foundation_app
# 指定该模型为抽象模型。声明此类是否为抽象
abstract = True
# 添加授权,为模型自定义权限
permissions = (
('can_deliver_pizzas', 'Can deliver pizzas'),
)
# 指定该模型为代理模型
proxy = True
# verbose_name、verbose_name_plural为模型设置便于人类阅读的别名
verbose_name = '字段别名'
verbose_name_plural = '这是字段别名'
# 默认为True,如果为False,Django不会为这个模型生成数据表
managed = False
# 为数据表设置索引,对于频繁查询的字段,建议设置索引
indexes = []
# 给数据库中的数据表增加约束
constraints = 'name'
模型的方法
标准方法
以下三个方法是Django模型自带的三个标准方法:
- def __str__():给单个模型对象实例设置人为可读的名字(可选)。
- def save():重写save方法(可选)。
- def get_absolute_url():为单个模型实例对象生成独一无二的url(可选)
除此以外,经常自定义方法或Manager方法
自定义方法
# django_foundation_pro/foundation_app/models.py
def get_absolute_url(self):
print('模型实例对象生成独一无二的URL')
return reverse('demo:demo_detail', args=[str(self.id)])
def custom_func(self):
"""
自定义方法
:return:
"""
print('自定义方法')
self.views += 1
self.save(update_fields=['views'])
自定义Manager方法
# django_foundation_pro/foundation_app/models.py
class CustomManager(models.Model):
def get_queryset(self):
return super().get_queryset().filter(author='Rocket')
class Book2(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=50)
objs = models.Manager() # 默认manager方法
custom_objs = CustomManager() # 自定义manager方法
案例:Django Model模型
假设要开发一个餐厅(restaurant)的在线点评网站,允许用户(user)上传菜肴(dish)的图片并点评餐厅,可以设计如下模型。用户与餐厅,餐厅与菜肴,及用户与菜肴都是单对多的关系。可以这样理解:一个用户可以访问点评多个餐厅,一个餐厅有多个菜肴,一个用户可以上传多个菜肴的图片。
# django_foundation_pro/foundation_app/models.py
from django.db import models
from django.contrib.auth.models import User
from datetime import date
class Restaurant(models.Model):
name = models.TextField()
address = models.TextField(blank=True, default='')
telephone = models.CharField(max_length=22)
url = models.URLField(blank=True, null=True)
user = models.ForeignKey(User, default=1, on_delete=models.CASCADE)
date = models.DateField(default=date.today)
def __str__(self):
return self.name
class Dish(models.Model):
name = models.TextField()
description = models.TextField(blank=True, default='')
price = models.DecimalField('USD amount', max_digits=8, decimal_places=2, blank=True, null=True)
user = models.ForeignKey(User, default=1, on_delete=models.CASCADE)
date = models.DateField(default=date.today)
image = models.ImageField(upload_to='myrestaurants', blank=True, null=True)
# Related name "dishes" allows you to use restaurant.dishes.all to access all dishes objects
# instead of using restaurant.dish_set.all
restaurant = models.ForeignKey(Restaurant, null=True, related_name='dishes', on_delete=models.CASCADE)
def __str__(self):
return self.name
# This Abstract Review can be used to create RestaurantReview and DishReview
class Review(models.Model):
RATING_CHOICES = ((1, 'one'), (2, 'two'), (3, 'three'), (4, 'four'), (5, 'five'))
rating = models.PositiveSmallIntegerField("Rating", blank=False, default=3, choices=RATING_CHOICES)
comment = models.TextField(blank=True, null=True)
user = models.ForeignKey(User, default=1, on_delete=models.CASCADE)
date = models.DateField(default=date.today)
class Meta:
abstract = True
class RestaurantReview(Review):
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
def __str__(self):
return '{} review'.format(self.restaurant.name)
Django自带的models里有两个非常重要的类,一个是models.Model, 另一个是models.Manager。models.Manager的使用参考:Django开发总结(8):模型Models高级进阶。
Django模型设计细节
Dish模型里有一个restaurant的字段,建立了一个单对多的关系。可以通过dish.restaurant.name直接查询到菜肴所属的餐厅的名字。然而Restaurant模型里并没有dish的字段,如何根据restaurant查询到某个餐厅的所有菜肴呢?Django非常聪明,可以通过在dish小写后面加上'_set'进行反向查询。本来可以直接通过restaurant.dish_set.all的方法来进行查找的,然而这个方法并不直观。为了解决这个问题,在dish模型里设置'related_name = dishes", 这样就可以直接通过restaurant.dishes.all来反向查询所有菜肴了。
注意一但设置了related name, 将不能再通过_set方法来反向查询。
restaurant = models.ForeignKey(Restaurant, related_name='dishes', on_delete=models.CASCADE)
需要关注的是Review模型里,设置了META选项: Abstract = True。这样一来Django就会认为这个模型是抽象类,而不会在数据库里创建review的数据表。
微信公众号搜索【CTO Plus】关注后,获取更多,我们一起学习交流。
输入才有输出,吸收才能吐纳。——码字不易