データベース で null 許容のカラムを アプリケーション側でシンプルに扱う為の工夫

# 背景

データベースのテーブル設計において、様々なカラムに対して `NULL` を許容する場合があります。
カラムが `NULL` となった際に、どのような振る舞いをすべきか？ というのはアプリケーションに依存しますが、得てして、プログラムコードを複雑にしがちです。

この記事では、 Ruby on Rails の ActiveRecord を題材として、複雑性を低減させる為の一つの手法を紹介します。

# やり方

* 多様な値を取りうるカラムに直接アクセスすることをやめて、別のメソッドを通して間接的にアクセスする
* 新規定義するメソッドでは、 `NULL` か否かに関わらず、同じような振る舞いをするようにする
* `NULL` が、どのような振る舞いであるべきか？に基づいて、新しいメソッドを設計する

至極当たり前のことな気がしますが、ちょっとした工夫で複雑性が大きく低減されるので、実例を見ていくことにしましょう。

# 例

## 数値型の場合

例えば、魔法の箱が存在し、中に入れるものに対して、許容される最小重量と、最大重量がある場合を考えてみましょう。
`NULL` の場合は、それぞれ、「最小重量なし」、「最大重量なし」という仕様にして、最小限の validation を入れます。

```ruby
create_table :magical_boxes do |t|
  t.float min_weight
  t.float max_weight
end
```


```ruby
class MagicalBox < ApplicationRecord
  validates :min_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
  validates :max_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
end
```

ここに対して、以下の２つを追加実装したいと思います。

* 最大重量(`max_weight`)は 最小重量(`min_weight`) 以上であるという大小関係に関する validation の追加
* 重量 (`weight`) を引数として受け取り、それが許容可能かを返す `MagicalBox#allow_weight?` の実装

### before (素直に実装した場合)

```ruby
class MagicalBox < ApplicationRecord
  validates :min_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
  validates :max_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

  validate :validate_weight

  def validate_weight
    return if !min_weight || !max_weight # 何れかが定義されていれば大小関係は問題ない

    if max_weight < min_weight
      errors.add(:max_weight, 'must be greater than or equal to `min_weight`')
    end
  end

  def allow_weight?(weight)
    if min_weight && weight < min_weight # 定義されている場合だけ大小関係のチェックを入れる
      return false
    end

    if max_weight && weight > max_weight # 定義されている場合だけ大小関係のチェックを入れる
      return false
    end

    true
  end
end
```

`nil` のハンドリングが様々な場所で要求されるようになっています。
素直なコードなので分かりやすいというメリットはありつつ、 `null` の場合に動くのか？ というのを頭の中で考え続ける必要があります。

### after (工夫をした場合)

やり方を実践していきます。

* 多様な値を取りうるカラムに直接アクセスすることをやめて、別のメソッドを通して間接的にアクセスする
  * `nil` を返したり、 `Float` を返したりする、 `max_weight`, `min_weight` へのアクセスをやめてみます
* 新規定義するメソッドでは、 `NULL` か否かに関わらず、同じような振る舞いをするようにする
  * `NULL` 以外の場合と同じように、 `Float` として振る舞ってもらえると嬉しい感があります
* `NULL` が、どのような振る舞いであるべきか？に基づいて、新しいメソッドを設計する
  * 「最小重量なし」、「最大重量なし」 という仕様は、 「最小重量が0である」「最大重量が無限大である」とみなせます。

これらを踏まえて、 `numerical_min_weight`, `numerical_max_weight` を定義してみます。

```ruby
class MagicalBox < ApplicationRecord
  validates :min_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
  validates :max_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

  validates :numerical_max_weight, numericality: { greater_than_or_equal_to: :numerical_min_weight }  

  def allow_weight?(weight)
    numerical_min_weight <= weight && weight <= numerical_max_weight
  end

  def numerical_min_weight
    min_weight || 0.0 # nilの場合は最小重量が0であるとみなせる
  end

  def numerical_max_weight
    max_weight || Float::Infinity # nilの場合は最大重量が無限大であるとみなせる
  end
end
```

このようにすると、 `nil` の場合に、「どういう振る舞いをすべきか？」という仕様が、
`numerical_min_weight` や `numerical_max_weight` というメソッドに、仕様通りに隠蔽されます。

仕様に注目することで、これらを扱うメソッド群は、 `NULL` という実装の詳細に依存するのではなく、
「数値で表した最小重量」「数値で表した最大重量」という、抽象的な仕様に依存することが可能になり、
`nil` チェックを省略できるだけでなく、コード全体の見通しも良くなります。

ちょっとした工夫で、コードをシンプルにすることが可能な場合もあるので、
もしお困りの方がいらっしゃれば、是非お試しあれ。（もちろん、やりすぎは禁物です。）