Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20210524前編)Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗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シリアライザを追加

# 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に対応

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_loadloadの使い分けに対応している↓」「これは対応しないわけにいかないでしょうね」

# 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

ruby/psych - GitHub

「そういえば先週のhackmd.ioのRuby-dev office hour↓でもPsychが話題になっていました」「変更前にChangelogやアナウンスがあってもいいのにという気持ちはわかる」「Railsの場合はbreaking changesの前にdeprecationによる猶予期間を置きますけど、Railsの外のgemなのでリリースポリシーがRailsと違うのは仕方ないかも」

参考: Ruby-dev office hour - HackMD

🔗 ActiveRecord::Base.loggerclass_attributeになった


つっつきボイス:「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::Baseclass_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より大意

Rails: クラスレベルの3つのアクセサの使い方を比較する(翻訳)

🔗 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より)

billaul/active_period - GitHub


つっつきボイス:「Period.todayPeriod.year('01/01/2021')みたいな書き方ができるgemだそうです」「こういうのは普通に自分で書くこともできるでしょうし、RailsのActiveRecord::Durationがそもそも優秀なので、自分がこのgemを入れることはあまりなさそうかも」

参考: ActiveSupport::Duration

# 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"

rubyconfig/config - GitHub

「お、知る人ぞ知るActiveSupport::CurrentAttributesも紹介されていますね: 一種のスレッドセーフなグローバル変数的な機能」「そういえば以前翻訳した記事↓では反対意見が出ていました」「CurrentAttributesの是非はともかく、存在を知っておくことは重要でしょうね: Railsらしく書く方法が他にどうしても見つからないときに、スレッドセーフかつグローバルなデータアクセス方法として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を始めて間もない人やこの辺の歴史を知らない人は読んでおくとよいと思います: よさそうな記事👍」「翻訳してみたいです」

以下はつっつき後のツイートです。

🔗 その他Rails


つっつきボイス:「ついにtherubyracer gemがexecjsの対象から外されたのか〜」「therubyracerがインストールできなくてつらかった思い出があります😢」「therubyracerは、元々ビルド済みのtherubyracerを使えば環境に悩まされずに使えるJSエンジンとして登場したので、それだと逆ですよね😆」「まったくです...」

「事情があってJSエンジンをaptコマンドなどでインストールできない環境でも、gemとしてJSエンジンをインストールできるのがtherubyracerだったんですよ」「そういうものだったんですか」「それにしては随分このgemで苦労しました」

「therubyracerはたしかバージョン番号が偶数と奇数で違うんですよ: どちらかがローカルでビルドが走って、どちらかがビルド済みという違いだった気がします」「そうだったかも」「そのあたりを説明した記事が見つからない...いずれにしろお役御免なので別にいいかな」「今はこの辺で苦労しなくなって本当によかったと思います」


「Practicing Railsは、Railsチュートリアルを終えたぐらいの人をターゲットにした書籍みたいです」「たしかにそうした読者層にとって助けになるものがあるといいでしょうね」「新技術をどうキャッチアップしていくかみたいな、Railsエンジニアとしての心構えなどに重点が置かれていそうな感じかな👀」

参考: Ruby on Rails チュートリアル:プロダクト開発の0→1を学ぼう


前編は以上です。

バックナンバー(2021年度第2四半期)

週刊Railsウォッチ(20210518後編)RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。