週刊Railsウォッチ(20190924-1/2前編)Railsのconcernsを考える、Netflixのヘキサゴナルアーキテクチャ、Railsのアロケーション削減ほか

こんにちは、hachi8833です。RailsチュートリアルのRails 6対応翻訳をひとまず終えました🍵。お目見えはもう少し先になると思います。


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

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

第15回目公開つっつき会は、10月5日(木)19:30〜にBPS会議スペースにて開催されます。Railsウォッチのコンテンツにいち早く触れるチャンスです!皆さまのお気軽なご参加をお待ちしております🙇。

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

6-0-stableブランチが少し静かになったようなので、6.xマイルストーンなども覗いてみました。


つっつきボイス:「お、6.xなんてマイルストーンがあるし」「どう使い分けてるのかな?🤔」「6.1.0にも乗らないissueが6.xに乗るのかも」「新機能は6.xに入れるとか?」「そのうちやるToDo的なものも6.xに入ってるっぽいです😆」

(6.0)render :textrender :nothingオプションは削除完了と5.1アップグレードガイドに追記

ビューでrender :textを使っている場合は動かなくなる。MIMEタイプをtext/plainにしてテキストをレンダリングする新しいメソッドとしては今後render :plainを使うこと。
同様にrender :nothingも削除されるので、ヘッダーのみを含むレスポンスを送信するには今後headメソッドを使うこと。たとえばhead :okとするとbodyなしでレスポンス200を送信する。
同PRより大意


つっつきボイス:「render :textってもう動かなくなってたか😳」「ついに消えた」「そのことを忘れて古い方を使っちゃったりしますし😅」「今後はheadを使ってくれだそうです」「render :nothingってヘッダーだけ返してたのね」「renderしてないのにrenderを使うよりはheadの方がいいかも🤣」「たしかに🤣」

参考: render — ActionController::Renderer
参考: head — ActionDispatch::Integration::RequestHelpers

なお、つっつき後にRailsガイドにも反映しました(#890

はみ出し

「ところでrender :plainって、毎回plainの綴りで一瞬迷うんですよね😆」「そうそう、plainだっけplaneだっけみたいな😆」「調べるの面倒だし、どうせデバッグだからってついrender :textにしてみちゃったり😆」「plainとplaneって発音違ってたかな…?🤔」

発音は同じでした↓。

参考: Plain vs. Plane: How to Choose the Right Word

そういえば映画『マイ・フェア・レディ』で発音の特訓に使われた『スペインの雨は主に平野に降る』という早口言葉もありました。

参考: マイ・フェア・レディ (映画) - Wikipedia

(master)マイグレーションでDatabaseConfigオブジェクトを直接使うようにした

コネクション設定については、あちこちで直接引き回されているHashではなく、DatabaseConfigをもっと積極的に使おうという方針に移行しつつある。
ここではDatabaseTasksdatabases.rakeでデータベース設定オブジェクトを使うように変えている。以下はメモ。

  • DatabaseTaskscharset_currentcollation_currentのpublicメソッドのテストがなかったのでそれも足してある。
  • schema_up_to_date?の引数のうち重複してて紛らわしい部分を非推奨化した。うち1つ(environment)は以前使われていなかったが、現在はspec_namedb_config (DatatabaseConfig)オブジェクトを直接取れてしまう。
    同PRより大意
# activerecord/lib/active_record/tasks/database_tasks.rb#L342
-     def schema_up_to_date?(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary")
+     def schema_up_to_date?(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = nil, spec_name = nil)
+       db_config = resolve_configuration(configuration)
+
+       if environment || spec_name
+         ActiveSupport::Deprecation.warn("`environment` and `spec_name` will be removed as parameters in 6.2.0, you may now pass an ActiveRecord::DatabaseConfigurations::DatabaseConfig as `configuration` instead.")
+       end
+
+       spec_name ||= db_config.spec_name
+
        file ||= dump_filename(spec_name, format)

        return true unless File.exist?(file)

-       ActiveRecord::Base.establish_connection(configuration)
+       ActiveRecord::Base.establish_connection(db_config)
        return false unless ActiveRecord::InternalMetadata.table_exists?
        ActiveRecord::InternalMetadata[:schema_sha1] == schema_sha1(file)
      end

つっつきボイス:「冗長な書き方を整理する感じでしょうか?」「なるほど、わざわざ変換しなくても元オブジェクトを渡せばいいんじゃね?って話か」「わざわざhashにしなくてもええと」「もしかするとテストのときはhashの方がやりやすかったみたいなことが当時あったのかもしれませんけど☺️」「割と修正範囲広いな〜」「必須引数を増やすとついこうなりがちですよね😆」

「ところで元のコードはどういう経緯でhashを使う設計にしたんだろう?🤔」「永続化したかったとか?🤔」

(master)ActiveRecord::DatabaseConfigurationsto_hto_legacy_hashを非推奨化

# activerecord/lib/active_record/database_configurations.rb#L81
    def to_h
-     configs = configurations.reverse.inject({}) do |memo, db_config|
-       memo.merge(db_config.to_legacy_hash)
+     configurations.inject({}) do |memo, db_config|
+       memo.merge(db_config.env_name => db_config.configuration_hash.stringify_keys)
      end
-
-     Hash[configs.to_a.reverse]
    end
+   deprecate to_h: "You can use `ActiveRecord::Base.configurations.configs_for(env_name: 'env', spec_name: 'primary').configuration_hash` to get the configuration hashes."

つっつきボイス:「なぬ、Active Recordのto_hto_legacy_hashを非推奨化?」「あ、タイトルのドラフトはしょりすぎでした😅」『ActiveRecord::DatabaseConfigurationsの』が正しい😎」

to_legacy_hash↓ワラタ😆」「移行パスのためっぽい匂いを感じる🌷」

# activerecord/lib/active_record/database_configurations/database_config.rb#L41
-     def to_legacy_hash
-       { env_name => configuration_hash.stringify_keys }
-     end

「そういえばARにto_hってありましたっけ?手元でuser.to_hとかするとそんなのねえって言われる😭」「あ、ハッシュはattributesで取れるんだったか😆」

参考: attributes — ActiveRecord::AttributeMethods
参考: RailsでActiveRecordのデータをhashにする方法 - Qiita

(master)6.0でincludesとjoinsを併用するとjoinしたクエリの順序が変わる問題を修正

# activerecord/lib/active_record/associations/join_dependency.rb#L73
+     def base_klass
+       join_root.base_klass
+     end
+
# activerecord/lib/active_record/relation/query_methods.rb#L1103
      def build_joins(manager, joins, aliases)
        buckets = Hash.new { |h, k| h[k] = [] }
        unless left_outer_joins_values.empty?
          left_joins = valid_association_list(left_outer_joins_values.flatten)
          buckets[:stashed_join] << construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
        end

+       if joins.last.is_a?(ActiveRecord::Associations::JoinDependency)
+         buckets[:stashed_join] << joins.pop if joins.last.base_klass == klass
+       end
+
        joins.map! do |join|
          if join.is_a?(String)
            table.create_string_join(Arel.sql(join.strip)) unless join.blank?
          else
            join
          end
        end.compact_blank!.uniq!
        while joins.first.is_a?(Arel::Nodes::Join)
          join_node = joins.shift
          if join_node.is_a?(Arel::Nodes::StringJoin) && !buckets[:stashed_join].empty?
            buckets[:join_node] << join_node
          else
            buckets[:leading_join] << join_node
          end
        end
        joins.each do |join|
          case join
          when Hash, Symbol, Array
            buckets[:association_join] << join
          when ActiveRecord::Associations::JoinDependency
            buckets[:stashed_join] << join
          when Arel::Nodes::Join
            buckets[:join_node] << join
          else
            raise "unknown class: %s" % join.class.name
          end
        end
        build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
      end

つっつきボイス:「JOINの順序維持問題は@kamipoさんが以前も似たようなことを言及してたのを見たような気がしますね」「そういえば既視感あると思ったら、以前Arelの話題で取り上げた#36805↓が上と近い問題でした(ウォッチ20190819)」

参考: Preserve user supplied joins order as much as possible by kamipo · Pull Request #36805 · rails/rails

「@783cafeのコミットメッセージ↓でも#36805に言及してますね」「こういうのは見つけてつぶしていくしかなさそう🙏」

リレーションにeager_loadとstringのjoinsだけがある場合、#36805によってstringのjoinsが最初のjoinsとみなされ、従来と挙動が変わる。
joinの順序を従来どおりにするために、stringのjoinsより前にjoinのeager loadingを先にチェックする。
@783cafeより大意

「お〜、こういうふうにjoinsに生SQLを書く形でテストするのね↓」

# activerecord/test/cases/associations/inner_join_association_test.rb#L82
+ def test_eager_load_with_string_joins
+   string_join = <<~SQL
+     LEFT JOIN people agents_people ON agents_people.primary_contact_id = agents_people_2.id AND agents_people.id > agents_people_2.id
+   SQL
+
+   assert_equal 3, Person.eager_load(:agents).joins(string_join).count
+  end

参考: joins — ActiveRecord::QueryMethods

(master)amatsudaさんのアロケーション削減

# activesupport/lib/active_support/parameter_filter.rb#L59
      def self.compile(filters, mask:)
        return lambda { |params| params.dup } if filters.empty?

-       strings, regexps, blocks = [], [], []
+       strings, regexps, blocks, deep_regexps, deep_strings = [], [], [], nil, nil

        filters.each do |item|
          case item
          when Proc
            blocks << item
          when Regexp
-           regexps << item
+           if item.to_s.include?("\\.")
+             (deep_regexps ||= []) << item
+           else
+             regexps << item
+           end
          else
-           strings << Regexp.escape(item.to_s)
+           s = Regexp.escape(item.to_s)
+           if s.include?("\\.")
+             (deep_strings ||= []) << s
+           else
+             strings << s
+           end
          end
        end

つっつきボイス:「お〜、この地道な修正はまさにこの間の銀座Rails#13でamatsudaさんが話してたトピックのひとつですよ😋」「😀」「このときのスライド見たいんですけどね…」「探したけど見つかりませんでした🥺」

「ベンチマークをがっと出してメモリ消費量をリストアップして、『ここが多そう』と当たりをつけては修正してはまたベンチ回すという感じでメモリアロケーションを減らしているそうです💪」「すげぇ〜!」「@5c07e1aのコミットメッセージにつぶやきが↓」「苦労が偲ばれる🙇」


@5c07e1aより

「@2db4c02の修正↓を見ると、mapよりeachの方がアロケーションが少ないということ?」「修正前はmapのブロックの中でまたmapを回してたけど、修正後はどちらもeachに変えてますね」「つまりメモリアロケーション的にはmapよりeachの方が有利ということか😳」「おぉ〜」「あくまでフレームワークの最適化としてですけどね☺️」

# actionpack/lib/action_dispatch/journey/gtg/transition_table.rb#L44
        def move(t, a)
          return [] if t.empty?

          regexps = []
+         strings = []

-         t.map { |s|
+         t.each { |s|
            if states = @regexp_states[s]
-             regexps.concat states.map { |re, v| re.match?(a) ? v : nil }
+             states.each { |re, v| regexps << v if re.match?(a) && !v.nil? }
            end

            if states = @string_states[s]
-             states[a]
+             strings << states[a] unless states[a].nil?
            end
-         }.compact.concat regexps
+         }
+         strings.concat regexps
        end

mapeachだとアロケーション周りの内部実装が違うのかな?」「mapの挙動からしてメモリコピーはやってるでしょうね: ビックリ付きのmap!だと破壊的なので違いますけど」「そうそう☺️」「APIドキュメント↓によるとeachの戻り値はレシーバーだから確かにアロケーションは起きませんね」「mapは配列を返すから回すたびにアロケーションすると」

参考: map — Class: Array (Ruby 2.6.4)
参考: each — Class: Array (Ruby 2.6.4)

「お、上の修正前コードの外側のmapは生成した結果をわざわざ捨ててるし」「結果のarray使ってなかったのね😳」「eachでやれるところだったのにもったいない」「これは残念なパターン😆」「自分はmap大好きマンですけど、そのせいかたまにこんな感じでmapをうっかり繰り返しのためだけに使っちゃったりしますね😅」

「そういえば以前この↓記事でeachより先にmap使おうって言ってましたね😆」「メモリアロケーションみたいな最適化は後回しにしたい派だしmap好きなの❤️」「そういえば『最初から最適化するな』って普段からおっしゃってますよね☺️」「自分はeachで書いてみる派かな〜😆」「この辺は人による😆」

Ruby: `each`よりも`map`などのコレクションを積極的に使おう(社内勉強会)

「あと、mapcollectで書く方が集めるというイメージに合ってて好きですね😋」

Rails

スライド: Netflixの「Surrounded by Microservices」


つっつきボイス:「これは今年6月にNetflixが発表したRails設計話で、スライドないかと思ったらありました😂」

「やさいちさんのツイートのまとめがわかりやすいです↑」「ヘキサゴナルアーキテクチャ↓ですか!」「Netflixといえばカオスエンジニアリング💪」「強い」「Hanamiの話も出てきてる🌸」

参考: Netflix 驚異的なトラブル対応 カオスエンジニアリングとは、何か?

「今Netflixのスライドを見てみると、割と写真中心ですね😆」「おしゃべりの背景用かな😆」「動画の方がメインかな😅」

動画はよく編集されていて字幕も見やすくなっています😋。

「ツイートに『優れたアーキテクチャの目的は意思決定を遅らせることである』とありますね」「気持ちわかる」「わかる😂」「YAGNIはやめとけと」

YAGNIを実践する(翻訳)

「EuRuKoってEuropean Ruby conferenceなのね」「conferenceなのにKo?」「ヨーロッパだとKで書く感じの地域が割とありますね(ドイツとかチェコとかロシアとか)」

そういえば、ヨーロッパ系の人はcompanyを「コンパニー」みたいに発音する傾向がどことなくある印象です。

EuRuKoのまとめ記事: EuRuKo 2019: A recap

既に来年のeuruko2020.orgのドメインもできてますね😋。

スライド『Concernsに関する懸念』でconcernsを考える


つっつきボイス:「ruby-jp Slackで見かけたスライドです」「@willnetさん!」

「concernsというと、だいたいよくない使われ方の方をよく見かけますね😆」「そうそう😆」「『関心事』とは果たして何ぞやと😆」「concernsはピュアなRubyのモジュールという印象」「実際そうですし」

「『初心者が取り扱うのは難しい』、ほんとそう!」「やめといた方がいい😆」「適切なconcernsってそうそう作れませんし、基本的にはconcernsってほぼ使わないな〜☺️」「自分は割とconcerns好きなのでちょいちょい使ってますけど😆」

「concernsをRailsで使う意味がどこまであるかですよね: concernsでメソッド生やしてもいいけど、includeしたときにconcernsで生やしたものが全部ちゃんと動くように設計できるのか?って思ったりしますし」「まあそれはありますね」「ちゃんと設計しておかないと、こっちのモデルにはconcerns生やすと動くけど、あっちのモデルでconcernsすると動かないとか、そういうものがきっと残る😇」

「それで言うとRubyのEnumerableはeachを定義してincludeすれば必ず動くからキレイですよね✨」「たしかに!」「concernsもそんなふうに『このconcernsをincludeしていい条件はこれとこれだよ』というのが明らかになってないと気持ち悪い😓」

参考: module Enumerable (Ruby 2.6.0)

「あと、ソースコードの行数を減らすためだけにconcernsを使っている残念なパターンも見かけますよね😆」「おぉ、ちょうどスライドにもその事例が↓😆」「Postsでしか使ってないモジュール😇」「これはマジでやめて欲しい😤」「それ普通にPostsに書けばええやんって思いますし」「ほんに」「でもやりたくなっちゃうとき、あるんですよね〜😅」

「こういう形でconcernsを使われると困るのは、ファイルが分かれてしまうからどこでincludeされているかまで考えないといけなくなることがまず1つ」「うんうん」「2つ目は、こういうPostでしか使わないつもりのコードがconcernsになっていると、それを他の人が見て『お、concernsに入っているこれ使えるじゃん❤️』って勝手に他で入れようとする事案が発生すること😇」

「スライドにもあるけど、concerningで名前空間を分けるだけならわかる↓」「concerningってのがあるんですね😳」「concerningも一応同じように書けます😎」

参考: concerning — Module::Concerning
参考: Rails 4.1.0 で追加された Module#concerning と関心事の分離 | TECHSCORE BLOG

「そうそう、rubocopのClassLengthで怒られないためにconcerns使うのヤメレと↓😆」「『行数多すぎ』ってヤツですね」

参考: Metrics/ClassLength — Metrics Cops - RuboCop: The Ruby Linter that Serves and Protects

「とはいうものの、concernsだと一見それっぽくできちゃったりするんですよね〜😅」「RubyMineの右クリックリファクタリング的な感じの単なるコピペになりがち😆」「たしかに意味変わらないんだけどそれをconcernsでやらないで〜って😭」

「スライドに引用されているDHHのコードのconcernsの量がヤバい↓🤣」「🤣」「しかも# Depends on Readableとかがずらっと並んでるあたりconcerns地獄感ある😈」


同ツイートより

「concernsによほどいい名前が付けられていれば別だけど、この書き方は好きじゃないな〜😅」「CopyableとかReadableとかって、ライブラリの機能ならともかく業務コードでその機能使う?って気持ちになりますし😆」「でもPrintableだったらありかな、というかこれは他でも使われてるちゃんとしたconcernsという気がする」「そんな感じしますね」

「あとconcernsは読み込み順序も絡んでくるのがマジ面倒」「あ〜前後関係ありますね!」「スライドにもあるけど、concernsは最終的に1つのクラスに集約されちゃうから大変😭」「😆」「methodsしてgrepするとヤバいことになってる😇」「メソッド名がかぶると死にますし😇」「名前空間を分ける、はホントに大事」

「DHHはこれをいいと言うけど、結局ここに行き着くという↓🤣」「わかる〜🤣」「バンドが解散する理由ってたいていこれですよね🤣」

参考: 音楽性の違い - アンサイクロペディア

「フックをconcernsにする↓のもたまに見かけるし: コントローラのconcernsにbeforeなんちゃらを書いてincludeするヤツ、自分はあんまり読みやすいと思わない😆」「これがよほど特殊な計算で複数の場所で使うならconcernsもワンチャンありかなという気がしますけど、この例だと単にTime.zone.nowしてるだけだからconcernsにする意味がまったくないな〜って思いますし😆」

「コントローラの認証のコードをconcernsにしてincludeするとかなら、わかる」「それならわかりますね☺️」「ネストしたURLの末尾idチェックをconcernsにしてbefore_actionで動かすとかもやりますね」「concernsの中でのbefore_actionってリダイレクトしたときどうなるんだっけ?」「あ、before_actionはレンダリングするとリダイレクトしないでそこで終わっちゃうので、むしろきれい😆」「それはそれで、しれっと書かれたincludeにはじかれるという悲しいことが発生するという😆」「そう、だからやりすぎ注意😅」

「これ↑もね、とてもよくわかる😆」「わかるわかる〜!😆」「そういえば例の肥大化したActiveRecordモデル記事↓でも言ってましたね」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

「@willnetさんのスライドとてもためになる❤️」「つらいconcernsをたくさん見てくるとわかってくる😭」

「concernsディレクトリってデフォルトで作らなくてもいいのに、という気もするけど、たぶんここはDHHの思想なんだろうな…」「あれは要らないっすね〜😅」「悪い使い方を誘発しがちというか」「ゴミ置き場になりやすい」

「modelsディレクトリの直下のconcernsじゃなくて、そこからもうひとつ名前空間を掘ったところに置くconcernsなら、あっていいと思う」「そうそう!」「それなら『このconcernsはこの下でしか呼ばれない』ということが伝わるから😋」「それなら関心が分離されてる感ありますね」「逆にcontrollersの直下のconcernsに20個ぐらいあったりすると、その治安の維持は相当難しいと思うし😇」「そのconcernsはシステム全体を横断する関心事なのかと問い詰めたくなりますし😆」

後でDHHがRails 4でconcernsを導入したときの記事を見つけました↓。

参考: Put chubby models on a diet with concerns – Signal v. Noise

テストを不安定にさせる要因


つっつきボイス:「こちらもruby-jpで見かけました」

「CIはたまにこうやってコケる😆」「たまにコケるのやめて欲しい😭」「たまにコケたらrandom seedを固定して何回か回して…と😆」「時刻絡みでコケるとか多いっすね😆」「コケたらとりあえずもう1回回してみると😆」

「必ずORDER BYを付けよう、とか」「あ〜それたまに忘れちゃう😅」「この辺ってDBMSによっても違うことがあって、MySQLだと(ORDER BYがなくても)経験的には入れた順で毎回同じ結果が返ってきたりするんですけど、ぽすぐれはそうとは限らないという」「ぽすぐれのご機嫌次第😆」「でもSQL的には本来ぽすぐれの方が正しい」

参考: MySQLでORDER BYをつけないときの並び順 - かみぽわーる — InnoDBかMyISAMかで違ってくるそうです

GitHubがRails 6へのアップグレードを完了


つっつきボイス:「これもバズりましたね😋」「Publickeyに先越されましたorz」「わずか9日とあるけど、その前からやってたでしょきっと😆」「そのためにGitHubは2年半かけて独自forkを解消してましたね(ウォッチ20190603)↓」

その他Rails


techplay.jpより

つっつきボイス:「ちょうど定員越えてる」「よさげなイベントですが、気づくのが遅かった😭」「マルチDBの話もやるのね」「でかいシステム扱ってる人はたいていマルチDBやりますね☺️」


以下はつっつき後に見つけました。


yasslab.jpより


前編は以上です。

おたより発掘

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

週刊Railsウォッチ(20190918-2/2後編)RubyPrize 2019候補者発表、GoogleがTypeScript 3.5に熱烈フィードバック、日本語形態素分析kagomeほか

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

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

Rails公式ニュース

Publickey

publickey_banner_captured

デザインも頼めるシステム開発会社をお探しなら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ウォッチ