ARTICLE DETAIL

资讯详情

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

Django模型字段选择实战:从数据库映射到迁移避坑

Django模型字段选择实战:从数据库映射到迁移避坑 最近在带几位刚从教程转向实战的新手观察到一个很一致的现象写视图、写模板大家基本都能照猫画虎可一到models.py就集体卡住卡点高度集中在Django常用字段的选择上。文档里每个字段都认识组合到自己的项目里就不会选了——IntegerField和BigIntegerField到底什么时候用CharField和TextField的边界在哪里外键要不要手动写_id日期字段怎么处理时区这篇就把Django常用字段从头到尾捋一遍。重点不是抄文档而是讲清楚每个字段在数据库里的真实落点、参数之间的实际效果以及我在真实项目里踩过、带人时反复解释过的那些细节。适合刚学完基础、准备动手做第一个项目的读者也适合想给团队系统梳理一遍字段知识的同学。1. 字段类型与数据库映射先搞清楚你在操作什么很多教程整理字段时喜欢按“数字、字符串、日期”分类每个字段给一句话。这个方法学得快但真正开写时还是会犹豫。我个人的建议是先建立一个认知Django里的每个Field类最后几乎都会映射成数据库里的某一种类型。你选择字段类型其实就是在选择数据库列的类型和约束。理解了这一层后面很多纠结都会自动消失。1.1 数值字段空间与精度是两回事先从最常用的数值字段说起。如果你不手动指定主键Django会自动给你加一个id models.AutoField(primary_keyTrue)打开MySQL你会看到一列int类型的自增列。数据量预期会很大的时候建议显式使用BigAutoField否则int自增到上限之后会非常被动想要扩容麻烦得多。IntegerField、SmallIntegerField、BigIntegerField的区别主要在取值范围和存储空间。年龄、评分、排序值这类小数字SmallIntegerField在MySQL里对应smallint范围约三万左右完全够用。订单量、PV量这种可能持续增长的计数通常用IntegerField起步。而订单号、日志ID这类可能超过21亿的数值直接BigIntegerField。不要随手什么都用BigIntegerField——虽然现在磁盘和内存不那么值钱但数据库类型选择本身就是表设计的一部分能明确约束的业务值域就别放宽。FloatField和DecimalField的差别是真正的重点。FloatField在MySQL里对应double属于浮点存储做运算时会出现经典问题0.1加0.2结果是0.30000000000000004。金额、税率、库存成本这类需要精确计算的字段一律用DecimalField。它需要必填的max_digits和decimal_places两个参数比如价格字段price models.DecimalField(max_digits10, decimal_places2)max_digits表示总位数decimal_places表示小数部分占几位这个例子在MySQL里对应decimal(10,2)能存的最大值是99999999.99。新手最容易在这里漏掉参数迁移阶段会直接报错。我在实际项目里财务相关字段从没见过用FloatField的。整理一张数据库映射对照表写模型时可以快速参考字段类型MySQL中的列典型用途SmallIntegerFieldsmallint年龄、评分、排序值IntegerFieldint通用计数、数量BigIntegerFieldbigint订单号、大数量统计AutoFieldint auto_increment默认主键BigAutoFieldbigint auto_increment高并发业务主键FloatFielddouble科学计算等允许误差的数据DecimalFielddecimal(max_digits, decimal_places)金额、价格、税率注意DecimalField不指定max_digits和decimal_placesDjango会在迁移或校验阶段直接报错这两个参数没有默认值。1.2 字符串与文本字段长度和价值CharField是Django里使用频率最高的字段之一它必须指定max_length因为在MySQL里对应varchar(length)。设置长度不只是为了数据库Django的校验器也会拿它作为表单输入长度上限用户在前台填超了会被拦截。TextField则对应MySQL的longtextPostgreSQL里对应text可以存很长的内容不限制长度。但短文本不建议用TextField它没法在普通业务中直接建常规索引后台管理界面的输入控件默认也是个很大的textarea编辑体验反而差。通常的边界是标题、姓名、邮箱、手机号这类长度可控的用CharField正文、富文本、日志详情这类内容用TextField。EmailField、URLField看起来是独立字段类型本质仍继承CharField只是在Django的form层加了格式校验。SlugField也算这一类它在表单层会做标准化处理。新手可以用但心里要清楚它们是“带规则的CharField”底层还是varchar。这里有个容易混淆的点不是字段类型决定了表单控件而是字段类型参与决定。CharField在后台默认是单行输入框TextField默认是多行文本框但如果你愿意也可以给CharField指定textarea的widget。所以选类型时优先想的是数据库存储和业务语义而不是界面长什么样。1.3 日期时间与布尔容易被忽略的两个细节DateField、DateTimeField、TimeField在MySQL里分别对应date、datetime(6)、time(6)。Django的DateTimeField默认支持微秒精度虽然绝大多数业务用不到但要知道这是框架的默认行为建表后看到datetime(6)不要惊讶。两个常用参数名字特别像auto_now_add和auto_now。auto_now_add表示创建时写入一次当前时间之后不再变化auto_now表示每次调用Model.save()时都会刷新为当前时间。这里有个常见误区很多人以为auto_now会在任意一次数据库更新中自动记录实际上如果走Article.objects.filter(id1).update(title...)这种批量更新路径auto_now字段不会自动维护因为这条路径完全绕过了模型的save()方法。我在项目里做批量更新时如果某个表需要精确的修改时间通常会自己显式传updated_at值。关于时区settings里USE_TZ True是新项目默认配置。开启后DateTimeField写入数据库时统一存成UTC时间读取时再按当前时区输出。早期Django版本在表单渲染上还有use_utc参数的坑后续版本行为已经比较稳定但你在做历史数据展示时仍要注意时区转换不要手动把时间字符串拼进datetime对象。布尔字段里除了BooleanField老项目里还经常见到NullBooleanField。区别在于BooleanField在数据库里对应bool或tinyint(1)正常情况下只有True和False两种状态NullBooleanField额外允许NULL代表“未知”或“未设置”。较新版本的Django已经建议用BooleanField(nullTrue)直接表达同样的语义NullBooleanField正逐步淡出。2. 常用字段参数里的细节null、blank、default、choices字段类型选完之后紧接着就是参数。参数不会改变字段“是哪种类型”但决定了数据是否允许为空、是否允许重复、缺失时的默认行为以及表单层如何校验。实际业务中因为参数使用不当而返工的情况远多于字段类型选错。2.1 null与blank数据库层和表单层一定要分开看这是新手最容易混淆的一组也是评审时必问的一个点。null是数据库层面的概念决定数据库列是否允许NULLblank是表单层面的概念决定表单提交时字段是否允许为空。打个比方null管的是“库里有没有值”blank管的是“用户提交表单的时候能不能不填”。在CharField这类字符串字段上如果同时开了nullTrue和blankTrue你会很快遇到一个尴尬状况数据库里同时存在NULL和空字符串两种“空”。查询统计时where phone 和where phone is null会得到两拨不同的结果还得写额外的兼容逻辑。我个人的实践经验是字符串字段尽量不要开nullTrue用blankTrue加上default就够了数值字段、日期字段、外键字段如果要表达“目前没有值”反而建议用nullTrue因为空字符串在这些类型上没有对应含义。举个例子用户表的手机号字段如果允许暂不填写推荐写法phone models.CharField(max_length20, blankTrue, default)而订单表的发货时间在未发货时应该是空就不应该用空字符串表达shipped_at models.DateTimeField(nullTrue, blankTrue)2.2 default、unique、db_index、db_column决定默认值和约束default可以是一个固定值也可以是一个可调用对象。经典坑在于新手经常写成defaulttimezone.now()这是把函数执行结果固化成模块加载那一刻的时间正确写法是defaulttimezone.now不带括号这样每次创建新记录时才执行。生成随机token时同理写defaultget_random_token而不是defaultget_random_token()。这个细节直接影响线上数据质量。uniqueTrue会创建唯一索引这里有个附带知识uniqueTrue再加db_indexTrue是多余的因为唯一索引本身就是索引。db_indexTrue则创建普通索引适合在查询条件里频繁出现的字段。但要记住不是每个字段都需要索引索引会拖慢写入尤其是高并发写入场景索引数量和写入性能是相悖的。我给团队定的简单规则是只有出现在filter、order_by、join字段里的字段才值得考虑加索引。db_column用于设置数据库列名。默认情况下Django会把字段名直接作为列名多数时候不用管。当你对接的是历史数据库或者数据库命名规范和Python代码风格不一致时可以用它把两边桥接起来。还有个不常用的参数editableFalse会让字段不出现在表单里比如服务端自动生成的标识字段。2.3 choices用枚举组织业务状态而不是散落魔法值业务模型里经常会有状态字段比如文章有草稿、已发布、已下架。最直观的写法是自己记一套字符串然后到处比较——“如果status published就……”。状态一多代码里到处都是魔法值改一个状态名就得全局搜索替换。Django的choices参数可以把选项集中管理。常见写法有两种第一种是模型里定义常量列表class Article(models.Model): STATUS_DRAFT draft STATUS_PUBLISHED published STATUS_CHOICES ( (STATUS_DRAFT, 草稿), (STATUS_PUBLISHED, 已发布), ) status models.CharField( max_length20, choicesSTATUS_CHOICES, defaultSTATUS_DRAFT, )第二种是Django 3.0以来更推荐的TextChoicesfrom django.db import models class ArticleStatus(models.TextChoices): DRAFT draft, 草稿 PUBLISHED published, 已发布 class Article(models.Model): status models.CharField( max_length20, choicesArticleStatus.choices, defaultArticleStatus.DRAFT, )TextChoices的好处是定义、取值、标签都在一个地方用起来还是普通枚举类的风格团队协作时容易读。还有一点要特别说明choices只是Django表单层的校验规则它不会在数据库层对字段值做约束。你用ORM直接执行Article.objects.create(statusdeleted)数据库照样能写入。如果业务真的需要数据库层面强校验要么在save方法里写逻辑要么用CheckConstraint不能指望choices给你设一道数据库防火墙。3. 关系字段不只是加个外键on_delete、反向查询与自关联关系字段的行为比普通字段复杂得多这部分坑我也总能在代码评审里看到。外键、一对一、多对多看似只是字段类型不同背后的约束和操作方式差异很大。3.1 外键on_delete的六种选择本质是对业务风险的决策ForeignKey的第一个必填参数是on_delete。很多新手会随手写成models.CASCADE实际上它的含义是当被关联的对象被删除时当前对象怎么办。这个决策直接影响数据安全。比如删除一个用户用户发表的文章和评论是跟着一起消失还是保留但不再关联还是直接拒绝删除用户都需要想清楚。常用选项的语义可以这样理解选项行为典型场景CASCADE被关联对象删除时当前对象一并删除用户删除时其草稿文章也删掉PROTECT被关联对象存在时禁止删除抛出ProtectedError用户有资产数据时不允许删除用户RESTRICT与PROTECT类似批量删除时检查顺序不同对检查时机有精细化要求的场景SET_NULL外键置为NULL前提是字段已设置nullTrue文章删除后评论仍保留作者置空SET_DEFAULT外键置为default值前提是字段已设置default删除分类后商品归入默认分类DO_NOTHINGDjango不干预是否报错交给数据库外键约束极少用需自己保证数据一致性实际业务里我比较常用CASCADE和SET_NULL的组合。比如用户删除他个人的草稿文章级联删除很合理但文章删除后历史评论如果还想保留评论表的作者外键可以设为SET_NULL把作者置成“已注销用户”。如果业务不允许删除有资产数据的用户就在被引用的一侧用PROTECT删除用户时直接让上层操作报错逼着产品重新设计流程。给新手最核心的建议凡是打算用SET_NULL的外键字段一定记得nullTrue和blankTrue不然删除关联对象时Django会因为没有值可赋而抛IntegrityError。这个错误我见过很多次。3.2 related_name与related_query_name反向查询的管理员名定义外键时related_name到底有什么用很多人一开始理解不了。不写它反向查询时默认用“模型名小写_set”比如某个用户发布的所有文章引用写法是user.post_set.all()。这种写法很容易和正向外键混淆随着模型增多每次都要在脑子里绕一圈。设置related_nameposts之后直接user.posts.all()语义清楚得多。如果同一个模型里有两个字段都指向同一个目标模型比如评论表既有created_by又有last_edited_by不指定related_name会直接报错因为Django无法自动生成两个完全一样的反向查询名。这时必须手动指定不同的名称区分。自关联场景也同理典型例子是评论的楼层回复class Comment(models.Model): content models.TextField() parent models.ForeignKey( self, on_deletemodels.CASCADE, related_namechildren, nullTrue, blankTrue, )这样拿到一个评论后comment.children.all()就能取到所有直接子评论构造树形回复结构会顺手很多。related_query_name影响的是filter查询的字段名实际使用频率不如related_name高知道它存在即可。3.3 OneToOneField与ManyToManyField一对一扩展和多对多中间表OneToOneField用于一对一关系最典型的场景是扩展用户资料。用户的核心认证字段放主表头像、简介、生日这类扩展资料放另一张表用OneToOne关联。好处是主表干净扩展字段再多也不影响用户表查询性能坏处只是每次加资料字段要动资料表这属于正常成本。ManyToManyField最简单的用法是让框架自动生成中间表。但一旦中间表需要额外字段比如用户加入群组的时间、在群里的角色就必须改用through参数自定义中间模型class Membership(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) group models.ForeignKey(Group, on_deletemodels.CASCADE) date_joined models.DateField(auto_now_addTrue) role models.CharField(max_length30, defaultmember)然后在Group模型里声明members models.ManyToManyField(User, throughMembership)。通过中间模型可以在add关系时传额外字段group.members.add(user, through_defaults{role: admin})这里有个新手容易懵的地方使用through自定义中间表之后many-to-many manager不再支持add()、remove()、set()这些快捷方法你需要通过操作中间模型来管理关系比如Membership.objects.create(useruser, groupgroup, roleadmin)。文档里那句“自定义through后部分自动方法不可用”指的就是这个。4. 字段定义引发的迁移事故几次典型的排查现场字段定义不是写完models.py就结束了它和数据库之间还有一层迁移。新手在这块遇到问题时往往很慌下面几种情况几乎每个项目都会遇到我按排查思路整理出来。4.1 模型改了makemigrations却提示No changes detected先说一个最让人沮丧的现象明明改了models.py运行python manage.py makemigrations却提示“No changes detected.”。新手第一反应通常是再运行一遍结果还是一样。我的排查顺序是确认App真的在INSTALLED_APPS里注册了。新项目刚创建App时忘了注册Django根本不会监听这个App的模型自然会提示没有变化。确认文件确实保存了并且改的是当前App下的models.py。多App项目里改错了位置表现也是一样的。用python manage.py makemigrations --dry-run看检测结果。如果仍旧没有输出再回想一下你改的到底是什么。第三种情况最隐蔽。Django的迁移系统比较的是模型状态的变化而Meta里的ordering、verbose_name这类不影响表结构的元信息多数情况下不会触发迁移生成。如果你只改了字段的verbose_name或者帮助文本数据库结构没变没有新迁移是正常的。在实际项目里我主张定一条团队约定任何字段的增删改都伴随一次makemigrations和migrate并且把迁移文件提交到代码仓库。迁移文件本质上就是数据库结构的代码版本记录赶工时可以偷懒一旦上线后需要回滚就会明白它的价值有多大。4.2 给已有数据的表新增必填字段一个标准的加字段流程这是生产环境最常见的场景表里已经积累了几千行数据现在要加一个非空的新字段。如果你直接写成name models.CharField(max_length50)运行makemigrations时Django会陷入两难它无法替老数据决定这个字段填什么值于是会弹出交互式提问让你选择一次性提供一个默认值还是放弃这次迁移。如果新字段本身就应该有固定默认值比如status models.CharField(max_length20, defaultdraft)问题简单填draft即可。更多时候新字段的值需要根据老数据计算生成这时候我惯用的流程分三步第一步先加字段并允许为空执行makemigrations和migrate。第二步写一个数据迁移脚本读取老数据并计算填充新字段的值再执行迁移。第三步去掉nullTrue和blankTrue如果业务不允许为空再次执行makemigrations和migrate。三步之间存在缓冲不会出现一次性默认值拍脑袋填错就收不回来的情况。这里要提醒的是第二步的数据迁移脚本最好先在本地用与生产相似的数据量跑一遍别直接在生产环境执行长事务否则锁表时间长会拖累线上服务。4.3 字段定义常见报错的速查表把这几年带新人时高频出现的报错汇总成一张表遇到时可以对号入座报错或现象原因解决思路CharField must define a max_length漏写必填参数补上max_lengthDecimalField require max_digits and decimal_places未指定精度的两个参数按业务实际指定FieldError: Cannot resolve keyword user_id查询条件用了不存在的字段名ORM里外键用user查询不用user_idAttributeError: module has no attribute xxchoices常量或TextChoices成员引用错误检查枚举定义和导入路径新增非空字段时迁移交互提问老数据没有该字段值按4.2流程处理Cannot assign xxx must be a User instance给外键赋了整型而不是对象用外键对象赋值或用user_id属性赋值unique字段出现多个NULL唯一字段允许NULL数据库对NULL不计数看业务是否接受通常避免这种组合5. 字段设计经验和命名规范让models.py好读也好改前面聊的是字段本身最后这部分更像是我个人在带团队时定的规矩。字段设计得好不仅数据库结构清爽后期改需求的成本也会明显降低。这些内容可能不在任何一个文档的“字段列表”里但实战价值很高。5.1 从业务实体拆字段而不是从页面表单拆字段新手最常见的做法是网页注册页有哪些输入框用户表就建哪些字段昵称、头像、简介、性别、生日、城市全都堆在一张表里。这种思路短期没问题等业务发展起来就会很尴尬。比如之后要接入企业认证企业名称、营业执照、认证状态加到哪里都往用户表塞表结构会越来越杂。我的建议是按实体和生命周期拆。用户的核心认证信息是一张表比如username、password、email、is_active用户的扩展资料放另一张表比如昵称、头像、个人简介如果需要第三方登录再单独建一张绑定表。每张表只管自己那一类数据外键关系清晰加新需求时优先想到去扩展对应的表而不是动老表。订单类业务还要记住一个词快照字段。用户下单时把收货地址、商品单价复制进订单表而不是后续一直关联查询用户资料。因为用户以后改了地址、改了昵称历史订单不应该跟着变。订单表看起来有点冗余但正是这种冗余才是设计得当的体现。5.2 抽象基类坐镇公共字段减少重复代码几乎每个业务表都需要创建时间和更新时间有些还需要软删除标记。如果每张表都写一遍不仅啰嗦团队规范也很难统一。抽象基类是标准解法class BaseModel(models.Model): created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) is_active models.BooleanField(defaultTrue) class Meta: abstract Trueabstract True这行决定了Django不会为BaseModel单独建表子模型继承后字段全部落到子表上。迁移时你会看到每个子表都自带这几个列。这个模式建议从第一个项目就开始用后面省下来的重复劳动相当可观。唯一的提醒还是那个老坑auto_now只会在Model.save()里自动维护走queryset.update()批量更新时需要自己显式处理updated_at。项目里通常的做法是封装一个公共工具方法统一更新而不是散落在各处手动拼。5.3 字段命名规范先统一再谈优化命名这件事看起来不如类型选择重要但代码评审时最耗时间的就是到处不一致的命名。我这里只说三条硬规矩。第一外键字段在ORM里用语义名不要写user_id。定义一个user models.ForeignKey(User, on_deletemodels.CASCADE)数据库列自动叫user_idORM查询时想要数据库列也可以写user_id但正常业务代码里直接操作user对象可读性最好。第二布尔字段统一is_或has_开头时间字段统一created_at、updated_at这类表述比create_time、update_time顺手得多。第三字段名不要用Python关键字或Django内置的objects、delete、save这类名字否则会出现各种莫名其妙的属性冲突。这些规矩不花成本却在项目越写越大之后越来越值钱。Django的迁移系统非常成熟改字段类型本身并不可怕可怕的是每一处命名都不一样连查找一个字段在哪些地方被使用都要全项目搜索半天。说句实在话Django常用字段的内容多而杂我不建议一次全背完。最有效的做法是找个真实的小项目写几个模型建几张表把null、blank、choices、外键删除策略这些参数一个个试一遍。数据库是诚实的字段定义得对不对跑一次迁移、打开表结构就全清楚了。
返回列表