こんにちは。KOUKIです。
前回は、カスタムユーザーを作成しました。
今回は、マネジメントコマンドを自作しましょう、と言った話です。
今回もDjango REST frameworkは使用しません(タイトル詐欺ですね)。
<目次>
記事まとめ
マネジメントコマンドの作成
Djangoには、様々なマネジメントコマンドがあり、”python manage.py –help“で一覧が見れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
$ docker-compose run app python manage.py --help [auth] changepassword createsuperuser [contenttypes] remove_stale_contenttypes [django] check compilemessages createcachetable dbshell diffsettings dumpdata flush inspectdb loaddata makemessages makemigrations migrate sendtestemail shell showmigrations sqlflush sqlmigrate sqlsequencereset squashmigrations startapp startproject test testserver [sessions] clearsessions [staticfiles] collectstatic findstatic runserver |
例えば、「python manage.py migrate」と実行するとマイグレーションが実行されますよね?
このようなマネジメントコマンドは、自作することが可能です。
Docker上で「Django+PostgresSQL」を動かす時、データベースのスキーマー形成前にPostgresSQLコンテナが起動するため、エラーが発生することがあります。
この問題を解決しましょう。
DBが完全に起動するまで、ポーリングを投げ続けるコマンドがあればよさそうですね。
フォルダとファイルを作成します。
1 2 3 4 5 |
touch app/account/tests/test_commands.py mkdir -p app/account/management/commands touch app/account/management/__init__.py touch app/account/management/commands/__init__.py touch app/account/management/commands/wait_for_db.py |
まずは、テストを書きましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# app/account/tests/test_commands.py from unittest.mock import patch from django.core.management import call_command from django.db.utils import OperationalError from django.test import TestCase import django class CommandTests(TestCase): def test_wait_for_db_ready(self): # https://github.com/django/django/blob/1.5c2/django/db/__init__.py # https://docs.djangoproject.com/en/2.2/_modules/django/db/utils/ with patch('django.db.utils.ConnectionHandler.__getitem__') as gi: gi.return_value =True call_command('wait_for_db') self.assertEqual(gi.call_count, 1) @patch('time.sleep', return_value=True) def test_wait_for_db(self, ts): # https://github.com/django/django/blob/1.5c2/django/db/__init__.py # https://docs.djangoproject.com/en/2.2/_modules/django/db/utils/ with patch('django.db.utils.ConnectionHandler.__getitem__') as gi: gi.side_effect = [OperationalError] * 5 + [True] call_command('wait_for_db') self.assertEqual(gi.call_count, 6) |
テスト時にはDBに接続したくないので、Mockを利用します。
「test_wait_for_db_ready」は、データベースが起動確認用のテストです。
「django.db.utils.ConnectionHandler.getitem」を呼ぶとDBに接続済みオブジェクトを取得できるので、このオブジェクトを使ってテストをします。
「call_command(‘wait_for_db’)」は、これから作成するwait_for_db.pyを指します。
「test_wait_for_db」は、DBが接続されていないときのマネジメントコマンドの挙動をチェックします。DBに接続できていない時にマネジメントコマンドを実行すると「OperationalError」が発生します。
テストを実行しましょう。
1 2 3 |
make test E AttributeError: module 'account.management.commands.wait_for_db' has no attribute 'Command' |
wait_for_db.pyに何も実装していないのでエラーが出力されます。
それでは、実装して見ましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# app/account/management/commands/wait_for_db.py import time from django.db import connections from django.db.utils import OperationalError from django.core.management.base import BaseCommand class Command(BaseCommand): def handle(self, *args, **kwargs): self.stdout.write('Waiting for database...') db_conn = None while not db_conn: try: db_conn = connections['default'] except OperationalError as e: self.stdout.write('Database unabailable, waiting 1 second...') time.sleep(1) self.stdout.write(self.style.SUCCESS('Database available!')) |
connections[‘default’]にて、Djangoの’default’で設定しているDBを呼びます。
1 2 3 4 5 6 7 8 9 10 11 |
# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'HOST': os.environ.get('DB_HOST'), 'NAME': os.environ.get('DB_NAME'), 'USER': os.environ.get('DB_USER'), 'PASSWORD': os.environ.get('DB_PASS'), } } |
接続に失敗したら「OperationalError」を出すので、接続できるまでWhileループで回し続けます。
テストを実行しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
make test ... account/tests/test_commands.py::CommandTests::test_wait_for_db Waiting for database... Database unabailable, waiting 1 second... Database unabailable, waiting 1 second... Database unabailable, waiting 1 second... Database unabailable, waiting 1 second... Database unabailable, waiting 1 second... Database available! PASSED account/tests/test_commands.py::CommandTests::test_wait_for_db_ready Waiting for database... Database available! PASSED ... |
OKですね。
これで、Docker上で「python manage.py wait_for_db」コマンドを実行すれば、DBが立ち上がるまでポーリングしてくれる仕様に書き換えられますね。
docker-compose.yml修正
docker-compose.ymlに先ほどのマネジメントコマンドを組み込みましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# docker-compose.yml version: "3" services: # app(docker)サービス app: build: context: . ports: - "8000:8000" volumes: - ./app:/app command: > sh -c "python manage.py wait_for_db && python manage.py migrate && # 追加 python manage.py runserver 0.0.0.0:8000" environment: - DB_HOST=db - DB_NAME=app - DB_USER=postgres - DB_PASS=supersecretpassword depends_on: - db # db(postgresサービス) db: image: postgres:10-alpine environment: - POSTGRES_DB=app - POSTGRES_USER=postgres - POSTGRES_PASSWORD=supersecretpassword volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: |
それでは、docker-compose upで立ち上げてみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
docker-compose up django-recipe-api_db_1 is up-to-date Creating django-recipe-api_app_1 ... done Attaching to django-recipe-api_db_1, django-recipe-api_app_1 "/var/run/postgresql/.s.PGSQL.5432" db_1 | 2019-10-17 05:12:07.137 UTC [18] LOG: database system was shut down at 2019-10-11 03:51:12 UTC db_1 | 2019-10-17 05:12:07.163 UTC [1] LOG: database system is ready to accept connections app_1 | Waiting for database... ★ OK app_1 | Database available! ★ OK app_1 | Operations to perform: app_1 | Apply all migrations: account, admin, auth, contenttypes, sessions app_1 | Running migrations: app_1 | No migrations to apply. app_1 | Performing system checks... app_1 | ... |
OKですね。
次回
次回からいよいよDjango REST frameworkを使って、アプリケーション開発を行っていきます。
関連記事
基礎編はこちらです。
Djangoおすすめ書籍
Djangoを学ぶなら以下の書籍がオススメです。
緑 -> 赤 -> 紫の順でやればOKです。読みやすい英語で書かれているので、英語力もついでに上がるかもしれません^^
コメントを残す
コメントを投稿するにはログインしてください。