LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-04

背景

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

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

やり方

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

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

数値型の場合

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

create_table :magical_boxes do |t|
  t.float min_weight
  t.float max_weight
end
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

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

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

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

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 を定義してみます。

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_weightnumerical_max_weight というメソッドに、仕様通りに隠蔽されます。

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

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2