一、Django数据库迁移的那些坑

作为一个Django开发者,我最头疼的就是数据库迁移的问题。每次看到"migrate"命令报错的时候,都恨不得把电脑给砸了。但冷静下来想想,其实这些问题都是有规律可循的。

让我们先看一个典型的迁移失败场景。假设我们有一个简单的博客应用,models.py是这样的:

# blog/models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        db_table = 'blog_articles'  # 显式指定表名

当我们第一次运行python manage.py makemigrations时,一切都很顺利。但是如果在已有数据的情况下修改模型,比如:

# 修改后的models.py
class Article(models.Model):
    title = models.CharField(max_length=150)  # 从100改为150
    content = models.TextField(null=True)     # 添加null=True
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)  # 新增字段
    
    class Meta:
        db_table = 'blog_articles'

这时候问题就来了。特别是当updated_at字段需要设置默认值时,Django会询问你是选择立即提供默认值,还是退出修改模型。这就是迁移的第一个大坑 - 字段变更导致的默认值问题。

二、迁移问题的常见类型及解决方案

1. 新增非空字段问题

这是最常见的问题之一。当我们向已有数据的表中添加一个非空字段时,Django会要求提供默认值。比如上面的updated_at字段,如果我们不设置auto_now=True,而是普通的DateTimeField,就会遇到这个问题。

解决方案有三种:

  1. 设置null=True,允许字段为空
  2. 设置默认值
  3. 使用auto_now或auto_now_add(仅适用于日期时间字段)
# 解决方案示例
updated_at = models.DateTimeField(auto_now=True)  # 方案3
# 或者
updated_at = models.DateTimeField(default=timezone.now)  # 方案2
# 或者 
updated_at = models.DateTimeField(null=True)  # 方案1

2. 修改字段属性导致的数据丢失

另一个常见问题是修改字段属性可能导致数据截断或丢失。比如我们把CharField的max_length从100改为50,那么超过50个字符的数据就会被截断。

解决方案:

  1. 先增加长度(如从100到150),这很安全
  2. 如果要减少长度,应该先确保数据库中没有超长的数据
  3. 或者使用数据迁移(data migration)先处理已有数据
# 安全修改字段长度的示例
# 先检查是否有超长数据
from blog.models import Article
long_titles = Article.objects.filter(title__length__gt=50)
if long_titles.exists():
    # 处理超长数据
    for article in long_titles:
        article.title = article.title[:50]
        article.save()

三、高级迁移技巧

1. 自定义迁移文件

有时候自动生成的迁移文件不能满足我们的需求,这时候就需要手动编写迁移文件。比如我们要把Article模型拆分成Article和ArticleContent两个模型:

# 手动创建的迁移文件示例
from django.db import migrations, models
import json

def migrate_articles(apps, schema_editor):
    Article = apps.get_model('blog', 'Article')
    ArticleContent = apps.get_model('blog', 'ArticleContent')
    
    for article in Article.objects.all():
        ArticleContent.objects.create(
            article=article,
            content=article.content,
            created_at=article.created_at
        )

class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0001_initial'),
    ]
    
    operations = [
        migrations.CreateModel(
            name='ArticleContent',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('content', models.TextField()),
                ('created_at', models.DateTimeField(auto_now_add=True)),
            ],
        ),
        migrations.AddField(
            model_name='articlecontent',
            name='article',
            field=models.OneToOneField(on_delete=models.deletion.CASCADE, to='blog.Article'),
        ),
        migrations.RunPython(migrate_articles),
        migrations.RemoveField(
            model_name='article',
            name='content',
        ),
    ]

2. 处理多数据库迁移

在大型项目中,我们可能需要使用多个数据库。Django也支持多数据库迁移,但需要特别注意:

# 多数据库迁移示例
python manage.py migrate --database=users_db  # 指定用户数据库
python manage.py migrate --database=products_db  # 指定产品数据库

# 或者在迁移文件中指定
class Router:
    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'auth':
            return 'users_db'
        return 'default'

# settings.py配置
DATABASE_ROUTERS = ['path.to.Router']

四、最佳实践与总结

经过多年的Django开发,我总结出以下最佳实践:

  1. 总是先备份数据库再进行迁移操作
  2. 在开发环境充分测试迁移脚本
  3. 对于生产环境,先在临时环境验证迁移
  4. 使用版本控制系统管理迁移文件
  5. 考虑使用--fake-initial选项处理已有数据库
  6. 对于大型数据库,考虑在低峰期执行迁移

迁移虽然麻烦,但它是Django ORM强大功能的体现。掌握好迁移技巧,可以让我们在保持数据完整性的同时,灵活地调整数据库结构。记住,遇到迁移问题时,深呼吸,查看文档,搜索解决方案,你一定能找到出路。