Railsガイドにきちんと目を通して新しい知識を得る - Action View ヘルパー編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Action View ヘルパーの章です。

railsguides.jp

ベンチマーク

リンクはこちら

viewの中でかかった処理時間を計測できる。 結果はログに出力される。

<% benchmark "Benchmark Test" do %>
  <%= sleep 3 %>
<% end %>
Started GET "/books" for ::1 at
...
Benchmark Test (3005.2ms)
...
Completed 200 OK

distance-of-time-in-words

リンクはこちら

よく見かけるやつですね。Railsが用意してくれてるんですね。

DebugHelper

リンクはこちら

オブジェクトを見やすく表示してくれます。 試しにActiveRecordの1つのインスタンスを表示してみたら以下の感じでした。 デバッグは処理止めてサーバー内で値の確認などするので、使ったことなかったです。

<%= debug(@books.last) %>

NumberHelper

リンクはこちら

number_to_percentage

四捨五入してくれるみたいですね。

number_to_percentage(99.44, precision: 0) # => 99.4%
number_to_percentage(99.45, precision: 0) # => 99.5%

number_to_phone

デフォルトは英語なので、日本の電話番号形式に合わせるならパターンなど上手に設定する必要がありそうです。
また、日本の場合、基本的には0から電話番号が始まりますが、0から始まる数字だとSyntax Errorになったので、文字列にしないといけなさそうです。

number_to_phone(0120123456)
#=> ActionView::SyntaxErrorInTemplate
number_to_phone('0120123456')
#=> 012-012-3456
<%= number_to_phone('0120123456', pattern: /(\d{4})(\d{2})(\d{4})$/) %>
#=> 0120-12-3456

SanitizeHelper

リンクはこちら

sanitize_css

css = "background: url(javascript:alert('alert')); font-size: 10px;"
sanitize_css(css)
#=> "font-size:10px;"

リンクのテキストだけ抽出できる。ピンポイントで使うことがあるかもしれません。
以下で、strip_links(books_link)は「本の一覧へ」という文字列を返します。

<% books_link = capture do %>
  <%= link_to '本の一覧へ', books_path %>
<% end %>

<%= strip_links(books_link) %>

strip_tags

HTMLタグを除外する。
以下でstrip_links(books_link)は「本の一覧へアイウエオ」という文字列を返します。

<% books_link = capture do %>
  <!-- コメント -->
  <div><%= link_to '本の一覧へ', books_path %><span>アイウエオ</span></div>
<% end %>

<%= strip_tags(books_link) %>

Railsガイドにきちんと目を通して新しい知識を得る - レイアウトとレンダリング編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、レイアウトとレンダリングの章です。

railsguides.jp

render inline

リンクはこちら

ガイドの中でも「このオプションを実際に使う意味はめったにありません。」と記載がありますが、コントローラでerbを直接記載できます。

class BooksController < ApplicationController
  def index
    @books = Book.all
    render inline: "描画しました<br><%= @books %>"
  end
end

描画された画面

レイアウトの探索順序

リンクはこちら

例えば/books/のURLであれば、app/views/layout/books.html.erbのレイアウトが一番優先で使われる。

renderでコレクションがない場合はnilを返す

リンクはこちら

コレクションがない場合にnilを返すので||を使って、データがない場合の処理が書けるのは知らなかった。

<%= render(@books) || 'データはありません' %>

今までは↓みたいに書いていたの1行で簡単に書けるのは嬉しいです。

<% if @books.exists? %>
  <%= render @books %>
<% else %>
  データはありません
<% end %>

Railsガイドにきちんと目を通して新しい知識を得る - Action View の概要編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Action View の概要の章です。

railsguides.jp

spacer_template

リンクはこちら

parcialcollectionを使っているときに、要素ごとの描画の間に差し込むことができます。
以下のように書くと、各bookのタイトルの下に罫線が描画されます。

<%= render partial: @books, spacer_template: 'spacer' %>

<div>
  <%= book.title %>
</div>
<hr>

prepend_view_path, append_view_path

リンクはこちら

prepend_view_path

デフォルトのビューのパスより優先して別のディレクトリからviewファイルを検索できるようになる。
以下のように記載した場合、app/views/books.index.html.erbよりapp/custom_views/books.index.html.erbが優先されて描画される。

class BooksController < ApplicationController
  def index
    prepend_view_path "app/custom_views"
    ...
  end
end

append_view_path

デフォルトのビューのパスでviewファイルが見つからなかった場合に別のディレクトリからviewファイルを検索できるようになる。
以下のように記載した場合、app/views/books.index.html.erbがない場合はapp/custom_views/books.index.html.erbが描画される。

class BooksController < ApplicationController
  def index
    append_view_path "app/custom_views"
    ...
  end
end

Railsガイドにきちんと目を通して新しい知識を得る - Active Model の基礎編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Active Model の基礎の章です。

railsguides.jp

AttributeMethodsモジュール

リンクはこちら

prefixやsuffixのメソッドを定義できる機能等を提供しているモジュールのようです。
_changed?みたいなメソッドもこのモジュールを使って実現しているみたいですね。

module ActiveModel
  module Dirty
    included do
      attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
      ...
    end
end

ActiveModel::Model

リンクはこちら

railsソースコードを見てみたら、ActiveModel::ModelでincludeしているActiveModel::APIが色々と必要なモジュールを読み込んでいる仕組みでした。

module ActiveModel
  module Model
    extend ActiveSupport::Concern
    include ActiveModel::API
  end
  ...
end
# frozen_string_literal: true

module ActiveModel
  module API
    extend ActiveSupport::Concern
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    include ActiveModel::Conversion

    included do
      extend ActiveModel::Naming
      extend ActiveModel::Translation
    end
end

ActiveModel::Serializers::JSON

リンクはこちら

from_jsonという使い方は知らなかったです。

json = { content: 'hoge' }
memo = Memo.new
memo.from_json(json)
#<Memo:0x000000010af35150 id: nil, content: "hoge", user_id: nil, created_at: nil, updated_at: nil>

Railsガイドにきちんと目を通して新しい知識を得る - Active Record クエリインターフェイス編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Active Record クエリインターフェイスの章です。

railsguides.jp

データベースからオブジェクトを取り出すメソッド

リンクはこちら

create_with

APIドキュメントを見ましたが、そもそもwhereで絞り込んだ後にnewするとwhereで絞り込んだ値を持つのを知りませんでした、、

memos = Memo.where(content: 'sample01')
memo = memos.new
memo.content # => 'sample01'

memos = Memo.create_with(content: 'sample02')
memo = memos.new
memo.content # => 'sample02'

extract_associated

コレクションに対して、それに関連づくレコードを配列で取得できます。
注意点としては、Memo::ActiveRecord_Relation ではなく Arrayで返ってくるところでしょうか。

Memo.where(id: [3, 4]).extract_associated(:user)
# =>  [#<User id: 2, email: "sample04@example.com", ...>, 
# =>   #<User id: 1, email: "sample02@example.com", ...>]

from

FROM句のサブクエリを指定できます。

subquery = Memo.where(content: nil).to_sql
Memo.where(user_id: 1).select('id').from("(#{subquery}) AS memos")
#=> Memo Load (0.2ms)  SELECT "memos"."id" FROM (SELECT "memos".* FROM "memos" WHERE "memos"."content" IS NULL) AS memos WHERE "memos"."user_id" = ?  [["user_id", 1]]

readonly

readonlyを使い取得したコレクションは更新等ができなくなります。
バリデーションを無視するupdate_attributeでも無理でした。

memo = Memo.readonly.first

memo.update(content: 'update')
# => ActiveRecord::ReadOnlyRecord

memo.update_attribute(:content, 'update')
# => ActiveRecord::ReadOnlyRecord

references

includesと一緒に使い、where等を使った際にテーブル名を関連付ける。

User.includes(:memos).where('memos.user_id = 1').references(:memos)

# でもこれは以下のように書く方がよさそう
User.includes(:memos).where(memos: { user_id: 1})

単一のオブジェクトを返すクエリメソッド

リンクはこちら

findはidを複数受け取ることもできる

findってidを一つだけ受け取るものと思っていましたが、複数受け取ることもできるんですね。
where(id: ...)より短く書けるので、普通に使う場面多そうだなと思いました。 レコードがないときはエラーを返す点はwhereとは違いますね。

Memo.find(1, 2)
# =>  SELECT "memos".* FROM "memos" WHERE "memos"."id" IN (?, ?)  [["id", 1], ["id", 2]]
# => [#<Memo:0x0000000112f37880 id: 1, content: "sample01", ...>
 #<Memo:0x0000000112f377b8 id: 2, content: "sample02", ...>]

Memo.find(1, 100) # id: 100のレコードはない
# => ActiveRecord::RecordNotFound

take

「どのレコードが取り出されるかは指定されません。」ということで使うケースがあまりなさそうではあります。

Memo.take
# => SELECT "memos".* FROM "memos" LIMIT ?  [["LIMIT", 1]]

条件の上書き(rewhereなど)

リンクはこちら

いくつかは知っていましたが、一覧できちんと見れたのでよかったです。
rewhereなどは、使うとしたらdefault_scopeを使っていて、上書きしたいようなときでしょうか。
(その場合、rewhereを使わないといけない時点で実装がやや怪しい気がしますが)

class Memo < ApplicationRecord
  default_scope { where.not(content: nil) }
  ...
end

Memo.all
#=> [#<Memo:0x00000001083ea0b8 id: 1, content: "sample01", user_id: 1,...>,
 #<Memo:0x00000001083e9ff0 id: 2, content: "sample02", user_id: 1, ...>,
 #<Memo:0x00000001083e9f28 id: 3, content: "sample03", user_id: 2, ...>,
 #<Memo:0x00000001083e9e60 id: 4, content: "sample04", user_id: 2, ...>]

Memo.rewhere(content: nil)
# => [#<Memo:0x00000001079a38e8 id: 5, content: nil, user_id: 1, ...>,
 #<Memo:0x00000001079a3578 id: 6, content: nil, user_id: 1, ...>]

ifを使った際のscopeとクラスメソッドの違い

リンクはこちら

scopeはifの条件に当てはまらないときにはActiveRecord::Relationオブジェクトを返します。

class Memo < ApplicationRecord
  scope :content_presents, -> { where.not(content: nil) if false }
end

Memo.content_presents
# => [#<Memo:0x0000000109ee8ec8 id: 1, content: "sample01", ...>,
 #<Memo:0x000000010a1bda90 id: 2, content: "sample02", ...>, ...]
class Memo < ApplicationRecord
  def self.content_presents
    where.not(content: nil) if false
  end
end

Memo.content_presents
# => nil

動的検索(find_by_attribute)

リンクはこちら

find_by_カラム名で検索できるんですね。
レコードが見つからなかった場合にnilを返すのもfind_byと同じです。

Memo.find_by_content('sample01')
# => #<Memo:0x000000010afc7028 id: 1, content: "sample01", ...>

Memo.find_by_content('No Record Content')
# => nil

SQLで検索

リンクはこちら

積極的に使う機会は少ないかもですが、カスタムSQLを記述する方法として、コードリーディング等で見かけるかもなので、知れてよかったです。

find_by_sql

以下の例だと、Memoクラスのインスタンスが配列で帰ってきます。
インスタンスといえ、contentのみを取得しているので、created_atなどの情報は持っていません。

memos = Memo.find_by_sql("SELECT content FROM memos")
#=> [#<Memo:0x000000010acaf9a8 id: nil, content: "sample01">, #<Memo:0x000000010acaf8e0 id: nil, content: "sample02">, #<Memo:0x000000010acaf818 id: nil, content: "sample03">, #<Memo:0x000000010acaf750 id: nil, content: "sample04">, #<Memo:0x000000010acaf688 id: nil, content: nil>, #<Memo:0x000000010acaf5c0 id: nil, content: nil>]

memo.created_at
#=> ActiveModel::MissingAttributeError

select_all

ActiveRecord::Resultオブジェクトが返ってきます。
to_aを使うとハッシュの配列になります。

Memo.connection.select_all("SELECT content FROM memos")
#=> #<ActiveRecord::Result:0x000000010a80f470 @column_types={}, @columns=["content"], @hash_rows=nil, @rows=[["sample01"], ["sample02"], ["sample03"], ["sample04"], [nil], [nil]]>

Memo.connection.select_all("SELECT content FROM memos").to_a
#=> [{"content"=>"sample01"}, {"content"=>"sample02"}, {"content"=>"sample03"}, {"content"=>"sample04"}, {"content"=>nil}, {"content"=>nil}]

includesとpluck

includesが使われているときにpluckを使うと、eager_loadingが引き起こされるというのは知らなかったです。

User.includes(:memos)
#=> User Load (0.2ms)  SELECT "users".* FROM "users"
# => Memo Load (0.3ms)  SELECT "memos".* FROM "memos" WHERE "memos"."user_id" IN (?, ?, ?, ?, ?, ?, ?)  [["user_id", 1], ["user_id", 2], ...

User.includes(:memos).pluck(:id) # idsでも同じ
# => SELECT "users"."id" FROM "users" LEFT OUTER JOIN "memos" ON "memos"."user_id" = "users"."id"
# => [1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7]
# LEFT JOINされているので重複がある

Railsガイドにきちんと目を通して新しい知識を得る - Active Recordの関連付け編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Active Recordの関連付けの章です。

railsguides.jp

reload_xxxと、changerd?previously_changed?

リンクはこちら

reload_xxx

テストを書くときに、xxx.reloadみたいな使い方はよくしていましたが、関連モデルに使えるのは知りませんでした。

# (コンソールAで)
book.user
#=> #<User id: 1, email: "sample@example.com", created_at: ...>

# (別のコンソールBで)
User.find(1).update(email: 'sample02@example.com')

# (コンソールAに戻って)
book.user
#=> #<User id: 1, email: "sample@example.com", created_at: ...>

book.reload_user
#=> #<User id: 1, email: "sample02@example.com", created_at: ...>

changed?, previously_changed?

changed?は保存前、previously_changed?は保存直後に判定をしてくれます。 ActiveModel::Dirtyというモジュールで提供されているとのこと。

product = Product.last
product.price = 100

product.price_changed?
#=> true
product.price_previously_changed?
#=> false

product.save

product.price_changed?
#=> false
product.price_previously_changed?
#=> true

belongs_toで使える:autosave, :touch オプション

リンクはこちら

:autosave

class User < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :user, autosave: true
end

この場合、子モデルのレコードが保存されるときに、親モデルのレコードも保存処理が走る

user = User.last
user.email = 'update@example.com'

book = user.books.last
book.save # ここでuserも保存される

User.last.email
# => 'update@example.com'

逆に、falseだと、デフォルトで親モデルが保存される場合も保存されなくなる

class Book < ApplicationRecord
  belongs_to :user, autosave: false
end

user = User.new(email: 'aaa@example.com')
book = user.books.new

book.save
# => NOT NULL constraint failed: books.user_id (SQLite3::ConstraintException)

:touch

関連モデルのレコードのupdated_atが更新される

class Book < ApplicationRecord
  belongs_to :user, touch: true
end

user = User.last
book = user.books.last
book.title = 'update'
book.save
# ここで、userのupdated_atも現在時刻が入る

belongs_toで使えるスコープ

リンクはこちら

where

以下のように書くと、emailにsample123@example.comを持つuserしかbookを関連として持てなくなる

class Book < ApplicationRecord
  belongs_to :user, -> { where email: 'sample123@example.com' }
end


user = User.create(email: 'test@example.com')
user.books.create!
# => ActiveRecord::RecordInvalid

user = User.create(email: 'sample123@example.com')
user.books.create!
# => 成功

readonly

以下のように書くと、bookオブジェクトからuserオブジェクトは読み出し専用になる

class Book < ApplicationRecord
  belongs_to :user, -> { readonly }
end

book = Book.last
book.user.update!(email: 'update@example.com')
# => ActiveRecord::ReadOnlyRecord

select

selectで特定のカラムだけ取得できる。
重たいテーブルを読み出すけど特定のカラムしか使わない、というときに使えるのかなと思います。

class Book < ApplicationRecord
  belongs_to :user, -> { select :id }
end

book = Book.last
book.user
# => #<User id: 7>    

has_manyで追加されるcollection_singular_ids

リンクはこちら

idの配列ベースで、関連の付け替えや新規関連付けができる。
付け替えの場合は、元のidがnilになったりするので使えるタイミングは限定されるかもですね。=を使うとupdate文が即座にかかるということをきちんと理解する必要がありそうです。

新しく関連付けをするときの方が使うケースはあるかもなーと思いました。

Memo.all
# => 
[#<Memo:0x000000010aa64bf8 id: 1, content: "sample01", user_id: 7>,
 #<Memo:0x000000010aa64b30 id: 2, content: "sample02", user_id: 7>,
 #<Memo:0x000000010aa64a18 id: 3, content: "sample03", user_id: 1>,
 #<Memo:0x000000010aa64950 id: 4, content: "sample04", user_id: 1>]

user = User.find(1)
user.memo_ids # => [3, 4]
user.memo_ids = [1, 2] # ここでupdateされる

Memo.all
# => 
[#<Memo:0x000000010aa64bf8 id: 1, content: "sample01", user_id: 1>,
 #<Memo:0x000000010aa64b30 id: 2, content: "sample02", user_id: 1>,
 #<Memo:0x000000010aa64a18 id: 3, content: "sample03", user_id: nil>,
 #<Memo:0x000000010aa64950 id: 4, content: "sample04", user_id: nil>]

user = User.find(2)
user.memo_ids = [3, 4]

Memo.all
# => 
[#<Memo:0x000000010aa64bf8 id: 1, content: "sample01", user_id: 1>,
 #<Memo:0x000000010aa64b30 id: 2, content: "sample02", user_id: 1>,
 #<Memo:0x000000010aa64a18 id: 3, content: "sample03", user_id: 2>,
 #<Memo:0x000000010aa64950 id: 4, content: "sample04", user_id: 2>]

関連付けのコールバック

リンクはこちら

before_addなどのコールバックの存在自体知りませんでした、、

class User < ApplicationRecord
  has_many :memos, before_add: :before_add_method

  private

  def before_add_method(memo)
    p 'before_add called'
  end
end

user = User.first
user.memos.new
# => 'before_add called'

関連付けの拡張

リンクはこちら

普通に関連モデルにscopeとかクラスメソッドとか書いて解決している気がしますが、コンテキストが限られているのならアリな場面もあるかも?と思ったりしました。

class User < ApplicationRecord
  has_many :memos do
    def find_by_memo(content)
      find_by(content: content)
    end
  end
end

user = User.first
user.memos.find_by_memo('sample01')
# => #<Memo:0x000000010673e158 id: 1, content: "sample01", user_id: 1>

Railsガイドにきちんと目を通して新しい知識を得る - コールバック編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、コールバックの章です。

railsguides.jp

after_find

リンクはこちら

データベースからレコードを読み出すたびに使われるコールバック。
読み出したレコードを使う前にデータを整えたい場合に使えそうです。

Railsガイドにも「レコードを1つ読み込むたび」と書いてある通り、find以外でも呼び出されます。

class Product < ApplicationRecord
  after_find { puts 'オブジェクトが見つかりました。' }
end
Product.count
#=> 5
Product.all
#=> オブジェクトが見つかりました。                            
#=> オブジェクトが見つかりました。                            
#=> オブジェクトが見つかりました。
#=>オブジェクトが見つかりました。
#=> オブジェクトが見つかりました。

コールバックをスキップするメソッドたち

リンクはこちら

バリデーションをスキップするだけでなく、コールバックもスキップするんですね。   改めて使い方には注意が必要だなとわかりました。