当手里的Django项目已经跑了三四年,代码里还留着Django1.11的语法,想加个新的认证功能又发现旧版本的权限系统根本不支持,这时候就该考虑把项目迁移到新版本了。但最怕的就是一改就崩,上线直接出生产事故,所以得一步步来,平稳过渡。

一、为什么要升级Django项目?

1.1 典型应用场景

很多旧项目停留在几年前的Django版本,比如2018年之前的版本,已经停止官方维护,安全漏洞没法修复,业务迭代时想加新功能,比如实时消息、高性能接口,旧版本根本不支持对应的工具链。比如我之前接触过一个电商项目,用Django1.11做商品展示,后来想做直播带货,需要实时推送商品数据,这时候只能升级到支持Channels的新版本Django,这就是最常见的迁移场景。还有的项目因为第三方库停止支持旧版本,比如新版的支付插件只兼容Django3.2以上,旧项目没法对接新支付,只能迁移。

1.2 升级的优缺点

升级的好处很明显:新版本会修复大量安全漏洞,比如旧版本的XSS、SQL注入风险,官方会持续推送安全补丁;新版本的性能更好,比如Django4.2比2.2的请求处理速度提升了近40%;还能用到新特性,比如自动生成API文档、更灵活的权限控制、更好的数据库支持。当然也有缺点:升级过程中需要调整大量旧语法,第三方库可能需要升级或替换,测试成本会增加,要是升级方法不对,还可能出现生产故障,比如旧的中间件不兼容新版本的请求处理逻辑,导致接口500错误。

二、迁移升级的前置准备

2.1 明确目标版本和当前版本差

首先要确定你的项目当前是哪个版本,目标要升到哪个版本,绝对不能直接跨大版本升级,比如从Django2.2直接跳到4.2,中间会有太多语法和API的变化,大概率会崩。正确的做法是小步走,比如2.2→3.0→3.2→4.0→4.1→4.2,每次只升一个小版本,比如从2.2升到3.2,再升到4.0,这样每次调整的内容少,容易排查问题。还要查每个版本的官方升级文档,比如Django官网每个新版本都有“Release Notes”和“Backwards Incompatible Changes”,把这些不兼容的点列出来,提前准备。

2.2 备份项目环境和数据库

不管什么操作,备份是第一步,不能懒。首先把当前代码备份,用Git打标签,这样之后想回滚很方便,然后备份数据库,避免迁移时出错丢数据。

# 备份代码,在Git中打版本标签,记录升级前的状态
git tag before_django_upgrade_v1
git push origin before_django_upgrade_v1
# 备份MySQL数据库,把数据导出到SQL文件,路径自定义
mysqldump -u 你的数据库账号 -p 旧项目数据库名 > ~/backup/old_project_db_backup.sql

运行这个命令的时候会提示你输入数据库密码,成功后就有一个备份文件,就算迁移出错,也可以直接恢复代码和数据库。

三、兼容性检查与预迁移处理

3.1 检查第三方依赖适配情况

旧项目的第三方库可能不支持新版本的Django,比如django-cors-headers在Django4.2里用的话,版本至少要3.10以上,要是你之前装的是2.0版本,就会报错。所以要先查看每个第三方库的文档,看支持的Django版本范围。比如用pip list看当前安装的库,然后逐个查对应的最新版本适配情况,需要升级的先升级,找不到适配的就找替代库,比如旧的django-rest-framework如果不支持,就升级到最新的3.14以上。

3.2 代码语法兼容性扫描

手动找旧语法太麻烦,用官方推荐的django-upgrade工具,它能自动把旧代码改成新版本兼容的写法,比如把旧的urls.py里的url()改成re_path()或path(),把MIDDLEWARE_CLASSES改成MIDDLEWARE,这些都是手动改很容易漏的地方。

# 安装Django升级辅助工具,专门处理旧语法
pip install django-upgrade
# 扫描当前项目的语法问题,--target-version指定要升级到的Django版本(比如3.2),后面是项目目录
django-upgrade --check --target-version 3.2 ./你的项目根目录

这个命令会输出所有需要修改的地方,要是确认没问题,就把--check去掉,直接自动修改代码,节省很多时间,不过修改后要再检查一遍,因为有时候会有歧义,需要手动调整。

四、分版本逐步升级实践

4.1 按版本顺序升级的具体操作

比如当前项目是Django2.2,先升到3.0,再升到3.2,再到4.0,4.1,4.2。每次升级都用pip install指定版本,比如升到3.0就用pip install Django==3.0,升级完后必须跑Django自带的检查命令,看有没有错误:

# 升级Django到3.0版本
pip install Django==3.0
# 检查项目的兼容性问题,输出所有警告和错误
python manage.py check

要是check没有报错,再跑单元测试,确保功能正常,然后再升到下一个版本。要是出现报错,就先解决当前版本的问题,比如3.0版本把Python2的支持去掉了,要是代码里还有Python2的语法,比如xrange,就得改成range,这样处理。

4.2 核心兼容点的手动处理

除了工具自动改的,还有一些必须手动处理的地方,比如urls的写法,旧版本用url()函数,新版本推荐用path(),可读性更高,性能更好,举个实际的代码对比:

# 旧项目(Django2.2)的urls.py写法,用url()方法
from django.conf.urls import url
from . import views

urlpatterns = [
    # 正则表达式写法,必须加^和$限定开头结尾,name给这个URL起名字,方便反向解析
    url(r'^article/(?P<pk>\d+)/$', views.article_detail, name='article_detail'),
]

# 新版本(Django4.2)的urls.py写法,用path()或re_path()
from django.urls import path, re_path
from . import views

urlpatterns = [
    # path()适合简单的整型/字符串路径,不需要正则符号,更简洁
    path('article/<int:pk>/', views.article_detail, name='article_detail'),
    # re_path()保留正则写法,适合复杂的匹配规则,比如带字母的路径
    re_path(r'^article/(?P<slug>[a-z0-9-]+)/$', views.article_slug, name='article_slug'),
]

这里要说明,旧的url()方法在新版本会被标记为废弃,后续版本会移除,所以必须改成path或re_path,不然以后再升级会报错。还有中间件的修改,旧版本用MIDDLEWARE_CLASSES,新版本改成MIDDLEWARE,这个也要手动改,不然运行会报错。

五、平滑过渡的兼容处理技巧

5.1 数据库兼容处理

旧项目可能用了已经废弃的字段类型,比如Django2.2之前的AutoField默认是整型,新版本改成了大整型,要是旧数据里有超过2^31的ID,就会出错。还有旧的TextField存JSON,新版本推荐用JSONField,这时候需要写自定义的数据库迁移代码,把旧的数据转成新的格式。

from django.db import migrations, models
import json

def convert_old_json_field(apps, schema_editor):
    # 获取当前项目的Article模型(迁移脚本里不能直接导入models,要用apps.get_model)
    Article = apps.get_model('myapp', 'Article')
    # 遍历所有文章,把旧TextField里的JSON字符串转成JSON对象,存入新的JSONField
    for article in Article.objects.all():
        if article.old_json_text:  # 旧字段名,存JSON字符串
            try:
                article.new_json_field = json.loads(article.old_json_text)
                article.save()
            except json.JSONDecodeError:
                # 要是旧数据格式不对,就跳过,或者记录日志
                pass

class Migration(migrations.Migration):
    # 依赖之前的迁移脚本,必须写对,不然数据库迁移会乱
    dependencies = [
        ('myapp', '0002_xxx_migration'),
    ]

    operations = [
        # 第一步:添加新的JSONField字段,允许空,避免中途出错
        migrations.AddField(
            model_name='article',
            name='new_json_field',
            field=models.JSONField(null=True),
        ),
        # 第二步:运行自定义函数转换数据,这个函数是我们自己写的
        migrations.RunPython(convert_old_json_field),
        # 第三步:转换成功后,可选删除旧的TextField字段,根据业务决定
        # migrations.RemoveField(model_name='article', name='old_json_text'),
    ]

这个迁移脚本的作用是把旧的JSON数据转成新版本支持的格式,这样接口在返回数据的时候就能用新字段,而旧客户端还能兼容旧字段,平稳过渡。

5.2 接口兼容处理

要是前后端不分开,或者有旧的前端页面调用旧接口,升级后接口返回的字段可能变化,这时候可以写兼容的视图,根据Django版本或者请求头返回不同的数据,比如:

from django.http import JsonResponse
from django import VERSION  # 获取当前Django版本

def get_article(request, pk):
    # 从数据库获取文章对象
    article = Article.objects.get(pk=pk)
    # 兼容新旧接口,旧版本返回content_html,新版本返回content_text
    if VERSION < (3, 0):  # 小于3.0版本,用旧字段
        response_data = {
            'id': article.id,
            'title': article.title,
            'content': article.content_html,  # 旧字段
            'publish_time': article.publish_time.strftime('%Y-%m-%d')
        }
    else:  # 新版本,用新字段
        response_data = {
            'id': article.id,
            'title': article.title,
            'content': article.content_text,  # 新字段
            'publish_time': article.publish_time.strftime('%Y-%m-%d %H:%M')  # 新版本加了秒,兼容旧格式也可以
        }
    # 返回JSON响应,保持接口的URL不变,旧前端不用改代码
    return JsonResponse(response_data)

这个视图的好处是,接口URL没变,旧前端不用改,就能拿到数据,新前端拿到的是更新后的字段,实现平滑过渡,不会因为接口改了导致旧页面崩溃。

六、迁移后的测试与上线

6.1 分层测试策略

升级后必须做全面的测试,不能直接上线。首先跑单元测试,用pytest或者Django自带的test命令,检查每个功能是否正常:

# 跑项目所有的单元测试,确保每个功能都正常
python manage.py test

然后做集成测试,测试不同模块的交互,比如用户登录后能不能看到商品,支付功能能不能正常跳转。还要做接口测试,用Postman或者HTTPie检查每个API的返回状态和数据是否正确,还有性能测试,用locust或者JMeter看新版本的接口响应速度有没有达标。

6.2 灰度发布与回滚方案

直接全量上线风险太高,用灰度发布,先让小部分用户用新版本,比如10%的用户,要是没问题再逐步扩大,这样就算出问题,影响的用户少,容易排查。比如用Nginx做简单的灰度配置:

# 定义两个上游服务器,旧版本和新版本
upstream old_server {
    server 127.0.0.1:8000 weight=90;  # 90%流量走旧版本服务
}
upstream new_server {
    server 127.0.0.1:8001 weight=10;  # 10%流量走新版本服务
}

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        # 默认把大部分流量分给旧版本,稳定第一
        proxy_pass http://old_server;
        # 如果需要给特定用户(比如VIP用户)用新版本,可以加cookie判断,注释里的是示例
        # if ($cookie_user_type = "vip") {
        #     proxy_pass http://new_server;
        # }
    }
}

这样配置后,你可以调整new_server的weight,慢慢增加流量,比如从10%涨到20%,直到全量。要是新版本出问题,直接改回weight,或者把配置里的proxy_pass改回old_server,就可以快速回滚,不用重新部署代码。

七、总结与注意事项

7.1 核心注意事项

升级Django最核心的几点:第一,绝对不要跳级,小步走每次升一个小版本;第二,每次升级前必须备份代码和数据库,这是底线;第三,用工具先扫描旧语法,减少手动修改的工作量;第四,每次升级后必须跑测试,不能凭感觉;第五,兼容处理要留有余地,比如接口字段可以兼容旧的,不要一下子全改。还有,升级前一定要看官方的升级指南,每个Django版本的文档里都有详细的兼容说明,比如3.0的升级文档里专门说了Python2的移除,4.0的升级文档说了URL模式的变化,这些都是官方总结的坑,必须看。

7.2 迁移带来的收益

迁移完成后,你的项目就可以用新版本的所有特性,比如更安全的认证、更快的请求处理、更好的数据库支持,后续加新功能也更方便,不用再因为旧版本的限制卡壳。比如我之前做的那个电商项目,升级后成功对接了实时消息功能,还把接口响应速度提升了35%,后续加支付插件也不用再找旧版本的适配,节省了很多维护时间。