- Ruby / Rails関連
週刊Railsウォッチ(20210524前編)Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下のコミットリストのChangelogを中心に見繕いました。
🔗 無効なセッションストアへの書き込みでエラーが出力されるようになった
従来は
config.session_store :disabled
を設定しても、リクエスト処理が終了する時点で単にセッションハッシュを捨てるだけだった。
書き込み時に明示的に失敗させることで、バグの早期発見に役立つ。
読み出しは引き続き許可する。
不明な点がそこそこあるのでとりあえずこのプルリクをドラフトとしてオープンしている。
同PRより大意
つっつきボイス:「config.session_store :disabled
という設定がRailsにあったとは知らなかった」「セッションを使わない設定にしたときに従来だと書き込んでも警告なしで動いていたのか」「warningでもよさそうかなと思いましたけど、明示的にエラーを出すように変えたんですね」「この改修はbreaking changeだけど、これを踏む人はめったにいなさそう」
「Railsでセッションストアを無効にすることってあるんでしょうか?」「たとえば、静的なコンテンツを一方的に配信するだけのCMSサイトを構築する場合とかならセッションストアは不要になるので、無効にすることはあると思いますよ」「それもそうですね」「あるいはCookieだけで十分やれる状況なら、セッションストアをオフにする方がリソースは節約できるでしょうね」
「どちらもやったことはありませんけど😆」「Railsでセッションストアをオフにするなら、Rails以外の静的サイトジェネレータなどで作る方がよさそうですよね」「Railsでセッションストアをオフにできる機能があることがわかったのは収穫」
🔗 Active JobでRangeシリアライザを追加
- PR: Add Range serializer for ActiveJob by fsateler · Pull Request #42219 · rails/rails
- commit: Add Range to list of supported arguments for jobs · rails/rails@61fb58f -- ドキュメント更新
# activejob/lib/active_job/serializers/range_serializer.rb
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class RangeSerializer < ObjectSerializer
+ KEYS = %w[begin end exclude_end].freeze
+
+ def serialize(range)
+ args = Arguments.serialize([range.begin, range.end, range.exclude_end?])
+ hash = KEYS.zip(args).to_h
+ super(hash)
+ end
+
+ def deserialize(hash)
+ args = Arguments.deserialize(hash.values_at(*KEYS))
+ Range.new(*args)
+ end
+
+ private
+ def klass
+ ::Range
+ end
+ end
+ end
+end
つっつきボイス:「お、Active Jobに渡す引数でRangeオブジェクトもシリアライズできるようになったんですね」
「この改修の意味はこういうことだと思います: まずActive Jobはジョブにenqueueするプロセスとジョブを処理するプロセスが分かれますよね」「はい」「そのため、ジョブのパラメータをシリアライズしていったん何らかのデータベースに保存しておく必要があるわけです」「ふむふむ」
「Active Jobを使ってコードを書いているとわかると思いますけど、Active Jobの引数ではオブジェクトそのものではなくオブジェクトのid(DBのid)を渡すのが普通です: そしてその後Workerがジョブを処理するときに改めてfindするなどして取り直します」「ジョブを投入するRailsサーバーとジョブを処理するWorkerでプロセスが違っているから取り直さないといけないんですね」
「そうした理由があるのでジョブの引数はシリアライズできないといけないんですが、今回の改修ではRangeオブジェクトを渡したときもシリアライズできるようになったということですね」「つまり今までRangeはシリアライズしてくれなかったということか」「自分でシリアライズすることはできそうですが、これまではサポートしてなかったんでしょうね」
「テストコードを見ると..
や...
記法をサポートするようになってる↓」「ドキュメントにも自動でシリアライズされるオブジェクトにRangeが追加されていますね」「何度か話題にしたglobalidも含まれている(ウォッチ20181203)」
# activejob/test/cases/argument_serialization_test.rb#L21
[ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
"a", true, false, BigDecimal(5),
:a,
1.day,
Date.new(2001, 2, 3),
Time.new(2002, 10, 31, 2, 2, 2.123456789r, "+02:00"),
DateTime.new(2001, 2, 3, 4, 5, 6.123456r, "+03:00"),
ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, "59.123456789".to_r), ActiveSupport::TimeZone["UTC"]),
[ 1, "a" ],
{ "a" => 1 },
ModuleArgument,
ModuleArgument::ClassArgument,
- ClassArgument
+ ClassArgument,
+ 1..,
+ 1...,
+ 1..5,
+ 1...5,
+ Date.new(2001, 2, 3)..,
+ Time.new(2002, 10, 31, 2, 2, 2.123456789r, "+02:00")..,
+ DateTime.new(2001, 2, 3, 4, 5, 6.123456r, "+03:00")..,
+ ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, "59.123456789".to_r), ActiveSupport::TimeZone["UTC"])..,
].each do |arg|
参考: class Range (Ruby 3.0.0 リファレンスマニュアル)
🔗 Psych 4でのbreaking changesに対応
- PR: Fix compatibility with Psych 4 by byroot · Pull Request #42249 · rails/rails
- PR: Fix ruby-master test suite (Psych 4.0.0) by casperisfine · Pull Request #42257 · rails/rails
Psych 5の
load
がデフォルトでsafe_load
になった: #487
そのため、エイリアスを明示的に許可する必要がある。
実際、Ruby 3.0でもload(aliases: true)
を受け付けないPsychが同梱されている。
同PRより大意
つっつきボイス:「先週取り上げたPsych gemのbreaking changesがRailsにも来たそうです(ウォッチ20210518)」「先週出られなかったんですけどPsychって何でしたっけ」「yamlのパーサーですね」「修正はYAML.load()
でエイリアスを有効にしたのか」
# activesupport/lib/active_support/configuration_file.rb#L21
def parse(context: nil, **options)
- YAML.load(render(context), **options) || {}
+ source = render(context)
+ begin
+ YAML.load(source, aliases: true, **options) || {}
+ rescue ArgumentError
+ YAML.load(source, **options) || {}
+ end
rescue Psych::SyntaxError => error
raise "YAML syntax error occurred while parsing #{@content_path}. " \
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{error.message}"
end
「#42257のテスト更新でもunsafe_load
とload
の使い分けに対応している↓」「これは対応しないわけにいかないでしょうね」
# actionpack/test/controller/parameters/serialization_test.rb#L24
test "yaml deserialization" do
- params = ActionController::Parameters.new(key: :value)
+ roundtripped = YAML.load(YAML.dump(params))
+ payload = YAML.dump(params)
roundtripped = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(payload) : YAML.load(payload)
assert_equal params, roundtripped
assert_not_predicate roundtripped, :permitted?
end
「そういえば先週のhackmd.ioのRuby-dev office hour↓でもPsychが話題になっていました」「変更前にChangelogやアナウンスがあってもいいのにという気持ちはわかる」「Railsの場合はbreaking changesの前にdeprecationによる猶予期間を置きますけど、Railsの外のgemなのでリリースポリシーがRailsと違うのは仕方ないかも」
参考: Ruby-dev office hour - HackMD
🔗 ActiveRecord::Base.logger
がclass_attribute
になった
- PR: Make ActiveRecord::Base.logger a class_attribute by casperisfine · Pull Request #42237 · rails/rails
つっつきボイス:「mattr_accessor
からclass_attribute
に変更されたのか↓」「ロガーが速くなったんですね」「プルリクにも書かれているようにここがホットスポット(実行の頻度が高いコード)なのは確かなので、これが速くなるのはいいことだと思います👍」
# activerecord/lib/active_record/core.rb#L20
- mattr_accessor :logger, instance_writer: false
+ class_attribute :logger, instance_writer: false
cattr_accessor
が依存しているクラス変数は、長大なancestorチェインでのパフォーマンスがよくない(詳しくは#17763を参照)。
ActiveRecord::Base
のclass_attribute
はそれよりも7倍程度高速。
Calculating -------------------------------------
logger 1.700M (± 0.9%) i/s - 8.667M in 5.097595s
clogger 11.556M (± 0.9%) i/s - 58.806M in 5.089282s
Comparison:
clogger: 11555754.2 i/s
logger: 1700280.4 i/s - 6.80x (± 0.00) slower
高速な理由は
ActiveRecord::Base.ancestors.size == 62
による。
ActiveRecord::Base
のその他のcattr
の利用についても考慮すべきだが、さしあたってlogger
が最大のホットスポットなので、ここから議論を始めるのがよさそう。
同PRより大意
🔗 NULLS FIRST
をどのデータベースでも使えるnulls_first()
が追加
つっつきボイス:「NULLS FIRST
は、SQLを長くやっている人ならご存知の機能だと思います: nullableなカラムをソートしたときに、NULLが冒頭に来るか末尾に来るかを指定する」「そういえばそんな機能ありましたね」
「並び順でのNULLの位置を変更する機能はプルリクにも書かれているようにDBMS依存なので、DBMSによって使えるものとそうでないものがあります」「そうでした」「もっともSQLの概念上は、本来NULLはどの値でもないものなので、NULLを含むカラムをORDER BYするのは無理があるとは思いますけどね」「たしかに」「DBMSによってはNULLがあるカラムをORDER BYすると遅くなるものもあったと思いますし、まずはORDER BYするカラムがnullableにならないようにしておくことでしょうね」
「プルリクによると、ANSI SQLではASC NULLS FIRST
についてはサポートされているそうですね↓」「へ〜!」「MySQLだけFIRST固定でLASTを選べないのか...頑張って欲しいです」「DBMSごとにこういう微妙な差があることは知っておくべきですね」
ほとんどのデータベースではテーブルを並べ替えると
NULL
値が冒頭に来て、その他の値がそれに続く。PostgreSQLではNULLS LAST
が使える。
ありがたいことに、ANSI SQLにはNULLS
をソート順の冒頭に置くか末尾に置くかをデータベースで指定できるオプションがある。
ORDER BY column ASC NULLS FIRST
MS SQL、SQLite、Oracle、PostgreSQLは上の構文をサポートしているが、残念ながらMySQLはこの構文をサポートしていない。
変更前:
* PostgreSQL:.nulls_first()
も.nulls_last()
も動く
* その他のDB: どちらの場合もエラーになる変更後:
* PostgreSQL: どちらも動く
* MySQL:.nulls_first()
は動くが.nulls_last()
はランタイムエラーになる
* その他のDB どちらも動くPostgreSQL向けの機能は#38131で導入された。このプルリクは他のDBにもこの機能を追加する。
同PRより大意
🔗Rails
🔗 active_period: 時間や期間のサポートを強化(Ruby Weeklyより)
つっつきボイス:「Period.today
やPeriod.year('01/01/2021')
みたいな書き方ができるgemだそうです」「こういうのは普通に自分で書くこともできるでしょうし、RailsのActiveRecord::Duration
がそもそも優秀なので、自分がこのgemを入れることはあまりなさそうかも」
# The FreePeriod from 01/01/2021 to 01/02/2021 has 5 weeks
Period.new('01/01/2021'...'01/02/2021').weeks.count # 5
# The StandardPeriod::Month for 01/01/2021 has 4 weeks
Period.month('01/01/2021').weeks.count # 4
# How many day in the current quarter
Period.this_quarter.days.count
# Get all the quarters overlapping a Period of time
Period.new(Time.now..2.month.from_now).quarters.to_a
「上のquarter(四半期)みたいなのはあってもいいかもと一瞬思いましたけど、四半期の概念は国や企業ごとに異なるのでたぶん統一的にやりづらいと思うんですよ」「言われてみれば、第1週みたいな"週"の概念も会社ごとに違ったりしますよね: 第1四半期の第一週はどの週なのか、とか」「たしかに」「週を日曜始まりにするか月曜始まりにするかとかも考え始めると大変そう」「日付と期間はややこしいですね...」
🔗 RSpecテストをシンプルにする5つのルール(Ruby Weeklyより)
つっつきボイス:「先週に続いてRSpecテストの書き方記事です」「ネストを深くしない、let
はトップに置く、この辺はわかる」
「ところで、このcreate_user.(name: "Jane", email: "jane@doe.org")
という書き方がカリー化的で変わってるな〜↓」
RSpec.describe CreateUser do
subject do
CreateUser.new
end
context "with valid params" do
specify {
expect(create_user.(name: "Jane", email: "jane@doe.org")).to be_success
}
end
end
4. デフォルトではモックを使わない
(中略)少なくとも本物を使うと以下の問題がある場合にのみモックを使おう:
* テストのセットアップが著しく複雑になる場合
* テストが著しく遅くなる場合
* 外部システム(ファイルシステム)で望ましくない副作用が起きる場合
* インターネット接続に依存する場合
同記事より抜粋・大意
「4.はデフォルトではモックを使うなとありますけど、これはどういうことでしょう?」「自分も、他で再利用されていないコードをわざわざモックでテストする必要性をあまり感じない方です: そういうコードは結合した状態でテストしないと意義が薄いのではないかと感じることもよくあります」「あ、なるほど」
「そのコードが複数の場所で相互に利用されているのであればモックを使う意味はあると思いますけど、1箇所でしか使われていないコードだったら、そのコードを使っている場所でまとめてテストする方がいいんじゃないかという気持ちになりますね」「言われてみればそうかも」「モックを使って両方でテストを書く方がたしかにきれいですし、エラーがどちらで起こったかがわかりやすいという面もあるんですけど、テストを2つ書くコストに見合うかどうかを考えるとモックを使わずに書くことが多いかな」
「5.はテストをDRYではなくベタに書くことを容認しようということですね」「よく言われるヤツ」「テストはそういうさじ加減が難しい...」
「ところで4.にもサンプルコードがあったらいいのにとちょっと思いました」「テストに関する記事って、あまり具体的すぎても伝わりにくいところがあるんですよ: テストを書いた背景を説明し始めるとテーマがどんどん広がって収拾がつかなくなりがち」「それたしかに!」「なので、テストについて説明しようとすると、この記事のように抽象的なさじ加減について書くことが多いですね: そういうのにハマったことのある人はこの記事を読んでみると思い当たるところがあると思います👍」
🔗 Active Supportのあまり知られていない機能5つ(Ruby Weeklyより)
つっつきボイス:「知らないかもしれないActive Supportの機能か、どれどれ」
「ActiveSupport::Callbacks
は意識せずに使っている人も多いんじゃないかな」「お〜」「こうやってinclude
して使える↓: コールバックは育ちすぎると後で苦しむことになるので、ほどほどにするのがよいと思います」
# 同記事より
class BaseService
include ActiveSupport::Callbacks
define_callbacks :call
def call
run_callbacks :call do
process
end
end
end
「ActiveSupport::Configurable
は単体で使ったことはありませんが、そういえばActive Supportにありますね」「自分ならconfig gemを使うかな」「自分もそうするかも」
# 同記事より
class Example
include ActiveSupport::Configurable
end
Example.config.some_option = 'value'
Example.config.some_option
=> "value"
「お、知る人ぞ知るActiveSupport::CurrentAttributes
も紹介されていますね: 一種のスレッドセーフなグローバル変数的な機能」「そういえば以前翻訳した記事↓では反対意見が出ていました」「CurrentAttributes
の是非はともかく、存在を知っておくことは重要でしょうね: Railsらしく書く方法が他にどうしても見つからないときに、スレッドセーフかつグローバルなデータアクセス方法としてCurrentAttributes
を知っておけば思い出せるので」「なるほど」
「CurrentAttributes
ではスレッドセーフは保証されていますが、グローバルであることには変わりないので、基本的には非推奨ぐらいに思っておくのがよいと思います」「そうですね」
参考: ActiveSupport::CurrentAttributes
「ActiveSupport::MessageVerifier
は、そういえば以前署名付きidに関連してRails 6.1で追加されてましたね(ウォッチ20200525)」「これはどういうものでしょう?」「署名付きidという一種のシグネチャを一方向に生成できて、後でverifyしたり期限切れにしたりできるというものですね」「お〜」
# 同記事より
key = 'my_secret_key' # use env variable or generate via ActiveSupport::KeyGenerator
verifier = ActiveSupport::MessageVerifier.new(key)
verifier.generate('my message')
=> "BAhJIg9teSBtZXNzYWdlBjoGRVQ=--078c6389020294311bb45f099ab56450d9127d44"
参考: ActiveSupport::MessageVerifier
「ActiveSupport::MessageEncryptor
はちょうどDeviseがらみで暗号化を手動で解除するのに使いましたよ」「うう、大変そう...」「ちなみにこれはRails 7に入る予定のActiveRecordのカラムの暗号化(ウォッチ20210330)とは別物で、Active SupportがRubyの世界の範囲内で暗号化を行うもので、昔からあります」「なるほど」
# 同記事より
key = SecureRandom.random_bytes(32)
crypt = ActiveSupport::MessageEncryptor.new(key)
message = crypt.encrypt_and_sign('my message')
crypt.decrypt_and_verify(message)
=> "my message"
参考: ActiveSupport::MessageEncryptor
「ちなみにActive Supportはよくできていて、自作のクラスにしれっとinclude
しても普通に使えるんですよ」「なるほど、独立性が高いんですね」「Active Supportのコードをたまに眺めてみると、自分のところでも使える機能がちょくちょく見つかるので、おすすめですよ👍」
参考: Active Support コア拡張機能 - Railsガイド
🔗 RSpecの歴史
つっつきボイス:「すべてはアサーションから始まって、やがてRSpecが誕生し、DSLでより自然言語的に書けるようになっていた...という感じで歴史をたどる記事」「Railsを始めて間もない人やこの辺の歴史を知らない人は読んでおくとよいと思います: よさそうな記事👍」「翻訳してみたいです」
以下はつっつき後のツイートです。
訳したいです!
尋ねてみますね😋— ハングリィ・ライク・カネゴン (@hachi8833) May 24, 2021
🔗 その他Rails
つっつきボイス:「ついにtherubyracer gemがexecjsの対象から外されたのか〜」「therubyracerがインストールできなくてつらかった思い出があります😢」「therubyracerは、元々ビルド済みのtherubyracerを使えば環境に悩まされずに使えるJSエンジンとして登場したので、それだと逆ですよね😆」「まったくです...」
「事情があってJSエンジンをapt
コマンドなどでインストールできない環境でも、gemとしてJSエンジンをインストールできるのがtherubyracerだったんですよ」「そういうものだったんですか」「それにしては随分このgemで苦労しました」
「therubyracerはたしかバージョン番号が偶数と奇数で違うんですよ: どちらかがローカルでビルドが走って、どちらかがビルド済みという違いだった気がします」「そうだったかも」「そのあたりを説明した記事が見つからない...いずれにしろお役御免なので別にいいかな」「今はこの辺で苦労しなくなって本当によかったと思います」
久々にこの電子書籍を読んでみたけど、どのページもすごくいいこと書いてあるなー。時間があったら翻訳したい。(時間があれば)
Practicing Rails: Learn Rails Without Getting Overwhelmed - Justin Weiss https://t.co/cX1hX18t8M
— Junichi Ito (伊藤淳一) (@jnchito) May 17, 2021
「Practicing Railsは、Railsチュートリアルを終えたぐらいの人をターゲットにした書籍みたいです」「たしかにそうした読者層にとって助けになるものがあるといいでしょうね」「新技術をどうキャッチアップしていくかみたいな、Railsエンジニアとしての心構えなどに重点が置かれていそうな感じかな👀」
参考: Ruby on Rails チュートリアル:プロダクト開発の0→1を学ぼう
前編は以上です。
バックナンバー(2021年度第2四半期)
週刊Railsウォッチ(20210518後編)RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか
- 20210517前編 Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210510前編 属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか
- 20210427後編 RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか
- 20210420後編 ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか
- 20210419前編 RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか
- 20210413後編 RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか
- 20210412前編 Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか
- 20210407後編 エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか
- 20210406前編 GitHubが修正したRailsセッションハンドリングの競合、erb/haml/slimの速度比較ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)