プログラムでチェックしているとしてもDBにも必ず制約をつけよう!


RDBに保存するデータの必須チェックやユニークチェックはプログラムでチェックする事もできますが、DBの制約でチェックする事もできます。

例えばusersテーブルのnameは必須という仕様を実装する場合、どのように実装しますか?

パターン1

プログラムでバリデーションして、DBは何もしない。

class User < ApplicationRecord
  validates :name, :email, presence: true
end
mysql> desc users;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint       | NO   | PRI | NULL    | auto_increment |
| name                   | varchar(255) | YES  |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+

Railsなど最近のフレームワークはDBやSQLをほとんど意識することなく利用できることもあり、パターン1のようにDBの制約ではなくプログラムでのチェックを重視している方も多いのではないでしょうか?

このパターンはNGです。
プログラムでのチェックは、当然プログラムを通してDBを触る時だけしかチェックされません。
DBはプログラムを通さずに直接触ることもできます。また、プログラムの不具合でチェックが動作しないことや別にプログラムからアクセスすることもあるかもしれません。

プログラムでチェックする場合、下記図のようにDBに書き込む可能性があるすべての箇所でチェックする必要があります。
もしチェックが漏れた場合にはDBに制約がないため、不正なデータが格納されてしまう可能性があります。

ちなみにこちらのパターンを採用する場合があります。
それはDBの制約では指定できない場合です。
例えば、最小文字数や数値の範囲、取りうる値が限られている場合などのチェックはDBの制約で行うことができないのでプログラムで制御する必要があります。
この場合、DB自体では制御できないので不正な値が入る可能性があります。データ編集できる箇所を限定するなど運用でカバーする必要があります。

パターン2

DBだけで制御する

class User < ApplicationRecord
end
mysql> desc users;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint       | NO   | PRI | NULL    | auto_increment |
| name                   | varchar(255) | NO   |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+

このパターンは人によって意見が分かれると思いますが、個人的には最近良くやるパターンです。

パターン1と異なりDBに制約がかかっているため、不正な値が入る可能性が完全になくなります。
呼び出し元でチェックする必要がないため、実装が楽になります。

ただ、不正なデータであってもSQLを実行する必要があるので、DB負荷を軽減したい場合は次に紹介するパターン3のようにプログラムでもチェックしておいたほうが良いでしょう。
また、パターン1の最後にも記載しましたが、数値の範囲などDBの制約ではチェックしきれないこともあるのでこのパターンはDBの制約でチェックできる場合のみ使用可能です。

パターン3

プログラムでもDBでも制御する。パターン1と2のハイブリッドです。

class User < ApplicationRecord
  validates :name, :email, presence: true
end
mysql> desc users;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint       | NO   | PRI | NULL    | auto_increment |
| name                   | varchar(255) | NO   |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+

このパターンは一番採用されることが多いパターンだと思います。
このパターンの場合、不正データの場合にSQLを実行することなくエラーにすることができるので、パターン2と比べてDBの負荷を下げることができます。
また、DBにも制約がかかっているため、パターン1とは異なりプログラムでのチェックが漏れたとしても不正なデータが入る可能性はありません。

このようにDBの負荷も軽減しつつ、不正なデータが入る可能性も排除しているので万能のように思えますが私の好みはパターン2です。
理由は、昨今のプロダクト開発の環境において、1回のDBアクセスを減らすことより、実装する量を減らす方が有意義だと考えているからです。

プログラムにコードを書くということは、今後そのコードをずっとメンテしていく必要があるということです。
そのため、コードは可能な限り少ないほうが好ましいです。
また、チェック条件が変わったときにプログラムとDBの両方を修正する必要があり手間がかかります。
このように考えているので、私の場合は

  • DBの制約で制御できるものはDBの制約だけで制御する
  • DBの制約で制御できないものはプログラムで制御する

という方針で実装することが多いです。