Djangoでテーブルのコメントを生成するコマンドを作ってみた

# はじめに
Djangoはmodels.pyに書いたModelから、自動的にCreate Table文を発行してテーブルを作ってくれるので便利…なんだけど、テーブルやカラムにコメントをつけてくれるところまでは対応していない。
作成されたテーブルからER図を逆生成したい場合なんかは、コメントがついててくれるとありがたいんだけどなぁ…

というわけで、無いなら自分で作るか、と試行錯誤してみた。

# テーブルコメント・カラムコメントの構文
テーブルコメント、あるいはカラムコメント追加の構文は↓の通り。

```
comment on table table_name is 'comment'
comment on column table_name.column_name is 'comment'
```

ただし、これはOracleとPostgreSQL（とDB2）の独自仕様で、SQL標準ではないらしい。
できればsqlite3も同じ構文で作れればよかったんだけど、無理みたいなんで今回は断念。
最低限、普段よく使ってるOracleでコメントつけれればいいや、と割りきることにする。

# Djangoカスタムコマンドの作成
普通にPythonモジュールとして作ってもいいが、せっかくなのでDjangoのコマンドを自作して、コマンドラインからサクッと実行できるようにする。
Djangoのコマンドなら、AppCommandクラスを継承することで、引数にDjangoアプリケーションのAppConfigオブジェクトを受け取れるので、コメント作成対象のModel一覧を取得するのも楽。

Djangoカスタムコマンドの作成については、以下の記事を参考にさせてもらった。

 * [Djangoを使ったアプリケーションのバッチ処理](http://www.18th-technote.com/django-batch-make_option)
 * [[Python][Django] manage.pyにコマンドを追加する方法](http://d.hatena.ne.jp/SumiTomohiko/20071203/1196709115)

さらに、django.core.manegement.commands.sqlsequenceresetのソースを参考にした。

## コマンドのフォーマット
コマンド名は「createtablecomment」としておく。フォーマットは、↓のような感じにする。

```
manage.py createtablecomment [options] <app_label app_label ...>
```

## コマンド用のモジュール配置
今回作るコマンドは特定アプリ用のものではないので、プロジェクトフォルダにモジュールを配置する。（以下は配置の参考。プロジェクト名は「myproject」とする）

```
myproject
　└ myproject
　　　└ management
　　　　　└ commands
　　　　　　　└ createtablecomment.py
```

## コマンドクラスのソース
handle_app_configの中身以外は、sqlsequencereset.pyをコピーしてほぼそのまま使ってます。

```py3:createtablecomment.py
from __future__ import unicode_literals

from optparse import make_option

from django.core.management.base import AppCommand
from django.core.management.sql import check_for_migrations
from django.db import connections, DEFAULT_DB_ALIAS
from django.db.models.fields.related import ForeignKey

class Command(AppCommand):
    help = 'Prints the SQL statements for create comment of tables and columns for the given app name(s).'

    option_list = AppCommand.option_list + (
        make_option('--database', action='store', dest='database',
            default=DEFAULT_DB_ALIAS, help='Nominates a database to print the '
                'SQL for.  Defaults to the "default" database.'),
    )

    output_transaction = True

    def handle_app_config(self, app_config, **options):
        if app_config.models_module is None:
            return
        connection = connections[options.get('database')]
        check_for_migrations(app_config, connection)
        models = app_config.get_models(include_auto_created=True)
        cursor = connection.cursor()
        statements = []

        for model in models :
            statement = "COMMENT ON TABLE %s IS '%s'" %
                      (model._meta.db_table, model._meta.verbose_name)
            statements.append(statement)
            cursor.execute(statement)
            for field in model._meta.fields :
                if isinstance(field, ForeignKey) :
                    column  = '%s_id' % field.name
                    comment = '%s(FK:%s)' %
                            (field.verbose_name, field.related.parent_model._meta.db_table)
                else :
                    column  = '%s' % field.name
                    comment = field.verbose_name
                statement = "COMMENT ON COLUMN %s.%s IS '%s'" %
                          (model._meta.db_table, column, comment)
                statements.append(statement)
                cursor.execute(statement)
        return '\n'.join(statements)
```

処理内容は↓のような感じ。

 1. `app_config.get_models()`でアプリ配下のmodelクラス一覧を取得、forで回す
 2. テーブル用のcreate comment文を生成＆実行（`connection.cursorl()`でカーソル取得して`cursor.execute(statement)`）
 3. `model._meta.fields`でmodelが持っているfieldの一覧を取得、forで回す
 4. カラム用のcreate comment文を作成＆実行（`ForeignKey`フィールドの場合は、カラム名に「_id」がつくので補完。また、コメントに「どのテーブルのFKか？」という情報を付加）
 5. 実行したcreate comment文の一覧を改行区切りの文字列にしてreturn（コンソールからコマンド実行した場合は、コンソールに実行されたSQL文が出力される）

# 実行結果
↓のmodels.pyを持つ、myappアプリを対象に実行してみた。

```py3:models.py
from django.db import models

class Author(models.Model):
    class Meta :
        db_table = 'AUTHORS'
        verbose_name = '著者'
        verbose_name_plural = verbose_name

    name = models.CharField('名前', max_length=50)
    birthday = models.DateField('生年月日')

class BooK(models.Model):
    class Meta:
        verbose_name = '本'
        verbose_name_plural = verbose_name

    name = models.CharField('書籍名', max_length=100)
    price = models.IntegerField('値段')
    author = models.ForeignKey(Author, verbose_name='著者')
```

コマンド実行

```
> python manage.py createtablecomment myapp
BEGIN;
COMMENT ON TABLE AUTHORS IS '著者'
COMMENT ON COLUMN AUTHORS.id IS 'ID'
COMMENT ON COLUMN AUTHORS.name IS '名前'
COMMENT ON COLUMN AUTHORS.birthday IS '生年月日'
COMMENT ON TABLE myapp_book IS '本'
COMMENT ON COLUMN myapp_book.id IS 'ID'
COMMENT ON COLUMN myapp_book.name IS '書籍名'
COMMENT ON COLUMN myapp_book.price IS '値段'
COMMENT ON COLUMN myapp_book.author_id IS '著者(FK:AUTHORS)'

COMMIT;
```

無事、テーブルとカラムにコメントが作成された。