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

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

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

railsguides.jp

wrap_parameters

リンクはこちら

パラメータを受け取るときに、パラメータがコントローラに応じたキー名でラップされるようになる。
これはデフォルトではJSONのリクエストの場合は機能するようになっているようです。

例えば、JSON{"title":"value1", "content":"value2"}を送ると、rails側で"book"=>{"title"=>"value1", "content"=>"value2"}のようにラップしてくれました。
ちなみに、falseを指定すると無効にできるみたいです。

class BooksController < ApplicationController
  wrap_parameters false
end

extract_value

リンクはこちら

Rails7.1から入ったメソッド。
複合主キーを持つモデルのため、1つのパラメータから値を配列にして返してくれます。

params
#=> #<ActionController::Parameters {"controller"=>"books", "action"=>"show", "id"=>"2_4"} permitted: false>

params.extract_value(:id)
#=> ["2", "4"]

permit!

リンクはこちら

パラメータのハッシュ全体を許可したい場合に使えます。
当然、セキュリティ面には注意する必要があります。

params
#=> #<ActionController::Parameters {"authenticity_token"=>..., "book"=>{"title"=>"本1", "content"=>"コンテンツ"}, "commit"=>"Create Book", "controller"=>"books", "action"=>"create"} permitted: false>
params.permitted?
#=> false

params.permit!
#=> #<ActionController::Parameters {"authenticity_token"=>..., "book"=>#<ActionController::Parameters {"title"=>"本1", "content"=>"コンテンツ"} permitted: true>, "commit"=>"Create Book", "controller"=>"books", "action"=>"create"} permitted: true>
params.permitted?
#=> true

flash.keep

リンクはこちら

flashを次のリクエストに引き継ぐ時などの使える、flashの寿命を伸ばすようなメソッドです。
scaffoldしたアプリで、showアクションにflash.keepと書いたら、createした後に何度リロードしてもフラッシュメッセージが表示されました。

before_actionのためのbeforeメソッドなど

リンクはこちら

例えばbefore_actionの処理内容を別クラスで実行するときにはbeforeという名前のクラスメソッドを、コントローラのインスタンスを引数に渡す形で作ってあげれば実行できるみたいです。
知らなかったですが、どこかで使える場面がありそうだなと思いました。

class Sample
  def self.after(controller)
    p 'after actionが実行された'
  end
end

以下のようにきちんと実行されていました。

Started GET "/books/1" for ...
Processing by BooksController#show as HTML
  Parameters: ...
  Rendering layout layouts/application.html.erb
  Rendering books/show.html.erb within layouts/application
  Rendered books/_book.html.erb (Duration: 1.3ms | Allocations: 179)
  Rendered books/show.html.erb within layouts/application (Duration: 6.8ms | Allocations: 1202)
  Rendered layout layouts/application.html.erb (Duration: 61.4ms | Allocations: 13927)
"after actionが実行された"
Completed 200 OK in 79ms (Views: 66.6ms | ActiveRecord: 3.4ms | Allocations: 18000)

authenticate_or_request_with_http_token

リンクはこちら

トークン所有の有無で認証するBearerトークンを利用可能にするもの。
Railsガイドのようにサンプルコードを書いて動かしたら、確かにアクセスできなくなりました。

HTTP Token: Access denied.

ライブストリーミング

リンクはこちら

ActionController::Liveを使うことでライブストリーミングを利用できるようになります。
例えば以下のように書いたら、1秒ごとに現在時刻が表示されました。

def show
  response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "#{Time.current}\n"
        sleep 1
      }
ensure
  response.stream.close
end

filter_parameters

リンクはこちら

ログに出力される情報をfilter_parametersで制御できます。
私がrails newした環境では、デフォルトでconfig/initializers/filter_parameter_logging.rbに以下のように記載されていました。

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]

試しに、ここに:titleを追加してログを見てみたところ、以下のように[FILTERED]となっていました。
デフォルトの設定のままになっていることも多そうなので、一度自分のアプリケーションを見直してみても良さそうだなと思いました。

Started POST "/books" for ...
Processing by BooksController#create...
  Parameters: {"authenticity_token"=>"[FILTERED]", "book"=>{"title"=>"[FILTERED]", "content"=>"内容4"}, "commit"=>"Create Book"}
...

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

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

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

railsguides.jp

fields_for

リンクはこちら

普段Formオブジェクトを使うので、そういえばこんな機能あったな、という感想です。 関連モデルのフォームをfields_forで簡単に記載できます。

formmethod

リンクはこちら

これを使うことでフォームビルダーの中でHTTPリクエストを上書き指定して送信できるらしい。 ただ、手元の環境(Rails7, turbo使っている)で動かしてみたら、DELETEは送られず、POSTが送られてしまいました。 Rails7, turboなし環境ではDELETEのリクエストが飛んだので、rails-turboを使っていることが原因ぽいかなと思いましたが、深追いはできてないです。

<%= form.button "Delete", formmethod: :delete, data: { confirm: "Are you sure?" } %>

scaffoldでの削除機能のように、素直にbutton_toを使うのが良さそうに思いました。

オプショングループ

リンクはこちら

Hashを使うことで、セレクトボックスの選択肢をグループ化できる。

<%= form.select :city,
      {
        "Europe" => [ ["Berlin", "BE"], ["Madrid", "MD"] ],
        "North America" => [ ["Chicago", "CHI"] ],
      },
      selected: "CHI" %>

タイムゾーンと国を選択する

リンクはこちら

form.time_zone_selectタイムゾーンと国の一覧のセレクトボックスが表示される

<%= form.time_zone_select :time_zone %>

個別の一時コンポーネント用のセレクトボックス

リンクはこちら

フォームビルダーとは独立してフォームを作成できる部品。 例えばselect_yearでは、APIドキュメントを見ると、選択肢の範囲を決めるオプションなど用意されていました。

<%= select_year 1999, prefix: "party", start_year: 2000, end_year: 2007 %>

チェックボックスのHTTPリクエストの工夫

リンクはこちら

check_boxヘルパーでは、同じ名前で予備の隠し入力を作成しておき、本来送信されないはずのチェックボックス値が見かけ上送信されるようになっています。

確認したところ、以下のようにerbに記載した場合、HTML側ではhiddenとしてチェックしていない値が入っていました。

<%= check_box 'attr', 'sample', {}, value='1', value='2' %>
<input name="attr[sample]" type="hidden" value="2" autocomplete="off">
<input type="checkbox" value="1" name="attr[sample]" id="attr_sample">

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されているので重複がある