django model 数据模型教程
2025年2月17日大约 12 分钟约 3697 字
优化User模型
用户模型必须在第一次迁移前就确定下来,后期如果修改则需要重新构建数据库。因为一旦迁移应用到数据库,内置应用(比如 admin)的迁移就会依赖于原有的用户模型(默认是 auth.User),而修改后会导致迁移依赖关系不一致,从而产生错误。
扩展内置User模型
users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser, Group, Permission, User
# 给用户表添加字段
models.CharField('公司名称', max_length=200, db_index=True, default='缺省').contribute_to_class(User, 'company')
models.BooleanField('是否为企业管理员', blank=True, default=False, choices=((True, '企业管理员'), (False, '普通账号')), help_text='请注意企业管理员可查看同企业其他用户数据。').contribute_to_class(User, 'is_admin')
models.BooleanField('是否为平台成员', blank=True, default=False, choices=((True, '平台成员'), (False, '公司成员')), help_text='请注意平台用户可使用平台媒体账号。').contribute_to_class(User, 'is_platform')
User.__str__ = lambda self: self.first_name if self.first_name else self.username
# 给多对多中间表动态添加字段(中间表无法被makemigrations检测,请手动在数据库添加字段)
through_model = User.groups.through
models.DateTimeField(auto_now_add=True, verbose_name='创建时间').contribute_to_class(through_model, 'create_time')
users/management/commands/create_user_group_through_field.py
"""
在数据库中手动创建字段
执行命令 python .\manage.py create_user_group_through_field 在数据库中创建字段
"""
from django.core.management.base import BaseCommand
from django.db import models, connection, OperationalError
from django.apps import apps
class Command(BaseCommand):
help = '给用户与组多对多中间表添加 创建时间 字段'
def handle(self, *args, **kwargs):
through_model = apps.get_model('auth', 'User').groups.through
models.DateTimeField(auto_now_add=True, verbose_name='创建时间').contribute_to_class(through_model, 'create_time')
try:
with connection.schema_editor() as schema_editor:
schema_editor.add_field(through_model, through_model._meta.get_field('create_time')) # 在数据库中创建字段
self.stdout.write(self.style.SUCCESS("成功添加字段 create_time 到多对多中间表"))
except OperationalError as e:
if 'Duplicate column name' in str(e):
self.stdout.write(self.style.WARNING("字段 create_time 已经存在于多对多中间表中"))
else:
raise e
自定义用户模型
users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
SEX_CHOICES = [(True, '男'), (False, '女')]
# 扩展内置的user表(内置字段有:first_name,last_name,username,password,email,is_superuser,is_active,is_joined,date_joined,last_login)
class UserModel(AbstractUser):
"""
用户表
"""
nickname = models.CharField(verbose_name="昵称", max_length=50, default="", help_text="昵称")
birthday = models.DateField("生日", null=True, blank=True, help_text="生日")
sex = models.BooleanField('性别', choices=SEX_CHOICES, default=1, help_text="性别")
address = models.CharField("地址", max_length=100, default="", help_text="地址")
mobile = models.CharField("手机号", max_length=11, null=True, blank=True, help_text="手机号")
image = models.ImageField("头像路径", upload_to="head/%Y/%m", default="head/default.png", max_length=200, help_text="头像路径")
class Meta:
verbose_name = "用户信息"
verbose_name_plural = verbose_name
db_table = "auth_user" # 自定表名(选填,不过这里建议还是这么写。默认表名为:app名_模型名小写)
def __str__(self):
return self.username
扩展完用户表,必须要在settings.py文件内设置:
AUTH_USER_MODEL = ‘users.UserModel’
创建其他模型
persons/models.py
关于登记和搜集的人员信息表和人员的家庭关系表
from django.db import models
from django.contrib.auth import get_user_model # UserModel的快捷方式,当然你也可以自己手动导入用户的Model
UserModel = get_user_model()
SEX_CHOICES = [(1, '男'), (0, '女')]
# 人员信息表
class PersonModel(models.Model):
id = models.IntegerField(primary_key=True) # 手动设置主键,不建议填写,django会自动添加id主键,这里只是手写示例
name = models.CharField(verbose_name='姓名', max_length=10, help_text="姓名")
sex = models.BooleanField('性别', choices=SEX_CHOICES, help_text="性别")
age = models.IntegerField('年龄', blank=True, null=True, help_text="年龄")
mail = models.EmailField('邮箱', blank=True, "邮箱")
phone = models.CharField('电话', max_length=20, blank=True, default='', help_text="电话")
describe = models.TextField('人员其他详细信息', , help_text="人气其他详细信息")
icon = models.FileField('头像', upload_to="head/", blank=True, default='', help_text="头像")
create_time = models.DateTimeField('创建时间', auto_now_add=True, editable=False, help_text="创建时间")
owner = models.ForeignKey(UserModel, related_name='persons', on_delete=models.CASCADE, verbose_name='创建人', help_text="创建人") # 把数据外键关联到一个所有者
class Meta:
ordering = ('create_time', '-age') # 默认排序为创建时间正序,年龄字段倒序
verbose_name = "人员信息"
verbose_name_plural = verbose_name # 后台admin中,复数形式展示
def __str__(self): # 在后台admin列表中显示的字段
return self.name # 这里填入的字段的数据内容不要为空
# 如果需要处理保存前的模型数据,重写save(),这里只是示例
def save(self, *args, **kwargs):
self.phone = self.phone and '+86' + self.phone or ''
super(PersonModel, self).save(*args, **kwargs)
# 自定义一个admin管理页面显式模型字段
# 这里仅作为了解
def displayNameAge(self):
return self.name + ' ' + str(self.age) + '岁'
displayNameAge.short_description = '姓名-年龄' # 字段描述
nameInformation = property(displayNameAge)
# --------------自定义管理器-----------------
# 这里仅作为了解
class FamilyManager(models.Manager):
def number_count(self, keyword):
return self.filter(number=keyword).count()
# ----------------------------------------
RELATION_CHOICES = [('父亲', '父亲'), ('母亲', '母亲'), ('姐姐', '姐姐'), ('妹妹', '妹妹'), ('哥哥', '哥哥'), ('弟弟', '弟弟')]
# 家庭成员表
class FamilyModel(models.Model):
objects = FamilyManager() # 加载自定义的管理器
relation = models.CharField('关系', max_length=10, choices=RELATION_CHOICES, help_text="关系")
name = models.CharField(verbose_name='姓名', max_length=10, help_text="姓名")
phone = models.CharField('电话', max_length=20, blank=True, default='', help_text="电话")
# 外键,数据库键名:person_id,关联到了Person表id键
# 可以通过关联名related_name反向查询子表的数据,当然也可以通过"小写的子表名_set"的形式代替
person = models.ForeignKey(PersonModel, on_delete=models.CASCADE, related_name='personFamily', help_text="所属人")
def __str__(self):
return self.relation
迁移到数据库
将模型更新到数据库:python manage.py makemigrations users persons
python manage.py migrate
设置字段内容可为空
- blank=True:表单中可以留空,表单填写该字段的时候可以不填
- null=True:数据库字段中可以留空,数据库中会将空值(empty)存储为 NULL
- 默认值是:blank和null都是False,即不能留空
- 设置字符串和文本型字段:也可以仅将blank设为空,django会让数据库将空值(empty)存储为""
- 设置日期和数字浮点型字段:在数据库不能接受空字符串,需要将blank,null均设为True
时间字段自动更新
- auto_now:添加和修改都更新时间。
- auto_now_add:添加时更新时间,修改时不变动。
- 也可以使用default=datetime.now选项在添加时更新时间
外键的on_delete参数
- CASCADE:级联操作。如果外键对应的那条数据被删除了,那么这条数据也会被删除。
- PROTECT:受保护。即只要这条数据引用了外键的那条数据,那么就不能删除外键的那条数据。如果我们强行删除,Django就会报错。
- SET_NULL:设置为空。如果外键的那条数据被删除了,那么在本条数据上就将这个字段设置为空。如果设置这个选项,前提是要指定这个字段可以为空。
- SET_DEFAULT:设置默认值。如果外键的那条数据被删除了,那么本条数据上就将这个字段设置为默认值。如果设置这个选项, 前提是要指定这个字段一个默认值 。
- SET():如果外键的那条数据被删除了。那么将会获取SET函数中的值来作为这个外键的值。SET函数可以接收一个可以调用的对象(比如函数或者方法),如果是可以调用的对象,那么会将这个对象调用后的结果作为值返回回去。 可以不用指定默认值
- DO_NOTHING:不采取任何行为。一切全看数据库级别的约束。
以上这些选项只是Django级别的,数据级别依旧是RESTRICT!
mysql数据库层面的约束有四种:
- RESTRICT:默认的选项,如果想要删除父表的记录时,而在子表中有关联该父表的记录,则不允许删除父表中的记录;
- NOACTION:同 RESTRICT效果一样,也是首先先检查外键;
- CASCADE:父表delete、update的时候,子表会delete、update掉关联记录;
- SET NULL:父表delete、update的时候,子表会将关联记录的外键字段所在列设为null,所以注意在设计子表时外键不能设为not null;
多级分类的表设计
举例说明,假如你要对商品信息进行归类,而且每个类别下面还有子类别,无限可分。
goods/models.py
from django.db import models
from datetime import datetime
# 商品类别表
class GoodCategoryModel(models.Model):
CATEGORY_LEVEL = ( (1, "一级分类"), (2, "二级分类"), (3, "三级分类") )
name = models.CharField('类别名称', default="", max_length=30, help_text="类别名称")
code = models.CharField('类别编号', default="", max_length=30, help_text="类别编号")
description = models.TextField('类别描述', default="", help_text="类别描述")
category_level = models.IntegerField(choices=CATEGORY_LEVEL, verbose_name="类目级别", help_text="类目级别")
parent_category = models.ForeignKey("self", null=True, blank=True, verbose_name="父类目级别", help_text="父目录", related_name="sub_cat") # 这里是无限分类的重点,self代表外键到自己,如果值为空代表是一级分类
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")
导入基础数据
在迁移完数据后,我们的数据库是没有数据的,有时候需要导入一些基础数据的需求。
apps/xxxapp/tools/import_template_data.py
"""导入模板数据"""
import os
import django
import sys
pwd = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(pwd, '../../../')) # 重要:将项目根目录加入包搜索路径
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conf.settings")
django.setup()
from accounts.models import MediaModel
def import_media_data():
media_data = [
['百度', 'https://www.baidu.com/favicon.ico', 'https://www.baidu.com']
]
for item in media_data:
media, created = MediaModel.objects.get_or_create(
name=item[0],
logo=item[1],
home_page=item[2],
)
if not created:
print(f'该数据已存在,自动跳过:{item[0]}')
if __name__ == '__main__':
# 导入模板数据
import_media_data()
print('模板数据导入成功')
Django 自定义Field
字段功能:想保存一个 列表到数据库中,在读取用的时候要是 Python的列表的形式
from django.db import models
class ListField(models.TextField):
#__metaclass__ = models.SubfieldBase
description = "Stores a python list"
def __init__(self, *args, **kwargs):
super(ListField, self).__init__(*args, **kwargs)
def to_python(self, value):
if not value:
value = []
if isinstance(value, list):
return value
return ast.literal_eval(value)
def get_prep_value(self, value):
if value is None:
return value
return str(value)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
如何初始化数据库
当你想删除数据库,重新初始化时:
rm -f db.sqlite3 # 删除数据库文件
rm -r persons/migrations # 删除数据库操作记录
# 重新迁移
python manage.py makemigrations persons
python manage.py migrate
模型交互示例
python manage.py shell
from persons.models import *
# ------------------------------------获取对象-------------------------------------
# 数据表管理器
PersonModel.objects
# 指定数据库,参数为别名(setting中的设置)。如果不写会使用default默认数据库连接
PersonModel.objects.using('database_name').order_by('-craete_time')[0:500]
# 返回所有数据(QuerySet类型)
person_queryset = PersonModel.objects.all()
# 切片操作,不支持负索引,切片可以节约内存
p = PersonModel.objects.all()[:10] # 取前10条数据
p = PersonModel.objects.all()[0:2] # 取前两条数据
p[0].title
# 升序排序
d = PersonModel.objects.all().order_by('age', 'create_time')
# 降序排序
d = PersonModel.objects.all().order_by("-age")
# ------------------------------------过滤器-------------------------------------
# filter过滤器,找出符合条件的 (多个对象)
person_queryset = PersonModel.objects.filter(name=request.user, age=23) # 两个条件同时满足相等时过滤出来
person_queryset = PersonModel.objects.filter(age = 16) # 严格等于16,等于PersonModel.objects.filter(age__exact=16)
person_queryset = PersonModel.objects.filter(title__iexact="xiongda") # title为 xiongda但是不区分大小写,可以找到 XiongDa, XIONGDA, xiongda,这些都符合条件
person_queryset = PersonModel.objects.filter(age__gt = 16) # 大于
person_queryset = PersonModel.objects.filter(age__gte = 16) # 大于等于 (小余是lt,小于等于是lte)
person_queryset = PersonModel.objects.filter(name__contains = "张") # 包含关键字
person_queryset = PersonModel.objects.filter(name__icontains = "张") # 包含关键字,且不区分大小写
person_queryset = PersonModel.objects.filter(name__regex="^张") # 正则表达式查询
person_queryset = PersonModel.objects.filter(name__iregex="^张") # 正则表达式,且不区分大小写
# exclude排除符合某条件的:
PersonModel.objects.exclude(name__contains='张') # 排除姓名包含张的PersonModel模型对象
PersonModel.objects.filter(title__contains='张').exclude(age=23) # 找出姓名含有张, 但是排除年龄是23的
# 获取昨天的数据
# 获取create_time时间键昨天的数据
yesterday_datetime = datetime.datetime.now().date() - datetime.timedelta(days=1)
chain_queryset = click_task_queryset.filter(start_datetime_date=yesterday_datetime)
# ------------------------------------外键数据-------------------------------------
# 反向获取外键数据
# 获取子表FamilyModel的数据
family_queryset = User.objects.get(username=request.user).familymodel_set.all()
# 过滤出tesk外键对应的表中user外键对应的user表中某用户的chain表数据
# select_related获取相应外键对应的对象,提高查询效率,从而在之后需要的时候不必再查询数据库了
chain_queryset = Chain.objects.select_related("task").filter(task__user=request.user)
# 查找到外键数据
d = FamilyModel.objects.get(name='张发财').person.name # person是外键
# 删除外键的数据
d = FamilyModel.objects.get(number='张发财').person.delete()
# 查找到关联到自己的外键数据
d = PersonModel.objects.get(name='张三').personFamily.all() # personFamily为外键的关联名(related_name)
d = PersonModel.objects.get(name="张三").familymodel_set.all() # 没有关联名可以用默认值
# ---------------------------------------聚合---------------------------------------
# 求和、平均数、最大值、最小值、总行数、标准偏差、方差
from django.db.models import Sum, Avg, Max, Min, Count, StdDev, Variance
# 单个字段
yesterday_score = chain_queryset.aggregate(Sum('use_score'))
# 可以指定key的名字,返回一个键值对字典
Book.objects.aggregate(average_price=Avg('price'))
# 多个字段同时
Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
# 使用Count时,当参数distinct=True时,返回unique的对象数目。
# 使用StdDev时,如果sample=True,返回样本标准偏差。默认False,返回总体标准偏差。
# 使用Variance时,sample设为True时返回样本方差;默认为False,返回总体方差。
# ---------------------------------------查看---------------------------------------
# 条件查询某个数据(单个模型对象)
person = PersonModel.objects.get(name='张三')
# ---------------------------------------增加---------------------------------------
# 插入数据,方法一:
PersonModel.objects.create(name="xiongda", sex=1, owner_id=1) # 前两种方法返回的都是对应的 object
# 插入数据,方法二:
p = PersonModel(name='张三', sex=1, owner_id=1)
p.age = 23
p.save()
# 插入数据,方法三:防止重复很好的方法,但是速度要相对慢些,返回一个元组,(PersonModel对象, True/False),创建时返回 True, 已经存在时返回 False
p, result = PersonModel.objects.get_or_create(name="xiongda", sex=1, owner_id=1)
# ---------------------------------------修改---------------------------------------
# 更新数据
p = PersonModel.objects.get(name='xiongda')
p.age = 28
p.save()
# 批量更新(不用调用save函数)
PersonModel.objects.filter(age__gte=28).update(name='熊大')
# ---------------------------------------删除---------------------------------------
person_queryset = PersonModel.objects.filter(age__gte=28)
# 删除查询集数据(批量删除数据)
user_queryset.delete()
for person in person_queryset: # 循环获取每条数据
print(person.name)
person.delete() # 删除该数据(删除单条数据)
# 删除数据
p = PersonModel.objects.get(id=2)
p.delete()
# 删除部分数据
PersonModel.objects.filter(age__gt=16).delete()
# 删除所有数据
PersonModel.objects.all().delete()
# ---------------------------------------多对多---------------------------------------
# 高级命令,多对多和外键
m = Book.objects.get(id=1)
m.title
m.authors
m.authors.filter(names_icontains='p')
m.publisher
m.publisher.name
p = Publisher.objects.get(id=1)
p
p.book_set.all()
m[0].title
a = Author.objects.get(id=1)
a.book_set.all()
m[0].title
登录的视图示例
persons/views.py
from django.shortcuts import render
from django.contrib.auth import authenticate, login
from django.views.generic.base import View
from django.contrib.auth.hashers import make_password
# 注册视图
class RegisterView(View):
def get(self, request):
register_form = RegisterForm()
return render(request, "register.html", {'register_form': register_form})
def post(self, request):
register_form = RegisterForm(request.POST)
if register_form.is_valid():
user_name = request.POST.get("email", "")
pass_word = request.POST.get("password", "")
user_profile = UserModel()
user_profile.username = user_name
user_profile.email = user_name
user_profile.is_active = False # 默认未邮箱激活
user_profile.password = make_password(pass_word)
user_profile.save()
else:
pass
# 登录视图
class LoginView(View):
def get(self, request):
return render(request, "login.html")
def post(self, request):
login_form = LoginForm(request.POST) # 对比POST字典键(即前端name属性)与Form键相同的值
if login_form.is_valid(): # 先使用表单进行初步验证数据类型是否合法
user_name = request.POST.get("username", "")
pass_word = request.POST.get("password", "")
user = authenticate(username=user_name, password=pass_word) # 认证函数
if user is not None:
login(request, user) # 登录函数(在cookie和数据库django_session表中生成sessionid)
return render(request, "index.html")
else:
return render(request, "login.html", {"msg": "用户名或密码错误"})
else:
return render(request, "login.html", {"login_form": login_form})
# 自定义登录(使用用户名或邮箱登录)
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from .models import User
class CustomBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(email=username))
if user.check_password(password):
return user
except Exception as e:
return None
排坑指南
警告
报错: 在创建迁移表时,报错:ValueError: too many values to unpack
解决: 多个app汇集在apps内,app的migrations内的文件中ForeignKey
外键的to=
参数把apps
去掉。