今回は、「django.db.utils.IntegrityError: (1364, "Field 'フィールド名' doesn't have a default value")」のエラーについての説明していきたいと思います。
私の場合は、ブログ記事などを投稿しようとした時に上記のエラーに遭遇しました。
しかも、開発環境では起こらなかったのに本番環境でこのようなエラーが起こることで、最初は検討がつかなかったのですが、冷静にデプロイした際の事を思い返すと問題が1つあったので、そのような事も含めエラーとなった理由から、解決・注意事項までをご紹介していこうと思います。
そしてデータベースの操作では、SQLite・MySQLでの異なる処理を実装していくので、参考にして頂けたら幸いです。
実行環境(開発) |
---|
Window Subsystem for Linux |
Python 3.6 |
pip 9.0.1 |
実行環境(本番) |
---|
CentOS7 |
Python3.6 |
使用パッケージ | ライセンス |
---|---|
Django==2.2.5 | BSD |
この「django.db.utils.IntegrityError: (1364, "Field 'フィールド名' doesn't have a default value"」をGoogle翻訳に充てると、「フィールドの’フィールド名’にはデフォルト値がありません」となります。
’フィールド名’の部分に入るところが問題の部分です。
データベース(SQLiteやMySQLなど)内のフィールドと、Djangoで設定されているモデルのフィールドが一致していないという事になります。
私の例で見てみますと、ブログの投稿時に「django.db.utils.IntegrityError: (1364, "Field 'views' doesn't have a default value"」のようなエラーが起こったとします。
「フィールド’views’にはデフォルト値がありません」ということなので、ブログアプリのモデルで定義されているフィールドを確認してみることにします。
# models.py
from django.db import models
class Blog(models.Model):
title = ...
text = ...
created_at = ...
....
3つのフィールドがDjangoDBのデフォルト値として設定されています。
「views」というフィールドは過去に設定していましたが、削除したはずです。
ブログアプリ内にあるマイグレーションファイルを確認したら、初期化するために移行ファイルを削除していました(諸事情で)。
なのでmigrateコマンドを打っても「No migrations to apply」となってしまいます。
では実際に保存されているデータベースのBlogテーブルではどのようなフィールドとなっているか見てみます。
SQLite3の場合
# 実行
$ python3 manage.py dbshell
# テーブルの確認
sqlite> .table
auth_group blog_blog
auth_group_permissions django_admin_log
auth_permission django_content_type
auth_user django_migrations
auth_user_groups django_session
auth_user_user_permissions
sqlite>
# フィールド(カラム)の確認
sqlite> .schema blog_blog
CREATE TABLE IF NOT EXISTS "blog_blog" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(50) NOT NULL, "text" text NOT NULL, "created_at" date NOT NULL, "views" integer NOT NULL);
MySQLの場合
# 実行
$ python3 manage.py dbshell
# テーブルの確認
mysql> show tables;
+----------------------------+
| Tables_in_DB名 |
+----------------------------+
| auth_group |
| auth_group_permissions |
| auth_permission |
| auth_user |
| auth_user_groups |
| auth_user_user_permissions |
| blog_blog |
| django_admin_log |
| django_content_type |
| django_migrations |
| django_session |
+----------------------------+
mysql>
# フィールド(カラム)の確認
mysql> show columns from blog_blog;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| title | varchar(150) | NO | | NULL | |
| text | longtext | NO | | NULL | |
| created_at | date | NO | | NULL | |
| views | int() | NO | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
データベース内には削除したと思われる「views」フィールド(カラム)がまだ存在していました。
やはりマイグレーションファイルをやたら無暗に削除してしまうと、このようにデータベースではしっかり変更が行われないことが分かります。
Djangoで設定したBlogモデル内と、データベースのテーブル内を一致させるために、データベースの「views」フィールド(カラム)を削除していきたいと思います。
なお、MySQLでは簡単にカラムの削除を行えますが、SQLiteではひと手間かかるので、先にMySQLでの処理から行っていきます。
ではdbshellコマンドでデータベースにアクセスします。
# 実行
$ python3 manage.py dbshell
# フィールドの確認
mysql> show columns from blog_blog;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| title | varchar(150) | NO | | NULL | |
| text | longtext | NO | | NULL | |
| created_at | date | NO | | NULL | |
| views | int() | NO | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
中身が確認できたら、問題のblog_blogテーブル内にあるviewsを削除します。
# 問題の根幹を削除
mysql> alter table blogs_blog drop views;
......(アラート)
mysql>
# もう1度問題のテーブル内を確認
mysql> show columns from blog_blog;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| title | varchar(150) | NO | | NULL | |
| text | longtext | NO | | NULL | |
| created_at | date | NO | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
# 削除されていたら終了
mysql> exit
これで問題は解決されました。
SQLite文では、テーブルは削除できてもフィールド(カラム)の削除には対応していないとのことで、ひと手間加えた処理を行います。
流れを簡単に説明すると、新しくテーブルを作成し、viewsフィールドを外したそれ以外のフィールドを定義します。そして既存テーブルから新テーブルへとデータを追加し確認して終了です。
今回例に挙げているブログアプリにおいて、Djangoではデータベースと紐づける際の名前空間が「blog_blog」と定まっているため、新しく作成するテーブル名は「blog_blog」とする必要があります。
ではさっそく実装していきましょう。
# 実行
$ python3 manage.py dbshell
# テーブル内を確認
sqlite>
sqlite> .schema blog_blog
CREATE TABLE IF NOT EXISTS "blog_blog" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(50) NOT NULL, "text" text NOT NULL, "created_at" date NOT NULL, "views" integer NOT NULL);
問題のviewsフィールド(カラム)があることを確認しました。
新しく作成するテーブル名は「blog_blog」である必要があるので、既存の「blog_blog」テーブルの名前を変更します。
# 既存のテーブル名を変更
sqlite> alter table blog_blog rename to blog_remove;
sqlite>
# 確認
sqlite> .tables
auth_group blog_remove
auth_group_permissions django_admin_log
auth_permission django_content_type
auth_user django_migrations
auth_user_groups django_session
auth_user_user_permissions
削除用のテーブル名として変更しました。
これで「blog_blog」というDjangoが名前空間を持つテーブル名を使えるようになったので、新しくテーブルを作成していきます。
viewsフィールド(カラム)以外のフィールドも記述していくので、schemaで「blog_remove」のフィールドを見ながら記述していきます。
# フィールドの確認
sqlite> .schema blog_remove
CREATE TABLE IF NOT EXISTS "blog_remove" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(50) NOT NULL, "text" text NOT NULL, "created_at" date NOT NULL, "views" integer NOT NULL);
sqlite>
# 新しくテーブルの作成
sqlite> create table blog_blog("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(50) NOT NULL, "text" text NOT NULL, "created_at" date NOT NULL);
では確認してみます。
# テーブルの確認
sqlite> .tables
auth_group blog_remove
auth_group_permissions django_admin_log
auth_permission django_content_type
auth_user django_migrations
auth_user_groups django_session
auth_user_user_permissions
blog_blog
sqlite>
# フィールドの確認
sqlite> .schema blog_blog
CREATE TABLE blog_blog("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(50) NOT NULL, "text" text NOT NULL, "created_at" date NOT NULL);
問題のviewsフィールドが抜けた以外全く等しいフィールド内が作成されています。
最後に、元のテーブル(blog_remove)内にあるデータを、新しく作成した「blog_blog」テーブルに追加していきます。
# 既存のデータを追加
sqlite> insert into blog_blog("id", "title", "text", "created_at") select id, title, text, created_at from blog_remove;
sqlite>
しっかりとデータが追加されているか確認します。
sqlite> select id, title from blog_blog;
1|〇〇×△×△
2|......
3|......
sqlite>
# コンソール終了
sqlite> .exit
データが追加されていると確認できたら、元のテーブルは削除して大丈夫ですが、不安であれば動作確認も行った後に削除するのが良いと思います。
余分にテーブルが存在しますが、Djangoはしっかり動きます。
データベース関連のエラーが出たら、もう一度記述漏れやミスが無いか確認してみてください。
全ての確認が終わったらテーブルを削除します。
# 削除
sqlite> drop table blog_remove;
sqlite>
# テーブルの確認
sqlite> .tables
auth_group blog_blog
auth_group_permissions django_admin_log
auth_permission django_content_type
auth_user django_migrations
auth_user_groups django_session
auth_user_user_permissions
sqlite>
sqlite> .exit
今回このようなエラーとなってしまった理由は、マイグレーションファイルを闇雲に削除してしまったことです。
初心者にありがちなミスだと思いますが、Djangoはマイグレーションファイルを元にデータベーススキーマを構築していくので、途中経過が抜けていると何らかのエラーとなりますし、失敗してマイグレーションファイルを初期化してもどこかでデータベース側とのミスマッチが起きてしまいます(今回のように)。
特にGitHub経由でのデプロイでは、マイグレーションファイルを除外するなどの処置を取らないと、本番環境下に移行済み(開発環境でのマイグレート)ファイルを結合してきてしまうため、うっかり本番環境下でマイグレートを実行してしまってミスが起こります。
そして焦りから、サーバーエラーから逃れたいがための裏ワザ(初期化)の結果でした。
しかしこの経験のおかげでDjangoでは中々触らないSQL文に少しは慣れる事ができたので、結果オーライでした。
それでは以上となります。
最後までご覧いただきありがとうございました。