【Docker】Djangoで複数データベース(MySQL) & django-jetの管理サイト & django-reversionによるテーブル変更履歴管理

# バージョン情報
Python:3.7.2
Django:2.2.5

# pyenvで仮想環境の準備・適用
### 仮想環境作成
```
$ pyenv virtualenv 3.7.2 sample_project_env
```

### 仮想環境適用
```
$ cd /~省略~/sample_project
$ pyenv local sample_project_env
```
  
# Djangoプロジェクトの作成
### Djangoをインストール
```
$ pip install django==2.2.5
```
  
### プロジェクト作成
```
$ mkdir sample_project　※sample_projectフォルダの直下にさらにsample_projectフォルダを作成
$ cd sample_project
$ django-admin startproject config .
```
  
※こんな感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/2d3a7b92-0e11-e9d3-fcc0-6609c202347f.png)


# dockerで各種コンテナ立ち上げ
### docker-compose.yml
dbコンテナを2つ（db1, db2）立てていますが、CREATE DATABASE文を２つ記載したDDLをdocker-entrypoint-initdb.dにマウントすればdbコンテナ1つで複数データベースの構築はできますし、むしろそっちのが良いですw
（db_dataフォルダの中にDDLを入れればdocker-entrypoint-initdb.dにマウントされるようになってます。）

```yml
version: '2'
services:
  web:
    build:
      context: ../
      dockerfile: sample_project/Dockerfile
    container_name: sample_project
    volumes:
      - '../:/src'
    environment:
      - LC_ALL=ja_JP.UTF-8
    ports:
      - '8000:8000'
    depends_on:
      - db1
      - db2
      - redis
    entrypoint: sample_project/wait-for-it.sh db1 user user
    command: python3 sample_project/manage.py runserver 0.0.0.0:8000 --settings=config.settings
    tty: true

  redis:
    image: 'redis:4.0.8'
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - './redisdata:/data'

  db1:
    image: mariadb:latest
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_ROOT_USER=root
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=sample_project_1
      - MYSQL_USER=user
      - MYSQL_PASSWORD=user
    volumes:
      - db1_data:/var/lib/mysql
      - ./db1_data:/docker-entrypoint-initdb.d
    ports:
      - '3307:3306'

  db2:
    image: mariadb:latest
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_ROOT_USER=root
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=sample_project_2
      - MYSQL_USER=user
      - MYSQL_PASSWORD=user
    volumes:
      - db2_data:/var/lib/mysql
      - ./db2_data:/docker-entrypoint-initdb.d
    ports:
      - '3308:3306'

volumes:
  db1_data:
    driver: local
  db2_data:
    driver: local
```
  
### Dockerfile
```dockerfile
FROM ubuntu:18.04
RUN apt-get -y update \
    && apt-get install -y locales python3-pip python3.7 libmysqlclient-dev mysql-client\
    && mkdir /src \
    && rm -rf /var/lib/apt/lists/* \
    && echo "ja_JP UTF-8" > /etc/locale.gen \
    && locale-gen
WORKDIR /src
ADD ./ /src/
RUN LC_ALL=ja_JP.UTF-8 pip3 install -r sample_project/requirements.txt
```
  
### wait-for-it.sh
```sh
#!/bin/sh

set -e

host="$1"
shift
user="$1"
shift
password="$1"
shift
cmd="$@"

echo "Waiting for mysql"
until mysql -h "$host" -u "$user" -p"$password" &> /dev/null
do
        >$2 echo -n "."
        sleep 1
done

>&2 echo "MySQL is up - executing command"
exec $cmd
```
  
### requirements.txt
```
celery==4.2.1
cryptography==2.1.4
Django==2.2.5
django-celery-results==1.0.4
django-debug-toolbar==1.9.1
django-environ==0.4.5
django-jet==1.0.8
django-lint2==0.1
django-redis==4.10.0
django-reversion==3.0.4
mysqlclient==1.3.13
redis==3.2.0
requests==2.22.0
sqlparse==0.2.4
urllib3==1.25.3
vine==1.3.0
python-dateutil==2.8.0
```
  
### コンテナ立ち上げ
```
$ docker-compose up --build -d
```
  
```shell
.
..
...
Successfully built aa2166b8e593
Successfully tagged sample_project_web:latest
Creating sample_project_db1_1   ... done
Creating sample_project_db2_1   ... done
Creating sample_project_redis_1 ... done
Creating sample_project         ... done
```
  
### Djangoの起動画面確認
OK!!!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/7084ed84-7742-372e-8dc5-cb6744373cf0.png)
  
# 複数データベースの作成
### モデル作成用にアプリケーション作成
```
$ python3 manage.py startapp app1
$ python3 manage.py startapp app2
```
  
### モデル作成
```app1/models.py
from django.db import models

# Create your models here.
class Db1Table1(models.Model):
    column1 = models.CharField(max_length=10)

    def __str__(self):
        return self.column1
```
  
```app2/models.py
from django.db import models

# Create your models here.
class Db2Table1(models.Model):
    column1 = models.CharField(max_length=10)

    def __str__(self):
        return self.column1
```
  
### Database routerを作成
データベースルーター(Database router)を設定すると、アプリケーション毎にデータベースを切り替えることができます。
（app1がdb1、app2がdb2といった感じに。）


```config/db_router.py
class DbRouter:
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'app1':
            return 'db1'
        if model._meta.app_label == 'app2':
            return 'db2'
        return None
 
    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'app1':
            return 'db1'
        if model._meta.app_label == 'app2':
            return 'db2'
        return None
 
    def allow_relation(self, obj1, obj2, **hints):
        return True
 
    def allow_migrate(self, db, app_label, model=None, **hints):
        if app_label == 'app1':
            return db == 'db1' or db == 'default'
        if app_label == 'app2':
            return db == 'db2'
        return None
```

### .envにDB接続情報を記載
```.env
DEBUG=True

DATABASE_URL_1=mysql://user:user@db1:3306/sample_project_1
DATABASE_URL_2=mysql://user:user@db2:3306/sample_project_2
```
  
### settings.pyを編集
```config.settings.py
.
..
...
import os
from config import environ # 追加
.
..
...
 # 追加
env_file = os.path.join(BASE_DIR, '.env')
env = environ.Env()
env.read_env(env_file)
.
..
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app1', # 追加
    'app2', # 追加
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
.
..
...
DATABASES = {
    'default': env.db_url(var='DATABASE_URL_1'),
    'db1': env.db_url(var='DATABASE_URL_1'),
    'db2': env.db_url(var='DATABASE_URL_2'),
}
# 作成したDatabase routerを設定
DATABASE_ROUTERS = ['config.db_router.DbRouter']
.
..
...
# ついでにLANGUAGE_CODE、TIME_ZONE、USE_I18N、USE_L10N、USE_TZも書き換える
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = False
.
..
...
```
  
### マイグレーションファイル作成
ホストマシンからはマイグレーション操作が失敗するので、一旦dockerで立ち上げたwebコンテナに潜ってからマイグレーションファイルを作成。

```
$ docker exec -it sample_project /bin/bash
# cd sample_project/
# python3 manage.py makemigrations
Migrations for 'app1':
  app1/migrations/0001_initial.py
    - Create model Db1Table1
Migrations for 'app2':
  app2/migrations/0001_initial.py
    - Create model Db2Table1
#
```
  
### マイグレート実行（DBに各種テーブル作成）
マイグレートは2回に分けて実行します。
▼1回目
→db1にapp1のモデルに定義したテーブルとDjangoの管理テーブルを作成します。

```
# python3 manage.py migrate
```

▼実行結果

```
# python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, app1, app2, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying app1.0001_initial... OK
  Applying app2.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
#
```
  
▼2回目
→db2にapp2のモデルに定義したテーブルとDjangoの管理テーブルを作成します。
→ここではマイグレートを実行するデータベースを指定しています。
（データベースを指定しない場合、settings.pyの'default'に指定したデータベースが使用されてしまいます。）

```
# python3 manage.py migrate --database=db2
```

▼実行結果

```
# python3 manage.py migrate --database=db2
Operations to perform:
  Apply all migrations: admin, app1, app2, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying app1.0001_initial... OK
  Applying app2.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
#
```
  
### データベース確認
データベースごとに作成するテーブルを分けることができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/cb8b2035-740c-4cb0-9d3c-2b91bac8f6d9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/49f090a9-2d78-d52d-c0d9-24c22b5a2c96.png)

# スーパーユーザーの作成
ユーザー名とパスワードは「root」にしました。
（adminとかにしたら管理サイトには入れなくなったりしたのでrootが良さげ（原因不明））

```
# python3 manage.py createsuperuser
ユーザー名 (leave blank to use 'root'): root
メールアドレス: root@root.jp
Password:
Password (again):
このパスワードは ユーザー名 と似すぎています。
このパスワードは短すぎます。最低 8 文字以上必要です。
このパスワードは一般的すぎます。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
```
  
もう一個のデータベースにも作成したいなら同様にデータベースを指定してあげればOK

```
# python3 manage.py createsuperuser --database=db2
```
  
# Django管理サイト確認
### 管理サイトに表示するテーブルを指定する
```app1/admin.py
from django.contrib import admin
from app1.models import Db1Table1

# Register your models here.
admin.site.register(Db1Table1)
```

```app2/admin.py
from django.contrib import admin
from app1.models import Db2Table1

# Register your models here.
admin.site.register(Db2Table1)
```
  
### 管理サイト表示
http://localhost:8000/admin
に接続
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/78c7e386-0c96-df3d-4062-f704d03497c4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/cd44b043-af26-bc25-90b1-4050dfeef72b.png)
  
問題なく表示され、管理サイトからテーブルへのデータ追加等も行うことができました。
  
# 管理サイトのテンプレートにdjango-jetを使用する
[django-jetのreadme](https://github.com/geex-arts/django-jet)
### settings.pyを編集

```config.settings.py
.
..
...
INSTALLED_APPS = [
    'jet.dashboard', # django.contrib.adminとjetより前に記載する
    'jet', # django.contrib.adminより前に記載する
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app1',
    'app2',
]
...
..
.
```
  
### urls.pyを編集する
```config.urls.py
from django.conf.urls import url, include # 追加
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^jet/', include('jet.urls', 'jet')), # 追加
    url(r'^jet/dashboard/', include('jet.dashboard.urls', 'jet-dashboard')), # 追加
]
```
  
### jetとdashboardのマイグレートを実行する
例のごとく一度dockerで立ち上げたwebコンテナに潜ってから

```
$ docker exec -it sample_project /bin/bash
# cd sample_project/
```
  
jetのマイグレート

```
# python3 manage.py migrate jet
Operations to perform:
  Apply all migrations: jet
Running migrations:
  Applying jet.0001_initial... OK
  Applying jet.0002_delete_userdashboardmodule... OK
```
  
dashboardのマイグレート

```
# python3 manage.py migrate dashboard
Operations to perform:
  Apply all migrations: dashboard
Running migrations:
  Applying dashboard.0001_initial... OK
```
  
### Djangoの管理サイトがかっこよくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/f0a2db87-cc3f-1dc8-c23b-a847b8ccb8bf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/05ad5b26-d00e-1dd8-72b7-015e9710dc44.png)
  
# django-reversionで変更履歴管理
django-reversion導入前の変更履歴ページはこんな感じ
[ドキュメントはココ](https://django-reversion.readthedocs.io/en/stable/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/c25d7501-9f08-4579-12dd-053d1a4087a9.png)
  
### settings.pyを編集
```settings.py
.
..
...
INSTALLED_APPS = [
    'jet.dashboard',
    'jet',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'reversion', # 追加
    'app1',
    'app2',
]
...
..
.
```
  
マイグレート実行

```
# python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, app1, app2, auth, contenttypes, dashboard, jet, reversion, sessions
Running migrations:
  Applying reversion.0001_squashed_0004_auto_20160611_1202... OK
```
  
### app1とapp2のadmin.pyを編集
```app1/admin.py
from django.contrib import admin
from reversion.admin import VersionAdmin
from app1.models import Db1Table1

# Register your models here.
@admin.register(Db1Table1)
class Db1Table1Admin(VersionAdmin):
    pass
```
  
```app2/amin.py
from django.contrib import admin
from reversion.admin import VersionAdmin
from app2.models import Db2Table1

# Register your models here.
@admin.register(Db2Table1)
class Db2Table1Admin(VersionAdmin):
    pass
```
  
### 管理サイトの変更履歴ページを確認
「日付/時刻」がリンクになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/bcbe484c-e4e6-42d4-04a0-aefe90bddc10.png)
  
押してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/dfa54d15-21b9-10de-e7bf-20dede8e3926.png)

# 最後に
django-jetの管理サイトはカスタマイズできるようなので適当にググってカスタマイズしてみてください！