浏览知识库目录

Python

模型、ORM 与数据库迁移

模型、ORM 与数据库迁移

模型决定了项目的数据结构,也是大多数 Django 业务的核心。本篇将从字段定义、关联关系、查询集一直讲到迁移和事务。


一、模型与数据库表

一个模型类通常对应一张表,一个字段对应一列,一个模型实例对应一行数据。

from django.conf import settings
from django.db import models


class Message(models.Model):
    class Status(models.TextChoices):
        DRAFT = "draft", "草稿"
        PUBLISHED = "published", "已发布"
        ARCHIVED = "archived", "已归档"

    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="messages",
    )
    name = models.CharField("姓名", max_length=50)
    content = models.TextField("内容")
    status = models.CharField(
        "状态",
        max_length=20,
        choices=Status.choices,
        default=Status.DRAFT,
        db_index=True,
    )
    created_at = models.DateTimeField("创建时间", auto_now_add=True)
    updated_at = models.DateTimeField("更新时间", auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["owner", "status"]),
        ]

    def __str__(self):
        return self.name

使用 settings.AUTH_USER_MODEL,不要在模型字段中直接写死 User


二、常用字段与参数

常见字段:

  • CharField:短文本,必须提供 max_length
  • TextField:长文本。
  • IntegerFieldDecimalField:整数和精确小数。
  • BooleanField:布尔值。
  • DateFieldDateTimeField:日期和时间。
  • EmailFieldURLField:带基础格式校验的字符串。
  • FileFieldImageField:文件路径,文件本身通常位于媒体存储。
  • JSONField:结构不固定的 JSON 数据,不应替代清晰的关系建模。

常用参数:

  • null=True:数据库允许 NULL
  • blank=True:表单校验允许为空。
  • default:默认值,可传可调用对象,如 default=timezone.now
  • unique=True:唯一约束。
  • db_index=True:建立单字段索引。
  • choices:限制可选值并提供显示文本。
  • editable=False:默认不出现在 ModelForm 和后台编辑表单中。

字符串字段通常用空字符串表示“无值”,不要随意同时设置 null=Trueblank=True


三、模型关系

1. 一对多:ForeignKey

class Comment(models.Model):
    message = models.ForeignKey(
        Message,
        on_delete=models.CASCADE,
        related_name="comments",
    )
    content = models.TextField()

访问关系:

comment.message
message.comments.all()

on_delete 常用策略:

  • CASCADE:主对象删除时一起删除。
  • PROTECT:存在引用时禁止删除。
  • SET_NULL:设为空,字段必须允许 null=True
  • SET_DEFAULT:设为默认值。

2. 多对多:ManyToManyField

class Tag(models.Model):
    name = models.CharField(max_length=30, unique=True)


class Message(models.Model):
    # 省略其他字段
    tags = models.ManyToManyField(Tag, blank=True, related_name="messages")

3. 一对一:OneToOneField

适合用户扩展资料等“每个主对象最多一条扩展记录”的场景。


四、迁移的正确工作流

修改 models.py 后执行:

python manage.py makemigrations
python manage.py migrate

辅助命令:

python manage.py showmigrations
python manage.py sqlmigrate notes 0002
python manage.py migrate notes 0001
  • makemigrations:根据模型变化生成迁移文件。
  • migrate:按依赖顺序执行迁移。
  • 迁移文件属于源代码,应提交版本控制。
  • 已在共享环境执行过的迁移,不要随意修改或删除;应创建新迁移修正。

给已有表增加非空字段时,需提供合理默认值,或分阶段操作:先允许空值、回填数据、再改为非空。


五、数据迁移

结构迁移改变表,数据迁移负责批量修正已有数据。

from django.db import migrations


def publish_existing_messages(apps, schema_editor):
    Message = apps.get_model("notes", "Message")
    Message.objects.filter(status="").update(status="published")


class Migration(migrations.Migration):
    dependencies = [("notes", "0002_message_status")]
    operations = [
        migrations.RunPython(
            publish_existing_messages,
            migrations.RunPython.noop,
        ),
    ]

数据迁移中应使用 apps.get_model() 取得历史模型,不能直接导入当前 models.py


六、ORM 增删改查

进入交互环境:

python manage.py shell
from notes.models import Message

# 创建
message = Message.objects.create(
    owner_id=1,
    name="小林",
    content="正在学习 Django",
)

# 查询
all_messages = Message.objects.all()
published = Message.objects.filter(status="published")
one = Message.objects.get(pk=1)

# 更新
message.status = Message.Status.PUBLISHED
message.save(update_fields=["status", "updated_at"])

# 批量更新
Message.objects.filter(status="draft").update(status="published")

# 删除
message.delete()

get() 找不到会抛出 DoesNotExist,找到多条会抛出 MultipleObjectsReturned。不确定是否存在时可用:

message = Message.objects.filter(pk=1).first()

七、查询条件与表达式

from django.db.models import Count, Q

Message.objects.filter(name__icontains="林")
Message.objects.filter(created_at__year=2026)
Message.objects.exclude(status="draft")
Message.objects.filter(Q(name__icontains="林") | Q(content__icontains="Django"))
Message.objects.annotate(comment_count=Count("comments"))
Message.objects.values("status").annotate(total=Count("id"))

常用查找后缀:exactiexactcontainsicontainsingtgteltlteisnull

QuerySet 是惰性执行的。创建、过滤和排序时通常不会立刻查询数据库,迭代、切片、list()len() 等操作才可能真正执行 SQL。


八、避免 N+1 查询

访问外键或多对多关系时,循环可能产生大量额外查询。

# ForeignKey / OneToOne
messages = Message.objects.select_related("owner")

# ManyToMany / 反向 ForeignKey
messages = Message.objects.prefetch_related("tags", "comments")
  • select_related() 使用 SQL JOIN,适合单值关系。
  • prefetch_related() 使用额外查询后在 Python 中合并,适合集合关系。

九、事务

多个写操作必须全部成功或全部失败时使用事务:

from django.db import transaction


@transaction.atomic
def publish_message(message, log_model):
    message.status = Message.Status.PUBLISHED
    message.save(update_fields=["status"])
    log_model.objects.create(message=message, action="publish")

不要在长事务中执行网络请求、发送邮件或运行耗时任务,以免长时间占用数据库锁。


十、常见问题

  • no such table:通常尚未执行 migrate
  • NOT NULL constraint failed:新增非空字段时旧数据没有值。
  • 数据重复:只在代码里检查不够,关键规则应加数据库唯一约束。
  • 查询很慢:先确认 SQL 与查询次数,再考虑索引,不能盲目给所有字段加索引。
  • 金额误差:金额使用 DecimalField,不要用 FloatField

十一、本篇检查清单

  • 能选择合适字段并区分 nullblank
  • 能设计一对一、一对多和多对多关系。
  • 理解迁移文件为什么要纳入版本控制。
  • 能使用过滤、聚合和事务。
  • 能识别并处理 N+1 查询。

上一篇:URL 路由与视图 | 下一篇:模板、静态文件与媒体文件