- Ruby / Rails関連
週刊Railsウォッチ(20210222)ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下のコミットリストのChangelogを中心に見繕いました。
🔗 render 'partial'
でのインスタンス変数代入が非推奨化された
Railsのパーシャルレンダリングでは、以下のようなインスタンス変数の代入が許されている。
render 'partial', :@name => "Maceo"
上は
ActionView::Base
で@name
を"Maceo"に設定する。
ユーザーが定義したインスタンス変数は代入が許されているのみならず、@_assigns
や@output_buffer
や@_config
や@_default_form_builder
といったprivateなRails変数の上書きも可能になってしまっている。この問題は当初Hackeroneに投稿され、@tenderloveがこの挙動をpublicで非推奨化することを決定した。
同PRより大意
つっつきボイス:「え、今までrender 'partial', :@name => "Maceo"
みたいな書き方が可能だったの?」「自分もさっきrender 'partial'
でインスタンス変数に:
が付いているのを見てびっくりしました」「こんな書き方がシンタックスエラーにならなかったとは知らなかった...」「初めて見ました」「こういうふうに書くこと自体思い付かなかった」
🔗 Active Jobのperform_later
でジョブのenqueue失敗を処理できるようになった
perform_later
に、アダプタがジョブのenqueue試行後に実行されるブロックをオプションとして渡せるようになった。このブロックは、enqueueが成功しなかった場合であってもジョブのインスタンスを受け取る。
さらに、ActiveJob
アダプタがActiveJob::EnqueueError
エラーをraiseできるようになった。これはジョブインスタンス内でキャッチされて保存されるので、raiseされたEnqueueError
をジョブのenqueueを試行するコードがブロックを用いてinspectできるようになる。
MyJob.perform_later do |job|
unless job.successfully_enqueued?
if job.enqueue_error&.message == "Redis was unavailable"
# invoke some code that will retry the job after a delay
end
end
end
Daniel Morton
Changelogより大意
つっつきボイス:「perform_later
にブロックを渡してジョブのenqueueに失敗した時の処理を書けるようになったのか」「perform_later
ブロックを渡す方法、Active Jobで発生するようになったEnqueueError
を使う方法、successfully_enqueued
と
enqueue_error
プロパティを使う方法の3つが実装されたみたい」「テストコードを見ると3とおりともテストしてますね↓」
# activejob/test/cases/queuing_test.rb#42
test "job is yielded to block after enqueue with successfully_enqueued property set" do
HelloJob.perform_later "John" do |job|
assert_equal "John says hello", JobBuffer.last_value
assert_equal [ "John" ], job.arguments
assert_equal true, job.successfully_enqueued?
assert_nil job.enqueue_error
end
end
test "when enqueuing raises an EnqueueError job is yielded to block with error set on job" do
EnqueueErrorJob.perform_later do |job|
assert_equal false, job.successfully_enqueued?
assert_equal ActiveJob::EnqueueError, job.enqueue_error.class
end
end
# activejob/test/jobs/enqueue_error_job.rb#3
class EnqueueErrorJob < ActiveJob::Base
class EnqueueErrorAdapter
class << self
def enqueue(*)
raise ActiveJob::EnqueueError, "There was an error enqueuing the job"
end
def enqueue_at(*)
raise ActiveJob::EnqueueError, "There was an error enqueuing the job"
end
end
end
self.queue_adapter = EnqueueErrorAdapter
def perform
raise "This should never be called"
end
end
「Changelogのコード例にある"Redis was unavailable"みたいに、enqueueに失敗する可能性は常にあるので、EnqueueError
は必要ですね👍」「このやり方覚えとこうっと」
「この機能が追加されたということは、これまでperform_later
でジョブのenqueueが失敗したときの公式な処理手段がなかったということなんでしょう」「今まではどうやって処理してたんでしょうね?」「変更前のperform_later
ではenqueue
を返してはいたけど↓、successfully_enqueued
とenqueue_error
などに相当するものがなかったので、enqueueが成功したかどうかを知る方法がなかったということなのかな?」「この感じだと公式な処理方法はこれまでなかったみたいですね」「従来はsuccessfully_enqueued
とenqueue_error
などに相当するものを自分で書いて処理するしかなかったんだろうと想像できます」
# activejob/lib/active_job/enqueuing.rb#L
def perform_later(*args)
- job_or_instantiate(*args).enqueue
+ job = job_or_instantiate(*args)
+ enqueue_result = job.enqueue
+
+ yield job if block_given?
+
+ enqueue_result
end
🔗 ActiveRecord::Relation
の新メソッド2つ: load_async
とexcluding
このメソッドは、スレッドプールから非同期実行されるようにクエリをスケジューリングする。
バックグラウンドスレッドがクエリを実行可能になる前にこの結果にアクセスすると、フォアグラウンドで実行されるようになる。
これは、結果が必要になるより前の実行時間が長いクエリや、独立したクエリを複数実行する必要のあるコントローラで有用。
def index
@categories = Category.some_complex_scope.load_async
@posts = Post.some_complex_scope.load_async
end
Jean Boussier
#41372 Changelogより大意
つっつきボイス:「1つ目はActiveRecord::Relation#load_async
メソッドですか」「コードを見るとRubyのThread
で実装されていますね↓:将来もしかするとThread
の代わりにRubyのRactorを使うようになるのかもしれないとちょっと想像してみました」
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L460
def schedule_query(future_result) # :nodoc:
@async_executor.post { future_result.execute_or_skip }
+ Thread.pass
end
参考: class Thread (Ruby 3.0.0 リファレンスマニュアル)
参考: ractor - Documentation for Ruby 3.0.0
「コントローラでlazy loadingする方法だとビューなどで実際に参照されるまで読み込みが開始されないんですが、このload_async
ならビューなどで参照される前に読み込みが開始されるので、期待どおりに動けばブラウザにレスポンスを早く返せるようにできるでしょうね」「お〜」
「ビューをレンダリングしている間にクエリを投げられるという感じでしょうか?」「ビューに限らず使えるでしょうね: たとえば以下のコードのCategory.some_complex_scope
が仮にやや遠くにあるサーバーへのクエリだとすると、ここでeager loadingもせずに普通にfind
を使ってクエリを発行すると、結果が戻ってくるまでここでブロックされてしまうという問題がまず考えられます」「ふむふむ」
# Changelogより
def index
@categories = Category.some_complex_scope.load_async
@posts = Post.some_complex_scope.load_async
end
「同じ箇所でeager loadingする場合は、必要になった瞬間から読み込みが開始されますが、その場合もコードがブロックされるのは同じです」「たしかに」
「このload_async
を使えば、そのクエリを実行するためだけのスレッドが新たに生成されるので、純粋な待ち合わせの時間は短縮されるでしょうね」「なるほど、コードがブロックされなくなるということですか」「重たくなりそうなクエリをバックグラウンドのスレッドで扱うというのはなかなかうまい方法だと思います👍」「言われてみると速くなりそうな気がしてきました」「load_async
だとスレッド生成のコストも少しはかかると思いますが大きくはなさそう」「マルチプルデータベースにもいいかも」
「その代わり、別スレッドでエラーが発生した場合にどうやって拾うかは考えないといけないでしょうね」「あ、その場合どうすればいいんでしょうか?」「エラーそのものは別スレッド側で拾うしかないと思います」
「load_async
したクエリはトランザクションが効かなくなりそうなので、使い方を間違えるとデッドロックするかも」「それもそうか」「たぶんデッドロックしそうな使い方はしないように、ということなんでしょうね」
「プルリクを見た感じでは、エラーの場合どうするかについては特に書かれてないのかな?」「お、トランザクションの中でload_async
した場合のテストが書かれてますよ↓」「あ、ホントだ」
# activerecord/test/cases/relation/load_async_test.rb#55
def test_load_async_from_transaction
posts = nil
Post.transaction do
Post.where(author_id: 1).update_all(title: "In Transaction")
posts = Post.where(author_id: 1).load_async
assert_predicate posts, :scheduled?
assert_predicate posts, :loaded?
raise ActiveRecord::Rollback
end
assert_not_nil posts
assert_equal ["In Transaction"], posts.map(&:title).uniq
end
「上のテストコードを見ると、トランザクションの中で実行したPost.where(author_id: 1).load_async
は、トランザクションが終わるまで待ってから動き出すようですね」「なるほど、このload_async
ではupdate_all
が完了した後の結果が取れるのか」「完了まで待つのはRDBMS自体の機能です: 別スレッドがロックを掴んでいたら、タイムアウトするまでは待つ機能がRDBMSにあります」「なるほど」「いずれにしろトランザクションブロックの中ではload_async
するかどうかで結果が変わる可能性があるので注意が必要でしょうね」
「たしかこのload_async
的な機能は、これまで長らく待ち望まれていたけど実現していなかった機能だったと思います: 複数クエリを呼び出す処理をマルチスレッドで並列実行できるという意味で、その夢を実現する偉大なる一歩なんだろうなと思いました」「たしかに」
# 41439より
Post.excluding(post)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1
Post.excluding(post_one, post_two)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)
# And on associations (also supports collections as above).
post.comments.excluding(comment)
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2
「2つめのexcluding
はプルリクに書かれているとおりPost.where.not(id: post.id)
のショートハンドですね↓」「NOT INに相当するのか」「where.not
を書かなくていいのがよさそう」「where.not
をたくさん書くときにはいいでしょうね」
This is short-hand for
Post.where.not(id: post.id)
(for a single record) andPost.where.not(id: [post_one.id, post_two.id])
(for a collection).
「その後で@kamipoさんがエイリアスも追加していました↓」「なるほど、excluding
はwithout
でも書けるんですね」
🔗 データベースアダプタの平均値集計を修正
:average
で呼ばれるActiveRecord::Calculations.calculate
(ActiveRecord::Calculations.average
のエイリアス)が、カラムベースのtype castingを使うようになった。これによって、浮動小数点値(float
)のカラムがFloat
として集計され、固定小数点値(decimal
)のカラムがBigDecimal
として集計されるようになった。
整数は特殊ケースとして、常にBigDecimal
として扱われる(これは既にそうなっている)。
この変更前は、Railsのデータベースアダプタで平均値集計のときにto_d
を呼んでいた。これは今後行われなくなる。この種のマジックに依存していた場合は、独自のActiveRecord::Type
を登録する必要がある(ActiveRecord::Attributes::ClassMethods
のドキュメントを参照)。
Changelogより抜粋
つっつきボイス:「decimal
型のカラムをaverage
メソッドで集計した際、スキーマ側で定義された型を無視してfloat
に変換されて返ってくる問題を修正したということみたい」「テストを見ると、integerは常にBigDecimal
で返すべきとなっている↓」「floatはFloat
で、decimalはBigDecimal
で返すべきということか」
# activerecord/test/cases/calculations_test.rb#50
def test_should_return_decimal_average_of_integer_field
value = Account.average(:id)
+
assert_equal 3.5, value
+ assert_instance_of BigDecimal, value
end
# activerecord/test/cases/calculations_test.rb#L64
def test_should_return_float_average_if_db_returns_such
NumericData.create!(temperature: 37.5)
-
value = NumericData.average(:temperature)
- assert_instance_of Float, value
+
assert_equal 37.5, value
+ assert_instance_of Float, value
+ end
+
+ def test_should_return_decimal_average_if_db_returns_such
+ NumericData.create!(bank_balance: 37.50)
+ value = NumericData.average(:bank_balance)
+
+ assert_equal 37.50, value
+ assert_instance_of BigDecimal, value
end
「あれ、37.5と37.50って型が違うんですか?」「お?テスト用の定義はスキーマのどこかにあるかな(しばらく探す): あった↓」「temperature
がfloatでbank_balance
がdecimalなのか、なるほど理解しました」「このスキーマ定義の型に応じてtype castingできているかどうかのテストということですね」
# rails/activerecord/test/schema/schema.rb#L651
create_table :numeric_data, force: true do |t|
t.decimal :bank_balance, precision: 10, scale: 2
t.decimal :big_bank_balance, precision: 15, scale: 2
t.decimal :unscaled_bank_balance, precision: 10
t.decimal :world_population, precision: 20, scale: 0
t.decimal :my_house_population, precision: 2, scale: 0
t.decimal :decimal_number
t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.numeric :numeric_number
t.float :temperature
t.decimal :decimal_number_big_precision, precision: 20
# Oracle/SQLServer supports precision up to 38
if current_adapter?(:OracleAdapter, :SQLServerAdapter)
t.decimal :atoms_in_universe, precision: 38, scale: 0
else
t.decimal :atoms_in_universe, precision: 55, scale: 0
end
end
「以下がその前にマージされたプルリクですね↓」
🔗Rails
🔗 Railsにマイクロサービスを追加する(Ruby Weeklyより)
つっつきボイス:「Railsにマイクロサービスですか?」「Railsをマイクロサービス化するんじゃなくて?」
# 同記事より
class QuizController < ApplicationController
# Other controller methods omitted here
# This is our new service method
def participants
response = {}
response["participants"] = Attempt.all.map { |attempt| attempt.taker }
render :json => JSON[response]
end
end
「え、QuizController.action(:participants)
でコントローラのアクションを直接呼び出してるのか↓、しかもpumaの引数で.ruファイルを指定してる」「すごい書き方ですね😳」
# 同記事より
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run QuizController.action(:participants)
# 同記事より
bundle exec puma -b tcp://0.0.0.0:8080 config_svc_participant.ru
「これってマイクロサービスなんでしょうか?」「自分たちが考えているマイクロサービスとは設計思想が違いますけど、単機能のサービスという意味での『マイクロなサービス』と考えることも一応できるでしょうね」「なるほど、そういう意味ですか」「unbundled seriesと銘打たれている記事だから、いろいろ自由に試しているのかなと想像してみました」
「ところでこの記事、よくみるとEngine Yardのブログなんですね」「Engine YardのWebサイトってもっと赤っぽい色彩だった覚えがあるんですけど、随分青みがかってる」「Engine YardはHerokuと同じぐらい歴史がありますけど、今はRails以外もいろいろサポートするようになっているので、シンボルカラーをRailsを連想する赤から変えたのかもしれませんね」「あ、そうかも」「そういえば、昔の赤色の時代のEngine Yardのステッカーがそこに転がっているパソコンに貼ってありますよ」
Follow Darren Broemmer on his #Ruby Unbundled series. In this blog, we learn how to create a #singlefile #rubyapp. Click below to know more.https://t.co/T2Y8yOzfzi pic.twitter.com/7XYRxDeHcK
— Engine Yard (@engineyard) February 8, 2021
🔗 Railsのcycle
メソッド(Ruby Weeklyより)
つっつきボイス:「i % 2 == 0
を避ける、とは?」
<!-- 同記事より -->
<% @foods.each_with_index do |food, i| %>
<tr class="<%= i % 2 == 0 ? 'bg-gray-200' : 'bg-gray-100' %>">
<td><%= food %></td>
</tr>
<% end %>
「なるほど、以下のようにeach_with_index
の中でcycle
ビューヘルパーを使うと、上みたいに1行ずつ交互に背景色を変えたりするときにi % 2 == 0 ?
で偶数奇数を判定したりせずに書けるのね↓」「へ〜!」「Railsにこんなヘルパーもあるとは」
<!-- 同記事より -->
<% @foods.each_with_index do |food, i| %>
<tr class="<%= cycle('bg-gray-200', 'bg-gray-100') %>">
<td><%= food %></td>
</tr>
<% end %>
「odd?
やeven?
で書くよりもcycle
の方が意味的にも自然かも」「背景色に限らず、ループの中で何かを順繰りに表示したいときに幅広く使えそうですね👍」
🔗Ruby
🔗 reductionとは何か、FiberがRubyコンカレンシーの解である理由(Ruby Weeklyより)
つっつきボイス:「お、まだこの記事の図しか見てませんが、こんなふうに↓スレッドを丁寧に図で説明しているのがすごくよさそう」「おぉ!」
「Fiberだけじゃなくて最後の方でRactorまで説明してくれている」「こうやって時系列に沿った図になっているのがうれしいですね↓」「マルチスレッド系の解説って、こういう図がないと読んでてつらいんですよ」
「歯ごたえありそうだけど、この記事は読む価値がありそうな予感がします👍」「週末に頑張って読んでみようかな」
🔗 M1 MacでRailsアプリを開発するときのコツ(Ruby Weeklyより)
- Rails discussions: Tips and tricks for developing Rails applications on Apple Silicon - rubyonrails-talk - Ruby on Rails Discussions
つっつきボイス:「Rails discussionがRuby Weeklyで紹介されていました」「Appleシリコンだ」「Dockerもアップデートされて、ようやくいくつかの機能がM1チップでまともに動くようになったという記事も今日見かけましたね」
参考: Apple M1チップ対応のDocker Desktop、同梱のKubernetesも実行可能に - Publickey
「次に買うMacBookはM1チップにしようかな」「たぶん今後はM1チップのしか買えなくなると思いますよ」「え、そうなんですか?」「AppleがIntelへのCPUの新規発注を絞り込んでいるらしいという話もあるので、おそらく現行のIntel Macはいったんディスコンになる流れなんじゃないかなと予想しています」「あ〜なるほど」「最近Intelがこういう広告↓を打っているのが話題になってますけど、そのあたりも理由じゃないかと言われていますね」
参考: インテル、「MacにできないことがPCにできる」キャンペーンを展開 - Engadget 日本版
「ところで、最近一部で話題になったcpu-monkey.comのベンチマーク↓は、スペック情報は参考になる部分もあるんですけど、シングルコアでM1とM1Xの差がほとんどないとか、自分も含めてこのベンチマークの信憑性にはちょっと疑問があるんですよ」「あ、そうなんですか」
参考: Apple M1X vs. Apple M1 - Benchmark and Specs
「ここに載ってたスペックを見ると、M1Xにはメモリが32GB搭載できるらしいので次に買ってみてもいいかなと思うんですが、M1XのTDP(Thermal Design Power)が35Wもあるのでバッテリーの持ちという面では優位性がちょっと下がるかもと思っているところです」「それでも32GB積めたらいいですよね」「今の自分の使い方だと16GBではもうやっていけないレベルです」「今のMacBookは最大16GBですもんね」(以下延々)
参考: TDP - CPU の選び方
今回は以上です。
バックナンバー(2021年度第1四半期)
週刊Railsウォッチ(20210209後編)Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか
- 20210208前編 Rails次期リリースがバージョン7に決定、thoughtbotのアプリケーションセキュリティガイドほか
- 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ウォッチタグ)