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

週刊Railsウォッチ(20190311-1/2前編)「Rails Conductor」14年ぶり復活なるか?、RubyGemsに複数の脆弱性、2009年のRailsエコシステムほか

こんにちは、hachi8833です。

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

3/7(木)の公開つっつき会

3/7(木)の公開つっつき会にお集まりいただいた皆さま、ありがとうございます!🙇。

通常は週刊Railsウォッチに収録されている会話(つっつきボイス)を匿名化していますが、今回はRailsチュートリアルRailsガイドCoderDojo Japanの運営、2019年度未踏ジュニアのPMなど多方面で活躍中のYassLab株式会社の安川要平さん(@yasulab、下写真右)が初参加され、顔出し/名前出しを快諾いただきましたので、安川さんのみ例外的に一部で顔出し/名前出ししております。


yasslab.jpより

今回はいつもより変則的な流れで充実した公開つっつきだったにもかかわらず、今回も録画に失敗してしまいました🙇🙇。誤りがありましたら@hachi8833までお知らせください。

臨時ニュース: RubyGemsに複数の脆弱性(Ruby公式ニュースより)

以下のバージョンが影響を受けるそうです。

  • Ruby 2.4(2.4.5以前)
  • Ruby 2.5(2.5.3以前)
  • Ruby 2.6(2.6.1以前)
  • trunk 67168以前

回避方法として、以下を実行して最新のRubyGems(2.7.9/3.0.3またはそれ以降)にアップグレードする方法が説明されていました。

gem update --system

つっつきボイス:「おっと来ましたね」「自分はこの間教えてもらったrbenv-each↓使ってRubyGemsの全バージョンのアップグレードを速攻で完了させましたよ😎」「えっそんないいものあったんですか!😂」

後日、rbenv-eachをインストール後rbenv each gem update --systemを実行すると続々とRubyGemsが2.7.9または3.0.3にアップグレードされました。2.3より古いRubyでは物がなくてインストールできなかったりしましたがそれはOKということで。

私のローカル環境ではRuby 2.0以降が全部rbenvに入っているのでとっても助かりました🙏。後日以下のツイートを見つけました。jnchitoさんもrbenv-eachお使いなんですね。

特集: Rails Conductorとは?

つっつきボイス:(つっつきの途中で)「ところで今回のウォッチでRails Conductorって取り上げます?」「え、何ですかそれ??😳」「何それ??😳」「koicさんやa_matsudaさんが昨日こんなツイートしてたんですよ↓😎」

「ウォッチではマージされたコミットじゃないと取り上げないとか?」「いえいえ、キリがないので基本的にマージされたものから見繕ってます」「このプルリク、確かにまだオープンですね」「それにしてもConductorって...?」

というわけで急遽Rails Conductorの話題です。

Railsそのものの開発に使えるWebインターフェイスの構築、これはRailsというフレームワークをリリースする前からずっと夢だった。このアイデアはメイラーのプレビューなどでも頭をよぎったのだけど、Rails 6でAction Mailboxによる受信メール処理も加わったことだし、構想ががっつり固まるのを待つよりも、今あるものでとにかくやってみようと思った次第。
同PRより大意(強調は編集部)

「何でも構想14年とかで、ツイートによるとRailsのweb-consoleがその成果のひとつだったと」「へぇ〜!😳Railsのエラー時に表示されたりする、あの赤っぽいページ↓ですよね」「昨年Rails 6に追加されたAction Text(ウォッチ20181009)でしたっけ、あれももしかしたらこのRails Conductorの布石のひとつだったんじゃないかなってこっそり想像したり☺️」「Action TextはBasecampのTRIXを元にしたリッチテキスト表示のライブラリだから直接は関係なさそうかなという気もちょっとしますが😆」

「何というか『〜to build a web interface for developing Rails itself』ってがありますよね🤩」「わかる!これでIDEも付いてきちゃうみたいな😍」「DHHの文章は名調子というか、コミュニティを盛り上げるタイミングとかが実にうまい😋」「いい意味で燃料を投下するというか」

なお、#35489のコメントでは「『a web interface for developing Rails itself』はちょっと紛らわしいかな:『Railsアプリの開発やメンテに役立つさまざまなツール向けのWebインターフェイス』ぐらいの方がよさげだけど、とにかく素敵なアイデアだね!」とありました。


同PRコメントより

ツイートの続き↓。

その後にもいろいろ計画があります。Conductorなどがそうですね。ただ、”彼”についてはもうしばらく秘密にしておこうと思います。
同記事より

「ところでconductorってこの場合きっと車掌(railway conductor)になぞらえて命名したんじゃないかなと思いました」「あ〜そうかも」「Railsだし鉄道絡みのネーミングも今までいろいろあったし🛤」「Journey(ルーティング関連)とかRailtie(犬釘)とか」「conductorって音楽の『指揮者』って意味もありますよね」「ですです」「電気方面だと『導体』だったりするし」「意味多すぎ😆」

後で調べると、conductorが車掌を指すのは主に米国英語のようです。そういえばツアコン(tour conductor)なんてのもありますね。

「ところで、このconductorっていう名前の別のgemが既にあったそうなんですが、これが以下のような流れになったんですよ😆」「DHHがちゃっかりconductorのオーナーに追加😆」「何という世界線🌍」「つか発見したの安川さんだし😆」「☺️」

鉄道唱歌の「電車ごっこ」を埋めようかと思いましたがあまりにベタなのでリンクを貼るだけにしました。

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

公式更新情報コミットログの両方からです。

reselectメソッドが追加

reselectunscope(:select).select(fields)と同等だそうです。

# activerecord/lib/active_record/relation/query_methods.rb#L252
+   # selectステートメント全体がunscopeされることに注意
+   def reselect(*args)
+     check_if_method_has_arguments!(:reselect, args)
+     spawn.reselect!(*args)
+   end
+
+   # #reselectと同じだがコピーではなくリレーションに対して操作する
+   def reselect!(*args) # :nodoc:
+     self.select_values = args
+     self
+   end

rewherereorderというメソッドがあるのを今頃知りました。


つっつきボイス:「社内Slackでも話題になりましたね↓」「re*系のメソッドってunscopeの副作用に気をつけないとハマりそう」

unscopeで以下の記事を思い出しました↓。

Railsのdefault_scopeは使うな、絶対(翻訳)

#selectDISTINCTsizeを取るとcountが誤っていたのを修正

# activerecord/lib/active_record/relation/calculations.rb#L228
      def perform_calculation(operation, column_name)
        operation = operation.to_s.downcase
        # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
        # considered distinct.
        distinct = distinct_value
        if operation == "count"
          column_name ||= select_for_count
          if column_name == :all
-           if distinct && (group_values.any? || select_values.empty? && order_values.empty?)
+           if !distinct
+             distinct = distinct_select?(select_for_count) if group_values.empty?
+           elsif group_values.any? || select_values.empty? && order_values.empty?
              column_name = primary_key
            end
-         elsif column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
+         elsif distinct_select?(column_name)
            distinct = nil
          end
        end

        if group_values.any?
          execute_grouped_calculation(operation, column_name, distinct)
        else
          execute_simple_calculation(operation, column_name, distinct)
        end
      end

+     def distinct_select?(column_name)
+       column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
+     end

つっつきボイス:「おーバグだし🐛」「ちょうど今日のBPS社内勉強会でも触れたんですが、Railsには『まだこんなバグ残ってたの?』みたいなバグが見つかることがあって、kamipoさんたちがActive Record周りを熱心に修正してくれてしますよね」「そうでしたね」

「中にはアプリケーションバグにつながるようなものもあったりするので、Railsの挙動を信じすぎずに実データに近いデータシナリオでテストをきちんと行うべきだと考えます」「たしかに〜」「ポジティブなテストだけだと見落とす可能性もありますので」

「Railsウォッチやy_yagiさんのブログ↓を日頃からチェックして、Railsでこういうバグが出ていたなという脳内インデックスを作っておくと後々身を助けると思います😹」

参考: なるようになるブログ

has_oneで_blob関連付けが正しく読み込まれなかったのを修正

# activestorage/lib/active_storage/attached/changes/create_one.rb#L31
    def save
      record.public_send("#{name}_attachment=", attachment)
+     record.public_send("#{name}_blob=", blob)
    end
# 同PRより
class User < ActiveRecord::Base
  has_one_attached :avatar
  has_many_attached :highlights
end

user.avatar.attach(blob)
user.avatar_attachment.present?  => true
user.avatar_blob.present?        => false    # 誤り

つっつきボイス:「これはActive Storageの修正?」「あ、そうですね: attachmentは添付ファイルでしたか😅」

参考: Active Storage の概要 - Rails ガイド

nil関連修正2つ

# actionview/test/template/testing/fixture_resolver_test.rb#L5
class FixtureResolverTest < ActiveSupport::TestCase
  def test_should_return_empty_list_for_unknown_path
    resolver = ActionView::FixtureResolver.new()
    templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: [], handlers: [])
    assert_equal [], templates, "expected an empty list of templates"
  end
  def test_should_return_template_for_declared_path
    resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text")
    templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
    assert_equal 1, templates.size, "expected one template"
    assert_equal "this text",      templates.first.source
    assert_equal "arbitrary/path", templates.first.virtual_path
-   assert_equal :html,            templates.first.format
+   assert_nil templates.first.format
  end
end
# activerecord/test/models/developer.rb#L103
class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base
  self.table_name = "developers"

+ default_scope { }
  default_scope -> { where(name: "Jamis") }
  default_scope -> { where(salary: 50000) }
end

つっつきボイス:「対象は違いますが2つともnilを使えるようにする改修だったので」「2つ目はdefault.rbの改修箇所が貼ってあるけど、テストコードを貼る方がわかりやすいかも?」「そうでした😅更新しておきます(済)」

findメソッドのSQLキャッシュをベースクラスでも有効にした

# activerecord/test/cases/bind_parameter_test.rb#L57
+     def test_statement_cache_with_find
+       @connection.clear_cache!
+
+       assert_equal 1, Topic.find(1).id
+       assert_raises(RecordNotFound) { SillyReply.find(2) }
+
+       topic_sql = cached_statement(Topic, Topic.primary_key)
+       assert_includes statement_cache, to_sql_key(topic_sql)
+
+       e = assert_raise { cached_statement(SillyReply, SillyReply.primary_key) }
+       assert_equal "SillyReply has no cached statement by \"id\"", e.message
+
+       replies = SillyReply.where(id: 2).limit(1)
+       assert_includes statement_cache, to_sql_key(replies.arel)
+     end

つっつきボイス:「#35431は、前回のウォッチの「STIのサブクラスでfind_byをキャッシュしないようにした」(ウォッチ20190304)と関連してるようです」

番外: #31221の変更を5.2リリースノートに追記

# guides/source/5_2_release_notes.md#L618
+ *   Idle database connections (previously just orphaned connections) are now
+   periodically reaped by the connection pool reaper.
+   ([Commit](https://github.com/rails/rails/pull/31221/commits/9027fafff6da932e6e64ddb828665f4b01fc8902))
+

Railsガイドの更新ですが、コミットメッセージに「#31221はPostgreSQLとpgpoolにおいて(いい意味で)非常に影響が大きいのに5.2リリースノートに書かれてないのがもったいないので」「#31221より前はスレッドプールやPumaワーカーなど広範囲でコネクションがお漏らししないようものすごく注意深くやらなければならなかった」だそうです。


つっつきボイス:「過去のリリースノートの更新って新しい😆」「そういえば一昨年のウォッチ((ウォッチ0171201))でも#31221を取り上げてました」「例のReaper(ウォッチ20190115)が不要なコネクションを刈り取ってくれるんでしたっけ」

「上のコミットメッセージで自らhonorable mention↓と書かれているので、番外らしくていいかなと😆」

honorable mention: 選外佳作、等外賞

Rails

AWS S3 APIリクエスト認証の署名バージョン2以下が6月に終了

署名バージョン 2 から署名バージョン 4 への移行
Amazon S3 API リクエスト認証に署名バージョン 2 を現在使用している場合は、署名バージョン 4 の使用に移行する必要があります。「Amazon S3 における AWS 署名バージョン 2 の廃止」で説明するように、署名バージョン 2 のサポートは終了します。
同記事より

参考: aws-sdk-ruby/CHANGELOG.old.md at master · aws/aws-sdk-ruby


つっつきボイス:「今日これでSlackがざわついてましたね」「これはまだ調査中なんですが、自分たちWebチームにとって割と影響が大きい🥶: なにしろ扱っているWebサービスが(Rails以外も含めて)いっぱいあって、どのアプリがどんな影響を受けるかを調べるところからやらないといけないので💦」「古いアプリでRubyバージョンとかgemの依存関係あたりが絡んでくると厄介そうですね」


docs.aws.amazon.comより

「AWS CloudTrailを使うと古いAPIを使っているか調べられるようで、どうやら署名バージョンの項目もあるっぽい」

参考: AWS CloudTrail (AWS API の呼び出し記録とログファイル送信) | AWS

「幸いfogというgemはかなり前からv4署名を使ってるようなので、fogを使うcarrierwaveでは大きな心配はなさそう」

参考: fog-aws/signaturev4.rb at master · fog/fog-aws

Rails 6の新しいメソッド4つ(RubyFlowより)

Array#including
Array#excluding
Enumerable#including
Enumerable#excluding

# https://github.com/rails/rails/commit/bfaa3091c3c32b5980a614ef0f7b39cbf83f6db3#diff-e3d63442dcd5dc00aa09c82c7daa4934R41#L
  #   people = ["David", "Rafael", "Aaron", "Todd"]
- #   people.without "Aaron", "Todd"
+ #   people.excluding "Aaron", "Todd"
  #   # => ["David", "Rafael"]

つっつきボイス:「DHH自らの追加なんですね」「Array#withoutEnumerable#withoutをリネームしてArray#excludingEnumerable#excludingにしたのか」「リネームしたお気持ち、何となくわかります: withoutだと引数との関係がふわっとしててどういう意味なのか考えちゃうので」「たしかに〜」「withoutと逆の動作をwithにするのもどうかと思うし」

[ 1, 2, 3, 4, 5 ].without(4, 5)
#=>  [1, 2, 3]


[ 1, 2, 3, 4, 5 ].excluding([4, 5])
#=>  [1, 2, 3]

そういえば英語のwithとかwithoutは割とどこにでもくっつけられるので、意味があいまいになりやすいですね。有名な例ですが、『I saw a girl with a telescope.』という文章は、コンテキストがなければ以下のどちらとも受け取れます。日本語で「の」を2つ以上使うと係り受けのスコープが怪しくなるのと少し似ているかもしれません。

  • 私は、ある女の子を望遠鏡越しに見た。
  • 私は、望遠鏡を持っている女の子を1人見かけた。

参考: 東芝デジタルソリューションズ|The翻訳プロフェッショナル -- 『I saw a girl with a telescope.』を機械翻訳で回避するテクニックが紹介されています。

2009年のRailsエコシステム


つっつきボイス:「このツイートとトピックは昨日安川さんに教えていただきました🙇」「☺️」



同スライドより

「このスライドは200ページ超えですが、この頃のa_matsudaさんはまだコミッターではなかったとか、今はなきMerbも登場してたりと色々楽しめます😋」

参考: MerbがRails 3に統合、人気Rubyフレームワークが合体へ - ITmedia エンタープライズ

実はこのページ↓を見るの初めてでした。スライドでは当時のコアメンバーが紹介されていましたが、以下は現時点の最新です。

「今回上のスライドをご紹介したのは、言ってみればRails考古学とでも言うべきシリーズ記事があってもいいんじゃないかと思ったのがひとつありまして☺️」「今Railsガイド3.2以前のリリースノートを私が翻訳中なんですが、訳していて『あー、この機能はこの時期に登場したのか』とか『当時はもてはやされたけど後につまづきの元になってたりするな』なんていうのがあって面白いんですよ」「そんな感じで、現在の視点から見た過去のRailsの変遷はいいコンテンツになるんじゃないかと思って」「タイムスリップ的な」「タイムマシンで当時の自分に教えたいみたいな😆」「😆」

参考: Ruby on Rails ガイド:体系的に Rails を学ぼう


railsguides.jpより

「今のところRails考古学を冠した記事はTakeuchiさんの以下の記事ぐらいしかなさそうなので」「何なら共同で作っていってもいいですし☺️」(以下延々)

参考: Rails考古学:WebAPIを取り巻く環境の変化とRailsの対応 - Qiita

strong parametersをバリデーションする(RubyFlowより)

# 同記事より
class Api::RoomsController < ApplicationController
  class UnpermittedParameterValue < RuntimeError
    def initialize(parameter:, value:)
      @parameter = parameter
      @value = value
    end
    attr_reader :parameter, :value
  end
  rescue_from UnpermittedParameterValue, with: :invalid_parameters
  def create
    validate_create_params
    Room.create!(create_params)
  end
  private
  # Could be extracted to a dedicated class within e.g. Validation module.
  # Validation checks should support all the permitted parameters; below
  # only owner_type parameter is handled.
  def validate_create_params
    owner_type = create_params[:owner_type]
    raise UnpermittedParameterValue.new(parameter: :owner_type, value: owner_type) if !Set['Company', 'Person'].include? owner_type
    # and so on...
  end
  def invalid_parameters(exception)
    render json: { errors: { exception.parameter => "'Value #{ exception.value }' is not supported value for the parameter." } }, status: 400
  end
  def create_params
    params.require(:room).permit(:owner_type, :owner_id, :floor, :price)
  end
end

つっつきボイス:「コントローラのstrong parametersをバリデーションする話だそうです」「たしかにstrong parametersはパラメータのフィルタ機能であってバリデーションではありませんしね」

参考: Strong Parameters -- Action Controller の概要 - Rails ガイド

「普通ならモデルでバリデーションしますが、RailsをAPIサーバーとして使う場合はこの記事みたいにコントローラでバリデーションさせることでAPI仕様と挙動を一致させやすくなるでしょうし: つか記事でそのあたりをやってるみたいですし」「そうでした💦」

「で記事ではバリデータを素で書くほかに、apipie-railsというgemでもやってますね」「あぴぱい?」

# 同記事より
class Api::RoomsController < ApplicationController
  api :POST, "/rooms", "An end-point used for creating new rooms in the system. Standard basic auth is required."
  formats ['json']
  error 401, "Unauthorized"
  error :unprocessable_entity, "Could not create the room."
  param :room, Hash, desc: "Room details" do
    param :owner_type, ["Company", "Person"], desc: "Owner of the room", required: true
    param :owner_id, :number, desc: "ID of the room's owner", required: true
    # Below a simple ':number' validation is used. It could be extended to allow only supported floor e.g. from 1 to 12.
    param :floor, :number, desc: "Floor on which the room is located", required: true
  end
  def create
    Room.create!(create_params)
  end
  private
  def create_params
    params.require(:room).permit(:owner_type, :owner_id, :floor)
  end
end

対決!Action Cable vs AnyCable


anycable.ioより

AnyCableは一昨年にウォッチで軽く取り上げていました(ウォッチ20170210)。上のAnyCableサイトを下にスクロールしたときのサイコロくんの動きがかわゆいです❤️。


つっつきボイス:「Action CableとAnyCableの比較記事です」「AnyCableは使ったことないな〜」「とりあえず大規模になったときのパフォーマンス面ではAnyCableの方が強いっぽいですね」


同記事より

「何やかんやで、Action Cableという機能が公式にRailsに入っているというのは大きいと感じますね: プロトタイピングのために短期間で動くものを作るんだったらAction Cableは手間が少なくてとても助かります😍」「Action Cableならモデルに保存するのも楽ですし」「そのかわり、それがそのまま本稼働してユーザー数が爆発的に増えてくると苦しくなってくるという😆」「😆」「まあそこは設計上というかビジネス上の決断でしょうね」

「規模が大きくなってくると、モノリシックなRailsサーバーでバックエンドとAction Cableを両方動かすのはちょっと心配ではありますね」「死ぬときはいっぺんに死んでしまいますからそうですね: ささやかにやってるうちはいいんですが」「分けたらそれはそれでデプロイとか面倒になりますけど😆」「😆」

1年前からあったSprocketsのバグを潰した話(Ruby Weeklyより)

Herokuの動画付きブログです。


つっつきボイス:「あ、動画のサムネイル見たらSchneemanさん↓の記事だった」「bundle open sprocketsでgemのソースを一発で開けるんですって」「え?!知らなかった〜」「bundlerは他にもいろいろ機能があったと思います☺️」

ガイジン向けRubyKaigiガイド(翻訳)

後でbundle installしてあるディレクトリでbundle open pryすると確かに開きました👍。

参考: bundle open | Bundler日本語ドキュメント | Ruby STUDIO

[Ruby] Bundler 1.15の全コマンド

Railsアプリをわずか数行のRubyコードに置き換えてやった(Ruby Weeklyより)

# 同記事より
# bin/entrypoint

require_relative "../lib/push_event"

file_path = ARGV.first
push_event = PushEvent.new(File.read(ENV.fetch("GITHUB_EVENT_PATH")))

if push_event.modified?(file_path)
  puts "#{file_path} was modified"
  exit(0)
else
  puts "#{file_path} was not modified"
  exit(1)
end

同記事より


つっつきボイス:「これは何を使ってやってるのかな?」「どうやらGitHub Actions使ってるみたいです」「GitLabにもその前から同じような機能『GitLab Pipelines』がありますね(ウォッチ20181022): アクションを組み立ててワークフローにする的なヤツ🧱」「お、面白そうですね〜: 今度使ってみようかな😋」(以下延々)

参考: GitHub Action for Slack · Actions · GitHub Marketplace


github.comより

参考: Introduction to pipelines and jobs | GitLab


docs.gitlab.comより

その他Rails

そういえば以前のウォッチでも取り上げました(ウォッチ20171208)。


今回は以上です。

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

週刊Railsウォッチ(20190305-2/2後編)PostgreSQL強者から見たMySQL、SEO良記事、分散アルゴリズムChordほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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