Javaエンジニア、Ruby on Railsでアプリを作る

整理券アプリを作成中です

エンジニアが使ってみたい英語表現5個 : React Tutorial編

Railsも5.2がリリースされ、フロントエンド周りがいい感じになってきたと聞いたので、思い立ってReactを勉強し始めました。"React Tutorial"で検索した時に、一番上に出てきたのが英語のチュートリアルだったので、思いつきで英語で読むことにしました。

読んでる時に、機会があればいつか使ってみたい英語表現があったので書き留めておきます。

読んだのはこちら↓ reactjs.org

To develop a muscle memory : 体で覚える

It’s fine to copy and paste code as you’re following along the tutorial, but we recommend to type it by hand. This will help you develop a muscle memory and a stronger understanding.

チュートリアルを進めるなかで、コードをコピー&ペーストしてもかまいませんが、1文字ずつ手打ちすることをおすすめします。そうすることにより、手で覚え、よりよく理解することができます。

ひとこと

技術的な内容を読んでいるときに”マッスル”と出てきてびっくりしました。書いてある内容もマッチョ。

習うより慣れろですね。コードを書いて覚えろと新人に言う時に使えますよ!

To have familiarity with 〜 : 〜の知識がある、〜に精通している

We’ll assume that you have some familiarity with HTML and JavaScript, but you should be able to follow along even if you’re coming from a different programming language. 

このチュートリアルはある程度のHTMLとJavaScriptの知識のある読者を想定していますが、他のプログラム言語を学んだ方でも読み進められるようになっています。

ひとこと

“HTMLの知識がある”の”知識”は”knowledge”くらいしか知らなかったですが、”familiarity”もこの文脈で使えるので覚えておこうと思いました。

To get one’s feet wet : 手始めに

Just to get our feet wet, let’s try passing some data from our Board component to our Square component.

手始めに、BoardコンポーネントからSquareコンポーネントに何か値を渡してみましょう。

ひとこと

【語源】海で泳ぐのを怖がる子どもに向かって、とにかく「足だけでもいいから水につけてみなさい」と助言するところから。

"[get] (one's|my|...の意味・用例|英辞郎 on the WEB:アルク

だそうです。

 To have a grasp : 把握する

Nice work! We hope you now feel like you have a decent grasp on how React works.

よくできました!Reactの動作をきちんと把握できたと感じていただければ幸いです

ひとこと

“理解する(把握する)”にあたる英単語も”understand”くらいしか思い当たらないので、1つ表現が増えました。"grasp"は"手でぎゅっとつかむ"という意味で、まさしく"把握する"という日本語訳がぴったりです。

To get stuck : 行き詰まる

Help, I’m Stuck!

If you get stuck, check out the community support resources.In particular, Reactiflux Chat is a great way to get help quickly.

行き詰まった時は

もし行き詰まってしまったら、技術者のコミュニティサイトを見てみましょう。特に、Reactiflux Chatはすぐに回答を得ることができる素晴らしいコミュニティです。

ひとこと

行き詰まった時には助けてあげるよと言えるエンジニアになりたいです。(自分のことでいっぱいいっぱい)

おわりに

上の日本語訳はブログ主が訳したものですが、日本語に訳したサイトがいくつかあるようです。"React チュートリアル"で検索すると出てくるので参考にしてください。

Reactを勉強しようとして書籍をあたりましたが、レビューがあまりよくなかったので公式のチュートリアルをやりました。結果的に概要は掴めたのでこの方法でよかったと思います。

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

ITエンジニアが覚えておきたい英語動詞30 [ 板垣政樹 ]
価格:1944円(税込、送料無料) (2018/9/2時点)


Railsでカスタムバリデータとvalidate_withで2つの日時の前後、最小間隔、最大間隔のチェック

開始時間と終了時間を入力するアプリで、入力の前後関係と最小間隔、最大間隔をチェックしたい場合があります。 チェックを実装するには、Railsではカスタムバリデータとカスタムメソッドの方法があります。開始時間、終了時間をもつアプリは多いので、今回は汎用的に使えるカスタムバリデータを実装します。

こちらの記事で作成した会議室予約アプリを使います。

yucatio.hatenablog.com

入力画面はこんな感じです。

f:id:yucatio:20180826224710p:plain

仕様

  • 開始時間は終了時間より後
  • 開始時間と終了時間の間は15分以上
  • 開始時間と終了時間の間は6時間以下

実行イメージ

開始時間が終了時間より後

f:id:yucatio:20180826224921p:plain

間隔が狭すぎる

f:id:yucatio:20180826225451p:plain

正常範囲内

f:id:yucatio:20180826230416p:plain

間隔が広すぎる

f:id:yucatio:20180826225804p:plain

TimeRangeValidatorの仕様

TimeRangeValidatorは、今回作成するカスタムValidatorで、2つの日付の前後、最小間隔、最大間隔のチェックをします。

エラーの表示で"15分以上にしてください"や"6時間以下にしてください"と、数字と単位を表示したいので、数字と単位を別々に受け取ります。

  • 開始時間、終了時間の属性名をシンボルで受け取る
  • 最小の間隔(整数)と時間単位(:second :minute :hour :day のうちのいずれか)を受け取る
  • 最大の間隔(整数)と時間単位(:second :minute :hour :day のうちのいずれか)を受け取る
  • 開始時間より終了時間が前ならばエラー。開始時間と終了時間が同じ場合もエラー
  • 開始時間と終了時間の間が、最小間隔より小さければ、エラー
  • 開始時間と終了時間の間が、最大間隔より大きければ、エラー

エラーメッセージファイルと言語ファイル

先にエラーメッセージを記述します。

config/locales/app_errors_ja.yml

ja:
  errors:
    messages:
      end_time_before_start_time: は%{end_attribute}よりも前に設定してください
      too_long_time: と%{end_attribute}の間は%{duration}以下にしてください
      too_short_time: と%{end_attribute}の間は%{duration}以上にしてください

%{end_attribute}には終了時間の属性名、%{duration}には"1日"や"5時間"、"10分" などが入るようにします。

"x日" "x時間" "x分" "x秒"はja.ymlに記載されています。 ここからダウンロードしました。

rails-i18n/ja.yml at master · svenfuchs/rails-i18n · GitHub

x_hoursが記載されていなかったので追加します。

config/locales/ja.yml

ja:
  # 略
  datetime:
    distance_in_words:
      # 略
      x_days:
        one: 1日
        other: "%{count}日"
      x_hours:
        one: 1時間
        other: "%{count}時間"
      x_minutes:
        one: 1分
        other: "%{count}分"
      x_months:
        one: 1ヶ月
        other: "%{count}ヶ月"
      x_seconds:
        one: 1秒
        other: "%{count}秒"
  # 後略

Validatorの実装

appディレクトリの下に、validatorsディレクトリを作成し、time_range_validator.rbファイルを作成します。

class TimeRangeValidator < ActiveModel::Validator
  ACCEPTABLE_TIME_UNIT = %i[second minute hour day]

  def validate(record)
    start_time = record.send(options[:start_time])
    end_time = record.send(options[:end_time])
    min_number = options[:min_number]
    min_time_unit = ACCEPTABLE_TIME_UNIT.include?(options[:min_time_unit]) ? options[:min_time_unit] : :day
    max_number = options[:max_number]
    max_time_unit = ACCEPTABLE_TIME_UNIT.include?(options[:max_time_unit]) ? options[:max_time_unit] : :day

    return unless start_time.present? && end_time.present?

    if start_time >= end_time
      record.errors.add(
          options[:start_time],
          :end_time_before_start_time,
          end_attribute: record.class.human_attribute_name(options[:end_time]))
    elsif min_number && start_time + min_number.send(min_time_unit) > end_time
      record.errors.add(
          options[:start_time],
          :too_short_time,
          end_attribute: record.class.human_attribute_name(options[:end_time]),
          count: min_number,
          duration: I18n.t("datetime.distance_in_words.x_#{min_time_unit}s", count: min_number)
      )
    elsif max_number && start_time + max_number.send(max_time_unit) < end_time
      record.errors.add(
          options[:start_time],
          :too_long_time,
          end_attribute: record.class.human_attribute_name(options[:end_time]),
          duration: I18n.t("datetime.distance_in_words.x_#{max_time_unit}s", count: max_number)
      )
    end
  end
end

最初に、ACCEPTABLE_TIME_UNIT = %i[second minute hour day] で指定可能な時間単位の配列を宣言しています。%i[]でシンボルの配列を作成できます。

バリデータクラスに渡された引数は、options[ハッシュのキー]で取得することができます。

全体的にsend()メソッドを多用しています。send()メソッドは、レシーバに対して、引数に与えられたメソッドを呼び出すメソッドです。min_number=5, min_time_unit=:dayのとき、min_number.send(min_time_unit) は、5.dayを呼び出したのと同じになります。 send()メソッドは、メソッド名が変数で与えられた時に有用です。

エラーの追加は、record.errors.add()ですることができます。

start_time + min_number.send(min_time_unit) > end_timeという条件は、開始時間と終了時間の間隔が最小間隔より短いか判別しています。end_time - start_time < min_number.send(min_time_unit)と書いた方がわかりやすい気がしますが、:monthへの時間単位の拡張を考え、このような表現にしています。(monthを考慮に入れると、2つの条件式は同じではありません)

呼び出し側の実装

app/models/meeting.rb

class Meeting < ApplicationRecord
  belongs_to :meeting_room

  # 略  

  validates_with TimeRangeValidator,
                 start_time: :start_time, end_time: :end_time,
                 min_number: 15, min_time_unit: :minute, max_number: 6, max_time_unit: :hour

  # 略  

end

完成

以上で2つの日付の前後、最小間隔、最大間隔のチェックするカスタムvalidatorの完成です。

"15分"や"6時間"など、時間単位を動的に表示したかったので、数字部分と単位部分を分けて渡すことにより実現しました。Duration を使用して、min: 15.minutes, max: 6.hourのように書いて、"15分"や"6時間"と表示できればよかったのですが、力及ばず。 time_ago_in_words (ActionView::Helpers::DateHelper) は期待する挙動と違いますし。

また、引数に:monthを取れればよかったのですが、日数が月によって違うことによる影響を調査しきれなかったので今回は実装しませんでした。

その他色々つっこみどころはあると思いますが、とりあえず自分が必要な分は実装できたので、よしとします。

環境

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/26時点)


Validationがごちゃごちゃしてきたら、分類して整理する : 整理編

前回の記事の続きです。

★前回の記事

yucatio.hatenablog.com

前回の記事では、validationの軸として

  • 単属性/属性間
  • モデル値/DB値/入力値
  • マスタ情報に値が存在するかのチェックが必要/不要、さらにマスタ情報の値を用いてvalidationが必要/不要
  • Validationするために、DBの同じテーブルの他のレコードの情報が必要/不要
  • Validationするために、現在時刻が必要/不要

を紹介しました。 これらの組み合わせは、168通り(2 x 7 x 3 x 2 x 2通り)あるのですが、ここでは大きく4つのステップに分類します。

目次

Step 分類表

単属性/属性間 モデル値/DB値/入力値 マスタ情報 他レコード 現在時刻
Step 1 単属性 モデル値、入力値 不要 不要 どちらでも
Step 2 単属性、属性間 モデル値、入力値 どちらでも 不要 どちらでも
Step 3 単属性、属性間 モデル値、DB値、入力値 どちらでも 不要 どちらでも
Step 4 単属性、属性間 モデル値、DB値、入力値 どちらでも 必要 どちらでも

Step 1 : 単属性のvalidation

登録しようとする情報の、それぞれの項目だけを対象にvalidationを行います。必須項目や文字の長さ、フォーマット、数値が範囲内かをチェックします。

Step 2 : 属性間のvalidation

登録しようとする情報の、属性間でのvalidationを行います。時間の前後関係や、数値の大小、他方が存在したらもう片方が存在しなければならない、片方の値によってもう片方の正常となる範囲が違う場合などをチェックします。

マスタ情報が必要となるvalidationもここで行います。ただ、マスタ情報の取得はコストがかかる場合が多いため、値の変更がされている時に行った方がよい場合が多いです。その場合はStep 3で行います。

Step 3 : DB値を用いたvalidation

DB値(変更前の値)が必要なvalidationを行います。状態遷移が妥当かや、値が変更されているかどうかをチェックします。

値が変更された時だけ行いたいチェックもここに記述します。

Step 4 : 同テーブルの別レコードとの関係が必要なvalidation

すでに登録されている他のモデルとの関連性のvalidationを行います。期間の重複や全体の個数をチェックします。

Stepごとに分けてvalidationを記述する

Railsで書くならこんな感じです。会議室予約アプリを例にします。仕様は以前の記事を参照してください。

yucatio.hatenablog.com

class Meeting < ApplicationRecord
  belongs_to :meeting_room

  # 単属性
  validates :subject, length: {maximum: 32}, presence: true
  validates :start_time, presence: true
  validates :end_time, presence: true

  # 属性間
  # start_timeはend_timeより前であること
  validate :start_time_should_be_before_end_time

  # DB値
  # 新規登録時、start_timeは現在時刻より後であること start_timeが変更された時、start_timeは現在時刻より後であること
  validate :start_time_should_be_after_current, if: :new_or_start_time_changed
  # DBのstart_timeが現在より前の場合は、start_timeを変更できない
  validate :start_time_cannot_be_changed
  # DBののstart_timeが現在より前の場合は、end_timeを変更できない
  validate :end_time_cannot_be_changed

  # 他レコードとの関連性
  # 同一の会議室に属するmeetingの期間は、重複してはならない
  validate :meeting_time_should_not_overlap, if: :new_or_start_or_end_time_changed

  # 略

end

この例だとvalidationが少ないのであまりメリットは感じられないですが、項目が多くなった時のことを想像してみると、分類してあった方が目的の情報を探しやすく、追加もしやすくなります。

Stepごとに、もしエラーがあればvalidationを中断する

以前に実装したJavaのプロジェクトでは以下のようにしました。

Step 1 を実行する
 ↓
この時点でvalidationエラーがあれば、validationを中断する
 ↓
Step 2 を実行する
 ↓
この時点でvalidationエラーがあれば、validationを中断する
 ↓
Step 3 を実行する
 ↓
この時点でvalidationエラーがあれば、validationを中断する
 ↓
Step 4 を実行する

見ての通り、Stepごとにエラーがあればvalidationを中断していました。 ユーザからすれば、一度に全てのエラーが表示されず、少し不親切な設計ではありますが、実装の面では例えばStep2の段階で数値範囲やformatは正しいということがわかっているので、実装の負担が軽くなります。

あとがき

validationが、数値の範囲とか文字列の長さとかだけだと楽なのですが、規模が大きいシステムになるとそうも言ってられないですね。

Railsの場合Stepごとにエラーがあればvalidationを中断するといった機能はないので、別の方法が必要そうです。いい方法があれば教えてください。

あと、DB値が必要かどうかで区切ったけど、マスタ情報が必要かで区切った方が一般的にはいいかもしれないです。

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/21時点)


Validationがごちゃごちゃしてきたら、分類して整理する : 分類編

アプリを作成・改修していくうちに、モデルの属性が多くなり、validationも複雑になってきます。既存の項目と同時に設定できない項目が追加されたり、また、システムが大きくなってくると、外部のAPIと通信が必要といったケースもあります。

新しく追加した項目のvalidationをどんどん後ろに追加していくと、統一性がなく、新しく入ってきたメンバーにとって読みにくいコードになってしまいます。

そこで、validationを分類して整理することによって、コードを見やすく保ちます。

このページでは、どのような分類軸があるのかをみていきます。

なお、ユーザが情報を登録・編集・削除できて、一覧・詳細で入力内容を確認できるようなアプリを想定して話を進めます。金融システムやゲームは対象外です。

Validationの軸

単属性/属性間

単属性…モデルの属性うち、1属性だけを対象にvalidationする場合

属性間…モデルの属性のうち、2つ以上の属性を対象としてvalidationする場合

単属性のvalidationは、多くのwebフレームワークであらかじめ便利な方法(ヘルパーやアノテーション、設定ファイル)が用意されています。

属性あいのvalidationはほとんどのwebアプリケーションでは便利な方法が用意されていません。そのため、多くの場合独自でコードを書く必要があります。

  • 単属性のvalidationの例

    • 氏名は1文字以上、32文字以下
    • 郵便番号は半角数字、7桁
    • パスワードに使える文字は半角英数字と:~@"'%+-*/;#&<>^=[]{}、8文字以上32文字以下
  • 属性間のvalidationの例

    • 開始時間は終了時間より前でなければいけない
    • 自宅電話番号または携帯電話番号は必須
    • ユーザの年齢が18歳未満の場合、保護者の氏名が必要

モデル値/DB値/入力値

モデル値…これからDBに保存しようとしている値

DB値…現在DBに登録されている値

入力値…ユーザが入力した値(サーバに送信されてきた値)

f:id:yucatio:20180823172417p:plain

モデル値、DB値、入力値のうちどの組み合わせを使うかで全部で7パターンありますが、多いのは、モデル値のみか、モデル値とDB値を使う場合です。

  • モデル値のみ必要な例

    • イベント名は2文字以上32文字以下
    • 価格は50円以上、50万円以下である必要がある
    • ユーザが法人の場合には、会社名が必要
  • DB値とモデル値が必要な例

    • タスク管理アプリにて、タスクのステータスを着手中から未着手には変更できない
    • タスク管理アプリにて、タスクのステータスが完了の場合には、担当者を変更できない
    • イベント登録アプリで、イベント開始時間を過ぎている場合は、イベントの開始時間を変更できない
  • DB値とモデル値と入力値が必要な例

    • タスクのステータスを着手中から保留に変更するには、入力値に変更理由が必要

DBの値は、値が変更されているかや状態遷移が妥当かチェックする場合に使用されます。 Railsの場合は、#{attr_name}_in_databaseでDB値の取得、#{attr_name}_before_type_castで型変換前の値を取得することができます。

yucatio.hatenablog.com

マスタ情報に値が存在するかのチェックが必要/不要、さらにマスタ情報の値を用いてvalidationが必要/不要

ユーザがIDを入力して、そのIDがマスタテーブルに存在するかのチェックです。さらにそのIDに紐づいている情報を元にvalidationが必要なケースがあります。 マスタ情報はDBだったりAPIだったりします。マスタ情報の更新頻度が低い場合はキャッシュの導入など検討します。

  • マスタ情報に値が存在するかチェックが必要な例

    • 郵便番号は実在するものとする。郵便番号が正しいかどうかは外部のAPIを利用し、レスポンスコードで判別する
    • 配送方法はIDで指定する。配送方法マスタテーブルにそのIDが存在すること
  • マスタ情報に値が存在するかチェックが必要かつ、マスタ情報の値を用いてvalidationが必要な例

    • フリマアプリにて、入力された商品カテゴリIDがカテゴリマスタに存在する必要がある。カテゴリが存在するかはAPI問い合わせするものとし、レスポンスに{size: "required"}が含まれる場合は、サイズの入力が必要
    • クーポンコードは存在するものであること。クーポンが存在するかはAPIに問い合わせるものとし、レスポンスに対象の商品IDが記載されていること

Validationするために、DBの同じテーブルの別レコードの情報が必要/不要

uniqなどは、登録しようとするレコードだけでなく、登録しようとするレコードと同じテーブルにあるデータが必要になります。そのため、DBへの問い合わせが必要になります。 期間の重複チェックなどはDBへ適切なindexをはる必要があります。

  • 必要となる例
    • 登録に使用するメールアドレスは一意でなければいけない
    • 会議室予約アプリにて、1つの会議室で、会議の予約時間が重複してはいけない
    • フリマアプリで、1ユーザが出品できる件数はひと月あたり100件まで

DBへの問い合わせを必要な時にだけするにはこちらを参考にしてください。

yucatio.hatenablog.com

Validationするために、現在時刻が必要/不要

現在時刻の要不要でvalidationの実装の仕方や実装の難易度はあまり変わらないのですが、テストデータを用意する際に注意が必要となります。

  • 必要となる例
    • 会議室予約アプリにて、開始時間は現在時刻より後でなければいけない
    • 生年月日として入力できる値は、現在から100年前から、18年前まで

具体例

ここではどのvalidationがどのように分類されるか具体例でみていきます。

validation 単属性/属性間 モデル値/DB値/入力値 マスタ情報 DB他レコード 現在時刻
氏名は1文字以上、32文字以下 単属性 モデル値 不要 不要 不要
開始時間は終了時間より前でなければいけない 属性間 モデル値 不要 不要 不要
タスクのステータスが完了の場合には、担当者を変更できない 属性間 モデル値、DB値 不要 不要 不要
郵便番号は実在するものとする。郵便番号が正しいかどうかは外部のAPIを利用し、レスポンスコードで判別する 単属性 モデル値 必要 不要 不要
会議室予約アプリにて、1つの会議室で、会議の予約時間が重複してはいけない 属性間 モデル値 不要 必要 不要
生年月日として入力できる値は、現在から100年前から、18年前まで 単属性 モデル値 不要 不要 必要
フリマアプリで、1ユーザが出品できる件数はひと月あたり100件まで - モデル値 不要 必要 必要

続く

整理編に続く

yucatio.hatenablog.com

おわりに

少し前に携わったプロジェクトで、多くのvalidationを既存のアプリに追加しました。既存のアプリはvalidationが順不同に書かれており、バグも残っていました。既存の仕様と新たに追加する仕様を整理する中で、validationの分類をするとコードが書きやすくなることに気づきました。それをまとめたのがこのページです。

ここに挙げたvalidationの分類は、ブログ主が見たことあるvalidationからとったものなので、ほかにもっと分類の軸があるかも知れません。開発するシステムに合わせてアレンジしてみてください。

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/21時点)


RubyMineの正規表現チェックがString#matchと挙動が違う件

RubyMineには文字列が正規表現にマッチするか調べる機能があります。

1. 正規表現にカーソルを移動し、 電球マークまたはoption+エンターキー(windowsの場合はalt+エンターキ)を押す。

f:id:yucatio:20180817230427p:plain

2. Check RegExpを選択する

f:id:yucatio:20180817230610p:plain

3. Sample:にテストしたい文字列を入力します

f:id:yucatio:20180817231821p:plain

4. マッチする場合はMatches!と表示され、緑色の背景になります

f:id:yucatio:20180817231201p:plain

マッチしない場合はNo matchと表示され、赤色の背景になります

f:id:yucatio:20180817231413p:plain

部分マッチに対応していない

とても良い機能を見つけたと思っていたのですが、\.txt$sample.txtにマッチしません。

f:id:yucatio:20180817232019p:plain

もちろんrubyのプログラムを実行すると、マッチします。

# テキストファイルの正規表現
text_file_regex = /\.txt$/

puts text_file_regex =~ 'sample.txt' ? 'テキストファイルです' : 'テキストファイルではありません'
# => テキストファイルです

色々動作を確かめたところ、正規表現にマッチする部分以外が文字列に含まれると'No match'が表示されるようです。

f:id:yucatio:20180817232212p:plain

Check RegExpは入力全体に正規表現がマッチするか調べる機能

この件に関しては、すでにIssueがあがってました。

https://youtrack.jetbrains.com/issue/RUBY-15550

コメントを読むに、このRubyMineのCheck RegExpは、JavaのString#matches (もしくはPattern#matches)と同様の結果を返すようになっているようです。

JavaのString#matchesは、こちらの記事にあるように、

Javaのmatches()が期待する動作にならない落とし穴 | 読書とプログラミングを中心とした覚書ブログ

matches()では正規表現が入力全体に完全にマッチするか確認するという動作になります。

とのことです。

RubyMineはJavaIDEであるIntellijを元に作られていると思われますが、RubyのmatchとJavaのmatchesでの挙動の差異によりRuby開発者にとって思わぬ挙動となっています。

あとがき

とても良い機能を見つけたと思ったのですが、rubyのmatchと異なる結果を返すので、Check RegExpは個人的には「使えない」と感じました。

Issueは4年前のもので、未だに対応していないということは、今後もこのままなのでしょう。

環境

期間が重複しているかを判定する条件式の導出方法

開始時間(start_time)と終了時間(end_time)を持つテーブル(例: schedule)で、あるレコードXと時間が重複したデータを抽出するSQLは、

SELECT *
FROM schedule
WHERE
    start_time < レコードXのend_time
    AND
    end_time > レコードXのstart_time

です。 しかしstart_timeとend_timeの関係性を考えるのに毎回頭が混乱してしまうので、今回はこのSQLの導出方法を書いておきます。

ちなみに、このSQLは、 一方のend_timeが他方のstart_timeと同じときには重複してい ないと判定する場合です。 例えば、[start_time, end_time] = [14: 00, 15:00][start_time, end_time] = [15: 00, 16:00]の2つのレコードは重複として判定しません。 [start_time, end_time] = [14: 00, 15:00][start_time, end_time] = [15: 00, 16:00]を重複と判定する場合は、不等号にイコール記号を加えてください。

重なりのパターン

レコードXとほかのレコードとの重なりのパターンは以下のレコードABCDEFの6パターンです。

f:id:yucatio:20180816175358p:plain

このうち、レコードXと重複するのは、レコードB,C,D,E、重複しないのがレコードA,Fです。

レコードB,C,D,Eに当てはまり、レコードA,Fに当てはまらない条件を導き出せればよいのですが、自分にはすぐには導き出せませんでした。

重ならない条件

そこで、一旦重ならないパターンに焦点を当てます。

重ならないパターンはレコードA,Fの2つで、レコードAは、

end_time <= レコードXのstart_time

となっているもの、

もう片方(レコードF)は、

レコードXのend_time <= start_time

です。(不等式にイコールを、含むかどうかは仕様によります。今回は、一方のend_timeが他方のstart_timeと同じときには重複してい ないと判定するものとします)

このどちらかの条件を満たしたすべてのレコードがレコードXと重複しません。式で書くと以下になります。

(end_time <= レコードXのsrart_time) OR (レコードXのend_time <= start_time)

重なる条件

重ならない条件式ができあがったので、これの否定をとれば、重なる条件式になります。

NOT((end_time <= レコードXのstart_time) OR (レコードXのend_time <= start_time))

ド・モルガンの法則により、

NOT(end_time <= レコードXのsrart_time) AND NOT(レコードXのend_time <= start_time)
↓
(end_time > レコードXのsrart_time) AND (レコードXのend_time > start_time)

となり、不等式が導出できました。

こちらもどうぞ

yucatio.hatenablog.com


Railsでvalidation中にDBアクセスする場合は、will_save_change_to_#{attr_name}?を使用する

Webアプリでvalidationを書いていると、DBに問い合わせをしなければいけない時があるのですが、その時に、値が変更されているかチェックしてからDB問い合わせした方がよいよというお話です。値が変更されているかは、Railsではwill_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

値が変更されているか調べてからvaldiateする

前回に引き続き、会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

★前回の記事

yucatio.hatenablog.com

yucatio.hatenablog.com

仕様

  • 会議予約(前回実装済み)
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • end_timeはstart_timeより後であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること
    • 保存済みのstart_timeが現在より前の場合は、start_timeを変更できない
    • 保存済みのstart_timeが現在より前の場合は、end_timeを変更できない

これに、

  • 会議予約
    • 同一の会議室に属するmeetingの期間は、重複してはならない。つまり、ある時刻に1つの会議室で開催されている会議は最大で1つである。

というvalidationを追加します。

コード

仕様を満たすコード
class Meeting < ApplicationRecord
  # 略

  validate :meeting_time_should_not_overlap

  private
  # 略

  def meeting_time_should_not_overlap
    return unless start_time && end_time

    if Meeting
           .where(meeting_room_id: meeting_room.id)
           .where('start_time < ?', end_time)
           .where('end_time > ?', start_time)
           .where.not(id: id).exists?
      errors.add(:base, '他の会議と重複しています。')
    end
  end
end

Meeting.where(meeting_room_id: meeting_room.id).where('start_time < ?', end_time).where('end_time > ?', start_time)で登録しようとする会議予約と同じ会議室の中で、開始終了時間と重複するレコードを検索します。直感的にわかりづらい条件ですが、これで検索できます。

yucatio.hatenablog.com

続くwhere.not(id: id)で、更新対象のレコードを除外しています。除外しないと対象レコードも検索に引っかかってきてしまうためです。最後にexsists?をつけて、存在するかどうかだけを確認しています。

生成されるSQLは以下のようになります。

SELECT  1 AS one 
FROM "meetings" 
WHERE "meetings"."meeting_room_id" = ?
    AND (start_time < '2018-08-10 12:00:00')
    AND (end_time > '2018-08-10 11:00:00')
    AND "meetings"."id" != ? 
LIMIT ?
[["meeting_room_id", 1], ["id", 3], ["LIMIT", 1]]

このコードで問題なく動くのですが、ひとつ気になるのは、このSQLは、validationのたびに実行されるということです。例えば、subjectしか更新しないようなリクエストでも毎回重複チェックのためのSQLが実行されます。

一般的にSQLの実行はコストが高いため、必要なとき以外は実行しないようにコードを書き換えます。

値が変更された場合のみDBアクセスをするコード

validationが必要なのは、登録時と開始時間または終了時間が変更されたときです。

class Meeting < ApplicationRecord
  # 略

  validate :meeting_time_should_not_overlap, if: :new_or_start_or_end_time_changed

  private
  # 略

  def new_or_start_or_end_time_changed
    new_record? || will_save_change_to_start_time? || will_save_change_to_end_time?
  end

  def meeting_time_should_not_overlap
    return unless start_time && end_time

    if Meeting
           .where(meeting_room_id: meeting_room.id)
           .where('start_time < ?', end_time)
           .where('end_time > ?', start_time)
           .where.not(id: id).exists?
      errors.add(:base, '他の会議と重複しています。')
    end
  end
end

new_record?(登録時)、will_save_change_to_start_time?(start_timeが変更されているとき)、そしてwill_save_change_to_end_time?(end_timeが変更されているとき)にのみSQLを実行するように変更できました。

実行結果

現在の予約状況

f:id:yucatio:20180811230448p:plain

他の会議室101の会議時間を重複する会議は登録できない↓

f:id:yucatio:20180811231217p:plain

他の会議室101の会議と重複しない会議は登録できる↓

f:id:yucatio:20180811231627p:plain

他の会議室101の会議時間を重複する日時に変更できない↓

f:id:yucatio:20180811232313p:plain

他の会議室101の会議と重複しない会議は登録できる↓

f:id:yucatio:20180811233203p:plain

日時が変更されない(subjectのみ変更)場合は、重複チェックSQLが実行されない↓

f:id:yucatio:20180811233935p:plain

環境

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/9時点)