自動化無しに生活無し

WEBとかAIとかLinux関係をひたすら書く備忘録系ブログ

【Django】models.pyにフィールドを追加・削除する【マイグレーションできないときの原因と対策も】

thumbnail

models.pyを操作していく上で難しいのが、フィールドの追加とマイグレーション。

特に、追加するフィールドによってはマイグレーションファイル作成時に警告が出ることがある。

本記事では警告が出る理由も含め、フィールドの追加方法も含めて解説する。ソースコードはDjangoビギナーが40分で掲示板アプリを作る方法を元にする。

デフォルト値ありのフィールドを追加する【警告なし】

まず、安全なデフォルト値ありのフィールドを追加する。簡易掲示板であれば、投稿日時も含めたいので、DateTimeField()を追加した。

from django.db import models
from django.utils import timezone

class Topic(models.Model):

    comment = models.CharField(verbose_name="コメント",max_length=2000)
    dt      = models.DateTimeField(verbose_name="投稿日時",default=timezone.now)

    def __str__(self):
        return self.comment

通常、この投稿日時は、投稿された瞬間の日時を記録する。ユーザーが自由に日時を変更できるようにするならまだしも、投稿された瞬間の日時を記録するのであれば、フィールドオプションのdefaultを使用する。

このdefaultの値にはタイムゾーンを考慮した現在の時間、即ち、timezone.nowを指定する。このtimezoneは予め冒頭でimportしなければならない点に注意する。

これでコメント投稿時に自動的に投稿日時がセットされる。

この状態でマイグレーションを実行する。

python3 manage.py makemigrations
python3 manage.py migrate

これでdtフィールドの追加が実現された。

後は、テンプレートに{{ topic.dt }}を追加する。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>簡易掲示板</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>

    <main class="container">
        <form method="POST">
            {% csrf_token %}
            <textarea class="form-control" name="comment"></textarea>
            <input type="submit" value="送信">
        </form>

        {% for topic in topics %}
        <div class="border">
            <div>{{ topic.dt }}</div>
            <div>{{ topic.comment }}</div>
        </div>
        {% endfor %}

    </main>
</body>
</html>

これで日時が表示されるようになる。ちなみに、dtフィールドを追加する前の投稿の日時はどうなるかと言うと、マイグレーションした瞬間の日時が付与される。

フィールドが追加された後は、投稿された瞬間の日時が随時追加されていく。

日付が追加された

ちなみに、ビューのpostメソッドでコメントの保存を行うが、その時に日付を操作する必要はない。defaultがあるから。

【補足1】投稿日時のフィールドはいつ追加するべきか?

上記のように投稿日時のフィールドを追加した時、マイグレーションした時刻が自動的に入力される。

そのため、後から追加された投稿日時のデータは使い物にならない。そのため、最初から投稿日時のフィールドを追加しておいたほうが良いだろう。

【補足2】編集できない日付を自動的に入力するには?

auto_now_add=Trueもしくはauto_now=Trueのフィールドオプションを入れる。これで投稿編集されたときの時刻が自動的に記録され、編集ができないため、改ざんされるリスクが無くなる。

日時の真正性を担保したい場合、こちらを利用したほうが良いだろう。下記記事で解説されている。

【Django】DateTimeFieldに自動的に現在時刻を入れるには、auto_now_addもしくはauto_nowフィールドオプションを指定【新規作成時・編集時の時刻】【※編集不可】

デフォルト値なしのフィールドを追加する【警告あり】

続いて、デフォルト値がないフィールドを追加する。これが少々難しい。

簡易掲示板に名前の入力欄が必要になり、nameという名前のCharField()models.pyに追加する。

from django.db import models
from django.utils import timezone

class Topic(models.Model):

    name    = models.CharField(verbose_name="投稿者の名前",max_length=100)
    comment = models.CharField(verbose_name="コメント",max_length=2000)
    dt      = models.DateTimeField(verbose_name="投稿日時",default=timezone.now)

    def __str__(self):
        return self.comment

commentと同様にverbose_namemax_lengthを指定。ただし、ここではあえてdefaultを指定しない。

この状態でマイグレーションファイルを作る。

python3 manage.py makemigrations

すると、下記のような警告が出る。

You are trying to add a non-nullable field 'name' to topic without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 

要するに、Topicモデルに、NULL禁止のnameフィールドを追加する場合、既存のレコードの処遇をどうするか聞いている。

こちらの記事でも取り扱ったが、特段の指定がない場合、基本的にフィールドのデータは入力必須(null禁止、blank禁止)である。にもかかわらず、新しくフィールドを追加した時、既存のフィールドの値がどうしてもnullになってしまう。

一言で言うと、nullは値がない状態、blankは空の文字列("")。

この状況で与えられた選択肢は2つ。

    1. 1度限りのデフォルト値を入れる
    1. 一旦makemigrationsを中止して、models.pyに追加したフィールドにdefault属性を指定する

1か2を入力してEnterを押す。

一時的に値を入れ、以降は入力必須にしたい場合は1を選ぶ

1の場合、Pythonのインタラクティブシェルになるので、任意の値を指定する

まず、1を押してEnter、インタラクティブシェルになる。今回はCharFieldなので、ダブルクオーテーションで囲って文字列型のデータを入れると良い。

画像のようにすることで、一時的に空欄のnameに関しては『匿名』という文字列が入る。

永続的にデフォルト値を入れたい場合は2を選ぶ

2の場合、追加したnameにフィールドオプションとしてdefaultを指定する。今回は2を指定してmodels.pyを編集する。

from django.db import models
from django.utils import timezone

class Topic(models.Model):

    name    = models.CharField(verbose_name="投稿者の名前",max_length=100,default="匿名")
    comment = models.CharField(verbose_name="コメント",max_length=2000)
    dt      = models.DateTimeField(verbose_name="投稿日時",default=timezone.now)

    def __str__(self):
        return self.comment

このdefaultを指定することでmakemigrationsが可能になる。1を選んだときと同様に、既存のレコードは全て匿名として扱われる。

ただ、この場合、管理サイトで新規作成を行う時、最初から匿名と書かれるようになる。

フィールドを削除する

フィールドを削除する時は、モデルフィールドを削除して、マイグレーションをするだけ。

from django.db import models
from django.utils import timezone

class Topic(models.Model):

    #name    = models.CharField(verbose_name="投稿者の名前",max_length=100,default="匿名")
    comment = models.CharField(verbose_name="コメント",max_length=2000)
    dt      = models.DateTimeField(verbose_name="投稿日時",default=timezone.now)

    def __str__(self):
        return self.comment

nameを削除する。その後マイグレーション。

python3 manage.py makemigrations
python3 manage.py migrate

もし、モデルを継承したフォームクラスをビューでバリデーションとして使っている場合、フォームクラスのfieldsも削除しておく事をお忘れなく。

ただ、フィールドを削除して、マイグレーションを実行した後、再度同じ名前のフィールドを追加してマイグレーションしても、データは復活しない。空の状態からスタートする。

もし、削除対象のフィールドのデータを残しておきたい場合、DBのデータをバックアップリストアする方法があるので、そちらを使うと良いだろう。

マイグレーションできない状態に陥った時は

最後にマイグレーションできない状態になった時、その原因と対策を記す

マイグレーションできない原因

変にモデルをいじっているとマイグレーションできない事態が起こり得る。マイグレーションできない原因として主に考えられるのが、下記2つ

  • マイグレーションファイルの内容とDBのテーブル構造が食い違っている
  • フィールドが文字列型ではないのに、blank=Trueやdefault=““をフィールドオプションに指定している

【対策1】マイグレーションファイルを不用意に編集したり、DBにSQLを直接実行してテーブルを削除したりしない

対策と言うより、予防である。

migrateを実行した後に、マイグレーションファイルを不用意に編集をしてしまうと、DBの構造とマイグレーションファイルの辻褄が合わなくなってしまう。

マイグレーションファイル編集直後は問題ないが、次にmigrateを実行する時、マイグレーションファイルとDBの構造が一致していないため、必ずマイグレーションエラーを引き起こしてしまう。

同様の理由で、DBにアクセスしてSQLを実行して、DROP TABLE等を実行しようものなら、これもマイグレーションファイルとDBの構造が食い違うため、マイグレーションエラーになってしまう。

そのため、Djangoを開発していく上ではマイグレーションファイルを不用意に編集したり、DBに直接アクセスしてテーブルを削除するSQL等を実行してはならない。もしやってしまった場合は、手動でどうにかするか、後述の【対策3】を実施するしか術は無い。

【対策2】フィールドの型に適したフィールドオプションを指定する

文字列型ではないにもかかわらず、default=""等のフィールドオプションを指定してしまうと、必ずマイグレーションエラーを起こす。

そのため、DateTimeFieldであれば日付型を、IntegerFieldであれば数値型をdefaultとして割り当てる。

blank=Trueに関しては少々厄介で、厳密には、CharField以外のフィールドに対してblank=Trueを指定する時は、セットでnull=Trueを指定する。詳細は『Djangoで数値型もしくはUUID型等のフィールドに、クライアント側から未入力を許可するにはnull=Trueとblank=Trueのオプションを』を参照。

【対策3】マイグレーションファイルとDBを全て初期化する

※この方法は開発段階の時だけにして、本番運用後には本当の最終手段として考えたほうが良い。

マイグレーションファイル全てとDBのファイルを削除して最初からやり直す。

各アプリディレクトリ内にある、全てのmigrationsディレクトリを削除した後、プロジェクトディレクトリ直下にあるdb.sqlite3を削除する。

これでプロジェクトを新規作成した状態まで戻る。後は各アプリのモデルを書き直してマイグレーションのコマンドを実行する。

python3 manage.py makemigrations bbs
python3 manage.py migrate

手動でmigrationsディレクトリを削除した時、makemigrationsコマンドを実行する時は、アプリ名を明示的に指定しなければ、マイグレーションファイルを作ってくれない点に注意する。

つまり、bbsアプリのmigrationsディレクトリを削除した場合、上記のようにpython3 manage.py makemigrations bbsとする。

【対策4】エラーが起こっているマイグレーションファイルとそれ以降を削除する

マイグレーションを行ったときに表示されるエラー文をよく読むと、エラーが発生しているマイグレーションファイルのファイル名が表示されていることがある。

このマイグレーションファイル名とそれ以降に作られたマイグレーションファイルをすべて削除し、再度マイグレーションを行うと改善されることがある。

この方法の場合、【対策3】と違ってDBを消す必要はなくなる。

結論

モデルフィールドの追加と削除はルールをしっかり守れば実現できるものの、これからDjangoを扱う方にはやや難しい。

本件の他に、クライアントからデータを受け取る場合、テンプレートの編集とforms.pyでバリデーションをするフィールドの編集も同時に行う必要があるだろう。

また、nullとblankのフィールドオプションに関しても知っておいたほうが良い。これでフィールドの追加に迷わなくなる。

Djangoで数値型もしくはUUID型等のフィールドに、クライアント側から未入力を許可するにはnull=Trueとblank=Trueのオプションを

スポンサーリンク

シェアボタン

Twitter LINEで送る Facebook はてなブログ