※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。

<前のページ
【初心者チュートリアル】Django2でブログ作成(Part10)〜templateの作成~
次のページ>
【初心者チュートリアル】Django2でブログ作成(Part12)〜get_absolute_url()~

【初心者チュートリアル】Django2でブログ作成(Part11)〜uuidを使う~

web開発 python3 セキュリティ Django python Djangoチュートリアル

投稿日:2019年11月21日

このエントリーをはてなブックマークに追加
Djangoは簡単にWebアプリケーションを作成できるフレームワークです。この記事は初心者の方向けのDjangoチュートリアルです。

はじめに

今回の内容

前回のpart10まででひとまず簡単なブログ機能の作成までできました。このPartでは記事のテーブルに新たにuuidというフィールドを追加してみようとおもうのですが...通常レコードが入っていない状態なら特に問題なく進められるのですが、テーブルにが入っているとある問題が...
今回は

  • なぜuuidを使うのか?
  • 起こりうる問題とその原因
  • 問題への対処方法

を主に解説していきます。


UUID

urlでのid(Sequential Primary Key)の問題点

ここまで特に何も考えずブログ記事の詳細ページのリンクを/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とUUIDField|どっちをつかうの?

SlugFieldでは単純にURL(の末尾)をどのような値にするのかを、入力者が一意の値になるように考えて入力します。slugとはURLの一部分のことを指す業界用語です。テーブルに入る予定のレコードが少なくレコードを一言で一意に表現しやすいModelであれば、識別子に選ぶのはこのSlugFieldで問題ないでしょう。しかし、もしレコード数が多く、一意に表現しにくいModelでこれをえらんでしまうと...

『この値は?...ダメ?...じゃあこれは?...これもかぶってる?...えーと』
というように、使用可能なslugを見つけることに時間を使う様になってしまうでしょう。

それに対してUUIDFieldの場合は乱数値を自動で入力させます。『じゃあ全部こっち使えばいいじゃん!!』と思いますか?いえいえ、UUIDFieldにもデメリットがあります。例えば今見ているこの記事のURLをみてみてください。このサイトでは記事のURLが/{ 大カテゴリのuuid }/{ 小カテゴリのuuid }/{ 記事のuuid }となっているのですが...長い!!見づらい!!わかりにくい!!

本来ならブログのカテゴリの様なレコード数が少なく一意に表現しやすそうなテーブルのURLはSlugFieldで定義するべきなのです。そうすれば/server/django/{記事のuuid}/のような完結でわかりやすいリンクにできていたのに。

(反省!!!!)

それはともかくとして...

チュートリアルのArticleモデルに話をもどします。

今回の記事自体のURLはレコード数の予測もつかなく、一意のタイトルもつけにくそうなのでUUIDFieldを使いましょう。

uuidフィールドの追加

sample_blog/blog/models.py
[...]
import uuid as uuid_lib

[...]

class Article(models.Model):
    [...]
    ###### フィールド定義 ##################################################
    uuid = models.UUIDField(
        db_index=True,
        unique=True,
        default=uuid_lib.uuid4,
        editable=False)
    [...]

もし、この時点でテーブルにレコードが入っていなければそのままmakemigrationsmigrateコマンドをはしらせて終了で良いのですが、今回の様にすでにレコードが入っている場合実は大きな問題が...

大きな問題

ここである重大な問題について解説します。この問題はある特定の状況でおこります。その状況とは、すでにレコードが存在するModelに新しいフィールドをdefault={何かしらの関数}で追加した今回のような状況です。

ここからDBに変更を反映させようとした時、僕ら開発者の理想では
『新しくカラムが作成され、いま存在する全てのレコードに対してdefaultで指定した関数が個別に実行されその結果が格納される』
とイメージします。しかし実際には
『新しくカラムが作成され、defaultで渡した関数の結果がそれぞれのレコードに格納される』

のです。

つまりmigrate時に存在した全てのレコードのuuidに同じ値が格納される事になってしまうのです。今uuidはunique=Trueで重複を許さないようになっているので当然migrate時にエラーになります。

▲migrate実行時のデータの格納『理想』と『現実』

この問題には大きく3つの解決方法があります。

  1. とりあえずmigrateのときのみuuidの重複を許可し、全てのレコードのuuidを再設定。その後改めてuuidunique=Trueを設定する。
  2. migrate時の処理を自分でカスタムに記述する。
  3. 一端そのテーブルのレコードを全て削除してからmigrateを走らせる

難易度は 3 < 1 < 2 で、2が一番難しく3が一番簡単です。3は簡単なので開発中はこちらで良いと思うのですが、もしこの状況がサイトの公開中に起こってしまった場合...そう簡単にレコードを削除するわけにはいきません。そのようなばあいには1の方法を使うのが良いでしょう。

今回は1と3の方法を解説するので、このチュートリアルでは自分の好きな方を選んでやってみてください。

方法1:いちど重複を許可し手動で再設定する場合

※もしさっき、uuidを追加した後に間違えてmakemigrationsをしてしまった方は、その時作成されたmigrationファイルを削除してから進んでください。(/blog/migrations/ディレクトリの中にあります。)

まずはuuidの重複を許可してとりあえずフィールドの追加だけでもできるようにしましょう。

sample_blog/blog/models.py
[...]

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フィールドが追加されます。

▲articleテーブルの中身のイメージ

つぎにuuidを再設定します。まずはArticleクラスに自身のuuidを重複ない値に再設定するメソッドを追加してみましょう。

sample_blog/blog/models.py
[...]
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が重複ない値に更新できます。

▲修正後のArticleテーブルのイメージ

最後にuuidに重複禁止のルールを追加して定義を当初の状態にもどしましょう。

sample_blog/blog/models.py
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フィールドが追加できましたね!!

方法2:テーブルのレコードを全て削除してからフィールドを追加する方法

レコードの削除の方法は管理サイトからでも行えるのですが、レコードが大量があったときを考えて、プログラミング的にやってみます。まずはArticleモデルのuuidフィールドをコメントアウトします。

sample_blog/blog/models.py
[...]
class Article(models.Model):
    [...]
    # uuid = models.UUIDField(
    #     db_index=True,
    #     unique=True,
    #     default=uuid_lib.uuid4,
    #     editable=False)
    [...]

次にDjangoのshellArticleの全てのレコードを削除します。

ターミナル
$ 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してみましょう。

sample_blog/blog/models.py
[...]

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はその理由を肌で感じられたはず。

まとめ

  • idの様なSequrentialPrimaryKeyをユーザーの見えるところに置いてはいけない。
  • URLで識別子を使いたいときにはUUIDFieldSlugFieldを使おう。
  • 運用中にデータベースの構造を変えるような状況はできる限りさけよう!!
このエントリーをはてなブックマークに追加

<前のページ
【初心者チュートリアル】Django2でブログ作成(Part10)〜templateの作成~
次のページ>
【初心者チュートリアル】Django2でブログ作成(Part12)〜get_absolute_url()~

関連記事

記事へのコメント