投稿日:2019年11月21日
Djangoは簡単にWebアプリケーションを作成できるフレームワークです。この記事は初心者の方向けのDjangoチュートリアルです。
前回のpart10まででひとまず簡単なブログ機能の作成までできました。このPartでは記事のテーブルに新たにuuidというフィールドを追加してみようとおもうのですが...通常レコードが入っていない状態なら特に問題なく進められるのですが、テーブルにが入っているとある問題が...
今回は
を主に解説していきます。
ここまで特に何も考えずブログ記事の詳細ページのリンクを/blog/{Articleのid}/の様に指定していました。この時指定しているidはクラスでmodels.Modelを継承した段階で勝手に追されるSequential Primary Key(新しいレコードが追加されるたびにインクリメントされていく主キー)です。
この時URLは例えば/blog/13、/blog/14、/blog/15、...と連番の様になるためURLからこの値がSequential Primary Keyであることがすぐわかってしまいます。基本的にSequential Primary Keyはユーザーにたいして隠しておくべきものです。これを公開してしまうと、競合にデータの量を知られる、悪意のあるユーザーにXSSなどの攻撃を仕掛けられるという危険性が出てきてしまうのです。
そのため、今回の様にURLでModelを一意に識別したい場合には、Modelに新たにSlugField、またはUUIDFieldというフィールドを追加しそれを識別子として使うことで解決できます。
SlugFieldでは単純にURL(の末尾)をどのような値にするのかを、入力者が一意の値になるように考えて入力します。slugとはURLの一部分のことを指す業界用語です。テーブルに入る予定のレコードが少なくレコードを一言で一意に表現しやすいModelであれば、識別子に選ぶのはこのSlugFieldで問題ないでしょう。しかし、もしレコード数が多く、一意に表現しにくいModelでこれをえらんでしまうと...
『この値は?...ダメ?...じゃあこれは?...これもかぶってる?...えーと』
というように、使用可能なslugを見つけることに時間を使う様になってしまうでしょう。
それに対してUUIDFieldの場合は乱数値を自動で入力させます。『じゃあ全部こっち使えばいいじゃん!!』と思いますか?いえいえ、UUIDFieldにもデメリットがあります。例えば今見ているこの記事のURLをみてみてください。このサイトでは記事のURLが/{ 大カテゴリのuuid }/{ 小カテゴリのuuid }/{ 記事のuuid }となっているのですが...長い!!見づらい!!わかりにくい!!
本来ならブログのカテゴリの様なレコード数が少なく一意に表現しやすそうなテーブルのURLはSlugFieldで定義するべきなのです。そうすれば/server/django/{記事のuuid}/のような完結でわかりやすいリンクにできていたのに。
(反省!!!!)
それはともかくとして...
チュートリアルのArticleモデルに話をもどします。
今回の記事自体のURLはレコード数の予測もつかなく、一意のタイトルもつけにくそうなのでUUIDFieldを使いましょう。
[...]
import uuid as uuid_lib
[...]
class Article(models.Model):
[...]
###### フィールド定義 ##################################################
uuid = models.UUIDField(
db_index=True,
unique=True,
default=uuid_lib.uuid4,
editable=False)
[...]
もし、この時点でテーブルにレコードが入っていなければそのままmakemigrations、migrateコマンドをはしらせて終了で良いのですが、今回の様にすでにレコードが入っている場合実は大きな問題が...
ここである重大な問題について解説します。この問題はある特定の状況でおこります。その状況とは、すでにレコードが存在するModelに新しいフィールドをdefault={何かしらの関数}で追加した今回のような状況です。
ここからDBに変更を反映させようとした時、僕ら開発者の理想では
『新しくカラムが作成され、いま存在する全てのレコードに対してdefaultで指定した関数が個別に実行されその結果が格納される』
とイメージします。しかし実際には
『新しくカラムが作成され、defaultで渡した関数の結果がそれぞれのレコードに格納される』
のです。
つまりmigrate時に存在した全てのレコードのuuidに同じ値が格納される事になってしまうのです。今uuidはunique=Trueで重複を許さないようになっているので当然migrate時にエラーになります。
この問題には大きく3つの解決方法があります。
難易度は 3 < 1 < 2 で、2が一番難しく3が一番簡単です。3は簡単なので開発中はこちらで良いと思うのですが、もしこの状況がサイトの公開中に起こってしまった場合...そう簡単にレコードを削除するわけにはいきません。そのようなばあいには1の方法を使うのが良いでしょう。
今回は1と3の方法を解説するので、このチュートリアルでは自分の好きな方を選んでやってみてください。
※もしさっき、uuidを追加した後に間違えてmakemigrationsをしてしまった方は、その時作成されたmigrationファイルを削除してから進んでください。(/blog/migrations/ディレクトリの中にあります。)
まずはuuidの重複を許可してとりあえずフィールドの追加だけでもできるようにしましょう。
[...]
class Article(models.Model):
[...]
uuid = models.UUIDField(
db_index=True,
# unique=True, # uniqueの指定をコメントアウト
default=uuid_lib.uuid4,
editable=False)
[...]
特に説明は必要ありませんね。先程のコードのうちの重複を禁止するunique=Trueをコメントアウトしているだけです(unique=Falseにしたのと同じ)。この状態で一端DBに変更を適用させてみましょう。
$ python manage.py makemigrations
$ python manage.py migrate
するとテーブルの中身のレコードにはすべて以下のように同一のuuidフィールドが追加されます。
つぎにuuidを再設定します。まずはArticleクラスに自身のuuidを重複ない値に再設定するメソッドを追加してみましょう。
[...]
import uuid as uuid_lib
class Article(models.Model):
[...]
def reset_uuid(self):
"""
uuidが重複している時、リセットするメソッド
"""
while True:
uuid_value = uuid_lib.uuid4()
if len(Article.objects.filter(uuid=uuid_value))<=1:
self.uuid = uuid_value
self.save()
break
return
[...]
これを今Articleテーブルに入っている全てのレコードに対して実行させたいですね。
これにはDjangoのshellを使ってみましょう。
$ python manage.py shell
Python 3.6.8 (default, Oct 7 2019, 12:59:55)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from blog.models import Article
>>> for article in Article.objects.all():
... article.reset_uuid()
...
>>> exit()
これで以下のようにuuidが重複ない値に更新できます。
最後にuuidに重複禁止のルールを追加して定義を当初の状態にもどしましょう。
class Article(models.Model):
[...]
uuid = models.UUIDField(
db_index=True,
unique=True, # アンコメント
default=uuid_lib.uuid4,
editable=False)
$ python manage.py makemigrations
$ python manage.py migrate
これで既存のレコードを消さずに正しいuuidフィールドが追加できましたね!!
レコードの削除の方法は管理サイトからでも行えるのですが、レコードが大量があったときを考えて、プログラミング的にやってみます。まずはArticleモデルのuuidフィールドをコメントアウトします。
[...]
class Article(models.Model):
[...]
# uuid = models.UUIDField(
# db_index=True,
# unique=True,
# default=uuid_lib.uuid4,
# editable=False)
[...]
次にDjangoのshellでArticleの全てのレコードを削除します。
$ python manage.py shell
Python 3.6.8 (default, Oct 7 2019, 12:59:55)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from blog.models import Article
>>> for article in Article.objects.all():
... article.delete()
...
(1, {'blog.Article': 1})
(1, {'blog.Article': 1})
(1, {'blog.Article': 1})
(1, {'blog.Article': 1})
(1, {'blog.Article': 1})
>>> exit()
この状態で再度uuidフィールドを追加してmigrateしてみましょう。
[...]
class Article(models.Model):
[...]
uuid = models.UUIDField(
db_index=True,
unique=True,
default=uuid_lib.uuid4,
editable=False)
[...]
$ python manage.py makemigrations
$ python manage.py migrate
フィールド一つ追加することがここまで大変だとは思わなかったのではないでしょうか?
基本的にデータベースの構造というのは、運用中に変えるべきではないと言うのが常識です。今回のPartはその理由を肌で感じられたはず。