概要
一般的に、RDBのテーブルを定義する際には、管理目的で「どんなテーブルにも以下の4つのカラムは必ず定義すること」という決まりになっているシステムが多いと思う。
※ 括弧内はよくあるカラム名の例
- 作成者 (created_by)
- 作成日時 (created_at)
- 更新者 (updated_by)
- 更新日時 (updated_at)
少なくとも、私が今まで経験してきたPJでは、どのPJでもほぼ例外なくこの4つのカラムは定義するという決まりになっていた。
システムの運用保守を経験したことがある身としても、これらのカラムがなかったり、定義してあるがNullが入っていたりすると、運用時に往々にして困ったので、その方針自体は正しいと思っている。
今回は、DjangoのModelにこれらのカラムを定義する方法について考えてみたい。
※ 以下、この4つのカラムのことを便宜的に「お約束カラム」と呼ぶことにする。(正式な名称とかあったら知りたい)
以下の2つの方法について、それぞれメリット/デメリットをみていく。
- 愚直に全てのModelに「お約束カラム」を定義する。
- 「お約束カラム」を親のモデルに共通化する。
検証
1.愚直に全てのModelに「お約束カラム」を定義する方法
特に説明することはないと思うが、以下のように愚直に全てのModelに「お約束カラム」を定義する。
from django.db import models class Author(models.Model): class Meta: db_table = "author" name = models.CharField(max_length=255) # 以下、「お約束カラム」 created_by = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) updated_by = models.CharField(max_length=255) updated_at = models.DateTimeField(auto_now=True) class Publisher(models.Model): class Meta: db_table = "publisher" name = models.CharField(max_length=255) # 以下、「お約束カラム」 created_by = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) updated_by = models.CharField(max_length=255) updated_at = models.DateTimeField(auto_now=True)
※ DateTimeFieldの「auto_now_add」、「auto_now」というオプションについては本記事では説明しないので、知らなかった方は以下の記事を参照
akiyoko.hatenablog.jp
マイグレーション実行結果
マイグレーションを実行すると、以下のように意図した通りのテーブルが作成される。
mysql> desc author; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | created_by | varchar(255) | NO | | NULL | | | created_at | datetime(6) | NO | | NULL | | | updated_by | varchar(255) | NO | | NULL | | | updated_at | datetime(6) | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.01 sec) mysql> desc publisher; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | created_by | varchar(255) | NO | | NULL | | | created_at | datetime(6) | NO | | NULL | | | updated_by | varchar(255) | NO | | NULL | | | updated_at | datetime(6) | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.00 sec)
メリット
- カラムの順番が意図した並び順になる。
→ 後述するが、2の方は意図した並び順にならない。
デメリット
- 全てのModelで繰り返し同じフィールドを定義するので、DRY原則に反する。(具体的なリスクとしては、コピペミスでバグが混入するリスクがある、同じフィールド定義が何回も出てくるので可読性が下がるなど)
→ ただ、たかだかフィールド定義4行の話なので、チーム内で意識が共有されていれば、絶対に避けるべきレベルの重複ではないのかなと思う。
2.「お約束カラム」を親のモデルに共通化する。
以下の方法で「お約束カラム」のフィールドを共通化して切り出すことができる。
- abstractな親のモデル(仮にBaseModelとする)を定義してそこに「お約束カラム」のフィールドを定義する。
- 全てのModelはBaseModelを継承するようにする。
この場合は以下のような実装となる。
from django.db import models class BaseModel(models.Model): class Meta: # マイグレーション時にテーブルを作成しないModelは以下のオプションが必要 abstract = True # 以下、「お約束カラム」 created_by = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) updated_by = models.CharField(max_length=255) updated_at = models.DateTimeField(auto_now=True) class Author(BaseModel): class Meta: db_table = "author" name = models.CharField(max_length=255) class Publisher(BaseModel): class Meta: db_table = "publisher" name = models.CharField(max_length=255)
尚、この方法は、Djangoの世界では『Two Scoops of Django 1.11』という英語の本の中で、「TimeStampedModel」という名前で紹介されているプラクティスらしい。(私はこの本を読んだことがない)
マイグレーション実行結果
マイグレーション結果をみると、以下のように「お約束カラム」が並び順の先頭にきてしまっている。
(厳密にはPK → BaseModelで定義したカラム → 継承先のModelで定義したカラムの順番になっている。)
mysql> desc author; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | created_by | varchar(255) | NO | | NULL | | | created_at | datetime(6) | NO | | NULL | | | updated_by | varchar(255) | NO | | NULL | | | updated_at | datetime(6) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.00 sec) mysql> desc publisher; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | created_by | varchar(255) | NO | | NULL | | | created_at | datetime(6) | NO | | NULL | | | updated_by | varchar(255) | NO | | NULL | | | updated_at | datetime(6) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.00 sec)
「お約束カラム」はあくまで管理用の共通カラムなので、目立たないように一番後ろにきてほしいところではある。
このような並び順になってしまうのは、「Modelを継承している場合は親Modelで定義しているフィールドのカラムから先に並ぶ」というDjangoの仕様であり、調べたところお手軽な解決方法はなさそうだった。
※ なお、余談だが、Djangoの組み込みのUserモデルから作成される「auth_user」テーブルを初めてdescした時に、「なんでusernameがこんな後ろにあるんだろう?」と違和感を持ったが、今思うとこれが原因だった。
メリット
- 重複するフィールド定義を共通化できる。
デメリット
- カラムの順番が意図した並び順にならない。(お約束カラムが継承先のModelで定義したフィールドのカラムよりも前に来てしまう。)