拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django模型字段全解析:类型、参数与迁移实战

Django模型字段全解析:类型、参数与迁移实战 1. 为什么字段定义是Django模型的灵魂Django开发里除了路由、视图和模板这些“门面”真正决定数据结构和业务逻辑底子的其实是models.py里一个个字段定义。每次接手别人的项目我第一件事不是读视图而是打开models文件扫一遍字段——基本上字段长什么样项目是干嘛的、能查到什么数据、未来好不好扩展心里就有数了。这篇就把Django常用字段一次性理清楚从类型、参数到迁移和查询删除配合实战例子说透无论是刚入门的新手还是有经验想查漏补缺的同学都能从这里找到直接能用的东西。1.1 字段决定了数据的存储形态Django的ORM把Python类映射成数据库表类属性映射成表中的列。一个字段的声明表面上只是定义了“这一列叫什么名字、是什么类型”实际上它在数据库层面决定了三件事存储格式、约束规则、索引方式。比如IntegerField在MySQL里可能对应int在PostgreSQL里对应integerCharField对应varcharBooleanField在很多数据库里就是tinyint(1)或boolean。你换了后端数据库迁移时会自动根据Django的字段适配但业务代码里你依然是操作Python对象几乎不用感知这种差异。这种抽象带来的好处很直接你不用手写SQL建表也不用手写数据校验。Django会根据字段类型自动生成表单控件、Admin后台的输入框、序列化器的验证规则。比如EmailField会自动校验邮箱格式URLField会自动校验URLDecimalField自动限制小数位。这些能力全都是从字段声明里“长出来”的所以很多初学者发现Django的Admin很神奇其实底层就是字段定义在起作用。1.2 字段是业务逻辑的第一道关卡字段设计得好不好直接决定业务代码的复杂程度。一个常见的例子用户状态用BooleanField表示“是否启用”还是用CharField配合choices表示“启用/禁用/待审核”三种状态如果一开始只用了布尔值后面产品加需求说要“待审核”你就得改表、改迁移、改所有查询逻辑成本远大于一开始多用几分钟想清楚。字段就像是业务的“地基”地基歪了上面盖的楼再漂亮也危险。所以我给团队做代码评审时一定先从models.py看起。如果字段类型选错、null和blank混用、外键级联策略乱选后面全是隐性问题。字段定义不只是一个技术选择更是一个沟通业务边界的工具。理解了这一点才会明白为什么花时间把字段设计做扎实比急着把功能跑通更重要。2. 常用字段类型一览与应用场景Django内置的字段类型非常多但实际开发中高频使用的也就十几个。下面这张表是我自己常用的字段清单标注了核心参数和典型场景方便对照着选型。字段类型数据库对应以MySQL为例核心参数典型使用场景AutoFieldint auto_incrementprimary_keyTrue自增主键默认主键CharFieldvarcharmax_length必填短文本姓名、标题、手机号TextFieldlongtext无长文本文章正文、备注IntegerFieldint无整数数量、年龄、星级FloatFielddouble无浮点数不建议用于金额DecimalFielddecimalmax_digits,decimal_places金额、税率等精确小数BooleanFieldtinyint(1)无开关状态是否启用、是否删除DateTimeFielddatetimeauto_now_add,auto_now创建时间、更新时间DateFielddate无生日、日期EmailFieldvarchar 校验max_length邮箱地址URLFieldvarchar 校验max_length链接地址UUIDFieldchar(32)defaultuuid.uuid4分布式主键、公开IDForeignKeyintto,on_delete多对一关系文章归属作者ManyToManyField中间表to,related_name多对多关系文章与标签OneToOneFieldint 唯一约束to,on_delete一对一扩展用户资料JSONFieldjson无动态JSON数据、配置存储2.1 文本类字段CharField与TextField怎么选CharField是使用频率最高的字段但也是最容易出错的——因为**max_length是必填参数**。很多人第一次写模型直接写name models.CharField()一迁移就报错提示缺少max_length。这个参数对应数据库里的varchar长度上限Django在表单层也会用它做最大长度校验。注意max_length不是越大越好如果用CharField存了大段文本数据库层面会很臃肿索引也会变慢。正确的做法是255以内、需要参与搜索或索引的短文本用CharField纯长内容、不需要精确匹配的用TextField。TextField没有长度限制对应longtext或text类型。它不适合做unique约束也不建议直接对它建索引某些数据库会限定index长度。如果你要存富文本、Markdown原始内容、JSON字符串都应该用TextField。我见过有人为了防止报错强行给TextField也加max_length其实这是无效的Django会忽略这个参数。2.2 数字类字段Integer、Float与Decimal的恩怨数字字段的选择核心是“精度”和“存储体积”的权衡。IntegerField适合整数比如点赞数、库存量FloatField是浮点数底层是IEEE754标准会有精度损失。如果你拿FloatField存金额比如9.99存进去可能变成9.990000000000002计算结果就差了那么一点点这在财务场景是绝对不能接受的。所以涉及金额、单价、折扣率等需要精确计算的场景一定要用DecimalField。DecimalField必须提供两个参数max_digits总位数和decimal_places小数位。比如max_digits10, decimal_places2表示最大“整数部分8位 小数2位”一共10位。定义的时候多花一秒算清楚能避免很多脏数据。举个例子price models.DecimalField(verbose_name商品价格, max_digits10, decimal_places2)这一行就保证了数据库里的价格不可能出现超过8位整数和2位小数进而避免超范围数字。2.3 时间字段DateTimeField的坑与技巧DateTimeField在开发中几乎是每张表必备因为绝大部分业务都需要记录创建时间和更新时间。Django提供了两个神奇的参数auto_now_add和auto_now。auto_now_addTrue只在对象第一次入库时自动设为当前时间之后修改不会变。适合存“创建时间”。auto_nowTrue每次调用save()时都会更新为当前时间。适合存“修改时间”。这两个参数用起来很方便但注意它们只在Model.save()时生效。如果你用QuerySet.update()批量更新这两个字段是不会自动刷新的。比如Article.objects.filter(author_id1).update(statuspublished)此时updated_at不会自动变化。如果你需要批量更新也能维护时间要么改成在循环里逐个save()要么自己手动带上当前时间。时区问题也是老生常谈。项目中设置USE_TZ True时Django会统一使用UTC时间存取数据库在渲染到前端模板或API返回时才转成当前时区。初学者经常发现Admin后台显示的时间和本地时间相差8小时多半是TIME_ZONE没设置或者模板里没做时区转换。建议开发时就把TIME_ZONE Asia/Shanghai配好并且保持USE_TZ True。2.4 关系字段ForeignKey、ManyToManyField与OneToOneField关系字段是Django模型的精髓。ForeignKey表示“多对一”比如“一篇文章属于一个作者”就在文章里放一个author models.ForeignKey(Author, on_deletemodels.CASCADE, related_namearticles)。这里的on_delete是Django 2.0之后必填的它定义了当被关联的对象被删除时当前对象怎么处理。常见的on_delete选项有选项行为CASCADE级联删除父记录删了子记录一起删PROTECT阻止删除如果还有子记录引用会抛出ProtectedErrorSET_NULL把外键设为NULL前提是该字段必须设置nullTrueSET_DEFAULT把外键设为默认值前提是设置了defaultDO_NOTHING什么都不做但数据库层面可能因约束报错ManyToManyField会生成一张中间关系表比如文章和标签的关系。你可以在Article里写tags models.ManyToManyField(Tag, related_namearticles, blankTrue)。操作时通过.add()、.remove()、.set()、.clear()来管理关联关系。OneToOneField本质上是ForeignKey加uniqueTrue适合“用户资料表”这类一对一扩展。它与ForeignKey最大的区别在于反向查询时返回的是单个对象而不是QuerySet。3. 字段通用参数详解除了字段类型Django还提供了一组通用参数用来控制字段的约束、展示和行为。这些参数放在每个字段的括号里排列方式非常灵活但是每个参数背后的语义必须搞清楚尤其是null和blank这对“难兄难弟”。3.1 null、blank与default的取舍null是数据库层面的概念表示该列能否存NULL值。blank是表单层面的概念表示该字段是否允许不填。两者经常被混用但必须记住一个黄金法则字符串字段不要用nullTrue用blankTrue就够了。为什么因为Django的CharField和TextField内部会通过get_default拿到空字符串如果你允许nullTrue那么数据库里既可能有也可能有NULL这会让查询“是否为空”变得异常痛苦——你要同时过滤field和field__isnullTrue两种情况。正确做法是nickname models.CharField(verbose_name昵称, max_length50, blankTrue, default)这里default保证了新创建的对象至少是空字符串而不是NULL。对于整型、浮点型、日期型字段如果希望“未填写时存NULL”那就可以用nullTrue同时配合blankTrue表示表单里可以不填。default参数支持固定值和可调用对象这个特性非常有用。比如token models.UUIDField(verbose_name随机令牌, defaultuuid.uuid4, editableFalse)defaultuuid.uuid4每次新增对象时都会生成一个新的UUID。同理时间字段的defaulttimezone.now也很常见。注意default只在创建对象时生效update()批量更新时不会自动填充默认值。3.2 choices、validators和verbose_namechoices用来固定字段的可选值本质上是一种“枚举约束”。它让数据在入口处就不可能出现奇奇怪怪的值同时Django会自动把枚举值渲染到Admin表单的下拉框里。推荐在Python 3.10 的Django中使用TextChoices枚举类可读性和维护性甩开老式二元组两条街class Article(models.Model): class Status(models.TextChoices): DRAFT draft, 草稿 PUBLISHED published, 已发布 ARCHIVED archived, 已归档 status models.CharField(verbose_name状态, max_length20, choicesStatus.choices, defaultStatus.DRAFT)这样在代码里你就可以写Article.Status.DRAFT不用记裸字符串也不会拼写错。validators参数用来追加自定义校验函数适合在字段类型自带校验之外再加规则。比如phone models.CharField(verbose_name手机号, max_length11, validators[RegexValidator(r^1\d{10}$)])RegexValidator会在表单和full_clean()时触发防止脏数据入库。verbose_name则是给人看的字段名如果不写Django默认把字段名下划线转空格当作标签展示在Admin后台。我建议每个字段都显式加上verbose_name中文项目里这个习惯尤其重要不然后台一堆英文标签产品经理看着头疼。3.3 db_index、unique与几个高级参数db_indexTrue表示给这一列加数据库索引适合经常作为查询条件的字段。注意索引不是越多越好唯一索引会略微影响写入性能需要在业务场景里权衡。uniqueTrue表示该列不允许重复值并在数据库层面自动创建唯一约束。比如用户名字段、昵称字段、订单号字段都适合加unique。editableFalse用来隐藏字段在Admin后台的表单展示同时不影响该字段的写入。最典型的应用是created_at models.DateTimeField(auto_now_addTrue, editableFalse)让创建时间只能由程序自动维护后台不显示。db_column可以让你把Python字段名映射到一个不同的数据库列名一般用于兼容旧表平时不常用。4. 核心实操定义模型字段、迁移与数据库交互理论知识讲再多不如动手写一遍。下面用一个小型博客项目作为实战演练包含作者、文章、标签三张表覆盖了ForeignKey、ManyToManyField、choices、时间字段等高频用法。跟着敲完你对常用字段的理解会有质的提升。4.1 定义博客模型的字段先在自己的Django项目里创建一个app假设叫blog。然后打开blog/models.py把下面的代码贴进去from django.db import models from django.utils import timezone import uuid class Author(models.Model): name models.CharField(verbose_name作者姓名, max_length50) email models.EmailField(verbose_name邮箱, nullTrue, blankTrue) created_at models.DateTimeField(verbose_name创建时间, auto_now_addTrue) class Meta: verbose_name 作者 verbose_name_plural 作者 def __str__(self): return self.name class Tag(models.Model): name models.CharField(verbose_name标签名, max_length30, uniqueTrue) slug models.SlugField(verbose_name链接别名, max_length40, uniqueTrue, blankTrue) class Meta: verbose_name 标签 verbose_name_plural 标签 def __str__(self): return self.name class Article(models.Model): class Status(models.TextChoices): DRAFT draft, 草稿 PUBLISHED published, 已发布 ARCHIVED archived, 已归档 title models.CharField(verbose_name标题, max_length200) content models.TextField(verbose_name正文) author models.ForeignKey(Author, on_deletemodels.CASCADE, related_namearticles) tags models.ManyToManyField(Tag, related_namearticles, blankTrue) status models.CharField(verbose_name状态, max_length20, choicesStatus.choices, defaultStatus.DRAFT) views models.IntegerField(verbose_name浏览量, default0) created_at models.DateTimeField(verbose_name创建时间, auto_now_addTrue) updated_at models.DateTimeField(verbose_name更新时间, auto_nowTrue) class Meta: verbose_name 文章 verbose_name_plural 文章 ordering [-created_at] def __str__(self): return self.title这段代码里可以看到几个关键点ForeignKey的related_namearticles指定了反向查询的名字这样以后拿到一个Author对象可以直接用author.articles.all()获取他所有的文章。ManyToManyField的related_namearticles同理Tag.objects.filter(articles__title__icontainsDjango)这种跨关系查询也会用到。SlugField本质上就是一个限定了合法字符的CharField适合存URL里的短标识。IntegerField配合default0做计数器不需要每次手动初始化。ordering [-created_at]是Meta级别的排序设置让列表页默认按最新文章排序。4.2 生成并执行数据库迁移模型定义好之后需要让Django根据这些字段生成数据库表。这一步分两个命令python manage.py makemigrations blog python manage.py migrate执行完makemigrations后Django会在blog/migrations/目录下生成一个迁移文件。建议你打开这个文件看一下里面就是字段定义对应的迁移操作。很多新手看到生成的代码会害怕其实它只是把模型字段翻译成数据库操作而已。确认迁移文件里的字段和你写的一致再migrate。如果执行makemigrations时提示No changes detected说明Django认为模型字段没有变化。这时检查一下你的APP是否已经注册到了INSTALLED_APPS里或者是否改了其他APP的models却执行了错误的APP名。如果是第一次创建模型INSTALLED_APPS里漏了blog同样不会生成迁移文件。4.3 交互式Shell创建、查询、更新、删除对象字段定义和迁移都是为了最终的数据操作服务的。Django提供非常强大的ORM API下面通过python manage.py shell演示最常用的增删改查。进入shellpython manage.py shell先导入模型from blog.models import Author, Tag, Article创建对象author Author.objects.create(name张三, emailzhangsanexample.com) tag_python Tag.objects.create(namePython, slugpython) tag_django Tag.objects.create(nameDjango, slugdjango-section)注意Tag.objects.create(namePython, slugpython)这里的slug是我们在模型里定义的SlugField必须唯一。如果重复创建同名标签会抛出IntegrityError因为uniqueTrue。再创建文章article Article.objects.create( titleDjango常用字段实战, content这是一篇讲字段的实战文章, authorauthor, statusArticle.Status.PUBLISHED ) article.tags.add(tag_python, tag_django)这里通过article.tags.add()把两篇标签关联到文章。ManyToManyField的关联关系存在中间表里add()可以传多个对象也可以传一个列表。查询对象最常用的查询方法就是all()、filter()和get()。get()只允许返回一个对象如果结果多于一条或者为空会抛出MultipleObjectsReturned和DoesNotExist。# 获取所有已发布的文章 published_articles Article.objects.filter(statusArticle.Status.PUBLISHED) # 按照标题模糊查询 django_articles Article.objects.filter(title__icontainsDjango) # 跨外键查询查询某个作者的所有文章 zhang_articles Article.objects.filter(author__name张三) # 反查拿到作者对象再查文章 zhang Author.objects.get(name张三) articles_of_zhang zhang.articles.all() # 统计标签为Python的文章数量 count Article.objects.filter(tags__namePython).count()更新对象更新有两种方式。一种是先取到对象再改属性然后save()article Article.objects.get(titleDjango常用字段实战) article.status Article.Status.ARCHIVED article.save()另一种是批量更新直接对QuerySet用update()Article.objects.filter(statusArticle.Status.DRAFT).update(statusArticle.Status.PUBLISHED)注意之前提到过的批量update()不会触发auto_nowTrue的时间更新。如果你需要时间也刷新得手动加上Article.objects.filter(statusArticle.Status.DRAFT).update(statusArticle.Status.PUBLISHED, updated_attimezone.now())删除对象删除操作是面试和日常都要重点关注的内容。# 删除单篇文章 article Article.objects.get(id1) article.delete() # 删除所有草稿 Article.objects.filter(statusArticle.Status.DRAFT).delete() # 删除作者及其级联的文章 author Author.objects.get(name张三) author.delete()因为我们在ForeignKey里设置了on_deletemodels.CASCADE所以删除作者的时候张三的所有文章会被一起删除。如果文章比较多、数据很重要建议使用PROTECT或SET_NULL来防止误删。手动在真实环境执行delete()前最好先加一层确认逻辑或者用django-guardian之类的软删除方案。4.4 使用ImageField/FileField时静态文件显示不出来的排查思路有的同学在模型里加了ImageField或FileField上传图片后却发现页面上的img标签加载不出来。顺手提到这个“VSCode里写img标签在Django的static文件里显示不了”的高频问题其实它跟字段本身关系不大但确实会让很多新手误以为是字段写错了。如果你在models.py里写了cover models.ImageField(verbose_name封面图, upload_tocovers/%Y/%m/, blankTrue, nullTrue)要确保项目里配置了MEDIA_ROOT和MEDIA_URL# settings.py MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media然后在主urls.py里开发阶段加一句from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 你的路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)如果你的图片是手动放在static/目录下而不是通过ImageField上传的那还要保证STATIC_URL和STATICFILES_DIRS配置正确并且模板里使用了{% load static %}和{% static img/xxx.png %}。检查顺序先看浏览器Network面板里资源报不报404再看request路径是不是/media/...别再傻傻盯着models.py看了。5. 新手常踩的坑与排查技巧5.1 CharField忘记max_length这个错误出现频率极高。一执行makemigrations就报TypeError: __init__() missing 1 required positional argument: max_length缓解办法只有一个写字段时先想清楚最长输入是什么。用户名10位昵称50位标题200位判断题主字段要不要加max_length是白费脑筋必须加。5.2 nullTrue和blankTrue傻傻分不清楚再强调一遍null是数据库层面的blank是表单层面的。对于字符串字段只写blankTrue不要写nullTrue否则数据库里会混杂空字符串和NULL查询时总是要多写一层Q(field) | Q(field__isnullTrue)非常烦人。对于非字符串字段整数、日期、外键如果允许空就同时写nullTrue, blankTrue这样表单可以不填而且数据库里存NULL。5.3 ForeignKey忘记on_deleteDjango 2.0之后如果外键不写on_delete系统直接抛错。这个设计是对的逼你想清楚删除父表时子表怎么办。我建议默认都用CASCADE但凡是有关键业务数据的改成PROTECT或SET_NULL。比如文章作者的删除可能希望保留所有历史文章那外键就写成on_deletemodels.SET_NULL, nullTrue作者没了文章还在只是author变成NULL。5.4 ManyToManyField的操作没有生效多对多关系的增删改查里有一个很容易踩的坑不能直接给ManyToManyField赋值。比如article.tags [tag_python, tag_django] # 可以但会替换整个关联集合实际上想清楚到底是“加关联”还是“替换关联”# 新增关联 article.tags.add(tag_python) # 增加多个 article.tags.add(*[tag_python, tag_django]) # 替换整个关联集合 article.tags.set([tag_python]) # 移除指定 article.tags.remove(tag_python) # 清空全部 article.tags.clear()批量操作时别忘了先获取对象再操作。5.5 迁移失败No migration to apply / 表已存在有时候你删了migrations文件夹里的迁移文件手动改了数据库再执行makemigrations就提示没有变化。原因是Django会在django_migrations表里记录已经执行过的迁移文件名。可以先执行python manage.py showmigrations查看哪些迁移被打上了[X]标记。如果是本地开发环境最简单的办法是备份数据后清空django_migrations表并把相关表删掉再重新执行makemigrations migrate。但生产环境千万别乱来应该通过生成新的迁移文件来调整。5.6 DateTimeField时区导致的时间偏移上一篇提到的时区问题再说一遍。如果你设置了USE_TZTrue数据库里存的是UTC时间在代码里用datetime.now()去过滤就会错位。解决办法是在代码里统一使用from django.utils import timezone用timezone.now()获取当前时间。模板渲染时Django会自动根据TIME_ZONE转换所以后台展示基本不会出问题。坑在于你直接在Django shell里打印时间看到的是UTC以为是bug其实不是。5.7 ImageField/FileField的FileNotFoundError和路径问题字段声明了upload_tocovers/%Y/%m/上传的文件会按年份月份自动分目录存储。如果删除对象时没有清理物理文件会留下孤儿文件。Django官方并不支持自动清理文件需要自己在delete()或信号里处理。如果生产环境用Nginx直接托管MEDIA_ROOT目录还要注意Nginx进程要有读写权限。我见过不少部署问题最后都出在目录权限上而不是字段代码。6. 常用字段的最佳实践与个人体会6.1 字段命名统一且简洁不要用保留字字段名要让你自己三个月后还能一眼看懂。比如name、title、status、created_at这些命名通用又清晰。避免用user这种名字它在某些数据库里是保留字容易出状况也不要和模型类名完全一样。author字段放在Article里意思很清楚如果再叫author_id反而冗余因为Django会自动为外键生成author_id列。6.2 用抽象基类统一公共字段几乎所有模型都有created_at和updated_at每次复制粘贴很容易漏。我习惯定义一个抽象基类class BaseModel(models.Model): created_at models.DateTimeField(verbose_name创建时间, auto_now_addTrue) updated_at models.DateTimeField(verbose_name更新时间, auto_nowTrue) class Meta: abstract True然后其他模型继承class Article(BaseModel): ...这样每张表都自动拥有两个时间字段少写很多重复代码。注意abstract True的模型不会生成数据库表它只是字段集合的复用。6.3 使用TextChoices替代老式choices老式的choices是二元组列表可读性差。现在强烈推荐用TextChoices枚举class Article(models.Model): class Status(models.TextChoices): DRAFT draft, 草稿 PUBLISHED published, 已发布这个写法有两个好处一是代码里可以用Article.Status.DRAFT二是Django Admin和表单会自动显示可读标签“草稿”“已发布”。如果枚举很多还能直接通过Article.Status.values批量渲染。6.4 索引和约束不能盲目叠加字段里db_indexTrue和uniqueTrue值得认真对待。索引能加快查询但会拖慢写入。一个BooleanField或者CharField只有很少的枚举值加索引意义不大频繁作为filter条件的字段才值得加。多字段联合唯一约束需要在Meta里用constraints或unique_together比如订单表和订单明细表之间避免对单个字段加unique误伤业务逻辑。6.5 JSONField虽好但别贪杯JSONField在PostgreSQL里非常高效MySQL 5.7以上也支持。它能存储动态结构的数据免去建多张表的烦恼。但JSON字段有个问题很难做数据库层面的约束和关联查询后期数据量大了也不好维护。所以它适合存“确实结构不固定”的配置项、快照数据不适合存有明确业务关系的数据。能用外键关联表解决的问题不要图一时省事塞进JSON里。6.6 关于字段改动的一点体会最后分享一个实际工作里的感受。模型字段一旦上线改动成本会比想象中高很多。增字段相对轻松一次迁移就能搞定改字段类型、改关联关系、改related_name都会牵扯到线下数据、缓存、接口文档、甚至前端展示。所以每次写字段时我都会问自己三个问题这个字段的取值边界是什么max_length、choices、default都明确了吗如果以后要扩展当前的设计是否能平滑过渡比如状态字段是否需要预留“未知”状态外键被删时到底应该级联、保护还是置空这个字段真的需要自己存吗能不能通过关联字段计算出来这些问题想清楚了字段定义才不会成为项目的“债”。另一点是字段命名和verbose_name尽量在模型里写清楚因为Django Admin、DRF序列化器、甚至Swagger文档都会直接引用这些信息。我对团队的要求是模型定义就是低配版接口文档字段写得好沟通、维护、交接全都能省力。你以后维护老项目时看到字段定义清晰、choices规范、null和blank使用得当的代码真的会由衷感到舒服。
返回列表