Djangoのプロジェクト途中からDBを消さずにカスタムユーザーモデルへ変更する

# はじめに
業務の関係上新たにDjangoのユーザーモデルをカスタムする必要が生じたので調べてみるとプロジェクトの途中からは変更するのが難しいという情報が……

- [Djangoで認証用カスタムユーザモデルは途中で変更できません？ - Qiita](https://qiita.com/sheep96/items/752acbda64898f20aab2)
- [Djangoでは常にカスタムUserを使用すべき - Qiita](https://qiita.com/NAKKA-K/items/7627b6a22f364941b989)

Djangoの公式ページですら以下のような調子です。

> プロジェクト途中からのカスタムユーザーモデルへの変更
> AUTH_USER_MODEL をデータベーステーブルの作成後に変更することは、たとえば、外部キーや多対多の関係に影響するため、非常に困難となります。
>
> この変更は自動的には行うことができません。手動でのスキーマ修正、古いユーザーテーブルからのデータ移動、一部のマイグレーションの手動による再適用をする必要があります。ステップの概要は #25313 を参照してください。
>
> ……

https://docs.djangoproject.com/ja/2.2/topics/auth/customizing/#changing-to-a-custom-user-model-mid-project

一番簡単な方法はDBを消して再マイグレーションすることですが、
もう既に稼働中のDBを消すのは無理……という状況だったので上記のDjango公式ページで紹介された[#25313](https://code.djangoproject.com/ticket/25313)の方法を試してなんとかDBを消さずに済みました。
基本的には、この手順に従っていればできるのですが、手順がざっくりしているのと私のマイグレーションの知識が乏しかったせいで色々苦労したのでこの記事でまとめます。

# 目次

<!-- toc -->

- [はじめに](#はじめに)
- [手順](#手順)
  * [組み込みユーザモデルと同一テーブルを使用するカスタムユーザーモデルを作成](#組み込みユーザモデルと同一テーブルを使用するカスタムユーザーモデルを作成)
    + [ユーザモデルに関する前知識](#ユーザモデルに関する前知識)
    + [仮のカスタムユーザーモデル作成](#仮のカスタムユーザーモデル作成)
  * [既にある全てのmigrationsを削除する](#既にある全てのmigrationsを削除する)
  * [migrationsの新しいセットを再作成](#migrationsの新しいセットを再作成)
  * [DBをバックアップ](#DBをバックアップ)
  * [DB内のdjango_migrationsをTRUNCATEする](#DB内のdjango_migrationsをTRUNCATEする)
  * [migrationsの新しいセットを`--fake-initial`オプションを付けてマイグレーション](#migrationsの新しいセットを--fake-initialオプションを付けてマイグレーション)
  * [db_tableの設定を解除し、カスタムユーザーモデルに他の変更を加え、migrationsを作成＆適用](#db_tableの設定を解除し、カスタムユーザーモデルに他の変更を加え、migrationsを作成＆適用)
- [最後に](#最後に)
- [参考](#参考)

<!-- tocstop -->
# 手順

[#25313](https://code.djangoproject.com/ticket/25313)で書かれている手順（原文）は以下の通りです。

> 1. 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)
> 2. Throw away all your migrations
> 3. Recreate a fresh set of migrations
> 4. Sacrifice a chicken, perhaps two if you're anxious; also make a backup of your database
> 5. Truncate the django_migrations table
> 6. Fake-apply the new set of migrations
> 7. Unset db_table, make other changes to the custom model, generate migrations, apply them

これでふーんなるほど分かったぜ、という人はこの記事でなく元の文書を見てみた方がいいです。
ちなみに私は初見全く意味が分かりませんでした…
上記の内容をもうちょっと補足してまとめると以下のようになります。

1. 組み込みユーザモデルauth.UserがDBに登録する時に使用するテーブル名`auth_user`と同じテーブル名に登録するようなカスタムユーザーモデルを作成
2. 既にある全てのmigrationsを削除する
3. migrationsの新しいセットを再作成
4. DBバックアップ
5. DB内のdjango_migrationsテーブルのデータを削除する
6. migrationsの新しいセットを`--fake-initial`オプションを付けてマイグレーション
7. カスタムユーザーの登録するテーブル名を`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
```

```bash
$ 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のオプションでみることができます。

```bash
$ 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の依存関係を解決するためだけの一時的な存在だからです。
カスタムユーザーの作成手順は、[この記事](https://qiita.com/okoppe8/items/10ae61808dc3056f9c8e)を参考にしていきます。

カスタムユーザー用のアプリを作成
（この手順は任意。今回は`users`というカスタムユーザー用のアプリを作るものとする。）

```bash
$ python manage.py startapp users
```

models.pyに組み込みのユーザーモデルと同一名のテーブルに登録するような仮のカスタムユーザーモデルを作成
（ここでは`AbstractBaseUser`ではなく、必ず`AbstractUser`を継承すること。）

```python:users/models.py
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するように書き換える

```python
from django.contrib.auth.models import User
```

↓

```python
from users.models import User
```

4. 追加したアプリを`INSTALLED_APPS`に追加

```python:mysite/settings.py
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`にセット

```python:mysite/settings.py
AUTH_USER_MODEL = 'users.User'
```

## 既にある全てのmigrationsを削除する
一旦migrationsを消します。
ちまちま消してもいいですが、複雑なディレクトリ構成をしていなければアプリのトップディレクトリで以下のコマンドを実行すればOKなはずです。

```bash
$ find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
```

## migrationsの新しいセットを再作成
新たにmigrationsを生成します。

```bash
$ python manage.py makemigrations
```

今回の例の場合だとこんな感じで新たに作成したカスタムユーザーのみ、未適応の状態になるはずです。

```bash
$ 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毎に異なるのでここでは割愛します。

今回の例の場合だとこんな感じで全てのマイグレーションが未適応の状態になるはずです。

```bash
$ 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スキーマを変更せずにマイグレーションを適用済みにするためのオプションです。
これをつけないと既に同じテーブルがあるぞ的なエラーが出ます。

```bash
$ python manage.py migrate --fake APP_NAME
```

次に自作アプリを`--fake-initial`オプションをつけてマイグレーションします。
この`--fake-initial`は、先頭のマイグレーションだけ`--fake`を適用するオプションです。

```bash
$ python manage.py migrate --fake-initial APP_NAME
```

ここのマイグレーションの実行順番によっては色々エラーが出る可能性があります。
（実際私もここを通すのが一番大変でした……）
マイグレーション時のエラーに関しては以下の記事がかなり参考になったので、もし詰まったら一度見てみてください。
[Django 1.8: Create initial migrations for existing schema - Stack Overflow](https://stackoverflow.com/questions/29888046/django-1-8-create-initial-migrations-for-existing-schema/29898483)


最終的に全てのマイグレーションが適応済みになっていたらOKです。

```bash
$ 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`が残っています。

```bash
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`を削除し、
後は思い思いにカスタムユーザーモデルの変更を加えていきます。
ここではベタに住所と生年月日を付与してみます。

```python:users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    class Meta:
        db_table = 'auth_user'
```

↓


```python:users/models.py
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を作成＆適用します。

```bash
$ 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`) が増えていることが確認できます。

```bash
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   
```

モデルも増えています。

```bash
$ 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歴半年未満でチュートリアルを一通り触ってみてなんとなく使っている状態だったのでマイグレーション関係は個人的にとても勉強になりました。
ただ、微妙に理解が曖昧な所が多いので記事に不備があるかもしれないです。
何かここがおかしいという所があればどんどんコメントください。


# 参考
- Django公式  
  - [設定 \| Django ドキュメント | Django](https://docs.djangoproject.com/ja/2.2/ref/settings/#auth-user-model)  
- 参考にした手順  
  - [#25313 (Document how to migrate from a built-in User model to a custom User model) – Django](https:\\code.djangoproject.com\ticket\25313)
- カスタムユーザーモデル関係
  - [Djangoで認証用カスタムユーザモデルは途中で変更できません？ - Qiita](https://qiita.com/sheep96/items/752acbda64898f20aab2)
  - [Djangoでは常にカスタムUserを使用すべき - Qiita](https://qiita.com/NAKKA-K/items/7627b6a22f364941b989)
- マイグレーション関係 
  - [Django マイグレーション完全に理解した (基礎編) 🍏 - くろのて](https://note.crohaco.net/2018/django-migration/)