迁移数据库并准备练习数据
学习目标
把模型变为数据库表,理解 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