環境
Python:3.6
Django:2.1
やりたいこと
Djangoを使って開発を行っていましたが、最初は中間テーブルのプライマリキーをデフォルトのIDで使っていたものの、セキュリティ上の懸念に後から気がつき、変更ID→UUIDに変更したくなってしまった。
開発環境ではDB作り直せばよいのですが、せっかくなので消さずに修正を行ってみました。
※UUIDは確率論的に重複することのないランダムな値です。Webサービス等でIDをそのまま使っていた場合は、IDをベースにDBサイズやテーブル内容の推測、それに伴う攻撃が考えられるらしいです。
※今回は中間テーブルのIDのため、外部キーの被参照先となっていません。外部キーの被参照先となっている場合の検証はしていませんが、恐らくうまく動作しません。
作業手順
前提と考え
以下のようにthroughを定義して中間テーブルを独自に定義しているモデルがあるとします。
(もししていなかった場合は定義する必要があります。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 公式ヘルプのサンプルを改変 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に書き換えるてマイグレーションを行うのですが、そのままだとユニーク制約にひっかかってエラーが出てしまいます。
1 2 3 4 5 6 7 8 9 10 |
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)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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作成時にエラーが出るのでまだ削除はしない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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列をプライマリキーに設定」という順番で処理を書かないとエラーで実行出来ないので要注意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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
なかなか骨が折れますな。。きちんと設計はしようということですね…