- Ruby / Rails関連
週刊Railsウォッチ(20210315前編)Active Recordのenum関連改修、Active SupportのEnumerableでpluckが使えるほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
以下のコミットリストのChangelogを中心に見繕いました。
🔗 config.action_text.attachment_tag_name
が追加
Railsフォーラムで、Action TextのHTMLタグ名を変えたいと思う人たちがいる。今はデフォルトでは
action-text-attachment
となっている。現時点でそうする方法のひとつは、リッチテキスト形式の出力をパースしてnokogiriでタグ名を変更すること。
このプルリクは、action-text-attachment
を任意の文字列に変えられるconfig.action_text.attachment_tag_name
オプションを追加する。
同PRより大意
つっつきボイス:「従来のActionText::Attachment::TAG_NAME
は名前がaction-text-attachment
に固定されていたのをユーザーがコンフィグで名前を変えられるようにしたのか↓」「Action Text周りでは最近こんな感じの変更がちょくちょく入ってるようですね」「コンフィグで変えられるものが増えるのはいいと思います👍」
# actiontext/app/helpers/action_text/content_helper.rb#L5
module ActionText
module ContentHelper
mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.safe_list_sanitizer.new }
- mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] }
+ mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment.tag_name, "figure", "figcaption" ] }
mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES }
mattr_accessor(:scrubber)
...
# actiontext/lib/action_text/attachment.rb#L6
class Attachment
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
- TAG_NAME = "action-text-attachment"
- SELECTOR = TAG_NAME
+ mattr_accessor :tag_name, default: "action-text-attachment"
+
ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
...
🔗 enum
の予約済みオプション名に_
を付けずに書けるようになった
つっつきボイス:「これは今週のPRではありませんが、以下の記事で知りました↓」
参考: Rails introduces new syntax for enum | Saeloun Blog
「なるほど、enum
の引数受け取りをenum(attr_name, ..., **options)
とすることで、オプションを最後に付ければアンダースコア付きの_prefix
や_scopes
などではなくアンダースコアなしのprefix
やscopes
などを使えるようにしたのか」「アンスコなしで書けるのは嬉しい👍」
Attribute APIに組み込まれている他の機能と違い、
enum
の予約済みオプション名の冒頭には_
が付いている。
_
付きだった理由は、enum
がハッシュ引数を1個しか受け取らないため(このハッシュ引数はenumの定義と予約済みオプションの両方を含む)。
enum
の予約済みオプション名でこの_
を避けるために、他のAttribute APIの構文のようなenum(attr_name, ..., **options)
を使える新しい構文をここに提案する。変更前:
class Book < ActiveRecord::Base
enum status: [ :proposed, :written ], _prefix: true, _scopes: false
enum cover: [ :hard, :soft ], _suffix: true, _default: :hard
end
変更後
class Book < ActiveRecord::Base
enum :status, [ :proposed, :written ], prefix: true, scopes: false
enum :cover, [ :hard, :soft ], suffix: true, default: :hard
end
同PRより大意
「既存の_
付きオプションとの互換性も確保されているようですね↓」
- default = {}
- default[:default] = definitions.delete(:_default) if definitions.key?(:_default)
+ definitions = options.slice!(:_prefix, :_suffix, :_scopes, :_default)
+ options.transform_keys! { |key| :"#{key[1..-1]}" }
「コメントを見ると、このリリースでは既存の書式をdeprecateにしないけど、cop(注: RuboCopのルール)でオートコレクトするのが難しくなければdeprecateすべきと書かれてるので、今後非推奨になるのかも」「たぶん最終的には_
なしの書式が正式になるんでしょうね」「_
付きの_prefix
や_suffix
あたりは既に使っている人がいそう」「そういえば_prefix
や_default
は使ったことあります」
「enum
を本格的に使うようになると、_scope
や_default
などのオプションが欲しくなりますね: 特に_prefix
が使えないと不便」「なるほど」「次の2つはこの#41328に関連していそうなenum
関連のプルリクです」
🔗 Enum型の属性aggregationを修正
つっつきボイス:「sum
やminimum
やmaximum
といった集約系の処理を修正したのか: テストコードが変わっている↓」「"aggregation"は集約関数の集約なんですね」
# activerecord/test/cases/calculations_test.rb#L1165
- def test_aggregate_attribute_on_custom_type
- assert_nil Book.sum(:status)
- assert_equal "medium", Book.sum(:difficulty)
- assert_equal "easy", Book.minimum(:difficulty)
- assert_equal "medium", Book.maximum(:difficulty)
- assert_equal({ "proposed" => "proposed", "published" => nil }, Book.group(:status).sum(:status))
- assert_equal({ "proposed" => "easy", "published" => "medium" }, Book.group(:status).sum(:difficulty))
- assert_equal({ "proposed" => "easy", "published" => "easy" }, Book.group(:status).minimum(:difficulty))
- assert_equal({ "proposed" => "easy", "published" => "medium" }, Book.group(:status).maximum(:difficulty))
+ def test_aggregate_attribute_on_enum_type
+ assert_equal 4, Book.sum(:status)
+ assert_equal 1, Book.sum(:difficulty)
+ assert_equal 0, Book.minimum(:difficulty)
+ assert_equal 1, Book.maximum(:difficulty)
+ assert_equal({ "proposed" => 0, "published" => 4 }, Book.group(:status).sum(:status))
+ assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).sum(:difficulty))
+ assert_equal({ "proposed" => 0, "published" => 0 }, Book.group(:status).minimum(:difficulty))
+ assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).maximum(:difficulty))
end
参考: PostgreSQL 12.4文書9.20. 集約関数
参考: MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.19.1 GROUP BY (集約) 関数
「たとえばテストコードのBook.group(:status).sum(:difficulty))
は従来だと{ "proposed" => "easy", "published" => "medium" }
を返していたけど、修正後は{ "proposed" => 0, "published" => 1 }
を返すようになった: enumが設定されたカラムで#group
したときの挙動を修正したんですね」
issue #39248と#39271を修正するため、#39255と#39274では集約結果をattributeの型ごとにキャストするようにした。
しかし#41431を実装したときに、enumのマッピングを回避できることに気づいた。
今回の変更によって、#39039のexpectationが復帰する(特にEnumの場合)。
修正対象: #41600
同PRより大意
🔗 enum attributeに対するserialize(value)
がサブタイプごとにキャストされるよう修正
- PR: Serialized value on the enum attribute should be type casted by the subtype by kamipo · Pull Request #41516 · rails/rails
- PR: Add CHANGELOG entry for type casting enum values by the subtype by kamipo · Pull Request #41599 · rails/rails
つっつきボイス:「これもenum
のattribute関連ですね」「この#39039を含むChangelogが別プルリクになっていたので併記しました↓」「SQLite3ではnil
が返り、MySQLとPostgreSQLではエラーになってたのが修正されたんですね」
enum値を元のattributeの型で型キャストするようになった。
注目すべき変更点は、不明なラベルがMySQLの0
にマッチしなくなったこと。
class Book < ActiveRecord::Base
enum :status, { proposed: 0, written: 1, published: 2 }
end
変更前:
# SELECT `books`.* FROM `books` WHERE `books`.`status` = 'prohibited' LIMIT 1
Book.find_by(status: :prohibited)
# => #<Book id: 1, status: "proposed", ...> (for mysql2 adapter)
# => ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: invalid input syntax for type integer: "prohibited" (for postgresql adapter)
# => nil (for sqlite3 adapter)
変更後:
# SELECT `books`.* FROM `books` WHERE `books`.`status` IS NULL LIMIT 1
Book.find_by(status: :prohibited)
# => nil (for all adapters)
Ryuta Kamizono
同Changelogより大意
🔗 RAILS_DEVELOPMENT_HOSTS
環境変数のサポートを追加
つっつきボイス:「RAILS_DEVELOPMENT_HOSTS
とは?」「なるほど、ホスト名でアクセス制限するHostAuthorizationで許可済みのホストを、development環境でのみ環境変数からも渡せるようになったんですね↓」「言われてみれば欲しいときがありそう」「現状ではRAILS_ENV=production
では効かないようなので注意ですね」
# railties/lib/rails/application/configuration.rb#L28
def initialize(*)
super
self.encoding = Encoding::UTF_8
@allow_concurrency = nil
@consider_all_requests_local = false
@filter_parameters = []
@filter_redirect = []
@helpers_paths = []
@hosts = Array(([".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")] if Rails.env.development?))
+ @hosts.concat(ENV["RAILS_DEVELOPMENT_HOSTS"].to_s.split(",").map(&:strip)) if Rails.env.development?
@host_authorization = {}
Rails API: ActionDispatch::HostAuthorization
「CI連携で動作確認をプルリク単位で用意するようなHeroku Review Appsのなど環境を、ログ出力の関係などからRAILS_ENV=development
で動かしたいようなケースでは、環境変数から許可済みホスト名を渡したいというケースがありそうですね: これまでは必要になったら自分でそういうコードを書いていたでしょうけど、Railsが用意してくれるならそれを使いたい👍」「なるほど!」
参考: 環境変数 - Wikipedia
🔗Rails
🔗 Active Recordのconcernをテストする(Ruby Weeklyより)
つっつきボイス:「ActiveSupport::Concern
じゃないのかなと思ったら、Active Recordにinclude
するActiveSupport::Concern
モジュールが対象なのね」
「Active Recordのconcernそのものをテストするには、たしかに記事にもあるようにFakeReviewable
のようなfakeのクラスを作ることになるでしょうね↓」
# 同記事より
require_relative 'path/to/reviewable/shared/examples'
class FakeReviewable < ApplicationRecord
include Reviewable
end
describe Reviewable do
include InMemoryDatabaseHelpers
switch_to_SQLite do
create_table :fake_reviewables do |t|
t.datetime :reviewed_at
end
end
describe FakeReviewable, type: :model do
include_examples 'reviewable' do
let(:reviewable) { FakeReviewable.create }
end
end
end
「Rubyのモジュールは、ActiveSupport::Concern
も含めて単体ではテストできないので、このようにモジュールをinclude
するテスト用のfakeクラスを作ってテストするしかないでしょうね」「ふむふむ」
「これはテストにおけるfakeのパターンというヤツです」「fakeはテスト用語としてのfakeなんですね」「そうです」
参考: xUnit Test PatternsのTest Doubleパターン(Mock、Stub、Fake、Dummy等の定義) - 千里霧中
「テスト関連の記事にfakeとかdoubleという語が出てきたときに用語だとわかりにくくて😅」「慣れれば大丈夫です: テスト関連ドキュメントの文脈でたとえばfakeという語が出てくれば、それはテスト用語としてのfakeのはずです」「なるほど」「fakeやdoubleのような一般的な語が用語に使われるのは、コンピューターサイエンスだと割とあります」「こういう用語はそのつもりで読まないといけないんですね」
morimorihoge注)
専門用語は理解できる人たちにとっては解釈の揺れのない便利な用語なのですが、理解できない人たちにとっては意味不明だったりミスリードを誘ってしまう厄介なものでもあります。
# 類似のもので代表的なのはプロセスにSIGNALを送信するkill
コマンドですね
専門用語を知らない人にも分かるよう説明することももちろん大事ですが、技術者同士の会話では専門用語を使う方がより短い時間で精度の高い情報が伝えられるという要素もあるので、こうした技術用語の脳内辞書を育てておくのはエンジニアとして大事なポイントかもしれません。
🔗 Railsセットアップ時に既存データを永続化するときの注意
Everyday Railsサイトの技術ブログです。
2015年に買っていた Everyday Rails ちゃんと改訂版が出ていて一通りざっと読んだ。https://t.co/oaFrA7mWws rspec 完全に理解した。
— Takeshi Kondo (@chaspy_) February 14, 2021
つっつきボイス:「Railsのセットアップで注意すべき点に関する記事だそうです」
新しい開発者がプロジェクトに参加しやすくできるよう、Railsアプリの
bin/setup
スクリプトを活用していますか?そうなっていないのであれば、セットアッププロセスをできるだけ自動化しておく価値があると思います。rails new
で提供されているデフォルトのbin/setup
は最初に手を付けるのに手頃です。そうしておけば今後参加する開発者の貴重な初動時間を節約できますし、セットアップ手順の潜在的な抜け落ちも見つけられます。(bin/setup
は)Visual Studio Codeでコンテナベースの開発環境を構築する際にも非常に便利です。
しかしbin/setup
の利用には注意点があります。
同記事冒頭より大意
「なるほど、この記事に書かれているような問題はRailsで開発しているとときどきありますね: Railsアプリのプロジェクトをフルスクラッチでgit clone
したときはbin/setup
が完走するのに、開発を始めた後に「一度全部やり直したい」と思ってbin/setup
するとdb:prepare
あたりで既にDBが作られていてエラーになるといったことがあります」「あ、心当たりが...😅」
「記事にもあるように、bin/setup
のRubyコードを以下のように変更すれば↓、DBが存在していればdb:migrate
を実行し、存在しなければdb:setup
を実行するようになります」「へ〜、bin/rails
コマンドにはdb:exists
っていうオプションもあるんですね」
# 同記事より
puts "\n== Preparing database =="
system! 'bin/rails db:exists && bin/rails db:migrate || bin/rails db:setup'
後で調べると、bin/setup
の該当部分はデフォルトでは以下のようになっていました(Rails 6.1.2)。
puts "\n== Preparing database =="
system! 'bin/rails db:prepare'
「たとえばCI(Continuous Integration)環境などでも、コンテナおよび依存する一時ファイルが存在していれば使い回し、存在しなければ新規で作成することを意識しないといけませんが、それに通じるものを感じますね」「なるほど」
🔗 Active SupportはEnumerable
にpluck
を追加している
つっつきボイス:「お、再びboringrails.comの記事」「記事冒頭はActive Recordのpluck
の使い方の説明、これは普通かな」「map
するよりpluck
する方がいいというのはよく言われますね」「単にpluck
すると結果が重複することがあるので、一意にしたければdistinct.pluck
する、そうそう」
「おぉ、Active Supportにもpluck
があるって、マジで?」「やや」「Active SupportがEnumerable
にpluck
を追加しているとは知らなかった!」
# 同記事より
[
{ id: 1, name: "David" },
{ id: 2, name: "Rafael" },
{ id: 3, name: "Aaron" }
].pluck(:name)
# rubydoc.infoより
[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
# => ["David", "Rafael", "Aaron"]
[{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
# => [[1, "David"], [2, "Rafael"]]
「これができるということはハッシュにもpluck
できますね: 元記事でもJSON.parse
で取得したハッシュにpluck
を実行している」「おぉ〜!」「いいこと知りました!」「pluck
がEnumerable
に生えてるのって夢が広がりそう」「Ruby 3.0ならパターンマッチでやりそうな処理ですが、それ以前のRubyでもEnumerable#pluck
でやれるのはいい👍」
🔗 その他Rails
つっつきボイス:「お、Rails.benchmark
をいろんな場所で呼べるようになったのか」「割と最近の修正ですね」
# 同記事より: 従来
mattr_accessor :logger, default: Rails.logger
extend ActiveSupport::Benchmarkable
include ActiveSupport::Benchmarkable
def process
benchmark("=== Processing invoices ===") { process_invoices }
end
「従来だとモデルなどでbenchmark
を呼ぶときは上のようにextend
やinclude
などを使ったややこしい書き方をしないといけなかった↑けど、変更後は以下のようにRails.benchmark
のブロックに書けばできるようになったんですね↓」「なるほど!」「extend
やinclude
などを書かなくてもどこにでも書けるのは便利👍」
# 同記事より: 変更後
def process
Rails.benchmark("=== Processing invoices ===") do
logger.debug("=== Processing started ===")
process_invoices
logger.debug("=== Processing done ===")
end
end
=== Processing started ===
=== Processing done ===
=== Processing invoices === (400.7ms)
「Rails.benchmark
にlevel: :debug
やsilence: true
を指定できるのも便利ですね↓」
def process
Rails.benchmark("=== Processing invoices ===", level: :debug, silence: true) do
logger.debug("=== This won't be logged ===")
process_invoices
logger.debug("=== This won't be logged ===")
end
end
前編は以上です。
バックナンバー(2021年度第1四半期)
週刊Railsウォッチ(20210309後編)RubyのIRBに隠れているイースターエッグ、Power Automate Desktop、SQLクエリのありがちなミス6つほか
- 20210303後編 Bundlerのセキュリティ修正、Rubyのガベージコレクション記事、Rubyが2/24に誕生日ほか
- 20210222 ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか
- 20210209後編 Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか
- 20210208前編 Rails次期リリースがバージョン7に決定、thoughtbotのアプリケーションセキュリティガイドほか](/hachi8833/2021_02_08/103801)
- 20210202後編 Ruby 3 irbのmeasureコマンド、テストを関数型言語のマインドセットで考えるほか
- 20210201前編 Webpackerのガイドがマージ、RailsはRuby 3でどのぐらい速くなったかほか
- 20210126後編 Google Cloud FunctionsがRubyをサポート、Ruby 3のパターンマッチングでポーカーゲームほか
- 20210125前編 Railsリポジトリのデフォルトブランチがmainに変更、Rails 6.1はMySQLのENUM型に対応済みほか
- 20210120後編 Ruby 3.0の新機能で遊ぶ、RubyスニペットをJSに変換するRuby2JS、rspec-parameterized gemほか
- 20210113後編 Ruby 3.0 Ractor解説記事、Vercelホスティングサービス、教育用OS xv6ほか
- 20210112前編 Active Recordの範囲指定バリデーション改善、soleとfind_sole_byメソッド、AlgoliaとRailsほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)