はじめに
業務の関係上新たにDjangoのユーザーモデルをカスタムする必要が生じたので調べてみるとプロジェクトの途中からは変更するのが難しいという情報が……
Djangoの公式ページですら以下のような調子です。
プロジェクト途中からのカスタムユーザーモデルへの変更
AUTH_USER_MODEL をデータベーステーブルの作成後に変更することは、たとえば、外部キーや多対多の関係に影響するため、非常に困難となります。この変更は自動的には行うことができません。手動でのスキーマ修正、古いユーザーテーブルからのデータ移動、一部のマイグレーションの手動による再適用をする必要があります。ステップの概要は #25313 を参照してください。
……
一番簡単な方法はDBを消して再マイグレーションすることですが、
もう既に稼働中のDBを消すのは無理……という状況だったので上記のDjango公式ページで紹介された#25313の方法を試してなんとかDBを消さずに済みました。
基本的には、この手順に従っていればできるのですが、手順がざっくりしているのと私のマイグレーションの知識が乏しかったせいで色々苦労したのでこの記事でまとめます。
目次
手順
#25313で書かれている手順(原文)は以下の通りです。
- Create a custom user model identical to auth.User, call it User (so many-to-many tables keep the same name) and set db_table='auth_user' (so it uses the same table)
- Throw away all your migrations
- Recreate a fresh set of migrations
- Sacrifice a chicken, perhaps two if you're anxious; also make a backup of your database
- Truncate the django_migrations table
- Fake-apply the new set of migrations
- Unset db_table, make other changes to the custom model, generate migrations, apply them
これでふーんなるほど分かったぜ、という人はこの記事でなく元の文書を見てみた方がいいです。
ちなみに私は初見全く意味が分かりませんでした…
上記の内容をもうちょっと補足してまとめると以下のようになります。
- 組み込みユーザモデルauth.UserがDBに登録する時に使用するテーブル名
auth_user
と同じテーブル名に登録するようなカスタムユーザーモデルを作成 - 既にある全てのmigrationsを削除する
- migrationsの新しいセットを再作成
- DBバックアップ
- DB内のdjango_migrationsテーブルのデータを削除する
- migrationsの新しいセットを
--fake-initial
オプションを付けてマイグレーション - カスタムユーザーの登録するテーブル名を
auth_user
にするの設定部分を削除し、カスタムユーザーモデルに任意の変更を加え、migrationsを作成&適用
これ以降は、Djangoのバージョンが 2.2 で以下のようなプロジェクトがあるという想定で手順の詳細をまとめていきます。
(プロジェクト内容はDjango girlsのパクリです。)
djangogirls
├── blog
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
| ├── urls.py
│ └── views.py
├── db.sqlite3
├── manage.py
└── mysite
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
blog
[X] 0001_initial
[X] 0002_post_author
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
sessions
[X] 0001_initial
組み込みユーザモデルと同一テーブルを使用するカスタムユーザーモデルを作成
ユーザモデルに関する前知識
この手順を説明する前に知っているとなんとなくやっている内容が分かると思うのでちょっとだけユーザーモデルについて説明します。
(手っ取り早く手順だけ知りたい!という人は仮のカスタムユーザーモデル作成へ)
そもそもDjangoのユーザーモデルは、django.contrib.auth
のAUTH_USER_MODEL
で設定されたモデルをUserモデルとして使用するようになっており、
このAUTH_USER_MODEL
は、デフォルトではauth.User
になっています。
このUserモデル定義はmanage.pyのinspectdbのオプションでみることができます。
$ python manage.py inspectdb
...
class AuthUser(models.Model):
password = models.CharField(max_length=128)
last_login = models.DateTimeField(blank=True, null=True)
is_superuser = models.BooleanField()
username = models.CharField(unique=True, max_length=150)
first_name = models.CharField(max_length=30)
email = models.CharField(max_length=254)
is_staff = models.BooleanField()
is_active = models.BooleanField()
date_joined = models.DateTimeField()
last_name = models.CharField(max_length=150)
class Meta:
managed = False
db_table = 'auth_user'
プロジェクトの途中でカスタムユーザーに変更することが困難なのは、初回のマイグレーション時(0001)にこのAuthUser
モデルを他のDjangoの組み込みモデルが外部キーとして登録してしまうことが原因となっています。
ここで注目するポイントは上記のMeta
クラスに含まれているdb_table
です。
このdb_table
は、モデルをDBに登録する時のテーブル名を表しています。
つまりカスタムユーザーモデルのdb_table
をauth_user
にしてしまえば一時的に外部キーの依存問題は解決できます。
初回のマイグレーションさえ通してしまえば、このテーブル名の偽装は後で元に戻すことが可能です。
仮のカスタムユーザーモデル作成
ここから手順の説明です。
まず最初に仮のカスタムユーザーモデルを作成します。
ここで「仮」と付けているのは、DBの依存関係を解決するためだけの一時的な存在だからです。
カスタムユーザーの作成手順は、この記事を参考にしていきます。
カスタムユーザー用のアプリを作成
(この手順は任意。今回はusers
というカスタムユーザー用のアプリを作るものとする。)
$ python manage.py startapp users
models.pyに組み込みのユーザーモデルと同一名のテーブルに登録するような仮のカスタムユーザーモデルを作成
(ここではAbstractBaseUser
ではなく、必ずAbstractUser
を継承すること。)
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
class Meta:
db_table = 'auth_user'
既存のプログラム内で auth.User
をimportしている部分を全てカスタムユーザーモデルをimportするように書き換える
from django.contrib.auth.models import User
↓
from users.models import User
- 追加したアプリを
INSTALLED_APPS
に追加
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
・・・
'users.apps.UsersConfig',
]
使用するカスタムユーザーモデルをAUTH_USER_MODEL
にセット
AUTH_USER_MODEL = 'users.User'
既にある全てのmigrationsを削除する
一旦migrationsを消します。
ちまちま消してもいいですが、複雑なディレクトリ構成をしていなければアプリのトップディレクトリで以下のコマンドを実行すればOKなはずです。
$ find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
migrationsの新しいセットを再作成
新たにmigrationsを生成します。
$ python manage.py makemigrations
今回の例の場合だとこんな感じで新たに作成したカスタムユーザーのみ、未適応の状態になるはずです。
$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
blog
[X] 0001_initial
[X] 0002_post_author
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
sessions
[X] 0001_initial
users
[ ] 0001_initial
DBをバックアップ
このタイミングでDBのバックアップをとります。
バックアップ方法はDB毎に異なるのでここでは割愛します。
DB内のdjango_migrationsをTRUNCATEする
migrationsを消してもマイグレーション履歴自体はDBに残ったままなので、ここで履歴を削除します。
方法としては
-
$ python manage.py migrate --fake APP_NAME zero
で消す -
dbshell
でdjango_migrations
テーブルのデータを消す
のいずれかでOKですが、全アプリのマイグレーション履歴を消さなければいけないので後者のDBテーブルのデータを直接消す方法をオススメします。
その場合は当たり前ですが、django_migrations
以外のテーブルのデータを消さないように注意してください。
DBテーブルの削除手順もDB毎に異なるのでここでは割愛します。
今回の例の場合だとこんな感じで全てのマイグレーションが未適応の状態になるはずです。
$ python manage.py showmigrations
admin
[ ] 0001_initial
[ ] 0002_logentry_remove_auto_add
[ ] 0003_logentry_add_action_flag_choices
auth
[ ] 0001_initial
[ ] 0002_alter_permission_name_max_length
[ ] 0003_alter_user_email_max_length
[ ] 0004_alter_user_username_opts
[ ] 0005_alter_user_last_login_null
[ ] 0006_require_contenttypes_0002
[ ] 0007_alter_validators_add_error_messages
[ ] 0008_alter_user_username_max_length
[ ] 0009_alter_user_last_name_max_length
blog
[ ] 0001_initial
[ ] 0002_post_author
contenttypes
[ ] 0001_initial
[ ] 0002_remove_content_type_name
sessions
[ ] 0001_initial
users
[ ] 0001_initial
migrationsの新しいセットを--fake-initial
オプションを付けてマイグレーション
今回の肝であるマイグレーションをします。
まずはDjango組み込みのアプリであるadmin
、auth
、contenttypes
、sessions
だけ--fake
オプションを付けてマイグレーションします。
--fake
は、DBスキーマを変更せずにマイグレーションを適用済みにするためのオプションです。
これをつけないと既に同じテーブルがあるぞ的なエラーが出ます。
$ python manage.py migrate --fake APP_NAME
次に自作アプリを--fake-initial
オプションをつけてマイグレーションします。
この--fake-initial
は、先頭のマイグレーションだけ--fake
を適用するオプションです。
$ python manage.py migrate --fake-initial APP_NAME
ここのマイグレーションの実行順番によっては色々エラーが出る可能性があります。
(実際私もここを通すのが一番大変でした……)
マイグレーション時のエラーに関しては以下の記事がかなり参考になったので、もし詰まったら一度見てみてください。
Django 1.8: Create initial migrations for existing schema - Stack Overflow
最終的に全てのマイグレーションが適応済みになっていたらOKです。
$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
blog
[X] 0001_initial
[X] 0002_post_author
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
sessions
[X] 0001_initial
users
[X] 0001_initial
ちなみにDBテーブルを見てみるとこの時点ではauth_user
が残っています。
auth_group blog_post
auth_group_permissions django_admin_log
auth_permission django_content_type
auth_user django_migrations
auth_user_groups django_session
auth_user_user_permissions
db_tableの設定を解除し、カスタムユーザーモデルに他の変更を加え、migrationsを作成&適用
仮のカスタムユーザーモデル作成でカスタムユーザーモデルにくっつけていたdb_table
を削除し、
後は思い思いにカスタムユーザーモデルの変更を加えていきます。
ここではベタに住所と生年月日を付与してみます。
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
class Meta:
db_table = 'auth_user'
↓
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
address = models.CharField(max_length=50, blank=True)
birthday = models.DateTimeField(null=True, blank=True)
その後カスタムユーザーモデルの変更分のmigrationsを作成&適用します。
$ python manage.py makemigrations users
Migrations for 'users':
users/migrations/0002_auto_20191001_1229.py
- Change Meta options on user
- Add field address to user
- Add field birthday to user
- Rename table for user to (default)
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, users
Running migrations:
Applying users.0002_auto_20191001_1229... OK
DBテーブルを見てみると組み込みユーザモデルのauth_user
テーブルが消えて新たにカスタムユーザー用のテーブル (今回の場合だとusers_user
) が増えていることが確認できます。
auth_group django_migrations
auth_group_permissions django_session
auth_permission users_user
blog_post users_user_groups
django_admin_log users_user_user_permissions
django_content_type
モデルも増えています。
$ python manage.py inspectdb
...
class UsersUser(models.Model):
id = models.IntegerField(primary_key=True) # AutoField?
password = models.CharField(max_length=128)
last_login = models.DateTimeField(blank=True, null=True)
is_superuser = models.BooleanField()
username = models.CharField(unique=True, max_length=150)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=150)
email = models.CharField(max_length=254)
is_staff = models.BooleanField()
is_active = models.BooleanField()
date_joined = models.DateTimeField()
address = models.CharField(max_length=50)
birthday = models.DateTimeField(blank=True, null=True)
class Meta:
managed = False
db_table = 'users_user'
最後に一応ログインしてDBのデータが残っているかなどを確認してみてください。
ここまでできたら完了です!
最後に
Django歴半年未満でチュートリアルを一通り触ってみてなんとなく使っている状態だったのでマイグレーション関係は個人的にとても勉強になりました。
ただ、微妙に理解が曖昧な所が多いので記事に不備があるかもしれないです。
何かここがおかしいという所があればどんどんコメントください。