20
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-10-01

はじめに

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

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

プロジェクト途中からのカスタムユーザーモデルへの変更
AUTH_USER_MODEL をデータベーステーブルの作成後に変更することは、たとえば、外部キーや多対多の関係に影響するため、非常に困難となります。

この変更は自動的には行うことができません。手動でのスキーマ修正、古いユーザーテーブルからのデータ移動、一部のマイグレーションの手動による再適用をする必要があります。ステップの概要は #25313 を参照してください。

……

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

目次

手順

#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
$ 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.authAUTH_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_tableauth_userにしてしまえば一時的に外部キーの依存問題は解決できます。
初回のマイグレーションさえ通してしまえば、このテーブル名の偽装は後で元に戻すことが可能です。

仮のカスタムユーザーモデル作成

ここから手順の説明です。
まず最初に仮のカスタムユーザーモデルを作成します。
ここで「仮」と付けているのは、DBの依存関係を解決するためだけの一時的な存在だからです。
カスタムユーザーの作成手順は、この記事を参考にしていきます。

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

$ python manage.py startapp users

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

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するように書き換える

from django.contrib.auth.models import User

from users.models import User
  1. 追加したアプリをINSTALLED_APPSに追加
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にセット

mysite/settings.py
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で消す
  • dbshelldjango_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組み込みのアプリであるadminauthcontenttypessessionsだけ--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を削除し、
後は思い思いにカスタムユーザーモデルの変更を加えていきます。
ここではベタに住所と生年月日を付与してみます。

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

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

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を作成&適用します。

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

参考

20
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?