Django 从 0 到 1 打造完整电商平台:Django 模型进阶与数据迁移

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。


大家好,我是IT策士。上一节我们完成了电商项目的需求分析,并把 10 张核心表写进了模型代码,成功在数据库里"落了户"。但模型只是建出来还不够,实际开发中你会频繁遇到这些场景:修改字段类型、加索引、调整关联关系,甚至还可能要做数据迁移------把老数据"搬"到新结构中。这些操作如果不理解底层原理,早晚会踩坑。

今天我们就来一次 Django 模型进阶与数据迁移的深度解析,帮你彻底搞懂 ORM 的高级用法,并掌握迁移系统的正确打开方式。


一、上节回顾与今天目标

上节回顾:

  • 梳理了电商实体关系,编写了 users、products、cart、orders、payment 五个 app 的模型代码;

  • 配置了自定义用户模型 AUTH_USER_MODEL = 'users.User'

  • 执行了 makemigrationsmigrate,把所有表建在了 SQLite 中。

今天目标:

  1. 深入 Django 模型字段的高级选项(on_deleterelated_name、索引、元选项等);

  2. 理解 Django 迁移系统的工作原理;

  3. 实战:给已有模型添加索引、修改字段属性、创建数据迁移;

  4. 掌握迁移回滚、查看 SQL、合并迁移等常用操作。


二、模型进阶------那些你该掌握的细节

2.1 on_delete 的六种行为

外键字段都必须指定 on_delete。它决定了 被关联对象被删除时,当前对象的行为。六种取值如下:

在我们的项目中:

  • Order.sku 使用了 PROTECT,意思是商品在被订单引用时不允许删除,防止历史订单出现"幽灵商品"。

  • CartItem.sku 使用了 CASCADE商品删除,购物车里对应的条目也应该消失

  • Order.user 使用 PROTECT用户不能在有订单的情况下被直接删除

不同的业务场景需要不同的删除策略,这是模型设计的一个关键细节。

ForeignKey 会默认创建一个反向关系,名字是 模型名小写_set,例如 user.order_set.all()。为了语义更清晰,我们通常会自定义 related_name

在我们的 Order 模型中:

bash 复制代码
user = models.ForeignKey(
    settings.AUTH_USER_MODEL,
    on_delete=models.PROTECT,
    related_name='orders',  # 自定义反向名
)

这样就能用 user.orders.all() 直接获取该用户的所有订单。如果你不想创建反向关系,可以设置 related_name='+'

2.3 模型元选项 Meta

每个模型都可以通过内部类 Meta 配置元数据,常用的有:

  • db_table:自定义表名,我们统一用了 tb_ 前缀。

  • verbose_name / verbose_name_plural:可读名称,Admin 后台会用到。

  • ordering:默认排序,如 ['-create_time']

  • unique_together:联合唯一约束。我们在购物车模型中用了 ('user', 'sku') 防止重复添加。

  • indexes:声明数据库索引(后面详解)。

2.4 字段属性详解

挑几个项目中重要且容易忽略的字段属性说一说:

  • decimal_placesDecimalField 的精度至关重要。价格我们用 max_digits=10, decimal_places=2,表示最大 10 位数,其中 2 位小数,能表示到 99,999,999.99。

  • auto_now_add vs auto_nowcreate_timeauto_now_add(新增时自动取值),update_timeauto_now(每次保存都自动更新)。

  • null=True vs blank=True :前者是数据库层面的允许 NULL,后者是表单验证层面的允许为空。CharField 一般不要用 null=True,Django 推荐用空字符串代替。

  • JSONField:Django 3.1 起原生支持 JSON 字段(除 SQLite 外需要数据库支持)。我们用 JSON 存储商品规格快照和地址快照,灵活方便。


三、模型优化------给已有表加索引和默认值

索引能极大提升查询效率。电商系统里哪些字段经常作为查询条件?order_no(订单号)、user(用户外键)、create_time(时间范围查询)等。我们来给它们加上索引。

3.1 给订单表添加索引

编辑 apps/orders/models.py,在 Order 类的 Meta 中添加 indexes

bash 复制代码
class Order(models.Model):
    # ... 字段省略 ...
    class Meta:
        db_table = 'tb_order'
        verbose_name = '订单'
        verbose_name_plural = verbose_name
        ordering = ['-create_time']
        indexes = [
            models.Index(fields=['user', '-create_time'], name='idx_user_time'),
            models.Index(fields=['status'], name='idx_status'),
        ]

我们创建了两个索引:

  • 联合索引 idx_user_time:用户 ID 加创建时间降序,加速"某用户的订单列表"查询。

  • 单列索引 idx_status:加速按状态筛选订单。

3.2 给 SKU 表添加索引

编辑 apps/products/models.py,在 SKUMeta 里加上:

bash 复制代码
class SKU(models.Model):
    # ... 字段省略 ...
    class Meta:
        db_table = 'tb_sku'
        verbose_name = 'SKU'
        verbose_name_plural = verbose_name
        indexes = [
            models.Index(fields=['spu', 'is_active'], name='idx_spu_active'),
        ]

联合索引 idx_spu_active 用于加速"查询某个 SPU 下所有上架 SKU"。

3.3 给商品 name 字段增加长度上限优化

SKU.name 目前长度 200,对于某些长规格名的商品可能不够,我们扩展到 500,同时加上数据库级别的默认空字符串(避免 NULL):

bash 复制代码
name = models.CharField(
    max_length=500,
    default='',
    verbose_name='SKU 名称'
)

四、数据迁移------从原理到实战

4.1 Django 迁移系统是如何工作的?

每次执行 makemigrations,Django 会扫描所有已注册 app 的 models.py,与上一次迁移后的状态做对比,生成一个迁移文件 (存在各 app 的 migrations/ 目录下)。迁移文件里有两个关键部分:

  • state_operations:描述模型"应该长什么样"。

  • database_operations:描述要在数据库上执行的操作(ALTER TABLE、CREATE INDEX 等)。

执行 migrate 时,Django 会根据迁移文件里的操作,翻译成对应的数据库 DDL 语句。

4.2 生成新的迁移

修改完模型后,运行:

bash 复制代码
python manage.py makemigrations

控制台输出:

bash 复制代码
Migrations for 'orders':
  apps/orders/migrations/0002_auto_20260519_1430.py
    - Create index idx_user_time on field(s) user, -create_time of model order
    - Create index idx_status on field(s) status of model order
Migrations for 'products':
  apps/products/migrations/0002_alter_sku_name_add_index.py
    - Alter field name on sku
    - Create index idx_spu_active on field(s) spu, is_active of model sku

如果你对迁移文件名不满意,可以用 --name 参数自定义:

bash 复制代码
python manage.py makemigrations orders --name add_order_indexes
4.3 查看迁移对应的 SQL(很实用!)

在执行迁移前,我们最好先看一眼它会在数据库里执行哪些 SQL。使用 sqlmigrate 命令:

bash 复制代码
python manage.py sqlmigrate orders 0002

输出示例(SQLite 版):

bash 复制代码
BEGIN;
--
-- Create index idx_user_time on field(s) user, -create_time of model order
--
CREATE INDEX "idx_user_time" ON "tb_order" ("user_id", "create_time" DESC);
--
-- Create index idx_status on field(s) status of model order
--
CREATE INDEX "idx_status" ON "tb_order" ("status");
COMMIT;

换成 MySQL 或 PostgreSQL,生成的 DDL 会相应变化。这个命令可以让你在动手之前"审阅"SQL,避免意外。

4.4 执行迁移

确认无误后,执行:

控制台输出:

bash 复制代码
Operations to perform:
  Apply all migrations: orders, products
Running migrations:
  Applying orders.0002_add_order_indexes... OK
  Applying products.0002_alter_sku_name_add_index... OK

这时索引已经建立在数据库里了。可以用 dbshell 进入 SQLite,输入 .indexes tb_order 查看索引列表。


五、迁移的"后悔药"------回滚与恢复

5.1 撤销最近一次迁移

假设刚才的迁移有问题,我们可以退回到指定状态:

bash 复制代码
python manage.py migrate orders 0001

这个命令会将 orders app 的迁移状态退回到 0001,对应的表结构也会回退(索引会被删除)。

执行后会提示:

bash 复制代码
Operations to perform:
  Target specific migration: 0001_initial, from orders
Running migrations:
  Rendering model states... DONE
  Unapplying orders.0002_add_order_indexes... OK
5.2 撤销所有迁移(慎用)

如果想回到零迁移状态(表清空),可以指定 zero

bash 复制代码
python manage.py migrate orders zero

但在生产环境千万别这么做,除非你真的要删除整个 app 的数据表。


六、高级话题:数据迁移(Data Migration)

有时候你不仅要改表结构,还需要搬移或转换数据。比如我们想在 tb_users 表里给现有用户批量生成一个默认的"手机号"字段,让其不再是 NULL。

6.1 创建空迁移作为数据迁移的壳
bash 复制代码
python manage.py makemigrations users --empty --name populate_user_phone

这会生成一个空的迁移文件 apps/users/migrations/0002_populate_user_phone.py。我们打开它,在 operations 列表里写上数据迁移逻辑:

bash 复制代码
from django.db import migrations

def populate_phone_default(apps, schema_editor):
    User = apps.get_model('users', 'User')
    # 将 phone 为空的用户设置一个临时占位手机号,实际中可根据业务逻辑处理
    for user in User.objects.filter(phone__isnull=True):
        user.phone = f"1000000{user.id:04d}"  # 例如 10000000001
        user.save(update_fields=['phone'])

def reverse_phone(apps, schema_editor):
    # 回滚逻辑:将生成的手机号置空
    User = apps.get_model('users', 'User')
    User.objects.filter(phone__startswith='1000000').update(phone=None)

class Migration(migrations.Migration):
    dependencies = [
        ('users', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(populate_phone_default, reverse_phone),
    ]

然后执行迁移:

这样既修改了表结构,也填充了历史数据。


七、常用迁移命令速查


八、总结与下集预告

今天我们把 Django 模型从"会用"提升到了"懂得内部机制"的水平:

  • 吃透了 on_deleterelated_nameMeta 选项的用法;

  • 给订单和商品表添加了关键索引,优化查询;

  • 深入理解了 Django 迁移的原理,学会了生成、查看、回滚迁移;

  • 初次体验了数据迁移,处理了历史数据问题。

有了扎实的模型基础,下一步就该让数据"活"起来了。第 4 天 ,我们将迎来 Django 最让人兴奋的功能之一------Admin 后台管理。我会教你如何在 10 分钟内搭出一个能管理用户、商品、订单的后台,并实现一些高级定制,比如列表过滤、搜索、自定义表单......让运营人员也能直接上手。

想了解更多还可以去其它平台搜索「IT策士」,一起升级 IT 思维 !


本文为《Django 从 0 到 1 打造完整电商平台》系列第 3 篇,作者:IT策士,未经授权禁止转载。

相关推荐
胡志辉的博客6 小时前
完全开源、本地 SQLite 管理一切:我写了一个桌面邮件客户端 OneMail
java·sqlite·开源
OsDepK6 小时前
AudioSplit音频多轨免费分离工具即将发布
ide·git·python·音视频·集成学习
Metaphor6926 小时前
使用 Python 将 Excel 转换为 PDF
python·pdf·excel
彦为君6 小时前
长时间运行的 Agent:如何设计可靠的执行框架
python·ai·ai编程
qqqweiweiqq6 小时前
Jetson Orin nx 无法train pi0
人工智能·python·深度学习
AAA大运重卡何师傅(专跑国道)6 小时前
scrapling框架源码5/19
python
xingyuzhisuan6 小时前
Jupyter Notebook 云GPU配置全解析(含实操+选型指南)
ide·python·jupyter·gpu算力
ITIRONMAN6 小时前
开源data-compare:轻量级数据对比工具
人工智能·python
云姜.6 小时前
如何快速使用Langchain上手编程
python·langchain