迁移 Django Model id 为 uuid

https://www.klette.us/migrate-django-models-to-uuid-primary-key/

又是一篇渣翻

老设计决策有时候会让你很难受,下面就是一个例子。

我手上的一个 Django Model 是这样的:

class Municipality(models.Model):
    code = models.CharField(max_length=2, primary_key=True)
    name = models.CharField(max_length=100)

此外,还有一个外键指向它

class ZipCode(models.Model):
    code = models.CharField(max_length=2, primary_key=True)
    municipality = models.ForeignKey(Municipality)

现在,我们需要让 municipality 支持多个国家,显然一个唯一 field 将会引起冲突,不够用。(译者:这个理由有点牵强)

对于所有代码的现代部分,我们使用UUID作为主键。所以我们想要 migrate municaipality的主键到UUID,并且维持了原有的关系。(译者:外键和自增主键都是数据库性能的杀手。)

2017年9月份,Django 依然不支持优雅的迁移主键,所以我们自己做了(译者:我也不知道现在行不行。)

我们尝试了许多 magic 解决方案,但是我们在 migrations 系统这个被难住了,并且不能检测以及很好的处理改变。

在经过一小段研究和错误,我们找到了以下的解决方案。尽管这还有一些小问题,但是的确有效。

再一次提醒,从数据库的角度,当你定义了一个ForeignKey field 在 Django 中,Django将会创建一个数据库列,是同样的类型,作为 referenced model 的主键,并且增加外键约束。所以在上面的例子中,我们有两个表:

CREATE TABLE municipality (
   code varchar(2) PRIMARY KEY NOT NULL,
   name varchar(100)
);

CREATE TABLE zipcode (
   code varchar(2) PRIMARY KEY NOT NULL,
   municipality_id VARCHAR(2) REFERENCES(municipality.id) NOT NULL
);

所以我们需要解除外键约束,更换 root model,然后映射新的主键到旧的上,并且重新应用外键到上面去。

我们首先打破外键

class ZipCode(models.Model):
    code = ...  # Same as before
    municipality = models.CharField(max_length=2)
python manage.py makemigrations -n break_zipcode_muni_foreignkey

现在,·Municipality·modle 没有被任何外键所 refer,我们可以在上面进行工作了。

增加一个新的 id field:

class Municipality(models.Model):
    id = models.UUIDField(default=uuid.uuid4)
python manage.py makemigrations -n add_id_field_to_muni

处于某些原因,默认值在我的案例中不 work,所以我增加了一个过程到创建的 migration,来创建新的唯一 id。

def create_ids(apps, schema_editor):
    Municipality = apps.get_model('loc', 'Municipality')
    for m in municipality:
        m.id = uuid.uuid4()
        m.save()
        
# ...

operations = [
    migrations.AddField(...),
    migrations.RunPython(code=create_ids),
]

现在我们有一个UUID ·id·field 在·Municipality 中,然后我们应该可以更换主键了。

class Municipality(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    code = models.CharField(max_length=2, unique=True)

创建 migration,并且确保在code上的AlterField操作在id之前。我们已经在id上增加了primary_key并且增加了 unique=True 到code field。constraint 没了,在我们删除 primary_key 的时候就没了。

让我们开始一个空的迁移

python manage.py makemigrations --empty -n fix_zipcode_fk_to_muni_uuid loc

打开文件,输入

def match(apps, schema_editor):
    ZipCode = apps.get_model('loc', 'ZipCode')
    Muni = apps.get_model('loc', 'Municipality')
    for zip_code in ZipCode.object.all():
        zip_code.temp_muni = Muni.get(code=z.municipality)
        zip_code.save()
    
# ...
operations = [
    migrations.AddField(
        model_name='zipcode',
        name='temp_muni',
        field=models.UUIDField(null=True),
    ),
    migrations.RunPython(code=match),
    migrations.RemoveField(model_name='zipcode', name='municipality'),
    migrations.RenameField(
        model_name='zipcode', old_name='temp_muni', new_name='municipality'),
    migrations.AlterField(
        model_name='zipcode',
        name='municipality',
        field=models.ForeignKey(
            on_delete=django.db.models.deletion.PROTECT,
            to='municipality')
]
  1. 增加一个临时的field来保存 Municipality 的UUID,我们不让他是一个ForeignKeyfield,否则 Django confuse。
  2. 我们运行 match 函数来寻找新的 id,通过寻找旧的 key,并且存储到临时的 field。
  3. 删除旧的 municipality field
  4. 重命名临时 field 到 municipality
  5. 最后迁移到一个外键,然后创建我们需要 constraint
译者:这样改最大的好处就是改了 UUID,但是没有破坏之前的逻辑,可以说是谨慎的迁移吧。不过线上的数据既然在跑着,就这么迁移了,会不会出问题呢。

下面还有一些内容。自从我们将 migrations 分离成多个文件,我们让代码变得很脆弱(如果后面的某些 migrations 失败了)。这将会让我们的应用进入一个 unworkable 的状态。所以确保测试一下migrations。你可通过手工把这所有的步骤整合到一个migration里,但是如果你有来自多个不同app的 reference,你可能需要把这些步骤再分开。

logging

迁移的过程中,你可能遇到很多问题,所以一个比较不错的方式,是创建一个简单的迁移 logging。

def log(message):
    def fake_op(apps, schema_editor):
        print(message)
    return fake_op
    
    
 # ...
 
 operations = [
     migration.RunPython(log('Step 1')),
     migration.AlterField(..),
     migration.RunPython(log('Step 2')),
     # ...
 ]

想要观察 Django 运行了那些 SQL 语句,运行python manage.py sqlmigrate <appname><migration_number>,这是一个超级有用的方法。

 

打赏

发表评论

电子邮件地址不会被公开。 必填项已用*标注