- 開発
週刊Railsウォッチ(20191202前編)Rails 6のimplicit_order_columnはカスタマイズ可能、rubocop-rails 2.4.0リリース、Capistrano記事ほか
こんにちは、hachi8833です。git.ruby-lang.orgが先週から不調なようです。
- サイト: 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
つっつきボイス:「params
でkey?
が使えるならmember?
もある方がいいという感じでしょうか?」「Rubyのハッシュにmember?
なんてあったっけ?」「Rubyの公式ドキュメント↓を見ると、key?
もmember?
もinclude?
もhas_key?
も互いにエイリアスになってる😆」「あそうか😳」 「それなら確かに全部使える方がいいですよね😋」「むしろなぜmember?
だけなかったのかと😆」「4つもあるとは😆」「後からRubyに足されたとかかしら?🤔」「member?
使ったことないな〜😆」
参考: instance method Hash#has\key?
(Ruby 2.6.0)
⚓コレクション読み込みチェックを最初のレコードではなく全レコードでチェックするよう修正
- PR: Check that entire collection has been loaded before short circuiting by bradleyprice · Pull Request #37747 · rails/rails
- After_initialize on STI that references association breaks preloading · Issue #37730 · rails/rails
現在、コレクションが読み込まれたかどうかは最初のレコードしかチェックしていない。特定のシナリオで
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↓を見る方が早そう」「見事歯抜けに🦷」「ステップ実行とかやってみたら原因追えるかも🤔」
⚓初期化時に訳文を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を明示せずにfirst
やlast
などのorderありfinder系メソッドを呼ぶと、Active Recordは主キーでソートする。主キーがオートインクリメントの整数値でない場合(UUIDなど)、これによって予想外の振る舞いが生じる可能性がある。今回の変更では、first
やlast
の結果が予測可能になるように、それらの暗黙の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::Resolver
とDatabaseConfiguratons
という2つのオブジェクトに同じロジックが多数実装されている。一方はconfig/database.yml
で定義された設定に用い、もう一方はestablish_connection
などのメソッドにString
やHash
で生の設定を渡すのに用いる。
時間とともに2つのロジックが少し乖離してきたので、コードの複雑さを軽減して一貫性を高めるためにResolver
を削除して主なメソッドをDatabaseConfigurations#resolve
に置き換え、resolve_pool_config
をConnectionPool
に移動するなどした。
同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がリリース
つっつきボイス:「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のバージョンをきっちり指定する場合とかペシミスティックに指定する場合とかについての話ね☺️」
「ところで、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自体もそんなに変わってませんし☺️」
前編は以上です。
バックナンバー(2019年度第4四半期)
週刊Railsウォッチ(20191125)Ruby 3.0は2020年12月にリリース決定、Rails 5.2.4rc2とRuby 2.7.0-preview3がリリースほか
- 20191119後編 メソッド参照演算子が廃止、GitHub新機能続々、平成Ruby会議、GitHub OAuthバイパスほか
- 20191118前編 ActiveJob引数のログ抑制、RailsガイドProプランお試し、ファイルアップロードのレジュームgemほか
- 20191112後編 invisible gemで可視性を変えずにパッチ当て、スライド:「型なし言語のための型」、自然言語の言語名を推測ほか
- 20191111前編 Active Recordモデルをprivateで封じ込める、心折れないRailsスキーマ管理、Railsセッションをクロスドメイン共有ほか
- 20191106後編 holiday_japan gemで日本の祝日判定、小さい関数が有害になるとき、Gitブランチのファジー検索ほか
- 20191105前編 Rails 6のデフォルト設定解説、DHHも消したいaccepts_nested_attributes_for、スライド『実践Railsアプリケーション設計』ほか
- 20191029後編 Ruby 2.7.0-preview2、tapping_device gemとhumanize gem、平成Ruby会議ほか
- 20191028前編 RailsにSTI用メソッドsti_class_forとpolymorphic_class_forが追加、RuboCopを変更箇所だけにかけるgem、strftime書式生成サイトほか
- 20191021 Rails 6でhas_many関連の修正やSprockets 4.0対応、Shrine 3.0がリリース、Minitestスタイルガイドほか
- 20191015 スライド「Rails Performance issues and Solutions」を見る、dirtyに*_previously_was が追加、Sidekiq 6.0.1ほか
- 20191008後編 Ruby 2.7のInteger#[]でバイナリチェック、rubyzip gemは強力、13KBのJavaScriptゲームほか
- 20191001後編 RedisとRubyをつなぐredis-object gem、Fullstaq Rubyの新バージョン、COUNT(*)とCOUNT(1)の速度ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。