30.

Djangoカスタム管理コマンドの作成|BaseCommand・handleとcron連携

編集
この記事の要点
  • django 内のモデルを使った定期処理は カスタム管理コマンドとして作成し python manage.py コマンド名 で実行する
  • ディレクトリ構成は myapp/management/commands/コマンド名.py(途中の 2 つのフォルダに __init__.py が必要)
  • BaseCommand を継承し handle(self, *args, **options) を実装する
  • 引数オプションは add_arguments(parser)argparse 形式で定義
  • cron からは cd /path/to/project && /path/to/venv/bin/python manage.py コマンド名 として叩く

なぜ Django のカスタム管理コマンドが必要なのか

cron で python my_script.py を直接叩くと、django の設定(settings.py)が読み込まれず、ORM 経由でモデルを触ろうとした瞬間に django.core.exceptions.ImproperlyConfigured で落ちます。

Django のお作法に則り 「カスタム管理コマンド(custom management command)」 として実装すると、manage.py がプロジェクトを正しく初期化してくれるため、モデル・キャッシュ・メール送信などのフル機能をそのまま使えます。

1. ディレクトリ構成

対象アプリの下に以下のフォルダ階層を作ります。__init__.py を忘れずに置くのがポイントです(空ファイルで OK)。

myapp/
├── __init__.py
├── models.py
├── views.py
└── management/
    ├── __init__.py         ← 必須
    └── commands/
        ├── __init__.py     ← 必須
        └── my_command.py   ← ここがコマンド本体

ファイル名がそのままコマンド名になります。my_command.py なら python manage.py my_command で呼べます。

2. 最小コマンドの実装

my_command.py 内では BaseCommand を継承した Command クラスを定義し、handle() に処理を書きます。

# myapp/management/commands/my_command.py
from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = "Hello World を出力するサンプルコマンド"

    def handle(self, *args, **options):
        self.stdout.write("Hello World!")

実行は以下のとおり。

python manage.py my_command
# => Hello World!

# 一覧確認(自作コマンドが MyApp グループに出る)
python manage.py help

3. 引数オプションを受け取る

add_arguments(self, parser) をオーバライドして argparse 形式で引数を定義します。受け取った値は handleoptions 辞書から取り出せます。

from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = "ユーザを指定回数あいさつする"

    def add_arguments(self, parser):
        # 位置引数
        parser.add_argument("name", type=str)
        # オプション引数
        parser.add_argument(
            "--times",
            type=int,
            default=1,
            help="繰り返し回数(既定: 1)",
        )
        parser.add_argument(
            "--shout",
            action="store_true",
            help="大文字にする",
        )

    def handle(self, *args, **options):
        name  = options["name"]
        times = options["times"]
        shout = options["shout"]

        msg = f"Hello, {name}!"
        if shout:
            msg = msg.upper()
        for _ in range(times):
            self.stdout.write(msg)
python manage.py greet Taro --times 3 --shout
# => HELLO, TARO!
# => HELLO, TARO!
# => HELLO, TARO!

4. モデルを使った実用コマンド

cron で「30 日経過した一時データを削除」のような典型バッチを書く例です。

# myapp/management/commands/cleanup_old.py
from datetime import timedelta

from django.core.management.base import BaseCommand
from django.utils import timezone

from myapp.models import TempData


class Command(BaseCommand):
    help = "30 日経過した TempData を削除する"

    def add_arguments(self, parser):
        parser.add_argument("--days", type=int, default=30)
        parser.add_argument("--dry-run", action="store_true")

    def handle(self, *args, **options):
        days    = options["days"]
        dry_run = options["dry_run"]

        threshold = timezone.now() - timedelta(days=days)
        qs = TempData.objects.filter(created_at__lt=threshold)
        count = qs.count()

        if dry_run:
            self.stdout.write(self.style.WARNING(
                f"[dry-run] {count} 件削除対象"))
            return

        deleted, _ = qs.delete()
        self.stdout.write(self.style.SUCCESS(
            f"{deleted} 件削除しました"))

5. cron への登録

OS 標準の cron から呼ぶ場合、仮想環境の Pythonプロジェクトディレクトリ を明示するのが鉄則です。PATH に依存しないフルパスで書きます。

# crontab -e で開いて以下を追記
# 毎日 03:30 に cleanup_old を実行
30 3 * * * cd /var/www/myproject && /var/www/myproject/.venv/bin/python manage.py cleanup_old >> /var/log/myproject/cron.log 2>&1
ポイント理由
仮想環境の Python をフルパスで指定cron 環境では which python が想定と違う可能性
cd でプロジェクトディレクトリへ移動manage.py 起動時の作業ディレクトリを固定(settings 読み込み)
標準出力・標準エラーをログにリダイレクト失敗原因を後から追える(2>&1 で stderr も同じファイルへ)
環境変数(DJANGO_SETTINGS_MODULE など)も必要なら crontab で設定cron はログインシェルではないため、シェルで設定した env が読まれない

6. django-crontab / django-q / Celery beat という選択肢

OS の cron に直接登録する以外に、Python 側で完結させる選択肢もあります。

ツール特徴
django-crontabsettings.py にスケジュールを書くだけで OS cron に同期。シンプル
django-q / django-q2非同期タスクキュー。スケジュール + リトライ + 並列ワーカ
Celery + Celery beat本格的な非同期ジョブシステム。Redis / RabbitMQ をブローカに使う
OS cron + manage.py外部依存ゼロ。小規模なら最もシンプル(本記事の方法)

よくあるハマりどころ

症状原因 / 対処
Unknown command と言われるファイル配置ミス / __init__.py 抜け / アプリが INSTALLED_APPS に未登録
cron で動かないがシェルでは動くcron は最小限の環境変数しか持たない。crontab に PATH=DJANGO_SETTINGS_MODULE= を明示
ログが残らない標準出力をファイルへリダイレクトしていない。>> /var/log/.../cron.log 2>&1 を付ける
重複起動処理が長引き次の起動が被る。flock で排他制御するか、ジョブ側で in-progress フラグを持つ

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 環境構築とプロジェクト/アプリの作成
  2. MVC(MVT)のそれぞれの使い方と説明
  3. データベースへの接続と操作
  4. Django Administration
  5. git管理
  6. エラー一覧
  7. バージョンの確認方法
  8. ログ出力方法
  9. SQLのログ出力方法
  10. ログのローテート設定
  11. settings.pyの定数にアクセスする方法
  12. 本番環境へのインストールとアプリのデプロイ(apache編)
  13. 本番環境へのインストールとアプリのデプロイ(nginx編)
  14. djangoアプリの本番の開始URLを変更する
  15. 静的(static)ファイルの置き場所と読み込み(画像、css、js )
  16. CSRFトークンをAjaxで使用する方法
  17. ajaxの使用例(POST編)
  18. ファイルのアップロードとファイルの名前
  19. クイックスタート/チュートリアル
  20. ログイン機能
  21. テンプレート側のログイン判定
  22. ビュー側のログイン判定
  23. 管理者ユーザーの作成/判定と管理画面
  24. モデルのjson化とレスポンス
  25. runserverでポートを指定する方法
  26. cronによるバッチ実行
  27. テンプレートで利用する共通のcontextを定義する方法
  28. プログラムが本番サーバーで反映されない場合の対処法
  29. APIの作成
  30. cron用コマンド・ファイルの作成

最近更新/作成されたページ