【Python・Django】中間テーブルのプライマリキーを後からID→UUIDに変更したい
# django # python
環境
Python:3.6 Django:2.1
やりたいこと
Djangoを使って開発を行っていましたが、最初は中間テーブルのプライマリキーをデフォルトのIDで使っていたものの、セキュリティ上の懸念に後から気がつき、変更ID→UUIDに変更したくなってしまった。
開発環境ではDB作り直せばよいのですが、せっかくなので消さずに修正を行ってみました。
※UUIDは確率論的に重複することのないランダムな値です。Webサービス等でIDをそのまま使っていた場合は、IDをベースにDBサイズやテーブル内容の推測、それに伴う攻撃が考えられるらしいです。
※今回は中間テーブルのIDのため、外部キーの被参照先となっていません。外部キーの被参照先となっている場合の検証はしていませんが、恐らくうまく動作しません。
作業手順
前提と考え
以下のようにthroughを定義して中間テーブルを独自に定義しているモデルがあるとします。 (もししていなかった場合は定義する必要があります。)
// 公式ヘルプのサンプルを改変
import uuid
from django.db import models
class Person(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=50)
class Group(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=128)
members = models.ManyToManyField(
Person,
through='Membership',
through_fields=('group', 'person'),
)
class Membership(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
inviter = models.ForeignKey(
Person,
on_delete=models.CASCADE,
related_name="membership_invites",
)
invite_reason = models.CharField(max_length=64)
以下のようにidをUUIDに書き換えるてマイグレーションを行うのですが、そのままだとユニーク制約にひっかかってエラーが出てしまいます。
class Membership(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
inviter = models.ForeignKey(
Person,
on_delete=models.CASCADE,
related_name="membership_invites",
)
invite_reason = models.CharField(max_length=64)
Djangoのマイグレーションではカラム追加時に各行にユニークな値を入れるということが出来ません。 そのため、面倒ですが以下の手順を踏む必要があります。
- uuid列を初期かつ編集可能としてマイグレーションを実行
- uuid列に本来の値を設定
- プライマリキーが2つ出来ないように、もともとのプライマリキーを削除
- uuid列をプライマリキーに設定し、編集不可とする。
こうやってデフォルトのIDをUUIDに置き換える
普通に「python manage.py makemigration」を実行する
以下のようなマイグレーションファイルが出来上がる(このままだとNG)。
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('myapp', '0003_auto_20150129_1705'),
]
operations = [
migrations.RemoveField(
model_name='mymodel',
name='id',
),
migrations.AddField(
model_name='mymodel',
name='uuid',
field=models.UUIDField(editable=False, primary_key=True, serialize=False),
),
]
まずはuuidにnullとして登録するように変更。この時点で以前のプライマリキーの削除するとuuid作成時にエラーが出るのでまだ削除はしない。
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('myapp', '0003_auto_20150129_1705'),
]
operations = [
# migrations.RemoveField(
# model_name='mymodel',
# name='id',
# ),
migrations.AddField(
model_name='mymodel',
name='uuid',
field=models.UUIDField(default=None, editable=True, null=True, serialize=False),
preserve_default=False,
),
]
uuid列に本来の値を設定しつつ、旧プライマリキーの削除と本来の制約をかけるマイグレーションファイルを作成
空のマイグレーションファイルを作成。
「python manage.py makemigrations myapp —empty」を実行すればok。 手動でファイルを作っても問題ないですが、前のマイグレーションとの依存関係もファイル内に記載されるため、コマンドで作成するのが無難。
正しいプライマリキーの設定になるようマイグレーションファイルを設定
各レコードにユニークなuuidを設定する関数を作成してそれを呼び出す形を取ります。 当然ですが、ここの値の設定次第uuidに限らず好きな値の設定が可能です。
なお、「正しい値の設定→旧プライマリキー列削除→UUID列をプライマリキーに設定」という順番で処理を書かないとエラーで実行出来ないので要注意。
from django.db import migrations, models
import uuid
def gen_uuid(apps, schema_editor):
MyModel = apps.get_model('myapp', 'MyModel')
for row in MyModel.objects.all():
row.uuid = uuid.uuid4()
row.save(update_fields=['uuid'])
class Migration(migrations.Migration):
dependencies = [
('myapp', '0004_add_uuid_field'),
]
operations = [
# omit reverse_code=... if you don't want the migration to be reversible.
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
migrations.RemoveField(
model_name='MyModel',
name='id',
),
migrations.AlterField(
model_name='MyModel',
name='uuid',
field=models.UUIDField(editable=False, primary_key=True, serialize=False),
),
]
マイグレーションを実行
以上で必要なDBの操作はすべてマイグレーションファイルに記載が出来ましたので、通常通り 「python manage.py migrate」を実行すればきちんとuuidに一意な値が格納されています。
参考
https://docs.djangoproject.com/en/2.0/howto/writing-migrations/#migrations-that-add-unique-fields
http://www.denzow.me/entry/2017/12/23/150501
なかなか骨が折れますな。。きちんと設計はしようということですね…