Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rein: RailsのActiveRecordでDB制約やデータベースビューを使えるgem(README翻訳)

こんにちは、hachi8833です。今回はRailsウォッチでもご紹介したRein gemのREADME翻訳をお送りします。
多くの機能がPostgreSQL寄りなので、PostgreSQLで使うとさらに幸せになれるかもしれません。

概要

README末尾のMITライセンスおよびリポジトリのライセンスに基づき翻訳・公開いたします。

Rein: RailsのActiveRecordにDB制約を追加するgem(README翻訳)

データ完全性(data integrity)はよいものです。
値の制約は、アプリのレベルよりもデータベースのレベルでかける方が、データを正しく保つ方法としてより強力になります。

残念なことに、SQLを手書きせずにデータ完全性を実現しようとしても、ActiveRecordではそうしたサポートについて冷淡であり、許してすらくれません。Rein(発音はrainと同じ)は、データベース上のデータを正しく保つのに役立つさまざまなメソッドをActiveRecordのマイグレーションに追加するgemです。

ReinのDSLで使えるメソッドはすべて逆操作が可能なので、Railsのマイグレーションで可逆的な操作を利用できます。

クイックスタート

  • 1: gemをインストールします。
gem install rein
  • 2: マイグレーションに制約(constraint)を追加します。
class CreateAuthorsTable < ActiveRecord::Migration
  def change
    create_table :authors do |t|
      t.string :name, null: false
    end

    # authorには必ずnameがあること
    add_presence_constraint :authors, :name
  end
end

利用できる制約

外部キー制約

外部キー制約は、カラム内の値が別のテーブル内の行(row)の値を一致しなければならないことを指定します。

たとえば、「booksテーブルのauthor_idは、authorsテーブルのidにある値に限定したい」場合は、次のように書きます。

add_foreign_key_constraint :books, :authors

外部キー制約を追加しても、参照されるカラムにインデックスが自動で追加されるわけではありません。一般に、インデックスを追加することで外部キーのJOINを高速化できます。インデックスを作成するには、indexオプションを使います。

add_foreign_key_constraint :books, :authors, index: true

Reinは、テーブルに対応するカラム名を自動で推測します。明示的に指定したい場合は、referencedオプションやreferencingオプションを利用できます。

add_foreign_key_constraint :books, :authors, referencing: :author_id, referenced: :id

参照先の行のひとつが更新または削除されたときの動作も指定できます。

add_foreign_key_constraint :books, :authors, on_delete: :cascade, on_update: :cascade

DELETEやUPDATEで指定できる動作の全オプションを以下に示します。

  • no_action: 制約チェック時に、参照元の行がまだ存在している場合にはエラーを出力します。オプションを指定しない場合はデフォルトでこの動作になります。
  • cascade: 参照先の行が削除されたときに、参照元の行も同時に削除されなければならないことを指定します。
  • set_null: 参照先の行が削除されたときに、参照元のカラムにNULLを設定します。
  • set_default: 参照先の行が削除されたときに、参照元のカラムにデフォルト値を設定します。
  • restrict: 参照先の行の削除を禁止します。

外部キー制約を削除するには、次を使います。

remove_foreign_key_constraint :books, :authors

inclusion制約

inclusion制約は、カラムに設定できる値のリストを指定します。

たとえば、「stateカラムの値はavailableon_loanの2つのみを取れる」ようにするには、次のようにします。

add_inclusion_constraint :books, :state, in: %w[available on_loan]

inclusion制約を削除するには、次を使います。

remove_inclusion_constraint :books, :state

ifオプションも併用すると、次のように特定の条件を満たす場合にのみ制約をかけることもできます。

add_inclusion_constraint :books, :state,
  in: %w[available on_loan],
  if: "deleted_at IS NULL"

nameオプションで名前をカスタマイズすることもできます。

add_inclusion_constraint :books, :state,
  in: %w[available on_loan],
  name: "books_state_is_valid"

長さ制約

長さ制約は、文字列カラムの値が取れる長さの範囲を指定します。

たとえば、「call_numberの長さを1から255の間にする」には次のようにします。

add_length_constraint :books, :call_number,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 255

長さ制約の全オプションを以下に示します。

  • equal_to
  • not_equal_to
  • less_than
  • less_than_or_equal_to
  • greater_than
  • greater_than_or_equal_to

ifオプションも併用すると、次のように特定の条件を満たす場合にのみ制約をかけることもできます。

add_length_constraint :books, :call_number,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  if: "status = 'published'"

nameオプションで名前をカスタマイズすることもできます。

add_length_constraint :books, :call_number,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  name: "books_call_number_is_valid"

長さ制約を削除するには、次を使います。

remove_length_constraint :books, :call_number

数値制約

数値制約は、数値カラムが取れる値の範囲を指定します。

たとえば、「publication_monthの値は1から12の間だけを取れる」ようにするには、次のようにします。

add_numericality_constraint :books, :publication_month,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12

数値制約の全オプションを以下に示します。

  • equal_to
  • not_equal_to
  • less_than
  • less_than_or_equal_to
  • greater_than
  • greater_than_or_equal_to

ifオプションも併用すると、次のように特定の条件を満たす場合にのみ制約をかけることもできます。

add_numericality_constraint :books, :publication_month,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  if: "status = 'published'"

nameオプションで名前をカスタマイズすることもできます。

add_numericality_constraint :books, :publication_month,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  name: "books_publication_month_is_valid"

数値制約を削除するには次のようにします。

remove_numericality_constraint :books, :publication_month

presence制約

presence(存在)制約は、文字列カラムの値が空にならないよう指定します。

いわゆるNOT NULL制約は空文字列でも満たされますが、文字列に(NULLでない)何らかの値があることを保証するには、次のようにします。

add_presence_constraint :books, :title

特定の条件が満たされる場合にのみ制約をかけたい場合は、ifオプションを渡します。

add_presence_constraint :books, :isbn, if: "status = 'published'"

nameオプションで名前をカスタマイズすることもできます。

add_presence_constraint :books, :isbn, name: "books_isbn_is_valid"

presence制約を削除するには、次のようにします。

remove_presence_constraint :books, :title

NULL制約

NULL制約は、カラムにNULL値が含まれていないことを保証する制約です。これはカラムにNOT NULL制約を加えることと同じですが、条件を指定できる点が異なります。

たとえば、「本がon_loan(貸出中)の場合のみdue_dateが必ずあるようにする」には、次のようにします。

add_null_constraint :books, :due_date, if: "state = 'on_loan'"

NULL制約を削除するには、次のようにします。

remove_null_constraint :books, :due_date

データ型

列挙型

列挙型(enum)は、静的かつ順序の変わらない値のセットを表します。

create_enum_type :book_type, %w[paperback hardcover]

データベースから列挙型を削除するには、次のようにします。

drop_enum_type :book_type

ビュー

データベース・ビュー(以下単にビュー)は、通常のテーブルと同じように参照できる「名前付きクエリ(named query)」です。Reinを使うと、データベースでビューをサポートするActiveRecordモデルも作成できるようになります。

たとえば、「現在貸出可能な本のリストを返すavailable_booksというビューを定義する」には、次のようにします。

create_view :available_books, "SELECT * FROM books WHERE state = 'available'"

ビューをデータベースから削除するには、次のようにします。

drop_view :available_books

スキーマ

ひとつのデータベースには、名前付きスキーマ(複数可)を含めることができます。この名前スキーマにはテーブルが含まれることになります。データベースを複数のスキーマに分割して、複数のテーブルを論理的にグループ化すると便利な場合があります。

create_schema :archive

スキーマをデータベースから削除するには、次のようにします。

drop_schema :archive

簡単な図書館貸出アプリを用いて、データベースの値に制限を追加するマイグレーションの実例をいくつか見てみましょう。

class CreateAuthorsTable < ActiveRecord::Migration
  def change
    # The authors table contains all the authors of the books in the library.
    create_table :authors do |t|
      t.string :name, null: false
      t.timestamps, null: false
    end

    # 制約: authorには必ずnameがあること
    add_presence_constraint :authors, :name
  end
end

class CreateBooksTable < ActiveRecord::Migration
  def change
    # booksテーブルには図書館のすべての本や状態(貸出中・貸出可など)が収録されている
    create_table :books do |t|
      t.belongs_to :author, null: false
      t.string :title, null: false
      t.string :state, null: false
      t.integer :published_year, null: false
      t.integer :published_month, null: false
      t.date :due_date
      t.timestamps, null: false
    end

    # book 1冊につき、authorが1人いること
    # authorを1人削除すると、authorのbookもデータベースから自動削除されること
    add_foreign_key_constraint :books, :authors, on_delete: :cascade

    # book 1冊につき、空でないtitleが1つあること
    add_presence_constraint :books, :title

    # stateは"available"(貸出可)、"on_loan"(貸出中)、"on_hold"(保留)のいずれかだけを取ること
    add_inclusion_constraint :books, :state, in: %w[available on_loan on_hold]

    # 古典はこの図書館の対象外
    add_numericality_constraint :books, :published_year,
      greater_than_or_equal_to: 1980

    # 月は常に1〜12であること
    add_numericality_constraint :books, :published_month,
      greater_than_or_equal_to: 1,
      less_than_or_equal_to: 12

    # 貸出中の本1冊には、due_date(返却期限)が1つあること
    add_null_constraint :books, :due_date, if: "state = 'on_loan'"
  end
end

class CreateArchivedBooksTable < ActiveRecord::Migration
  def change
    # archiveスキーマは、(非公開の)書庫に関するデータをすべて含む
    # このスキーマは、一般公開用スキーマと別にしておきたい
    create_schema :archive

    # archive.booksテーブルは、非公開書庫にあるすべての本を含む
    create_table "archive.books" do |t|
      t.belongs_to :author, null: false
      t.string :title, null: false
    end

    # book 1冊につき、authorが必ず1人いること
    # このデータベースでは、著書があるauthorの削除を禁止すること
    add_foreign_key_constraint "archive.books", :authors, on_delete: :restrict

    # book 1冊につき、空でないtitleが1つあること
    add_presence_constraint "archive.books", :title
  end
end

ライセンス

ReinはMIT Licenseに基いて公開しています。

関連記事

Rails開発者のためのPostgreSQLの便利技(翻訳)

週刊Railsウォッチ(20170331)PostgreSQLの制約機能を使えるRein gemはビューも使えるほか

PostgreSQLを使う理由(更新5年目)(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。