DB設計のチェックスタイル (SchemaPolicyCheck)

SchemaPolicyCheckとは?schemaPolicyMap.dfpropの概要などについて説明をします。

DB設計のレビューコスト

自由過ぎるDB設計はいかが?

例えば、このようなDB設計を見たことはないでしょうか?

  • フラグが BOOLEAN だったり INTEGER だったり
  • フラグにNotNull制約が付いてないものがある
  • テーブル名やカラム名の大文字小文字がバラバラ
  • PKのないテーブルがある
  • FK制約名の形式がバラバラ

システムの挙動としては致命的ではないにしても、非常に勘違いを誘発しそうなテーブルやカラムが乱立していると、 様々な細かいリスクを抱えることになります。しかも、DBはなかなか思い切って修正できないものです。 (DBFluteでDB変更しやすいとしても、既にリリース済のシステムであれば気軽にとはいきません)

なので、このようなDB設計にならないように、DBに長けた先輩が一生懸命レビューしたりするわけです。

レビュータスクとプレッシャーはいかが?

レビューは非常に大事です。DB設計は、論理で自動で定まるようなものではない "デザイン" ですから、一貫性のあるスキーマにするためには、伝承やコミュニケーションがキーポイントになるでしょう。

ですが、レビューはタダではありません。DBに長けている方のそれなりの時間を奪います。 業務のことも理解しないとレビューできませんので、レビューするためのインプットも必要です。

また、DBは変更コストが高く、パフォーマンス問題も引き起こしやすいものなので、レビューするにはかなり気を使います。 "レビューでOKだったからもう大丈夫" と太鼓判を押されてしまいがちですから、相当なプレッシャーではあります。 (本来、レビューは補助的なもので、あくまで大きな責任はレビューイーが持つものではありますが...)

そのコストとプレッシャーがレビューワーの限界を超えてしまうと、レビューワーの他のタスクに支障が出るかもしれませんし、 レビューワーがそうならないようにするために自然とレビューの精度を落とさざるを得なくなります。 すると、先ほど紹介したような一貫性のないDB設計をレビューで見逃してしまう可能性が大きくなるのです。

レビューの指摘は定型的なものばかり!?

レビューで指摘される内容の多くが、定型的なものばかりというケースも珍しくありません。先ほど紹介したような... "_FLGになってないよ" とか "NotNull制約付いてなくない?" とか "テーブル名のケース統一して" とか "PK制約付け忘れてるよ" とか "FK制約名はこういう風にして" とか。

それだけレビュー会の半分以上の時間が奪われてしまうこともあります。それを避けるために、レビューワーは "本当はこうして欲しいんだけど細かいこと言ってたらキリがないな" ということで見逃すのです。 (また、"細かいことばっかり言ってる人だなぁ" と思われてしまうのを避ける心理も働きます)

高度なレビューに集中してもらいたい

そういう定型的な指摘は、プログラミングの世界でやってるのと同様に、ツールで自動化をするのが良いでしょう。 という発想で生まれたのが、DBFlute の SchemaPolicyCheck になります。

そのプロジェクトにおけるポリシー定義をすると、Docタスクにてチェックされるようになります。

e.g. _FLGカラムのデータ型は BOOLEAN であること @schemaPolicyMap.dfprop
map:{
    ...
    ; columnMap = map:{
        ...
        ; statementList = list:{
            ; if columnName is suffix:_FLG then dbType is BOOLEAN
            ...
        }
    }
}
e.g. _FLGカラムのデータ型は BOOLEAN のはずなのに、INTEGERだったとき @Console
org.dbflute.exception.DfSchemaPolicyCheckViolationException: Look! Read the message below.
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
...

[Violation]
column.statement: if columnName is suffix:_FLG then dbType is BOOLEAN
 |-The dbType should be BOOLEAN but INTEGER: (購入)PURCHASE.(支払完了フラグ)PAYMENT_COMPLETE_FLG INTEGER(10) (NotNull)
 +-The dbType should be BOOLEAN but INTEGER: (サービスランク)SERVICE_RANK.(新規受け入れ可能フラグ)NEW_ACCEPTABLE_FLG INTEGER(10) (NotNull)
* * * * * * * * * */

優れたレビューワーのレビュー時間は、業務的な指摘やパフォーマンス配慮の指摘に集中してもらいたいものです。

SchemaPolicyCheckを使おう

チェックの大まかな流れ

大まかな流れです。

  1. schemaPolicyMap.dfprop でポリシー設定
  2. Docタスクを叩く (sh manage.sh doc)
  3. ポリシー違反を、コンソールの例外メッセージで確認

1. schemaPolicyMap.dfprop でポリシー設定

[DBFluteクライアント]/dfpropディレクトリ配下に、schemaPolicyMap.dfprop という名前のテキストファイル (UTF-8) を作成します。

試しに、一つだけでも良いのでチェックしてみましょう。

e.g. _FLGカラムはNotNull制約が付いていること @schemaPolicyMap.dfprop
# /---------------------------------------------------------------------------
# schemaPolicyMap: (Default map:{})
#
# The definition of schema policy.
# You can check your schema as the policy.
# The table and column names are treated as case insensitive.
#
# Example:
# map:{
#     ; tableExceptList = list:{}
#     ; tableTargetList = list:{}
#     ; columnExceptMap = map:{}
#     ; isMainSchemaOnly = false
#     
#     ; wholeMap = map:{
#         ; themeList = list:{ uniqueTableAlias ; sameColumnAliasIfSameColumnName }
#     }
#     ; tableMap = map:{
#         ; themeList = list:{ hasPK ; upperCaseBasis ; identityIfPureIDPK }
#         ; statementList = list:{
#             ; if tableName is $$ALL$$ then fkName is prefix:FK_$$table$$
#         }
#     }
#     ; columnMap = map:{
#         ; themeList = list:{ upperCaseBasis }
#         ; statementList = list:{
#             ; if columnName is suffix:_FLAG then bad
#             ; if columnName is suffix:_FLG then notNull
#             ; if columnName is suffix:_FLG then dbType is INTEGER 
#             ; if columnName is suffix:_FLG then classification 
#         }
#     }
# }
#
# *The line that starts with '#' means comment-out.
#
map:{
    ; tableExceptList = list:{}
    ; tableTargetList = list:{}
    ; columnExceptMap = map:{}
    ; isMainSchemaOnly = false

    ; wholeMap = map:{
        ; themeList = list:{}
    }
    ; tableMap = map:{
        ; themeList = list:{}
        ; statementList = list:{
        }
    }
    ; columnMap = map:{
        ; themeList = list:{}
        ; statementList = list:{
            ; if columnName is suffix:_FLG then notNull
        }
    }
}
# ----------------/

2. Docタスクを叩く (sh manage.sh doc)

そして、Docタスクを叩きます。(すでに "JDBCタスク" で、メタデータが取得されていることが前提です)

e.g. Docタスクを実行するManageタスク @Command
...$ sh manage.sh doc

SchemaHTML や HistoryHTML が自動生成された直後に、SchemaPolichCheck が実行され、schemaPolicyMap.dfprop のポリシーに合致するかどうかの検査が行われます。

3. ポリシー違反を、コンソールの例外メッセージで確認

もし、ポリシー違反のDB設計になっていたら、Docタスクがエラーで中断し、コンソールに例外メッセージが出力されます。 (log/dbflute.logにも記録されています)

試しに、わざとポリシー違反を一つ作ってみて、例外メッセージを確認してみると良いでしょう。

[Advice]
自然言語によるアドバイス
[Schema Policy]
定義されているすべてのポリシー
[Violation]
ポリシー違反の結果 (ここに注目!)
e.g. _FLGカラムはNotNull制約が付いているはずなのに、付いていないとき @Console
org.dbflute.exception.DfSchemaPolicyCheckViolationException: Look! Read the message below.
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
The schema policy has been violated.

[Advice]
Make sure your violating schema (ERD and DDL).
And after that, execute renewal (or regenerate) again.
(tips: The schema policy is on schemaPolicyMap.dfprop)

[Schema Policy]
tableExceptList: []
tableTargetList: []
columnExceptMap: {}
isMainSchemaOnly: false
wholeMap: {themeList=[]}
tableMap:
  themeList: []
  statementList:
columnMap:
  themeList: []
  statementList:
    if columnName is suffix:_FLG then notNull

[Violation]
column.statement: if columnName is suffix:_FLG then notNull
 +-The column should be not-null: (購入)PURCHASE.(支払完了フラグ)PAYMENT_COMPLETE_FLG INTEGER(10) (NullAllowed)
* * * * * * * * * */

DBを直してから、もう一度 Doc タスクを叩き直します。エラーで落ちなければOKです。

プロジェクトのポリシーを設定していこう

それでは、schemaPolicyMap.dfpropのドキュメントを参考に、自分たちのプロジェクトにおけるDB設計の定型的な業務ポリシーを設定していきましょう。

dfprop上にコメント書いたり、エラー時に表示される "補足コメント" などを充実させて、いざチェックに引っかかってしまった時に、DB設計者が迷わないようにしておきましょう。

ReplaceSchemaの段階でチェック

もっと早くチェックしたい

ReplaceSchemaを利用している場合、Docよりも前に実行されるReplaceSchemaの時点で検知してもらいたいと思うかもしれません。

isCheckSchemaPolicyInReps

replaceSchemaMap.dfprop にて、isCheckSchemaPolicyInReps を true にすると、ReplaceSchemaの中のDDLを実行した直後(テストデータを入れる直前)に SchemaPolicyCheck が実行されます。

Docでのチェックの分身である

厳密には、DocタスクとReplaceSchemaタスクとでは、SchemaPolicyCheck のスコープが少し変わります。 なので、Docタスクが要らないわけではありません。

ReplaceSchemaでのチェックは、そのReplaceSchemaで対象としているスキーマのみがチェック対象になります。 その他、もしReplaceSchemaの時点ではチェックがしづらいものがあれば、それはチェック処理がスキップされます。

あくまで、チェックのタイミング早めるために、ReplaceSchemaの時点で可能なチェックだけを先に持っていったということです。 (実際には、ほとんどチェック内容は同じになりますが、扱いとしては分身です)

これから追加されるものだけチェック

チェックが便利だと思っても、すでにたくさんのポリシーバラバラなテーブルやカラムがたくさんあるような場合は、 なかなかチェックを適用できません。どんどんチェックをコメントアウトしていって、ほとんど意味のないチェックになってしまいがちです。

そんなときは、firstDate条件を使って、これから追加されるテーブルやカラムだけチェックする という風にすると良いでしょう。(既存のテーブルやカラムは諦めるか保留するという前提で)

e.g. firstDateを使って、これから追加されるテーブルだけチェック @schemaPolicyMap.dfprop
# 2018/05/04より未来に追加されたテーブルは、FK制約名は FK_[テーブル名] で始まること
; if firstDate is after:2018/05/04 then fkName is prefix:FK_$$table$$

ひとりDB設計者でもチェック

複数人のDB設計ではなく、ひとりでDB設計をするにしても、チェックはあった方が良いです。

一貫性という意味では、チェックがなくても担保はしやすいかもしれませんが、 ケアレスミスなどはやはり発生するので自己レビューはするわけで、そのレビューコストがかかります。 でも、自己レビューは抜けが多くなりがちです。ケアレスミスも見逃してしまうかもしれません。

また、50テーブルや100テーブル以上のDB設計をしていくとなると、ひとりで設計していても一貫性を保つのは大変なことです。 "前どうしてたっけ?" と周りのテーブルを見直すのもそれなり時間がかかりますので、そのうち新しいポリシーが作られ、一貫性を失っていきます。

ぜひ、気軽に PolicyCheck していきましょう。