将正在运行的 Django 1.7 应用程序升级到 Django 1.8
这是什么?
这是mixi团体圣诞日历的第二天。
由MIXI Group运营的「Ticket Camp」使用Python和Django进行服务器端开发。
现在使用的Django版本是1.7.11,以下是升级至Django 1.8时所进行的操作记录。
我认为TicketCamp有它自己独特的地方,但如果对考虑更新Django的人有所帮助的话,我会感到非常幸福。
票务网站TicketCamp的版本升级政策。
就Ticket Camp的框架和库的更新策略而言,虽然有一些需要依赖我的直觉的部分,很难明确表述,但总的来说可以通过以下三条规则来解释。
-
- セキュリティアップデートは出来る限り早期に適用
-
- Djangoのメジャーアップデートには出来る限り追従
- それ以外に理由がない限りアップデートしない
只要没有特殊原因,第三个选项是含糊不清的,它可能包括“想要使用X功能,但这种情况下需要将Y库升级到最新版本”的案例,其中原因是明确的;然而也有这样的情况,例如“由于有需要修复图片上传API的事项,所以顺便也升级了Pillow库”,在这种情况下并没有比“顺便提升了”的更深层次的原因。
无论如何,我想强调的是我们并没有采取“禁止升级版本”的策略进行开发。
作为一个使用Django从1.0版本以前就开始的人来说,我一直坚持跟进最新版本,这是因为保持应用程序的健全性,并且基于我所获得的经验,当实际需要更新时,这样做最终会更加轻松。
基于这个角度,我从Django 1.8的Beta阶段就想要进行升级,但是遇到了一些升级的障碍,一直没有动手。在这样的情况下,Django 1.9的RC版本已经发布了,所以我意识到这是一个问题,终于开始认真地升级版本了。
当我点击发布按钮查看此文章时,发现Django已经发布了1.9版本。
升级到Django 1.8所需要的修正工作。
更新最新版本的芹菜
在TicketCamp中,我们使用了Python中常用的Celery作为作业队列,通过AWS SNS发送推送通知以及与外部网络进行I/O的操作。另外,我们还利用Celery的工作进程来处理需要一定时间的任务,比如票券的上架和请求的匹配处理。
由于将Django版本升级到1.8后,之前使用的celery==3.1.7出现了错误,所以我将其更新到最新版本的celery==3.1.9。
pip install --upgrade celery==3.1.9
由于Celery非常稳定且用户众多,因此对于小型更新并不感到太多风险。
将django_nose升级到最新版本。
在进行单元测试时,我使用的测试运行器不是Django的标准运行器,而是使用了django_nose的测试运行器。
因为这也是固定在稍旧的版本上,所以出现了错误,所以我更新到最新版的django_nose==1.4.2。
pip install --upgrade django_nose==1.4.2
将TEST_NAME更改为TEST
由于Django 1.7版本起,测试数据库的配置方式已从TEST_前缀更改为使用独立的字典TEST,因此在此时要进行修正。
修订之前。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'USER': 'root',
'NAME': 'ticketcamp_dev',
'TEST_NAME': 'test_ticketcamp_dev',
},
}
校正完毕。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'USER': 'root',
'NAME': 'ticketcamp_dev',
'TEST': {
'NAME': 'test_ticketcamp_dev',
},
},
}
loader.get_template_from_string不存在了
在TicketCamp中,只有一个地方使用了名为django.template.loader.get_template_from_string的方法。
最简单的示范代码如下。
from django.template import loader, Context
tmpl = loader.get_template_from_string(content)
output = tmpl.render(Context(ctxt))
以下为中文的原生版本,
这段代码,
AttributeError: 'module' object has no attribute 'get_template_from_string'
我开始出现了这个错误。
据我所知,看起来Django 1.8中删除了django.template.loader.get_template_from_string。
在Django中,方法突然被删除是罕见的情况,绝对不会发生在公开的API中。通常情况下,被弃用的警告将在Django 1.x中产生,因此在日常开发中会自然地意识到。
那么,这个方法并不是公开方法,而是私有方法。
很幸运地,在这个案例中,只需要将loader.get_template_from_string替换为Template就可以轻松解决。
from django.template import Template, Context
tmpl = Template(content)
output = tmpl.render(Context(ctxt))
出现了ForeignKey(unique=True)的警告
在Ticket Camp中,我们使用django.contrib.auth进行用户注册和用户认证。然而,由于开发始于Django 1.4时代,为了让用户添加额外信息,我们仍然使用Profile对象模式。
这是一个类似以下代码的示例。
# -*- coding: utf-8 -*-
# このコードには罠があるので真似しないこと!
from django.conf import settings
from django.db import models
class Profile(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, unique=True)
# 省略
当将Django版本更新至1.8并运行Celery的工人时,就会出现以下警告。
myapp.Profile.user: (fields.W342) Setting unique=True on a ForeignKey has the same effect as using a OneToOneField.
HINT: ForeignKey(unique=True) is usually better served by a OneToOneField.
我刚到这里才知道,首先上面的代码是
# -*- coding: utf-8 -*-
from django.conf import settings
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
# 省略
看起来应该使用OneToOneField()而不是ForeignKey(unique=True)。而且由于在最初阶段产生了误解,因此在许多地方出现了类似的警告。
根据警告中的提示,只需将OneToOneField()进行更改,我就能够利用这个机会对全部进行修正。
DateTimeField的default、auto_now和auto_now_add属性已被排除了。
从这里开始是更新的困难部分。
TicketCamp的模型定义大致如下模式,其中的created_at和updated_at分别代表模型的创建日期和更新日期,这些日期会自动更新。
from django.db import models
class Ticket(models.Model):
price = models.IntegerField()
count = models.IntegerField()
created_at = models.DateTimeField(default=timezone.now, auto_now_add=True)
updated_at = models.DateTimeField(default=timezone.now, auto_now=True)
当试图在Django 1.8中运行包含此类模型定义的代码时,Celery的工作程序因以下错误而无法启动。
myapp.Ticket.created_at: (fields.E160) The options auto_now, auto_now_add, and default are mutually exclusive. Only one of these options may be present.
myapp.Ticket.updated_at: (fields.E160) The options auto_now, auto_now_add, and default are mutually exclusive. Only one of these options may be present.
由于错误信息所指出的原因,auto_now、auto_now_add和default是互斥的选项,不能同时指定。
为了解决这个问题,我考虑了以下解决方案。
-
- 以前と同じような、デフォルトで現在時刻が入り、モデル保存時に作成日時・更新日時が更新されるようなDateTimeFieldを自前で作る。
-
- モデルの定義はupdated_at = models.DateTimeField(default=timezone.now)とし、MySQL上のスキーマの定義をupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPのように強制する。
updated_atの初期値がNoneになってしまうことを許容する。
根据以下两个理由,我选择不自己编写代码,并且认为Django对这样的更改做了auto_now、auto_now_add和default的同时指定,因此很难实现一个合适的实现方式,因此我放弃深入研究:
– 我不想自己编写代码。
– 推测Django进行了这样的更改,增加了auto_now、auto_now_add和default的同时指定。
第二个选择是基于这样一个预测,即代码的更改较少,对应用程序的影响也较小。然而,我们决定暂时搁置这一选择,因为它与我们一直采用的设计原则相矛盾:“尽量不定义在Django模型中无法表达的架构”。
目前正在进行第三项修正策略,并进行更新测试。
首先,在上面的模型示例中,可以安全地删除created_at字段的auto_now_add=True选项。
在创建模型实例的时候,默认选项的timezone.now被评估并将其值设为created_at的当前时间。调用save()方法时,该值将被保存到数据库中。(考虑到这一点,我认为auto_now_add=True本身就是不必要的。)
问题在于updated_at这一方面。
updated_at = models.DateTimeField(default=timezone.now)
要么…要么…
(Translate: Either…or…)
updated_at = models.DateTimeField(auto_now=True)
必须选择是还是不是。
如果选择前者,当创建实例时,updated_at将为非None,但是即使调用save()函数,数据库中的updated_at列也不会更新。
$ python manage.py shell
>>> from myapp.models import Ticket
>>> ticket = Ticket(price=1000, count=1)
>>> ticket.updated_at is not None
True
>>> ticket.save()
>>> ticket.price = 1500
>>> ticket.save() # 変更したけどDBのupdated_atは更新されない
我考虑使用post_save信号来更新updated_at字段,但由于难以找出原因,可能会引起困难的故障,所以决定放弃这种方法。
选择后者`DateTimeField(auto_now=True)`的缺点是,当创建实例时,`updated_at`的值为`None`,只有在调用`save()`方法时,才会将当前时间更新到实例的`updated_at`属性。
$ python manage.py shell
>>> from myapp.models import Ticket
>>> ticket = Ticket(price=1000, count=1)
>>> ticket.updated_at is not None
False # updated_atはまだNone
>>> ticket.save()
>>> ticket.updated_at is not None
True # save()を呼んだ後に初めて非Noneになる
换句话说,如果在调用save()之前存在引用实例的updated_at的代码,那么可能导致其无法正常工作。
尽管如此,每种方法都有其优劣之处,最终我们认为最后一种方法符合框架开发者设想的使用方式,因此我们决定采用最后一种方法,并按照以下的方式进行修改。
from django.db import models
class Ticket(models.Model):
price = models.IntegerField()
count = models.IntegerField()
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
明らかな错误消失后,我开始进行操作确认,似乎大致正常运行。只剩下在save()之前引用updated_at会导致错误的部分需要调整,这样应该就能完成更新了,不过还存在一些陷阱……
未解决的问题
其实我本来希望在发布这个圣诞日历之前能完全更新,但是仍然存在一些未解决和原因不明的问题。
添加迁移
执行单元测试时,
$ python manage.py test myapp
# 略
django.db.utils.IntegrityError: (1215, 'Cannot add foreign key constraint')
我不小心出现了这个错误。
尽管原因调查花了很多时间,但看起来问题似乎是由于各个Django App目录下不存在迁移文件(migrations),导致单元测试数据库创建时无法正确反映模式所致。
票务营销自Django 1.7引入迁移之前就开始开发了。当升级到Django 1.7时,我们没有执行”manage.py makemigrations”命令,因为我们认为迁移是不必要的,我们没有计划使用。然而,现在似乎确实需要使用迁移了。
修改fixtures的YAML文件。
在Django的标准测试运行器中返回并手动创建测试数据库上。
$ python manage.py test myapp --keepdb
在使用 `keepdb` 选项运行测试时,测试顺利执行。
然而,正如先前预测的那样,由于updated_at模型定义发生了变化,因此尝试向updated_at插入NULL导致夹具无法加载的情况发生。
我只需要一个中文选项来改写这个问题。
-
- すべてのfixturesのYAMLを修正する。
-
- テスト時だけpre_saveシグナルでupdated_atが非NULLになるようにする。
- YAMLでfixturesを作るのをやめて、Factory Boyのようなライブラリに移行する。
我正在考虑各种修改方式。
Django 1.9中报废函数的警告
由于将 Django 版本从 1.8 升级到 1.9,预计某些函数将被废弃并且会有大量警告显示,因此我正在逐个解决这些问题。
Django 的警告 – django.core.cache.get_cache。
RemovedInDjango19Warning: 'get_cache' is deprecated in favor of 'caches'
修订之前。
from django.core.cache improt get_cache
get_cache('default').get('key')
经过修正后,此处已进行了机械替换。
from django.core.cache improt caches
caches['default'].get('key')
django.utils.functional.memoize的警告。
RemovedInDjango19Warning: memoize wrapper is deprecated and will be removed in Django 1.9. Use django.utils.lru_cache instead.
django.utils.datastructures.SortedDict发出警告。这并不是我们自己编写的代码问题,而是由于我们所使用的django-redis版本过于陈旧,因此需要进行升级。
RemovedInDjango19Warning: SortedDict is deprecated and will be $
emoved in Django 1.9.
在将Django模型进行pickle时出现的警告。
RuntimeWarning: Pickled model instance's Django version is not specified.
最后
虽然更新尚未完成,但根据Django的发布政策,我们目前使用的1.7版本一旦2.0发布,将停止维护,因此必须迁移到下一个长期支持版的1.8版本。
此外,我们也在调查从Python 2.7到Python 3.5的迁移是否可行,并且意识到为了进行Python 3.5的迁移,我们需要跟随最新版本的Django。
- Python 2.7で稼働中のDjangoアプリケーションをPython 3.5へ移行する
我希望在年末能够解决这个问题,心情愉快地迎接新年。
下一个计划是akkuma先生将写关于Android的ActionMode。