Django 从入门到精通:知行博客实战(6.1) · 第 10 / 36 节

迁移数据库并准备练习数据

吴秀峰 3 次阅读 0 条评论 ↓

学习目标

把模型变为数据库表,理解 makemigrations 与 migrate 的区别;建立可以重复执行的演示数据命令,不把数据库文件混入版本控制。

为什么迁移文件必须保存

模型描述现在的目标结构,迁移文件描述结构如何一步步变化,数据库中的迁移记录说明哪些步骤已经执行。三者需要保持一致。makemigrations 比较模型与迁移状态并生成代码;migrate 才实际操作数据库。修改模型后仅重启服务,不会自动改变表结构。

本章在空 SQLite 库初始化 accounts.User、Category 和 Post。示例数据使用管理命令,而不是每次启动服务器都写入数据库。命令仅在 DEBUG 开发环境可用,重复执行不会添加重复文章。演示作者使用不可登录密码,不能把演示密码带到公网。

具体怎么做

先确认第 8–9 章已完成,然后执行:

python manage.py makemigrations accounts blog
python manage.py migrate --plan
python manage.py migrate
python manage.py showmigrations

如果你使用课程快照,初始迁移已包含,第一条会显示没有变化。下方展示的迁移文件就是由这组模型生成的完整版本;手写学习路径生成自己的文件即可,不要重复创建第二份初始迁移。

创建 blog/management/commands/seed_demo.py,并确保 management/、commands/ 下各有一个空 __init__.py,随后运行:

python manage.py seed_demo
python manage.py seed_demo
python manage.py shell -c "from blog.models import Post; print(Post.objects.count())"
python manage.py createsuperuser

超级用户按提示输入你自己的用户名、邮箱和密码,不使用教材中的示例口令。Windows 的终端密码输入同样不会回显,这是正常现象。

本章新增或替换的完整文件

以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。

accounts/migrations/0001_initial.py

# Generated by Django 6.1.2 on 2026-11-01 11:12

import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]

    operations = [
        migrations.CreateModel(
            name='User',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                ('email', models.EmailField(max_length=254, unique=True, verbose_name='邮箱')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
            ],
            options={
                'verbose_name': 'user',
                'verbose_name_plural': 'users',
                'abstract': False,
            },
            managers=[
                ('objects', django.contrib.auth.models.UserManager()),
            ],
        ),
    ]

blog/management/__init__.py

创建空文件,用于声明 Python 包。

blog/management/commands/__init__.py

创建空文件,用于声明 Python 包。

blog/management/commands/seed_demo.py

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from blog.models import Category, Post

class Command(BaseCommand):
    help = "仅在 DEBUG 开发环境建立演示数据;重复执行不增加重复文章。"

    @transaction.atomic
    def handle(self, *args, **options):
        if not settings.DEBUG:
            raise CommandError("演示数据只能用于开发环境")
        author, created = get_user_model().objects.get_or_create(
            username="demo-author", defaults={"email": "demo-author@example.invalid"})
        if created:
            author.set_unusable_password()
            author.save(update_fields=["password"])
        category, _ = Category.objects.get_or_create(slug="django", defaults={"name": "Django"})
        for number in range(1, 16):
            Post.objects.get_or_create(
                author=author, title=f"Django 学习笔记 {number:02d}",
                defaults={"body": "今天练习 Django 的请求、路由与模板。记录问题,再通过测试验证自己的理解。",
                          "category": category, "status": Post.Status.PUBLISHED})
        self.stdout.write(self.style.SUCCESS("演示数据已就绪:15 篇文章,作者没有可用登录密码。"))

blog/migrations/0001_initial.py

# Generated by Django 6.1.2 on 2026-11-01 11:12

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=40, unique=True, verbose_name='名称')),
                ('slug', models.SlugField(unique=True, verbose_name='地址标识')),
            ],
            options={
                'ordering': ['name', 'pk'],
            },
        ),
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=120, verbose_name='标题')),
                ('body', models.TextField(verbose_name='正文')),
                ('status', models.CharField(choices=[('draft', '草稿'), ('published', '已发布')], default='draft', max_length=12, verbose_name='状态')),
                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
                ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='posts', to=settings.AUTH_USER_MODEL)),
                ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='posts', to='blog.category')),
            ],
            options={
                'ordering': ['-created_at', '-pk'],
                'indexes': [models.Index(fields=['status', '-created_at'], name='post_status_created_idx')],
                'constraints': [models.CheckConstraint(condition=models.Q(('status__in', ['draft', 'published'])), name='post_valid_status')],
            },
        ),
    ]

如何确认成功

两次 seed_demo 后仍为 15 篇文章;showmigrations 的应用迁移前面是 [X]。db.sqlite3 出现在项目根目录,但 git status 不应把它列为待提交文件。重新启动服务器后首页正常,管理员登录将在第 12 章使用。

练习:执行 python manage.py sqlmigrate blog 0001,找到文章表、作者外键和状态约束。它展示 SQL 但不执行;不要把输出直接复制到生产数据库绕开迁移记录。

常见问题

no such table? 核对当前目录、配置和迁移状态,可能连接了另一个 SQLite 文件。No changes detected 但字段没生效? 查看迁移是否生成但未执行。可以删除迁移文件重新生成吗? 有共享环境或真实数据后不能这样做,会破坏历史依赖。重命名字段、增加非空列和数据回填应设计独立迁移;RunPython 中要通过历史 apps 获取模型,不直接导入当前模型。

官方参考

把学习连起来

登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。

登录并保存学习进度 →
联系站长

微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。

章节讨论 0