最后我找到了以下解决方案。
首先我接受通过isDefault属性识别默认对象的想法,并编写了一些抽象模型来处理它,尽可能保持数据完整性(代码在帖子底部)。
我不太喜欢接受的解决方案是数据迁移与架构迁移混合在一起。很容易丢失它们,即在挤压过程中。有时我也会完全删除迁移,当我确定我的所有生产和备份数据库都与代码一致时,我可以生成单个初始迁移并伪造它。将数据迁移与模式迁移结合在一起会破坏此工作流程。
所以我决定将所有数据迁移保存在migrations 包之外的单个文件中。所以我在我的应用程序包中创建data.py,并将所有数据迁移放在单个函数migratedata中,记住这个函数可以在早期调用,当某些模型可能还不存在时,所以我们需要捕获@987654325 @ 应用程序注册表访问异常。比我对数据迁移中的每个RunPython 操作都使用此功能。
所以工作流程看起来像这样(我们假设 Model 和 ModelX 已经到位):
1) 创建ModelY:
class ModelY(Defaultable):
y_name = models.CharField(max_length=255, default='ModelY')
2) 生成迁移:
manage.py makemigration
3) 在data.py 中添加数据迁移(在我的情况下将模型名称添加到defaultable 列表中):
# data.py in myapp
def migratedata(apps, schema_editor):
defaultables = ['ModelX', 'ModelY']
for m in defaultables:
try:
M = apps.get_model('myapp', m)
if not M.objects.filter(isDefault=True).exists():
M.objects.create(isDefault=True)
except LookupError as e:
print '[{} : ignoring]'.format(e)
# owner model, should be after defaults to support squashed migrations over empty database scenario
Model = apps.get_model('myapp', 'Model')
if not Model.objects.all().exists():
Model.objects.create()
4) 通过添加操作RunPython编辑迁移:
from myapp.data import migratedata
class Migration(migrations.Migration):
...
operations = [
migrations.CreateModel(name='ModelY', ...),
migrations.RunPython(migratedata, reverse_code=migratedata),
]
5) 将ForeignKey(ModelY) 添加到Model:
class Model(models.Model):
# SET_DEFAULT ensures that there will be no integrity issues, but make sure default object exists
y = models.ForeignKey(ModelY, default=ModelY.default, on_delete=models.SET_DEFAULT)
6) 再次生成迁移:
manage.py makemigration
7) 迁移:
manage.py migrate
8) 完成!
整个链可以应用于空数据库,它将创建最终模式并用初始数据填充它。
当我们确定我们的数据库与代码同步时,我们可以轻松删除一长串迁移,生成单个初始迁移,将RunPython(migratedata, ...) 添加到其中,然后使用--fake-initial 迁移(删除django_migrations 表之前)。
嗯,如此简单的任务的解决方案如此棘手!
终于有Defaultable模型源码了:
class Defaultable(models.Model):
class Meta:
abstract = True
isDefault = models.BooleanField(default=False)
@classmethod
def default(cls):
# type: (Type[Defaultable]) -> Defaultable
"""
Search for default object in given model.
Returning None is useful when applying sqashed migrations on empty database,
the ForeignKey with this default can still be non-nullable, as return value
is not used during migration if there is no model instance (Django is not pushing
returned default to the SQL level).
Take a note on only(), this is kind of dirty hack to avoide problems during
model evolution, as default() can be called in migrations within some
historical project state, so ideally we should use model from this historical
apps registry, but we have no access to it globally.
:return: Default object id, or None if no or many.
"""
try:
return cls.objects.only('id', 'isDefault').get(isDefault=True).id
except cls.DoesNotExist:
return None
# take care of data integrity
def save(self, *args, **kwargs):
super(Defaultable, self).save(*args, **kwargs)
if self.isDefault: # Ensure only one default, so make all others non default
self.__class__.objects.filter(~Q(id=self.id), isDefault=True).update(isDefault=False)
else: # Ensure at least one default exists
if not self.__class__.objects.filter(isDefault=True).exists():
self.__class__.objects.filter(id=self.id).update(isDefault=True)
def __init__(self, *args, **kwargs):
super(Defaultable, self).__init__(*args, **kwargs)
# noinspection PyShadowingNames,PyUnusedLocal
def pre_delete_defaultable(instance, **kwargs):
if instance.isDefault:
raise IntegrityError, "Can not delete default object {}".format(instance.__class__.__name__)
pre_delete.connect(pre_delete_defaultable, self.__class__, weak=False, dispatch_uid=self._meta.db_table)