Tech Racho エンジニアの「?」を「!」に。
  • 開発

週刊Railsウォッチ(20190819-1/2前編)祝: Rails 6がついにリリース、RailsガイドもRails 6に対応、Arelはpublicだったかほか

こんにちは、hachi8833です。休日はトイレの壁紙を剥がして珪藻土を塗り塗りする作業で終わりました。

参考: 【これさえ読めば大丈夫】はじめての漆喰・珪藻土 塗り壁DIY完全ガイド


つっつきボイス:「自宅の壁?😆」「冬になるたびにトイレの壁にめちゃくちゃ結露してカビの温床になってました🦠」「構造上結露しちゃう家とかありますよね☺️」「団地ともお的な昭和な団地なので冬場はコンクリから外の冷気がもろに伝わってきます😅」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

臨時ニュース: Rails 6がついにリリース🎉㊗

つっつき会の翌日の金曜日に再び6.0.0マイルストーンを見てみるとissueがゼロになっていたので、もしかするとウォッチ公開前にリリースされたりして、と思っていたら週末にリリース情報が出ました。

そういえば例の画面も変わってましたね。


早くも@jnchitoさんががっつりRails 6記事を公開しています。他にもPublic Keyなどで続々速報が出ています。

私もRails 6 rc2をちょっといじり始めていたのですが、RailsガイドをRails 6に対応させる更新翻訳(次の記事参照)がせいいっぱいでした😅。

参考までに、Evil Martiansの少し前のRails 6記事を再掲します。

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

RailsガイドがRails 6に対応

YassLabの安川さんのお力添えで、RailsガイドをひとまずRails 6に対応いたしました。ありがとうございます!😂
上の記事で紹介されているように、今回はZeitwerkのガイドなども追加されています。見落としや誤りなどありましたら、Railsガイドまでフィードバックをお願いします🙇。

眼力頼りの差分翻訳を今後何とかしたいです😅。


というわけで、ここから下はRails 6リリースより前のつっつきを元にしています。ご了承ください🙇。

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

今回は主に最近の6-0-stableブランチから見繕いました。ドキュメントの修正が増えています。
なお6.0.0マイルストーンはつっつき時点で4件ありました。


つっつきボイス:「ある程度予想はしてたけど、やっぱりリリースまでには時間かかってますね☺️」「マイルストーンに残ってるissueはデグレっぽい感じですし」

aggregate_aliasの余分な"_"を削除

これは6.0.0マイルストーンにあったissueのようです。

# activerecord/lib/active_record/relation/calculations.rb#L308
      def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
        group_fields = group_values
        if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
          association  = klass._reflect_on_association(group_fields.first)
          associated   = association && association.belongs_to? # only count belongs_to associations
          group_fields = Array(association.foreign_key) if associated
        end
        group_fields = arel_columns(group_fields)
        group_aliases = group_fields.map { |field|
          field = connection.visitor.compile(field) if Arel.arel_node?(field)
          column_alias_for(field.to_s.downcase)
        }
        group_columns = group_aliases.zip(group_fields)

-       aggregate_alias = column_alias_for("#{operation}_#{column_name.to_s.downcase}")
+       aggregate_alias = column_alias_for("#{operation} #{column_name.to_s.downcase}")
# activerecord/lib/active_record/relation/calculations.rb#L377
      def column_alias_for(field)
-       return field if field.match?(/\A\w{,#{connection.table_alias_length}}\z/)
-
        column_alias = +field
        column_alias.gsub!(/\*/, "all")
        column_alias.gsub!(/\W+/, " ")
        column_alias.strip!
        column_alias.gsub!(/ +/, "_")
        connection.table_alias_for(column_alias)
      end

つっつきボイス:「コードに一箇所余分な_があったのを修正したそうです」「以前の修正でデグレってたのが解決された🎉」

Zeitwerk関連コミット

# railties/lib/rails/autoloaders.rb#L39
+     def log!
+       each(&:log!)
+     end

つっつきボイス:「Zeitwerkデバッグ用の#log!を追加して、Railsガイドでこれを使ったトラブルシューティング方法が追記されてました」「Rails 6のZeitwerkでハマる人もいそうなので、こういうサポートはありがたいですね😋」

「ところで上にポツンとあるeachって何でしょう?」「更新箇所のすぐ上にeachが定義されている↓からそれではないかと😆」「あ、これでしたか😅」「あまり見かけない書き方ですけどね☺️、mainonceは単独で使えそうな感じでそれをまとめて呼び出すショートハンド的なメソッドかなと」

# railties/lib/rails/autoloaders.rb#10
      def main
        if zeitwerk_enabled?
          @main ||= Zeitwerk::Loader.new.tap do |loader|
            loader.tag = "rails.main"
            loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
          end
        end
      end

      def once
        if zeitwerk_enabled?
          @once ||= Zeitwerk::Loader.new.tap do |loader|
            loader.tag = "rails.once"
            loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
          end
        end
      end

      def each
        if zeitwerk_enabled?
          yield main
          yield once
        end
      end

      def logger=(logger)
        each { |loader| loader.logger = logger }
      end

ActionDispatch::Responseのcontent_typeの内部でmedia_typeを使うよう変更

#36490でdeprecateされたcontent_type呼び出しが、アップグレードしたアプリでトリガーされていた。同PRのコメントで報告されていた。
media_typeを使えば、あらゆるケースでdeprecationを回避できる。
同PRより大意

# actionpack/lib/action_controller/metal/renderers.rb#L156
    add :json do |json, options|
      json = json.to_json(options) unless json.kind_of?(String)
      if options[:callback].present?
        if media_type.nil? || media_type == Mime[:json]
          self.content_type = Mime[:js]
        end

        "/**/#{options[:callback]}(#{json})"
      else
-       self.content_type ||= Mime[:json]
+       self.content_type = Mime[:json] if media_type.nil?
        json
      end
    end

    add :js do |js, options|
-     self.content_type ||= Mime[:js]
+     self.content_type = Mime[:js] if media_type.nil?
      js.respond_to?(:to_js) ? js.to_js(options) : js
    end

    add :xml do |xml, options|
-     self.content_type ||= Mime[:xml]
+     self.content_type = Mime[:xml] if media_type.nil?
      xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
    end

つっつきボイス:「@y-yagiさんの立てたPRがこのPRに反映されたそうです」「media_typeなんてのがあったとは😳: MDNを見るとContent-Typeの項目の中にmedia-typeってあるな↓」「ディレクティブですか」

参考: Content-Type - HTTP | MDN


/developer.mozilla.orgより

「これを見る限りでは、Content-Typeの中でmedia-type=なんちゃらMIME typeみたいにMIME typeを書けるらしいけどどういうふうに使うのかな...?🤔」「むむ😅」「media_typeがあればそっちを使って、なければContent-Typeを使うとかなという気もするけど...」「詳しい人の情報求む🙏」

その後もう少し追ってみました。

#36034ActionDispatch::Response#content_typeの戻り値を変えたが、#36034でアップグレードの邪魔になっているらしき。
そこでActionDispatch::Response#content_typeの振る舞いを5.2に戻しつつ、古い振る舞いをdeprecatedにした。さらに、振る舞いをconfigで制御できるようにした。
#36490より大意

# #36490より
# actionpack/lib/action_dispatch/http/response.rb#L89
+  cattr_accessor :return_only_media_type_on_content_type, default: false
...
    def content_type
-     super.presence
+     if self.class.return_only_media_type_on_content_type
+       ActiveSupport::Deprecation.warn(
+         "Rails 6.1 will return Content-Type header without modification." \
+         " If you want just the MIME type, please use `#media_type` instead."
+       )
+
+       content_type = super
+       content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type
+     else
+       super.presence
+     end
    end

さらに#36034のコメントを引用します。

#36034の変更がGitHub上で最新の6-0-stable更新に入ったが、breaking changeが発生していろいろfailすることがわかった。content_typeへの変更はたぶん正当だと思うものの、同時に5.2でもアプリを動かしている自分たちにとっては切り替え困難。#36034の変更の意図としては、5.2と6.0ではcontent_typeの戻り値が異なっていて、少なからぬ影響が生じる。この変更をひとまず戻してアプリで従来のcontent_type値を取れるアップグレードパスを整備するのがよさそう?

さらに遡ると、#35709の「content_typecharset以外のパラメータがRailsで無視される」というissueが始まりだったようです。それを修正するために、#36034ActionDispatch::Response#content_typeがContent-Typeヘッダーをすべて返すようにしたところ、既存の5.2アプリでMIME typeの取得などで不具合が生じることがあったので、段階的に移行することになった、冒頭の#36854はそれに伴う変更...という理解で合ってるかしら。

prevent_writesのスレッド安全性を修正

#36830で追加したテストやコードで示すように、prevent_writesはスレッドセーフではなかった。あるスレッドが読み出し、他のスレッドが書き込んでからもう一度読み出すと、最初の書き込みができなくなる。
この変更は、コネクションハンドラのインスタンス変数を削除してThread.current[:prevent_writes]のゲッター/セッターに変え、書き込みが許されるかどうかを設定するようにした。
同PRより大意

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1006
-     attr_reader :prevent_writes

      def initialize
        # These caches are keyed by spec.name (ConnectionSpecification#name).
        @owner_to_pool = ConnectionHandler.create_owner_to_pool
-       @prevent_writes = false

        # Backup finalizer: if the forked child never needed a pool, the above
        # early discard has not occurred
        ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
      end

+     def prevent_writes # :nodoc:
+       Thread.current[:prevent_writes]
+     end
+
+     def prevent_writes=(prevent_writes) # :nodoc:
+       Thread.current[:prevent_writes] = prevent_writes
+     end
+
      def while_preventing_writes(enabled = true)
-       original, @prevent_writes = @prevent_writes, enabled
+       original, self.prevent_writes = self.prevent_writes, enabled
        yield
      ensure
-       @prevent_writes = original
+       self.prevent_writes = original
      end

つっつきボイス:「マルチプルDBでのスレッド関連の問題を修正したみたいですね」

パーシャルレンダリングAPIの古い記述を削除

# actionview/lib/action_view/renderer/partial_renderer.rb#L106
  #   <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
  #
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
- # just keep domain objects, like Active Records, in there.
- #
  # == \Rendering shared partials

#36897によるとActionView::PartialRendererに以下のような記述がある:

後方互換性の懸念上、このコレクションにはハッシュを使えない。通常はActive Recordと同様、そこにドメインオブジェクトも保持する。

上の記述が追加されたのは2005年のことで、どこが懸念点だったかという情報は見当たらないものの、#36897で報告されているとおり、実際はできるらしい↓。自分もRails 6.0 rc2で試したところ、ハッシュを渡せた。
同PRより大意

= render :partial => "info_row", :collection => my_collection, :as => :d
# my_collectionはハッシュの配列

Rails

Arelはかつてpublicだったか?


つっつきボイス:「『先週の改修』でissueを追っていたら、このissueのスレがArelの話題でかなり伸びていて気になったので、手元で雑に訳してみました」

参考: File: README — Documentation for rails/arel (master)
参考: ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs


以下は#36761の大雑把なまとめです。

  • (元々このissueは、threddedというフォーラムエンジンのあるクエリがRC2で期待どおり動かなくなったという報告で始まった)
  • (やりとりの中でArelに言及されていて、Arelがかつてpublicだったことがあったかどうかという議論になった)
    • publicあった説: 3.2のRailsガイドarel_tableを使ったコード例がある、publicではなかったとしても、publicだったと思ってた人はかなり多い
    • publicなかった説: Arel gemはかつて外部gemだったがArelのAPIはpublicになったことはないはずだし、APIにはArelは出てこない
    • publicあった説: APIdockには今もpublicと表示されている
    • publicなかった説: APIdockは公式ではないし、Arelはやはりprivateであり予告なしに変更される可能性がある
  • Arelを使っている人は実際多いようなので、もしかするとArelをpublic APIとして再検討するタイミングなのかもしれない?
    • ActiveRecord::RelationでできないことはArelでやるべし(stringでやるのは禁止)」というプロジェクトに携わったこともある
    • stringでやる方法だとコンポジションもパラメータ化もできず、DB互換性もなくなるのでよくない
    • Arelはリリースのたびに改善されている(5.1はともかく)
    • (「Arelをpublicにすべき」説が後半で飛び交う)
    • 以下↓はArelでないとこんなに簡潔に書けない
scope :started, -> {
  where(arel_table[:valid_from].lteq(Date.current))
}

最終的にこの#36761はcloseされています。なお本来の問題は別途@kamipoさんが#36805で修正しました。


「(抄訳を見ながら)お〜なるほどなるほど、たしかにArelを生で使う人って以前は普通に見かけましたね☺️」「やはり〜」「RailsガイドにもArelを使った例があったとは(今はありませんが)」「あったあった、自分もRails 3の頃だったか、英語版Railsガイド見てやりましたもん😆」「😆」「ArelがprivateなAPIだったら、DB触りたいというだけでprivateなAPIをわざわざ使いたくないですし、使った覚えがあるということはガイドに書いてあったはずですし😆」「APIdock見に行くと今でも『public』って出ちゃってますし」「ありゃ〜😆」

「そういうこともあったりしたので『Arel、publicだったんでしょ?』という意見が出たのかもですね」「ま気持ちはわかる😆」「ArelってActive Recordに取り込まれる前は別gemだったんですね」「元々汎用的なORMビルダーみたいな位置づけでしたし」

「まあ公式としてはArelをpublic APIとみなしたことはないという見解になるでしょうね: ArelをArelとしてナマで使う人をどうこうするつもりもないでしょうけど」「その分いつAPIが変わっても仕方がないですね」「じっくり読んだらなかなか面白そうなissueではある😋」「issueを全文翻訳しようかとも思いましたが取りあえずやめときます😅」

StravaはRailsを使っている


同サイトより


つっつきボイス:「Matzのツイートで見かけたので拾ってみました: フィットネスSNS的なサービスのようです」「この辺は国とか地域で流行り方が違ったりしますね☺️」「同社のブログでRailsを5.0に上げたという記事があったのでRails使ってるようです」「2018年末で5.0、GitHubもその頃にRailsアップグレードを進めてましたね: どちらもユーザー数めちゃ多そうだから大変そう😅」

Rails 6のマルチDB応用編(Hacklinesより)

production:
  primary:
    adapter: postgresql
  animals:
    migrations_paths: db/animal_migrate
    adapter: postgresql
    url: <%= ENV["HEROKU_POSTGRESQL_OLIVE_URL"] %>
  animals_replica:
    adapter: postgresql
    url: <%= ENV["HEROKU_POSTGRESQL_PURPLE_URL"] %>
    replica: true
  • replicaから読む
  • replica読み取りとDB書き込みを自動切り替え
  • DB読み取りと書き込みを手動で切り替える

つっつきボイス:「Rails 6でやってくるマルチプルDBの機能紹介という感じかな」「タイトルにadvancedとあるけど基本機能っぽい😆」

config.active_record.database_selector = 2.seconds

「お、この2秒という数値↑、なるほど感」「というと?」「masterというかprimaryに書き込んだ後でそれがreplicaに複製されるまでにはタイムラグがあるので、タイムラグの上限を保証する的な設定なんでしょうね、2秒経過すればreplicaからも最新のデータを取れると」「database_selector、新しいガイドにあったかな...?🤔」

ありました↓。

参考: rails/active_record_multiple_databases.md at 98a57aa5f610bc66af31af409c72173cdeeb3c9e · rails/rails

### active_record_multiple_databases.mdより
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

「この辺の数値って意識しておかないとマズいときがあるんですよ: レプリケーションが詰まったりするとデータの参照に失敗してIDがかぶっちゃうとかありますし」「あ〜」「まあIDをデータベースで生成させていれば問題ないんですけど、別のものを使ってIDを生成しているとIDかぶりで落ちるとか普通に起きますので🥶」「なるほど!」「RailsのマルチプルDBがその辺もちゃんと面倒見てくれるとしたら賢い!記事の方も、マルチプルDBについて取りあえずどのぐらい大丈夫なのかとかを知るのによさそうですね👍」

Rails 6のAction Textのファイルアップロードを分解調査する

DHHがこの記事をリツイートしてました。


つっつきボイス:「これは例のst0012さんが書いた記事で、既に翻訳済みなので近々公開しますけど、オチはAction Textのファイルアップロード機能でN+1クエリ問題を見つけたというものでした😆(#36177)」「まあAction Textも機能でかそうだからそういうのありそうですし😆」「data-trix-attachmentとかにTrixエディタの名残りがありますね〜」

「Action Textの情報がなかなか見当たらなかったのでst0012さんは仕方なく自分で調べたそうです」「このあたりは内部実装だから、今はこうでも今後どう変わるかわかりませんけど☺️」「Linuxカーネルの解説書なんかもすぐ古くなりますしっ😆」

「記事によると、Action Textでは以前もつっつきで話題に出た例のglobalidを使ってるようです(ウォッチ)」「DHHなら使いそう😆」「しかもSignedGlobalID使ってる」「不正アクセス防止用」「まだ保存されてないデータでグローバルなIDを使いたい場合はやっぱりglobalid欲しいでしょうね☺️」

参考: rails/globalid: Identify app models with a URI

combustion: Railsエンジンを楽にテスト(Ruby Weeklyより)


つっつきボイス:「combustionって内燃機関の『燃焼』だから、Railsエンジンにひっかけた命名っぽいですね」「これは何がうれしい点かな?」「RailsエンジンをテストするためにRailsアプリを丸ごと作成しなくてもいいようにするとかそういう感じみたいです」「ははぁなるほど😋、Railsのエンジンをデフォルト設定で作ると確かエンジンテスト用のRailsアプリが作られるという覚えが」「Railsエンジンのリポジトリを作ったときに、中にまるっとRailsアプリが入ってるのはちょい冗長な気はしますね」

「ところでRailsエンジンって、fullとmountableとかあってそれそれでちょっと違ってたような」「え、エンジンに種類があったんですか😅」「Railsガイドにありますね↓」

参考: Rails エンジン入門 - Rails ガイド
参考: Getting Started with Engines — Ruby on Rails Guides

--mountableだとマウンタブルエンジンで、--fullだとフルプラグインということか」「あ、Railsガイドの訳がちょい間違ってた😅修正します(その後修正済み)」「原文だとfullにさらにオプションを追加したものがmountableということになりますけど↓、まあfullよりmountableの方が機能が多いって普通思わないですし😆」「直感に反してる😆」「ある種のワナ?😆」「原文も単語がちょっと足りないような(負け惜しみ😅)」

The --mountable option will add to the --full option:
guides.rubyonrails.orgより

「mountableの方が独立性が高くて単独のミニRailsアプリに近そう」「でもfullでも丸ごとRailsが入るのは同じといえば同じかな」「fullオプションだと名前空間化まではやらないらしい」「fullオプションって使いみちあるんだろか?🤣」

参考: Gem、Railtieプラグイン、Engine(full/mountable)の違いとそれぞれの基礎情報 - Qiita

その他Rails


つっつきボイス:「ポイントは『RC1でアップグレードするのがいいよ』ということでした」「RC1で機能がほぼ固まるし、RC2の修正ってそんなに多くは発生しないでしょうし☺️」


前編は以上です。

おたより発掘

バックナンバー(2019年度第3四半期)

週刊Railsウォッチ(20190806-2/2後編)RSpec CopのLeakyConstantDeclaration、serveoでゼロコンフィグ公開、RuboCopのPerformance/RegexpMatch改修ほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


CONTACT

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