週刊Railsウォッチ(20191202前編)Rails 6のimplicit_order_columnはカスタマイズ可能、rubocop-rails 2.4.0リリース、Capistrano記事ほか

こんにちは、hachi8833です。git.ruby-lang.orgが先週から不調なようです。


つっつきボイス:「(DevToolsを覗いて)これは大変そう…😅裏でめっちゃ作業してそうな雰囲気」「お祈りします🙏」

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

お知らせ: 週刊Railsウォッチ「第17回公開つっつき会」(無料)

第16回目公開つっつき会は、今週12月05日(木)19:30〜にBPS会議スペースにて開催されます。

週刊Railsウォッチの記事やここだけの話にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇。

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

先週のウォッチに載せられなかった分も含んでます🙇。

params.member?を追加してHashの振る舞いに近づけた

実際はdelegate:member?を追加しただけの改修です。

# actionpack/lib/action_controller/metal/strong_parameters.rb#L214
-   delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?,
+   delegate :keys, :key?, :has_key?, :member?, :values, :has_value?, :value?, :empty?, :include?,
      :as_json, :to_s, :each_key, to: :@parameters
# actionpack/test/controller/parameters/accessors_test.rb#165
  test "member? returns true if the given key is present in the params" do
    assert @params.member?(:person)
  end

  test "member? returns false if the given key is not present in the params" do
    assert_not @params.member?(:address)
  end

つっつきボイス:「paramskey?が使えるならmember?もある方がいいという感じでしょうか?」「Rubyのハッシュにmember?なんてあったっけ?」「Rubyの公式ドキュメント↓を見ると、key?member?include?has_key?も互いにエイリアスになってる😆」「あそうか😳」 「それなら確かに全部使える方がいいですよね😋」「むしろなぜmember?だけなかったのかと😆」「4つもあるとは😆」「後からRubyに足されたとかかしら?🤔」「member?使ったことないな〜😆」

参考: instance method Hash#has\key? (Ruby 2.6.0)


docs.ruby-lang.orgより

コレクション読み込みチェックを最初のレコードではなく全レコードでチェックするよう修正

現在、コレクションが読み込まれたかどうかは最初のレコードしかチェックしていない。特定のシナリオでafter_initializeフック経由でレコードをフェッチした場合、コレクションの最初のレコードは読み込まれていても残りのレコードが読み込まれていない場合がありうる。
データをフェッチするときの短絡を成功させるには、コレクション内の全レコードが読み込まれたことを検証する必要がある。
これにより、関連付けを参照するSTIがafter_initializeでプリロードに失敗する問題(#37730)が修正される。
同PRより大意

# activerecord/lib/active_record/associations/preloader.rb#L186
        def preloader_for(reflection, owners)
-         if owners.first.association(reflection.name).loaded?
+         if owners.all? { |o| o.association(reflection.name).loaded? }
            return AlreadyLoaded
          end
          reflection.check_preloadable!
          if reflection.options[:through]
            ThroughAssociation
          else
            Association
          end
        end

つっつきボイス:「たしかにlazy loadされているとこうなる😆」「firstだったのをall?にして、ちゃんと最後まで読み出せるかどうかを確認するように修正したと」「short circuitはとりあえず『短絡』としてみたんですが、どう表すのがいいのか迷い中です😅」「追加された以下のテストをちゃんと読めばfirstだと落ちるということがわかるんでしょうきっと😆」「踏んだことないエラー🤔」「STIでpolymorphic associationを使った場合のafter_initializeという組み合わせか〜」

# activerecord/test/cases/associations/eager_test.rb#629
  def test_preloading_with_has_one_through_an_sti_with_after_initialize
    author_a = Author.create!(name: "A")
    author_b = Author.create!(name: "B")
    post_a = StiPost.create!(author: author_a, title: "TITLE", body: "BODY")
    post_b = SpecialPost.create!(author: author_b, title: "TITLE", body: "BODY")
    comment_a = SpecialComment.create!(post: post_a, body: "TEST")
    comment_b = SpecialComment.create!(post: post_b, body: "TEST")
    reset_callbacks(StiPost, :initialize) do
      StiPost.after_initialize { author }
      comments = SpecialComment.where(id: [comment_a.id, comment_b.id]).includes(:author).to_a
      comments_with_author = comments.map { |c| [c.id, c.author.try(:id)] }
      assert_equal comments_with_author.size, comments_with_author.map(&:second).compact.size
    end
  end

#37730 issue↓を見る方が早そう」「見事歯抜けに🦷」「ステップ実行とかやってみたら原因追えるかも🤔」


#37730より

初期化時に訳文をeager loadingするよう修正

訳文読み込みによる初期レスポンスの速度低下を回避するため、アプリ初期化中に訳文をeager loadingする。
同PRより大意

# activesupport/lib/active_support/i18n_railtie.rb#L13
    config.i18n.load_path = []
    config.i18n.fallbacks = ActiveSupport::OrderedOptions.new

+   config.eager_load_namespaces << I18n
+

つっつきボイス:「i18nの訳文を後から読み込むと一発目の表示が遅くなるので修正したと」「たしかに〜」「最初に読み込むと今度はその分Railsサーバーの起動が遅くなりますけど😆」「どっちにするか選べるようにして欲しい人いそう」「config.eager_load_namespacesにデフォルトでi18nを加えるようになったのね」

config.eager_load_namespacesは前からあったようです↓(Rails 4.0.2以降)。

「railtieに入ったということはサーバーの起動速度にちょい影響しそうではある」「i18nはヨーロッパの人なら確実に使うでしょうし」「この間正式になったGoogleのCloud Runみたいな環境で動かすRailsの起動速度を極力速くしたい人にとっては、要らんお世話なのかもしれませんけどっ😆」「なるほど」「逆にGitHubみたいに既にサーバーが立ち上がった状態で普通にリクエストを受けるシステムなら事前にi18nを読み込んでおいてくれる方がレスポンス速度が安定してうれしいでしょうし😋」「こっちの方が本来のRailsのユースケースなので、今回の変更はごもっともという感じ☺️」

implicit_order_columnに応じて主キーで第2ソート

# activerecord/lib/active_record/relation/finder_methods.rb#L561
      def ordered_relation
        if order_values.empty? && (implicit_order_column || primary_key)
-         order(arel_attribute(implicit_order_column || primary_key).asc)
+         if implicit_order_column && primary_key && implicit_order_column != primary_key
+           order(arel_attribute(implicit_order_column).asc, arel_attribute(primary_key).asc)
+         else
+           order(arel_attribute(implicit_order_column || primary_key).asc)
+         end
        else
          self
        end
      end

#34480のコメントにあったように、implicit_order_columnの現在の実装では、中で値が重複している場合に同じ順序でオブジェクトが返されるとは限らない。
このパッチは(implicit_order_columnとして設定されていない場合は)主キーを第2ソートとして追加することで出力の順序が一貫するようになった。
同PRより大意


つっつきボイス:「ここからは新しめの改修です」「むしろ#34480で足されていた元の機能↓の方が気になりますね: 自分は昔からこの機能があればいいのにって思ってましたし」「Rails 6で入ってたんですね😳」

暗黙のorder用カラムを指定可能に
ソートのorderを明示せずにfirstlastなどのorderありfinder系メソッドを呼ぶと、Active Recordは主キーでソートする。主キーがオートインクリメントの整数値でない場合(UUIDなど)、これによって予想外の振る舞いが生じる可能性がある。今回の変更では、firstlastの結果が予測可能になるように、それらの暗黙のorderで使われるカラムをオーバーライドできるようになった。
#34480のActive Record Changelogより

class Project < ActiveRecord::Base
  self.implicit_order_column = "created_at"
end

後で調べると、6.0リリースノートでも#34480についてはごく簡単にしか触れられていませんでした↓。

6.0のimplicit_order_columnカスタマイズ機能の使い所

「こういうふうに、ORDER BYを付けなかった場合にデフォルトで付けてくれる機能がある方が、世の中の人はおおむね幸せになれる😋」「なれます〜😋」「前はなかったのもびっくりですね😳」

「ただしこの機能はケースバイケースでもあるんですよ: ORDER BYが付くとクエリが激重になることがあるから😇」「インデックスがらみとか?」「というより、RDBMSにとってはORDER BYが付かない方が見つけた順に結果を返せるから総じて速くなります」「そうそうっ」「ソートは重たい操作なので、ORDER BYを付けるとクエリが最初に返り始めるまでの時間が長くなりますし」「でかいテーブルが要注意なんですね😳」

「デフォルトでORDER BYを付けるというのは環境によっては耐え難いほどクエリが遅くなることもあるので😎」「なるほど、だから機能を使うかどうかを選べるようになってるんですね」「新しいプロジェクトだったらあらかじめApplicationRecordで指定しちゃう手もありますし☺️」「外すかどうかはデータが育ってから考えると」「そうそう、データが育っちゃった後でこの機能をいきなり使うと遅くて死ぬ可能性あります😇」「後からこの機能を使うのはコワい💀」


「で元の#37626は何が修正されたんでしたっけ😆」「以下のAPIドキュメント↓を見ると、結果のソート順が主キーで確定するなら主キーでサブソートされるということみたい」「プルリクタイトルの『Additionally order by primary key if implicit_order_column is not uniq』とプルリクの内容が何だか微妙に食い違ってる気がする…😅」「Railsに限らないと思うんですけど、プルリクのタイトルってめちゃ走り書きなことがあって、これまでも先週の改修でちょくちょくダマされました😇」「🤣」「前はnon-uniqueなカラムを使うとソート順が確定しないことがあるという記述が消されてる」「created_atみたいにuniqueが保証されないカラムも指定できるようになった?」「意図がイマイチ汲み取りきれない😅」

# activerecord/lib/active_record/model_schema.rb#L116
    # Sets the column to sort records by when no explicit order clause is used
    # during an ordered finder call. Useful when the primary key is not an
-   # auto-incrementing integer, for example when it's a UUID. Note that using
-   # a non-unique column can result in non-deterministic results.
+   # auto-incrementing integer, for example when it's a UUID. Records are subsorted
+   # by the primary key if it exists to ensure deterministic results.

「でもimplicit_order_columnがカスタマイズできるということがわかったのはうれしい😂」「いいこと知った😋」「新しいRailsプロジェクトなら最初から入れといてもいいぐらい😋」「付けてないことによる事故の方が多かったりしますし🚦」「特に、MySQLはオートインクリメントされたカラムを割とその順序で返してくれるけどPostgreSQLはそうでなかったりしますし」「そうそうっ😤」「でもPostgreSQLの方がSQLとして本来の姿なんですよね😆」「ORDERを指定しないところに順序なんかないっ😆」

以下の記事によるとimplicit_order_columnではカラムを1つしか指定できないそうです。

参考: Rails6 のちょい足しな新機能を試す71(implict_order_column 編) - Qiita

ConnectionAdapters::Resolverを削除

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1042
      def establish_connection(config, pool_key = :default)
-       resolver = Resolver.new(Base.configurations)
-       pool_config = resolver.resolve_pool_config(config)
+       pool_config = resolve_pool_config(config)
        db_config = pool_config.db_config
# activerecord/lib/active_record/connection_handling.rb#L251
    private
      def resolve_config_for_connection(config_or_env)
        raise "Anonymous class is not allowed." unless name
        config_or_env ||= DEFAULT_ENV.call.to_sym
        pool_name = primary_class? ? "primary" : name
        self.connection_specification_name = pool_name

-       resolver = ConnectionAdapters::Resolver.new(Base.configurations)
-
-       db_config = resolver.resolve(config_or_env, pool_name)
+       db_config = Base.configurations.resolve(config_or_env, pool_name)
        db_config.configuration_hash[:name] = pool_name
        db_config
      end

つっつきボイス:「ロジックが重複していたので片方を削除したのね☺️」「リファクタリング」「お引越し🚛 」

ConnectionAdapters::ResolverDatabaseConfiguratonsという2つのオブジェクトに同じロジックが多数実装されている。一方はconfig/database.ymlで定義された設定に用い、もう一方はestablish_connectionなどのメソッドにStringHashで生の設定を渡すのに用いる。
時間とともに2つのロジックが少し乖離してきたので、コードの複雑さを軽減して一貫性を高めるためにResolverを削除して主なメソッドをDatabaseConfigurations#resolveに置き換え、resolve_pool_configConnectionPoolに移動するなどした。
同PRより大意

「ついでにdatabase.ymlに関するAPIドキュメントがちょっと足されているみたい↓」「お〜😋」「こうやってコメントがちゃんと書かれるとありがたい🙏」

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1149
        # Returns an instance of PoolConfig for a given adapter.
        # Accepts a hash one layer deep that contains all connection information.
        #
        # == Example
        #
        #   config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
        #   pool_config = Base.configurations.resolve_pool_config(:production)
        #   pool_config.db_config.configuration_hash
        #   # => { host: "localhost", database: "foo", adapter: "sqlite3" }
        #

Rails

rubocop-rails 2.4.0がリリース


docs.rubocop.orgより


つっつきボイス:「rubocop-railsは本家rubocopとバージョンの歩調を合わせないことにしたという話がありましたね」「そうそう、本家に構わずガンガン上げていくぜと💪」

新機能:

  • Rails/ApplicationController copとRails/ApplicationMailer copを追加
  • Rails/RakeEnvironment copを追加
  • Rails/SafeNavigationWithBlankを追加

Rails機能のエンジンへの切り出しにRuboCopを活用


同記事より


つっつきボイス:「Railsエンジンの切り離し?」「上の図↑はエンジンのデータを自由に読み書きできちゃまずいよねという話みたい」「ちゃんとしたインターフェイスを経由してやりとりしないと😆」

isolate_namespace↓ってあったそういえば」「おぉ?」「これを使うことで名前空間が分かれる: エンジンを書くときはこうしないとRails側とかぶっちゃうとか何とかだったと思う」「きっとRailsガイドにある気がします😋」「マウンタブルエンジンについては丁寧なガイドがあるはず☺️」「密結合したエンジンなんてエンジンじゃないし🤣」「🤣」

参考: 2.1.1 重要なファイル — Rails エンジン入門 - Rails ガイド

Engineクラスの定義に含まれるisolate_namespaceの行を変更・削除しないことを強く推奨します。この行が変更されると、生成されたエンジン内のクラスがアプリケーションと衝突する可能性があります。
Railsガイドより

「Railsアプリが太ってきたときにどう分割するかという方法はいくつかあるんですが、そのひとつが機能のエンジン化ですね」「おぉ」「複数のRailsアプリにする方法とは別に、1つのRailsアプリの中でエンジンに切り出すという方法☺️」「記事を書いた人は、その辺の作業を支援するためにcopをいくつか作ったみたいです↓」「危ないときに立ち会ってくれるcop👮」「マウンタブルエンジンとかそんなにしょっちゅうは書かないから😆、そういうのがあるとよさそう」

記事では、他の切り離し方法として「リードオンリーActive Record」や「明示的に定義された依存関係だけをテストで読み込む」「Active Recordのsaveなどにフックをかける」なども紹介されています。

Bundlerを健全に使うには

# 同記事より
# 正確なバージョン指定
gem 'nokogiri', '1.0.3'
gem 'webrat', '0.3.1'

# ペシミスティックなバージョン指定
gem 'nokogiri', '~> 1.0.3'
gem 'webrat', '~> 0.3.1'

# バージョン指定なし
gem 'nokogiri'
gem 'webrat'

つっつきボイス:「2012年という古い古い記事ですが、先週出した翻訳記事↓で言及されていたので」「gemのバージョンをきっちり指定する場合とかペシミスティックに指定する場合とかについての話ね☺️」

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

「ところで、gemとかのバージョンをピンポイントに指定するのって、それはそれでたまにリスクがあったりするんですよ」「マジですか?!😳」「ピンポイントに指定したバージョンが何かのミスで消えちゃったりしたら当然失敗するようになるので😭: もっとも最近はそういうことはあまりありませんけど☺️」

「上の翻訳記事の方ではJSのモジュールのバージョンを全部ピンポイントにしないと気が済まないという感じでした」「Rubyもそうですけどね☺️」「ただGemfile.lockにはピンポイントで書かれてそちらが正ですけど、Gemfileの方にはこのぐらいのバージョンなら動くよねという期待を込めてバージョンを書いたりしますし😆」「最近もGCPのgemがめちゃ古くないと動かないケースあった…😇」

Capistranoを使う(Ruby Weeklyより)

# 同記事より
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && ‘@’,
      server.hostname,
      server.port && “:”,
      server.port
    ].compact.join
  end
end

つっつきボイス:「こっちは新しくて普通にCapistranoやった記事ですが、Capistranoの新しい記事というのが珍しそうだったので」「そうそう、Capistranoの記事って意外に少ない😆」「記事書いた人もきっと同じ思いだったに違いない🤣」「『ないなら自分で書くわい』みたいな😆」「これは翻訳したいです😋」

「Capistranoのコードを書く人は、どちらかというとサーバーサイドのインフラエンジニアですよね: 書き方はいろいろだけど、いったん書き方が枯れて安定してしまえばそれで全然いいという感じですし☺️」「インフラエンジニアがCapistranoコードでテンプレートを一度がっつり書いておけば、後はみんなでそれを使い回す😆」「秘伝のタレ的なconfigがあったりしますね😆」「実際Capistrano自体もそんなに変わってませんし☺️」


capistranorb.comより


前編は以上です。

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

週刊Railsウォッチ(20191125)Ruby 3.0は2020年12月にリリース決定、Rails 5.2.4rc2とRuby 2.7.0-preview3がリリースほか

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

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

Rails公式ニュース

Ruby Weekly

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ