Web備忘録

雑多に書いています

Ruby on Railsガイドを通読してまとめる Part.1

なかなかボリューミーなことで有名なRuby on Railsガイド。

railsguides.jp

実務未経験、Rails触り初めて4ヶ月の人間が気になった所だけ抜き出ししてまとめます。 基本的に、「理解できなかった所」や「大事だなと思った所」を中心にしてまとめようと思います。

はじめに

Rails をはじめよう

ネストさせたモデルに値を送る(7.2)

このコードの1行目が初見だったので掲載。

<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

大まかな挙動はわかるけど、体系的にはどう動くのかなって思ってたらスタックオーバーフローで質問がありました。

Ruby on Rails Form For with two parameters - Stack Overflow

ネストさせたモデルに値を送りたいときの書き方のようですね。(実際の挙動見てないので、導入するときは要確認) 結構色々なところで頻出してそうなのに初対面だったのは驚き。@articleはArticle.find(params[:id])等してキャッチしてるのが前提です。

Basic認証(9.1)

検証環境のサーバー等で制限をかけたい場合によく使われる基本的な認証の1つ、BASIC認証Railsでの実装方法は以下の通り。

class ArticlesController < ApplicationController
 
  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
 
  def index
    @articles = Article.all
  end
 
  # (以下省略)

http_basic_authenticate_withメソッドにこのように引数を与えてあげれば、「Articleコントローラーのindexとshowアクション以外でBasic認証必要」という処理になります。

Qiitaに、アプリケーション全体に設定してる方がいました。環境変数でnameとpasswordを決めておいて、ソースコードで見れないようにしてますね。こっちのほうがより安全そうです。

RailsでBasic認証の導入 〜完全版〜 - Qiita

モデル

Active Record の基礎

命名ルール(2.1)

モデル名が単数形、テーブル名が複数形という慣習があるけど、英単語によってはただ「s」をつければ複数形になるわけではないので注意が必要。

モデル名→ テーブル名
Mouse → mice
Person → people

この挙動はActiveSupportによって、Stringが拡張されて#pluralizeメソッドを貰うことで保証しているようです。

[1] pry(main)> require 'active_support'
=> true
[2] pry(main)> require 'active_support/core_ext'
=> true
[3] pry(main)> 'cat'.pluralize
=> "cats"
[4] pry(main)> 'person'.pluralize
=> "people"

(require 'active_support' だけじゃNoMethodError出たので、'active_support/core_ext'もrequireしました。参考記事:Qiita:https://qiita.com/seri_k/items/4818af527bd0e94cc860

スキーマのルール(2.2)

テーブルのカラム名について、created_atなどは予約語なので独自に定めるべきではない。他の予約語として、以下のものがある。

  • 楽観的ロックのための lock_versionカラム。
  • 単一継承テーブルのための typeカラム。
  • ポリモーフィック関連のための「関連付け名_typeカラム」
  • 親が持つ子の数をキャッシュしておくための「テーブル名_countカラム」

これらはRailsルールがあるので、意図せずカラム名にしないよう注意。

命名ルールを上書きする(4)

以下のように記述することで、命名ルールを上書きできる。

class Product < ApplicationRecord
  self.table_name = "PRODUCT"
end
class Product < ApplicationRecord
  self.primary_key = "product_id"
end

Active Record マイグレーション

マイグレーションの概要(1)

マイグレーションがエラーを吐いたさい、全てがロールバックされるとは限らないので注意が必要。

(引用)スキーマ変更のステートメントを利用できるトランザクションがデータベースでサポートされている場合、マイグレーショントランザクションの内側にラップされて実行されます。これらがデータベースでサポートされていない場合は、マイグレーション中に一部が失敗した場合にロールバックされません。その場合は、変更を手動でロールバックする必要があります。

ん、これって…どこかで聞いたような…

ワイや!

スキーマ変更(カラム追加等)のDDL文がエラー吐いたらこうなるのはやはり一部RDBMSでは仕様のようです。 おとなしく手動でロールバックしましょう!

テーブルを作成する(3.1)

テーブルを作るさい:commentオプションというものがあるらしい。初めて見た。 db/schema.rbを直接見るか、DB用のGUIツールを使えば確認できるそうです。 使い方としては、「nameってカラム名だけど何のことだっけ?」みたいなものに、注釈を付けておける、といった効果がある。

案外使いやすそう?

class CreatePatients < ActiveRecord::Migration[5.0]
  def change
    create_table :patients do |t|
      t.string :name, comment: "患者のニックネーム"
      t.string :first_name, comment: "患者の下の名前"
      t.string :last_name, comment: "患者の名字"

      t.timestamps
    end
  end
end

参考記事: [Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

テーブル結合を作成する(3.2)

中間テーブルをサクッと作りたいあなたへ。

create_join_table :products, :categories

productテーブルとcategoryテーブルの中間テーブル(外部キー保証)が出来ます。 インデックスや、独自のテーブル名を設定することも可能。詳細はRailsガイドへどうぞ。

テーブルを変更する(3.3)

完全に知らなかったので全て引用してます。

既存のテーブルを変更するchange_tableは、create_tableとよく似ています。基本的にはcreate_tableと同じ要領で使いますが、ブロックに対してyieldされるオブジェクトではいくつかのテクニックが利用できます。たとえば次のようになります。

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

上のマイグレーションではdescriptionとnameカラムが削除され、stringカラムであるpart_numberが作成されてインデックスがそこに追加されます。そして最後にupccodeカラムをリネームしています。

ただ、ここまで一気にテーブル構造変えるのは保守性落ちそうですね。 作り直しがどうしても出来ない場合の策か。

データベースをリセットする(4.3)

bin/rails db:resetは一度テーブルをドロップしたあと、db/schema.rbに従って再びテーブルを作成しているだけで、マイグレーションをもう一度実行しているわけではないらしい。知らなかった。(db/schema.rbをスキーマダンプというらしい。後で説明がある)

既存のマイグレーションを変更する(5)

テーブルのカラム名などを変更したいときに、既存のマイグレーションファイルに手を加えて再実行するのはNG。コミットしてマージされてしまったマイグレーションファイルに対する変更をしてしまうとチームメンバーに影響がある。

新しいマイグレーションファイルを作成して対応しましょう。

スキーマダンプの種類(6.2)

db/schema.rbとして保存されているものを「スキーマダンプ」と呼ぶ。これは現在のDBの状態をダンプ(出力)したもの。 設定次第でdb/structure.sqlというsql文で書かれたスキーマダンプを手に入れることも出来る。(DBの高度な機能を使うためにはそうした方がいい。)

マイグレーションファイルとスキーマダンプとの関係を表す参考記事置いてあったので、貼っておきます。 schema.rbとmigrationファイルの関係をまとめてみたよ - Qiita

ActiveRecordバリデーション

バリデーション(1.1)

データをデータベースに保存する前に行えるバリデーションの種類は大別すると4つである。

  1. クライアントサイド(JS等)でのバリデーション

    • バリデーションとしての信頼性は低いが、ユーザーへの即座なフィードバックが可能である。モダンなアプリとかだったら、「そのパスワードの強度は低いですよ」みたいなのをリアクティブに教えてくれるものが多い気がする。もちろん、JSでの実装。
  2. コントローラーレベルでのバリデーション

    • 非推奨。テストと保守が困難になるため、避けたほうがいい。
  3. モデルレベル

    • 最も重要。さまざまなバリデーションが元々アクティブレコードに用意されている。また、自分でカスタムしたものを使うことも可能。テスト・メンテナンスも容易。バリデーションを行いたいときの基本である。
  4. データベースレベル

    • テストや保守を行うのは難しくなるが、Rails以外のアプリケーションからそのDBにアクセスするようなことがあるのなら、DB自体にバリデーションを付与しておくのは良い方法である。また、ユニークネス制約など、他の層のバリデーションでは実装が難しく、かつ使用頻度が高いものもあるので、覚えておこう。

モデル層での色々なバリデーション

acceptance(2.1)

class User < ActiveRecord::Base
  validates_acceptance_of :terms_of_service
end

> user = User.new(terms_of_service: true)
> user.valid?
#=> true

> user.terms_of_service = '1'
> user.valid?
#=> true

validates_acceptance_ofバリデーションは、利用規約同意のチェックボックス等に使える。 チェックボックスがチェックされて、その値が「true」か「"1"」なら通る(試してないけどInteger型の「1」でも通りそう) DBに存在していないカラムについても弾ける。「利用規約に同意しているかどうかカラム」なんて作ったところで、全部「true」に決まってるので、モデル層で弾く設計にした方が良いということでこのバリデーションは良さげですね。

参考記事: Rails 5 accepts 1 or true for acceptance validation | BigBinary Blog

confirmation(2.3)

パスワード確認用に使えるバリデーション。

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true  #2行目のバリデーションを働かせるためnil不許可
end

これでビューテンプレートに「email」と「email_confirmation」に関するフィールドを作ればOK。同じ値であることを保証する。

format(2.5)

withオプションで与えられた正規表現とマッチするものだけを通す。

class Product < ApplicationRecord
  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
    message: "英文字のみが使えます" }
end

メールアドレスのバリデーションでよく出てくる気がする。

uniquness(2.11)

モデル側で定める一意性制約。どうやって挙動を保証してるのかな〜と思ったらSQLクエリを発行しているようですね。まあ、DB見ないとユニークネスかどうかわからないのは当たり前か。(もしかして何か裏技あるのかと思ってた)

validates_with(2.12)

カスタムバリデーションを使う際の記載。

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any?{|field| record.send(field) == "Evil" }
      record.errors[:base] << "これは悪人だ"
    end
  end
end
 
class Person < ApplicationRecord
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

ちょっと複雑なコード。 言葉にしてみると、「Personモデルのインスタンスが保存されるさい、GoodnessValidatorにバリデーション処理が移譲される。移譲先ではvalidateメソッドが打たれることが保証され、引数としてインスタンス自身(record)が渡される。」ということ。

 if options[:fields].any?{|field| record.send(field) == "Evil" } 

この部分は、「もしオプションにfiledsという項目があったなら、そのfieldsという項目の数だけ、後ろのブロック{ record.send(field) =="Evil" }を評価し、その戻り値が結果とせよ。」という感じかな。record.send(field)は、recordにfield(:first_nameとか)のメソッドを打てということ。

ここ解説少なかったので掘り下げました。

(補足)

sendの挙動

[1] pry(main)> str = "str"
=> "str"
[2] pry(main)> str.send(:upcase)
=> "STR"

validates_each(2.13)

カスタムバリデーションを使うほどではないけど、既存のバリデーションじゃ不十分なときに使う。

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
  end
end

属性ごとに後ろのブロックを評価。ブロック引数として、「インスタンス自身(record)」、「インスタンスの属性名(attr)」、「属性の値(value)」が固定で渡される。順番大事。

:ifや:unlessでバリデージョンの前に呼び出すメソッドを指定

一番良く使うオプションらしい。

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?
 
  def paid_with_card?
    payment_type == "card"
  end
end

支払い方法がカードのときのみカードナンバーを入力させたいときは上のような書き方。 if:にProcオブジェクトを渡すことも可能。そうすると、そのオブジェクトの処理はバリデーションの前に呼び出されて評価される(ブロックの評価値はtrueかfalseにしておく。)

カスタムバリデータ(6.1)

個別の要素を検証したいときは、カスタムバリデータに「ActiveModel::EachValidator」を継承させるのが一番手っ取り早い。

(2.12で示したように、「ActiveModel::Validator」を継承させたあと、処理を委譲させてもよい。)

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "はメールアドレスではありません")
    end
  end
end
 
class Person < ApplicationRecord
  validates :email, presence: true, email: true
end

呼び出す側のモデルで「validates :email, email:true」とするだけで、簡単にカスタムバリデータさんを呼び出せる。

バリデーションエラーについて

valid?だったりsaveを打ったオブジェクトはActiveModel::Errorsクラスのインスタンスを一つ手に入れる。それはrecord.errorsで参照できる。

record.errorsに打てるメソッドとしては、[:attribute]、add、size、full_messages等…

詳しくはAPIガイドへ!

ActiveModel::Errors

感想

思ってたより読みやすい…すげえよ公式ガイド…。

「どういう挙動をするか」というコード例が助かります。

あと解説も想像してたよりは丁寧でありがたい(とはいえ、Rails学んだばかりの人だと難しいと思う。)

基礎部分が15記事あって、今回4つまとめたからPart.4までは続けようと思います。

Rails勉強してる方は、一度読んでみて「理解できるな」と思ったら読んでみると良いと思います!(ただ、作りながら覚える形式じゃないため、挙動を想像できない場合は非推奨かも。)