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

週刊Railsウォッチ(20201221前編)aws-sdk-rails gemの機能をチェック、RubyWorld Conference 2020のDHHインタビューほか

こんにちは、hachi8833です。約400年ぶりという木星と土星の超大接近は12/21(月)なので今夜ですね。と思ったらもう西の空に沈んでしまったようです。

参考: 【特集】2020年12月 木星と土星の超大接近 - アストロアーツ

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

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

今回は以下のコミットリストのChangelogを中心に見繕いました。


つっつきボイス:「Rails 6.1がリリースされた直後なので、コミットリストも6.1修正が多そうですね」「今手掛けているRailsアプリも早速6.1にアップグレードしました」「お、どうでした?」「論理削除でおなじみのparanoia gemが動かなくなりました😇: それ以外は大丈夫っす」「gemが追いついてないのはよくありますね」

rubysherpas/paranoia - GitHub

「そうなんですよね、リリースされてから使い始める自分💦」「rcが取れるまでに多くの人がお試しするのが本当は望ましいですよね」

translatenilキーを渡したときの挙動を修正


つっつきボイス:「コミットメッセージにnilがいっぱい書かれててややこしい...」「translatenilを渡すとエラーになったのを修正したということのようだけど、defaultオプションを効くようにもしたらしいとありますね」

I18n.translatenilキーを渡すと、defaultも同時に指定されていない場合はnilを返す。defaultも指定しておくと、nilキーは「見つからないキー」として扱われる。

Rails 6.0のtranslateヘルパーは、nilキーを渡すと常にnilを返すが、#40773以後はtranslateヘルパーにnilキーを渡すと常にI18n::ArgumentErrorをraiseするようになった。このコミットは、defaultを指定せずにI18n.translatenilキーを渡すときの振る舞いと同じになるようtranslateヘルパーを修正する。
同コミットより大意

「テストコードとChangelogの方が見やすいかな↓: default:オプションを指定しておけばnilキーを渡してもdefault:で渡したキーにフォールバックしてくれるようになったのね」「あ、そういうことですか」「例外を出されて止まっちゃうと困るので、これは必要な修正ですね」

# actionview/test/template/translation_helper_test.rb#80
  def test_returns_nil_for_nil_key_without_default
    assert_nil translate(nil)
  end

  def test_returns_default_for_nil_key_with_default
    assert_equal "Foo", translate(nil, default: "Foo")
    assert_equal "Foo", translate(nil, default: :"translations.foo")
    assert_predicate translate(nil, default: :"translations.html"), :html_safe?
  end

translateヘルパーにnilキーを渡したときに、常にnilを返すのではなく、defaultで指定したキーに解決するようになった。
Changelogより

nonnamed expression indexを追加した後マイグレーションで元に戻せるように修正


つっつきボイス:「revertibleって何をrevertするんでしょう?」「Changelogには、ロールバックすると以下でエラーになったとあるけど...」

add_index(:items, "to_tsvector('english', description)")

「元のissueを見る方がいいかな↓」

「普通のデータベースインデックスではない、以下の"to_tsvector('english', description)", { using: :gin, name: 'index_items_on_to_tsvector_english_description' }のような関数インデックスをマイグレーションで追加すると、その関数インデックス名が自動生成されていた場合はマイグレーションをロールバックしたときにエラーになるということか」「あ、そういうことですか」「ロールバックするときに自動生成済みインデックス名を解決できなくて落ちてたのを、ロールバックできるように修正したということのようですね」

# #40732より
class AddFullTextSearchIndexToItemDescription < ActiveRecord::Migration[6.0]

  # Note: this migration is irreversible, to revert it,
  # please use the __Reversable__ block below and comment out the Irreversible block
  # then rake db:rollback will work
  def change
    # __Reversable__
    # add_index(
    #   :items,
    #   "to_tsvector('english', description)",
    #   { using: :gin, name: 'index_items_on_to_tsvector_english_description' }
    # )

    # Irreversible
    add_index(
      :items,
      "to_tsvector('english', description)",
      { using: :gin }
    )
  end
end

「つまりrevertibleはマイグレーションをロールバックできるという意味なんですね」「この修正、超大事じゃないですか!」「知らずにロールバックしたら即死ですもんね😇」「こういうマイグレーションってあまり書かないだけに踏んだときがつらそう...」

Unreleased: 失敗したリクエストをconfig.exceptions_appに渡すときのリクエストメソッドをGETに変更


つっつきボイス:「これはChangelogのUnreleasedに書かれていたので、6.1には入っていないようです」「こっちの関連issueを見てみるかな↓」

「RailsガイドによるとActionController::UnknownHttpMethodは自動的にrescueされてmethod_not_allowed(HTTP 405)になるはずだったのにHTTP 500エラー(内部エラー)になってしまったのね」「ガイドと挙動が違ってた問題ですか」「RailsのRackミドルウェアの構成によっては期待どおりに動かないことがあったらしい」

参考: 3.9 Action Dispatchを設定する -- Rails アプリケーションを設定する - Railsガイド

「RailsのRackミドルウェアがUnknownHttpMethodのエラーを食べてしまうと正しく405エラーを返せないことがあったので、内部的にはrequest methodをGETということにして処理を継続するようにしたようですね↓」「それだけだと元のリクエストの種類がわからなくなるので、それをoriginal_request_methodに保存したということか」

# actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L43
      def render_exception(request, exception)
        backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
        wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
        status  = wrapper.status_code
        request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
        request.set_header "action_dispatch.original_path", request.path_info
+       request.set_header "action_dispatch.original_request_method", request.raw_request_method
        request.path_info = "/#{status}"
+       request.request_method = "GET"
        response = @exceptions_app.call(request.env)
        response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
      rescue Exception => failsafe_error
        $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}"
        FAIL

「GETじゃなくて405を返すのはダメでしょうか?」「ミドルウェアスタックの途中で処理を止めたくないでしょうし、未定義のリクエストメソッドが来る可能性もあるので、処理継続のために便宜上GETにしたんじゃないかな」

参考: Rails と Rack - Railsガイド

UnknownHttpMethod自体普通はあまりないエラーだと思います」


#40246で修正された#38998と似たような感じで、ActionDispatch::Requestでメソッドが呼び出されると必ず行われるHTTPメソッドバリデーションで、予想外の妙な結果が生じることがある。たとえば、config.exceptions_app = self.routesの場合、ActionDispatch::ShowExceptionsというミドルウェアフェイルセーフで例外が発生する。

この冗長な例外を防ぐため、envconfig.exceptions_appに渡る前にrequest_methodを上書きした。オリジナルのリクエストメソッドは維持されるのでaction_dispatch.original_request_methodで維持されるので、インスペクトもできる。
同PRより

6.1: attribute_for_databaseメソッドを追加

ここからは少し趣向を変えて、@kamipoさんの記事より見繕いました。


つっつきボイス:「上のマージ自体は今年10月に行われていて、ウォッチでは取り上げてませんでした」「ああ、例のenumのデータベース上の値を取れるようにした改修ですね」

enum state: {active: 0, inactive: 1}とかした時に、typecast前の0とか1を取りたい
blog.kamipo.netより

なお、attribute_for_databaseなどのattributeは属性名に置き換わるので、属性名_for_database属性名_before_type_castという感じの名前になります。

attribute_for_databaseメソッドはたしかに欲しい!」「attribute_before_type_castだとenumがtypecastしているかどうかを意識しながらになるので、それだと欲しいものとちょっと違うという気持ちもわかります」

参考: attribute_before_type_cast -- ActiveRecord::AttributeMethods::BeforeTypeCast

「以下で言うと、book.statusはデータベースに2が入っていて、そのデータベースの値を取ろうとすると、Rubyはbook.status"published"を返すので、book.status_before_type_castを使ってデータベースから読み込み済みの2を取っていた」「ふむふむ」「それがこの改修で、book.status_for_databaseと書けばenumの"published"に対応する2をデータベースから読み込んで取れるようになった」「あ、なるほどわかりました」

# 同PRより
book = Book.new(status: "published")

# returns "published", but what we really want is 2.
book.status_before_type_cast

attribute_before_type_castよりattribute_for_databaseというメソッド名の方がデータベースから取ってくる操作なのがわかりやすいかも」「何かの都合でデータベースにあるenumの値を取りたいことはよくありますね」

6.1: whereで関連付け名をjoinedテーブルのエイリアス名として参照できるようになった

belongs_to :author, class_name: 'User'したときにleft_joins(:author).where("author.id": nil)とか書きたい
blog.kamipo.netより


つっつきボイス:「こちらも今年8月にマージ済みでしたがウォッチで取り上げていなかったので」「そうそう、神速さんのツイートにもあるようにbelongs_toで別名を使うと、joinsは別名で、whereは元のテーブル名を使わないといけなかったんですよね↓」

「それがこの修正によって、以下のテストコードのFirm.includes(:clients).where("clients.new_name": "Summit")のようにドット付きの"clients.new_name"で参照できるようになったのか」「ネステッドハッシュによるアクセスとは違うけど、"clients.new_name"というドットアクセスをキーに書けるようになったんですね」

# activerecord/test/cases/associations/eager_test.rb#202
  def test_type_cast_in_where_references_association_name
    parent = comments(:greetings)
    child = parent.children.create!(label: "child", body: "hi", post_id: parent.post_id)

    comment = Comment.includes(:children).where("children.label": "child").last

    assert_equal parent, comment
    assert_equal [child], comment.children
  end

  def test_attribute_alias_in_where_references_association_name
    firm = Firm.includes(:clients).where("clients.new_name": "Summit").last
    assert_equal companies(:first_firm), firm
    assert_equal [companies(:first_client)], firm.clients
  end

「ツイートのコードで言うとauthor.何とかみたいに書けるようになったということですか?」「ですです」「お〜これは便利そう!」「joinsがスッキリ書けてありがたい🙏」「ネステッドハッシュで書けるかどうかは少なくともこのテストには見当たらないですね」


あるテーブルが複数回joinsされると、それらのテーブルは最初の名前でない別名になる。
これは自己参照的な関連付けで起こりやすく、現在はその場合にwhere条件の別名テーブルでカスタム属性(type casting)や属性エイリアス名の解決が効かない。
この問題を修正するために、whereで関連付け名をエイリアス名で参照できるようにする。関連付け名がwhereで参照されると、それらの名前にjoinedテーブルのエイリアス名が使われる。
同PRより大意

# 同PRより
class Comment < ActiveRecord::Base
  enum label: [:default, :child]
  has_many :children, class_name: "Comment", foreign_key: :parent_id
end

# ... FROM comments LEFT OUTER JOIN comments children ON ... WHERE children.label = 1
Comment.includes(:children).where("children.label": "child")

なお@kamipoさん記事3つ目の#39830は以下の記事をどうぞ。

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

この後さらに@kamipoさん記事が出ていました。こちらもつっつきで少し見てみましたが記事では割愛します🙇。

Rails

RubyWorld Conference 2020のキーノート: DHHインタビュー by Matz


つっつきボイス:「(2020/12/17 20:30頃)さっきRubyWorld Conference 2020がちょうど終わった頃だそうで、最後のキーノートであるMatzによるDHHインタビューが各所で評判になっていました↑」「お〜、DHHがRailsとフロントエンドの話もしたんだ!」「そこに関心のある人が多いからフロントエンドの話題は欠かせないでしょうね」「自分はRailsは当分滅びないと思っていますけど、フロントエンドをRailsでやる理由はだいぶ少なくなったとも思ってます」「今度動画見てみようっと」「誰か速攻で文字起こしとかしないかな」

なお、つっつきの時点では英語版動画にしか気づいていませんでしたが、以下の同時通訳付き動画でもキーノートインタビューを見られます(頭出し済み: 7:11:59)↓。

満を持してのDHH登壇だったそうです↓。

以下はつっつき後に見つけたツイートです。

RailsとCableReadyとStimulus ReflexでリアクティブなTwitterクローンを構築する

以下のツイートで知りました。

hopsoft/chatter - GitHub

hopsoft/cable_ready - GitHub

hopsoft/stimulus_reflex - GitHub


つっつきボイス:「Stimulusといえば、11月の銀座RailsでもStimulus関連の発表がありました↓」「お、そうなんですね」

「Stimulusってつっつきで取り上げてましたっけ...よく知らなかった😅」「割と登場してたと思います(サイト内検索)」「StimulusはRailsでお馴染みのBasecampが作って使っているJavaScriptライブラリで、Rails方面の記事でちょくちょく見かけますね↓」「なるほど〜」「StimulusはRailsのオフィシャルに近い印象あります」「今のRails wayに則ってフルスタックでフロントエンドまでやるならStimulusとRailsの相性はいいでしょうね」

stimulusjs/stimulus - GitHub


なお、Stimulusの2.0もリリースされたそうです。

@kamipoさんの記事より


つっつきボイス:「@kamipoさんのこの記事は唸りました: こうやって解説してもらわないとわからない世界」「ですよね」

「SELECT ... FOR UPDATEは割とよく使われるSQL構文で、デッドロックしないという都市伝説がまことしやかにあったんですが、実際はそんなことはなくてデッドロックは起きるというお話」「プライマリキーとセカンダリキーで並び順が変わるデータにしておくと、プライマリキーとセカンダリキーがクエリプランのSELECT ... FOR UPDATEでロックの一方が上から順、もう一方が下から順に同時に進んでいって、それでデッドロックになるというのを記事で再現してますね」「あ〜、そういうことですか!」「同じテーブルの中でのロックを取る順序というものをこれまで気にしたことがなかったことに気付かされました」「そんなことがあるとは...」

「ロックを取る順序が一意になるようにクエリやクエリプランを揃えるのが一般的な回避法だそうですが、そこまでチェックしないといけないのか」「@kamipoさんも『眼の良さを活かして気合いで対処』なんですね」「これはデータベース強者でないとなかなか気づけない問題」「データベース強者、なりたいです...」「取りあえずオラマスを目指すところからですかね↓」

参考: ORACLE MASTER Portal - be an ORACLE MASTER - | オラクル認定資格制度 | Oracle University


以下はつっつき後に見つけたツイートです。

aws-sdk-rails gemの新機能(Ruby Weeklyより)

aws/aws-sdk-rails - GitHub


つっつきボイス:「aws-sdk-rails gemって知らなかった」「aws-sdk-ruby↓ではないんですね😆」「どちらもAWS公式のgemですけど、そういえばaws-sdk-rubyの方は以前のSDKがバージョンアップされたときがちょっと大変でしたね」「私も思い出しました」

aws/aws-sdk-ruby - GitHub

参考: AWS SDK for Rubyの S3署名バージョン2廃止に対応しました [2019/6/24期限迫る!] - LCL Engineers' Blog

「どれどれ」「お、DynamoDBセッションストアなんて機能がある↓」

# aws.amazon.comより
rails generate dynamo_db:session_store_migration

参考: Amazon DynamoDB(マネージド NoSQL データベース)| AWS

「次はActive Support Notification Instrumentationサポート」「お〜、言われてみればRailsのInstrumentationで投げた通知をAWS X-Rayあたりで拾いたいことって割とありそうなので、このSDKでできるなら便利そう」「その分AWSにロックインすることになりますけどね」

参考: AWS X-Ray(分散アプリケーションの分析とデバッグ)| AWS
参考: Active Support の Instrumentation 機能 - Railsガイド

「これを見ると↓、ActiveSupport::Notificationssubscribeでaws-sdk-railsのメソッドを拾うこともできる」「これは有能そう」「こういうのは欲しい機能ですね」

# 同リポジトリより
ActiveSupport::Notifications.subscribe('put_object.S3.aws') do |name, start, finish, id, payload|
 # process event
end

# Or use a regex to subscribe to all service notifications
ActiveSupport::Notifications.subscribe(/S3[.]aws/) do |name, start, finish, id, payload|
 # process event
end

「aws-sdk-railsのリポジトリも見てみると、Railsのencrypted credentialもサポートしてるのか↓」「お〜便利そう!」「SDKにロガーも付いているとは知らなかった」

# 同リポジトリより
# config/credentials.yml.enc
# viewable with: `rails credentials:edit`
aws:
  access_key_id: YOUR_KEY_ID
  secret_access_key: YOUR_ACCESS_KEY

参考: Rails5.2から追加された credentials.yml.enc のキホン - Qiita

「RailsのAction Mailerのdelivery_methodにAWS SES(Simple Email Service)を指定することもできる↓」「便利そうなものがいろいろ入ってますね」「aws-sdk-rails、あまり注目してなかったけどちょっと見直しました」

# 同リポジトリより
# for e.g.: config/environments/production.rb
config.action_mailer.delivery_method = :ses

参考: Action Mailer の基礎 - Railsガイド
参考: Amazon SES(高可用性で低価格なEメール送信サービス)| AWS

「Active JobのバックエンドでAWS SQS(Simple Queue Service)を使うこともできる↓」「これもよさそう😋」

# 同リポジトリより
# config/application.rb
module YourApp
  class Application < Rails::Application
    config.active_job.queue_adapter = :amazon_sqs # note: can use either :amazon or :amazon_sqs
    # To use the non-blocking async adapter:
    # config.active_job.queue_adapter = :amazon_sqs_async
  end
end

# Or to set the adapter for a single job:
class YourJob < ApplicationJob
  self.queue_adapter = :amazon_sqs
  #....
end

「キューにSQSを使うだけかと思ったらワーカーをローカルでも動かせるのね↓」

RAILS_ENV=development bundle exec aws_sqs_active_job --queue default

参考: Active Job の基礎 - Railsガイド
参考: Amazon SQS(サーバーレスアプリのためのメッセージキューサービス)| AWS

「aws-sdk-rails、思ったよりよさそう👍」「こういうの見ると使いたくなっちゃいます」「最近やってるRailsアプリの環境がAWS ECSやAWS Fargateになっていることが増えてきて、ワーカーやキューをどうしようかと考えることが多いんですけど、aws-sdk-railsが使えるところがありそう」

参考: AWS Fargate(サーバーやクラスターの管理が不要なコンテナの使用)| AWS

「AWSが公式に提供しているのでサポート面でもありがたい🙏」「あとはSDKのアップグレードがaws-sdk-rubyのときほど大変にならなければさらに嬉しい」


前編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201216後編)Ruby 3.0.0-rc2とRuboCop 1.6がリリース、Ruby 3の静的型解析記事、CentOS 8のEOLが短縮ほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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