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

週刊Railsウォッチ: Evil MartiansのDocker+Rails記事が大幅更新、Railsガイドが電子書籍でほか(20220328前編)

こんにちは、hachi8833です。忌引のため刊行が空いてしまいました🙇

週刊Railsウォッチについて

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

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

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のChangelogから見繕いました

🔗 ActionController::Parameters.to_hにブロックを渡せるようになった

ActionController::Parameters.to_hにブロックを渡せるようにして、RubyのHash#to_hと整合するようにする。渡されるブロックはキーバーリューを受け取って要素2個の配列(キーペア)をyieldし、これをハッシュに変換できる。
同PRより


つっつきボイス:「to_h {|key, value| block }のようにブロックを渡してキーバリューを引数にすることで新しいハッシュを生成できるようになった🎉」「これは便利そう」「一見直感的でなさそうにも思えるけど、to_hにブロックを渡すなら|key, value|以外のものはあまり使わなそうなので納得」

# #Lhttps://github.com/rails/rails/pull/44756/files#diff-99dab0dfb4d0cfa044997480d5aa1b100dc60347a10cedd6d8f7a0395f6a6efd#300
-   def to_h
+   def to_h(&block)
      if permitted?
-       convert_parameters_to_hashes(@parameters, :to_h)
+       convert_parameters_to_hashes(@parameters, :to_h, &block)
      else
        raise UnfilteredParameters
      end
    end

🔗 MariaDBのデフォルト関数サポートを修正

"db/schema.rb"にデフォルトが誤った形で書き込まれてdb:schema:loadが正しく動かなくなることがある。さらに新規レコード保存時に関数名が文字列コンテンツとして追加される。
kaspernj
同Changelogより


つっつきボイス:「MariaDBはMySQLとほぼ互換のRDBMSですね」「これはバグ修正」「デフォルト関数って何だろう?」「スキーマに出力されるこのdefault:のことのようですね↓」

# activerecord/test/cases/migration_test.rb#
      def test_default_functions_on_columns
        with_bulk_change_table do |t|
          if current_adapter?(:PostgreSQLAdapter)
            t.string :name, default: -> { "gen_random_uuid()" }
          else
            t.string :name, default: -> { "UUID()" }
          end
        end

参考: MariaDB - Wikipedia
参考: DEFAULT - MariaDB Knowledge Base

「テストでPostgreSQLについて言及されているのは、UUID()は他のRDBMSにはあるけどPostgreSQLにはないので代わりにgen_random_uuid()を使うということみたい」「to_aas: :hashを渡せるのは初めて見た↓」「そう言えばこの書き方どこかで見て不思議に思ったことあります」「テストコードは学びありますね」

        if current_adapter?(:PostgreSQLAdapter)
          assert_equal "gen_random_uuid()", column(:name).default_function
          Person.connection.execute("INSERT INTO delete_me DEFAULT VALUES")
          person_data = Person.connection.execute("SELECT * FROM delete_me ORDER BY id DESC").to_a.first
        else
          assert_equal "uuid()", column(:name).default_function
          Person.connection.execute("INSERT INTO delete_me () VALUES ()")
          person_data = Person.connection.execute("SELECT * FROM delete_me ORDER BY id DESC").to_a(as: :hash).first
        end

🔗 audio_tagvideo_tagActiveStorage::Attachmentオブジェクトを渡せるようにした

以下のように書けるようにした。

audio_tag(user.audio_file)
video_tag(user.video_file)

従来は以下のように書かないといけなかった。

audio_tag(polymorphic_path(user.audio_file))
video_tag(polymorphic_path(user.video_file))

image_tagでは既にサポート済みなので、同じ書き方ができるようにした。
同PRより


つっつきボイス:「前はpolymorphic_pathを介して添付ファイルを渡さないといけなかったのが直接渡せるようになったんですね」「polymorphic_pathは何をするんでしょうか?」「添付ファイルがポリモーフィックになっていて、そのパスを取り出すのに必要だったんでしょうね: 添付ファイルみたいなblobオブジェクトにはパス情報以外のものも含まれているので」「なるほど」「たしかに直接渡せる方が簡潔でわかりやすい👍」

参考: Rails API: polymorphic_path -- ActionDispatch::Routing::PolymorphicRoutes
参考: Rails API: ActiveStorage::Attachment

🔗 Scaffoldで生成されるURLコメントの名前空間を修正

再現手順

rails new test_blog
cd test_blog
./bin/rails generate model Post title context:text
./bin/rails g scaffold_controller Admin::Post title context:text --model-name=Post

期待される振る舞い
生成されるURLコメントに名前空間がプレフィックスされるべき(例: # GET /admin/posts/admin/posts.json

実際の振る舞い
生成されるURLコメントに名前空間がプレフィックスされない(例: # GET /posts/posts.json

システム構成
Rails version: v7.0.2.2
Ruby version: v3.1.0
同PRより


つっつきボイス:「これはScaffoldジェネレータのバグ修正」「--model-name=Postを指定してコントローラはAdmin::Postsにしたときにコメントに名前空間が付かなかったのね」「コメントなので動作には影響はないけど」

「自分はScaffoldジェネレータをめっきり使わなくなった」「私はシンプルな用途が多いので割と使っちゃいます」「Scaffoldジェネレータは名前空間で使うと微妙に不便なところはありますね」

🔗 to_timeの古い振る舞いを非推奨化

  • Ruby 2.4より前のto_timeの振る舞いを非推奨化
    Ruby 2.4以降はto_timeが変更されて、従来はローカルシステム時間へ変換していたのがレシーバーのオフセットを維持するようになっている。当時のRailsは古いRubyをサポートしていたので、移行支援のために互換性レイヤを追加していた。Rails 5.0以降の新しいアプリケーションではRuby 2.4以降の振る舞いがデフォルトになり、Rails 7.0ではRuby 2.7以降のみがサポートされているため、この互換性レイヤは安全に撤去可能。
    非推奨警告は、この設定がfalseの場合のみ表示される(互換性レイヤの削除が影響を及ぼす唯一のシナリオであり、ノイズを減らすため)。
    Andrew White
    同PRより

つっつきボイス:「古いRubyのto_timeの振る舞いがもう使われなくなったので警告表示を変更したんですね」

# activesupport/lib/active_support.rb#L112
  def self.to_time_preserves_timezone=(value)
+   unless value
+     ActiveSupport::Deprecation.warn(
+       "Support for the pre-Ruby 2.4 behavior of to_time has been deprecated and will be removed in Rails 7.1."
+     )
+   end
+
    DateAndTime::Compatibility.preserve_timezone = value
  end

参考: Time#to_time (Ruby 3.1 リファレンスマニュアル)

🔗Rails

🔗『クジラに乗ったRuby』英語版が更新(Ruby Weeklyより)


つっつきボイス:「以前翻訳したEvil Martiansの『クジラに乗ったRails』の元記事がっつり更新されて章も1つ追加されたそうです↓」「この記事はアクセスが多いですよね」「私自身これでDockerを学んだようなものでした」

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

日本語版もじきに更新します↑。試しにdiffを取ってみたら全文更新されていたので別記事にするかもしれません。

「お、今回の元記事更新でDocker Compose v2の記法に対応していますね」「ちょうどさっきV2について話してたのでタイムリー(明日の記事で取り上げます)」

参考: Compose V2 | Docker Documentation

「Dockerはいろいろ自由すぎて難しい面がありますよね」「ほんとに」「Docker Composeと一緒に学ぶならある程度型にはめられるけど、Docker自体は型らしいものがあまりないので難しい」

「DockerとDocker Composeは一度セットアップできれば後はだいたい使い回すかな」「自分も使いまわしまくってます↓」

# 元記事より
x-app: &app
  build:
    context: .
    args:
      RUBY_VERSION: '3.1.0'
      PG_MAJOR: '14'
      NODE_MAJOR: '16'
      YARN_VERSION: '1.22.17'
  image: example-dev:1.0.0
  environment: &env
    NODE_ENV: ${NODE_ENV:-development}
    RAILS_ENV: ${RAILS_ENV:-development}
  tmpfs:
    - /tmp
    - /app/tmp/pids

「上のようにdocker-compose.ymlでx-*を使う記法は自分も使ってますね↑」「私もこれで記事を書きました↓: 定数を使い回せるのがありがたいです」「この記法にするとdocker-compose.ymlの可変要素をファイルの冒頭に集約できるもいいですよね」「そうそう」「docker-compose.ymlが長大になってくるとメンテナンスが難しくなってくるので、環境変数のような可変要素を集約できるのはいい👍」

docker-compose.ymlの中で値を使い回す方法

「Evil Martiansのdip↓は特に使っても使わなくてもいいかなという感じ」「自分はdipでDocker Compose操作を覚えたのでdip使いまくりで、もうコマンドオプションを長々と指定する気になれないです」「自分はpsqlをバージョン指定して使いたいこととかもよくあるので、素のDockerコマンドも未だに使いますね」

docker-composeを便利にするツール「dip」を使ってみた

🔗 Railsのパラレルテストの注意点(Ruby Weeklyより)


つっつきボイス:「AppSignalの記事です」「テストの個数が少ないときにパラレル化しても遅くなるだけと書かれている: データベースをdatabase-test-0とかdatabase-test-1みたいに複数立ち上げてfixtueを読み込めば当然セットアップに時間がかかる」「たしかに」「あと、データベースのテストそのものがボトルネックになっていればパラレル化しても遅くなる可能性は考えられます」

# 同記事
class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors)
end

「ファイルアクセスなどDB以外のリソースにアクセスするテストは、操作の重複を避けておかないとパラレル化したときにたまに失敗してしまうとも記事にある↓、たしかに」

# 同記事より
# Worker 1 executes
file = File.write('test.txt', 'created')

# Worker 2 executes
file = File.write('test.txt', 'deleted')
File.delete('test.txt')
assert_not(File.exist?('test.txt'))

# Worker 1 executes
assert_path_exists('test.txt')

🔗 RailsでRBSとSteepを試してみて


つっつきボイス:「Rails + RBS & Steep記事がだんだん出てくるようになりましたね」「ちょうど明日の銀座Rails#43↓にpockeさんが『RBSとRailsの今』というタイトルで登壇しますよ(編集部注: 同イベントは同日に終了しました)」

「RBSといえば、Ruby 3.1にrbs collectionという新しいコマンドが追加されていて、gem_rbs_collectionからコレクションを取得できるようになっているんですよ↓」「お、これは知りませんでした」「それもあって銀座RailsでpockeさんにRBSの話をしてもらおうと企画しました」

ruby/gem_rbs_collection - GitHub

「Ruby 3.0や3.1で細かな機能がいろいろ追加されていて見落としてた...」「3.1はコードの互換性にはほぼ影響しないので大きな変更はなさそうに見えるけど、ruby/debugの標準ライブラリ化やrbs collectionのように地味に機能が増えていますね」

参考: Ruby 3.1.0 リリース

「個人的にはそろそろRBSやSteepは普通に既存のRailsプロジェクトで使い始めてもいいかなと思います」「ふむふむ」「すべての機能をRBSなどでカバーするつもりはありませんが、少数のクリティカルな機能に絞って型検査を導入するならやってもいいかも👍」「たしかに」

「メソッドが想定外の使われ方をすることってよくあるじゃないですか」「想定外の使われ方は普通の方法だと見つけるだけでも大変ですよね」「そういう部分はなかなかテストが追加されないので、RBSなどでそういう部分を拾えるようになったら嬉しい」「セーフティーネット的に使えるといいですよね」

🔗 where.firstfind_byRuby Weeklyより)


つっつきボイス:「最近のActive Recordは賢いのでwhere.firstfind_byの違いを埋めてくれそうな気もしたけど、where.firstでやるとORDER BYが付くので全件スキャンが走って遅くなる可能性があるらしい↓」「記事のケースはいろいろ複雑で、単純なインデックスでは役に立たなかったと書いてますね」「単純なケースなら問題なくても、状況次第ではORDER BYが入るだけで遅くなる可能性があるのか」「VIEWでUNION ALLして複数テーブルを結合してるとかかな?」

# 同記事より
User.where(email: "andy@goodscary.com")
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"

User.where(email: "andy@goodscary.com").first
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
# ORDER BY "users"."id" ASC
# LIMIT 1

User.find_by(email: "andy@goodscary.com")
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
# LIMIT 1

「ちなみにこの2番目のfind_by_emailみたいな動的な書き方は一応使えるけど、もう古い↓」「あ、そうだったかも」

# 同記事より
User.find_by(email: "andy@goodscary.com")
User.find_by_email("andy@goodscary.com")

後で調べると、RuboCop Railsでも警告されるそうです↓。

参考: Class: RuboCop::Cop::Rails::DynamicFindBy — Documentation for rubocop (0.47.1)

「そういえば、find_byはレコードが複数あっても最初の1件しか返さないけど、Railsに最近入ったfind_sole_byは1件だけの場合にレコードを返してそれ以外はエラーにするというのがありましたね」「以前取り上げましたね(ウォッチ20210112)」

参考: find_sole_by -- ActiveRecord::FinderMethods

🔗 Railsで最初の頃に混乱しがちな点(Ruby Weeklyより)


# 同記事より
bob = Friend.find_or_initialize_by(first_name: "Bob")

つっつきボイス:「find_or_initialize_byだけだとsaveされないので、それに気づかずに操作して驚くという、よくある話」「永続化されてなかったんですね」「Railsを長く使っていれば気付ける話ではありますが、Railsがいろいろ便利な分、慣れないうちはRubyのオブジェクトを変更しただけなのかRDBに保存されたかはすぐに意識しにくいかもしれませんね」「たしかに」「最近のRailsはeager loadingされることも多いので、どのコードのどの部分でクエリが発生するかというのは見えづらいでしょうね: ビューを処理するまでクエリが発生しないこともあるので」

参考: find_or_initialize_by -- ActiveRecord::Relation

その他Rails

つっつきボイス:「ViewComponentにしたらロジックの置き場所が腑に落ちて、今までホームレスだったコードにホームができて嬉しいという感じの記事でした」「ViewComponentはどうしても数が増えがちなので、置き場所が落ち着くとやりやすいというのはワカル」

github/view_component - GitHub


「Railsガイドでこれまで後回しになっていたRails 2系のリリースノート2本をこの間やっと翻訳して未訳を埋められました」「Railsガイドそのものができたのも2.2からだったのね」「訳していて知らないものがいろいろ出てきて考古学な気持ちになりました」「2系は触ったことはあるけどrails newしたことがなかった」「歴史楽しい」

参考: Ruby on Rails 2.2 リリースノート - Railsガイド
参考: Ruby on Rails 2.3 リリースノート - Railsガイド

「それと前後して、Railsガイドの電子書籍を電子書籍でも買えるようになりました↓」「1000ページ超え」「紙に印刷したら立方体みたいになるかも😆」

「昨年末にRails 7がリリースされた直後からRailsガイドの差分更新翻訳をかけたんですが、過去の恥ずかしい訳や更新漏れなどの修正も含めて結果的に全文を見直しました」「すごい根性💪」「こういうことになるとムキになってしまいます」

参考: Ruby on Rails ガイド:体系的に Rails を学ぼう


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Crystal言語作者がRubyを愛する理由、TypeScript 4.6リリースほか(20220309後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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