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

週刊Railsウォッチ(20210301前編)Rails 6.1.3がリリース、Active Supportのbefore?とafter?、link_to_unless_currentほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

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

以下のコミットリストのChangelogを中心に見繕いました。

🔗 ActionController::Live::Buffer#writelnが追加

DHH自らによるプルリクです。

# 同PRより
send_stream(filename: "subscribers.csv") do |stream|
  stream.writeln "email_address,updated_at"

  @subscribers.find_each do |subscriber|
    stream.writeln [ subscriber.email_address, subscriber.updated_at ].join(",")
  end
end

つっつきボイス:「末尾で改行されるwritelnができたんですね」「名前がどことなくJavaなどのprintln風味かも」「Javaやったことないです…」「JavaやってたのもJava 5の頃なのであまり思い出せませんが」「私も同じぐらいJava忘れてますね😆」

参考: Javaで文字列を出力する:print(), println() | UX MILK

writelnend_with?を使って末尾の改行が重複しないように処理してくれている↓」「これは賢い👍」

# actionpack/lib/action_controller/metal/live.rb#166
      # Same as +write+ but automatically include a newline at the end of the string.
      def writeln(string)
        write string.end_with?("\n") ? string : "#{string}\n"
      end

🔗 to_strが使えるオブジェクトもredirect_toに渡せるようになった

Addressable::URIのように)#to_strが使えるものなら何でもredirect_toのlocationとして渡せるようにした。
ojab
Changelogより大意


つっつきボイス:「Addressable::URIって何だろうと思ったら、どうやら外部のgemらしい↓」

sporkmonger/addressable - GitHub

to_strって普段使わないけど、これってto_sとどう違うんだっけ?」「to_strto_sの違いをググったらTechRacho記事が出てきた↓」「自分も同じ記事を見てます😆」「to_sto_iみたいな短い変換メソッドは明示的な変換、to_strto_intみたいな名前の長い変換メソッドは暗黙的な変換、だそうです」「う〜んまだ違いがピンとこない」

Rubyの明示的/暗黙的な型変換についてのメモ(翻訳)

「記事を見ると、文字列を式展開しないで直接"string" + otherと結合すると、otherto_strが呼ばれるのか」「へ〜、otherで呼ばれるのはto_sじゃないんですね!」「式展開ならto_sが呼ばれます↓」

Rubyの文字列連結に「#+」ではなく式展開「#{}」を使うべき理由

「Rubyのドキュメントも見ると、to_strは『文字列が使われるすべての場面で代置可能』『それ自体が文字列とみなせるもの』のときにだけ定義する、と書かれてる」「to_strが定義されているということは、文字列とみなしてよいものということなのかな」

参考: Object#to_str (Ruby 3.0.0 リファレンスマニュアル)

オブジェクトの String への暗黙の変換が必要なときに内部で呼ばれます。デフォルトでは定義されていません。
説明のためここに記載してありますが、このメソッドは実際には Object クラスには定義されていません。必要に応じてサブクラスで定義すべきものです。
このメソッドを定義する条件は、

  • 文字列が使われるすべての場面で代置可能であるような、
  • 文字列そのものとみなせるようなもの

という厳しいものになっています。
docs.ruby-lang.orgより

to_strは定義されているとは限らないけど、to_s↓はObjectに実装されているからすべてのオブジェクトに対して使えますね」

参考: Object#to_s (Ruby 3.0.0 リファレンスマニュアル)

「元コードは、when節のRegexp#===比較が暗黙型変換(to_str)で評価されるのにwhen節内でoptionsをそのまま返してしまっていたため、_compute_redirect_to_locationの戻り値がStringにならないケースがあったのを、#41390では明示的にoptions.to_strすることで戻り値がStringであることを保証するようにしたんですね↓」

# actionpack/lib/action_controller/metal/redirecting.rb#101
    def _compute_redirect_to_location(request, options) #:nodoc:
      case options
...
      when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
-       options
+       options.to_str
      when String
        request.protocol + request.host_with_port + options
      when Proc
        _compute_redirect_to_location request, instance_eval(&options)
      else
        url_for(options)
      end.delete("\0\r\n")
    end

🔗 fixtureのhas_many :throughでタイムスタンプを設定するようになった

has_many :through関連付けのfixtureがjoin tableでタイムスタンプを設定するようになった。
以下のfixtureがあるとする。

### monkeys.yml
george:
  name: George the Monkey
  fruits: apple
### fruits.yml
apple:
  name: apple

このjoin table (fruit_monkeys)がcreated_atupdated_atを含む場合、fixtureの読み込み時に展開されるようになった。従来はこれらのカラムをrequireするとクラッシュし、しない場合はnullのままになった。
Alex Ghiculescu
同Changelogより大意


つっつきボイス:「fixtureで中間テーブルにタイムスタンプ定義がある場合にcreated_atupdated_atが自動設定されずにnullになっていたのが、このプルリクで修正されたようです」「お〜」「修正されたのはactive_record_fixture_set/のfixtureですね↓」「中間テーブルにタイムスタンプを付けることはありうるので、修正されてよかった」

# activerecord/lib/active_record/fixture_set/table_row.rb#L37
+       def timestamp_column_names
+         ModelMetadata.new(@association.through_reflection.klass).timestamp_column_names
+       end
      end
...

        def add_join_records(association)
          # This is the case when the join table has no fixtures file
          if (targets = @row.delete(association.name.to_s))
            table_name  = association.join_table
            column_type = association.primary_key_type
            lhs_key     = association.lhs_key
            rhs_key     = association.rhs_key

            targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
            joins   = targets.map do |target|
              { lhs_key => @row[model_metadata.primary_key_name],
                rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
-             join = { lhs_key => @row[model_metadata.primary_key_name],
-                      rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+             association.timestamp_column_names.each do |col|
+               join[col] = @now
+             end
+             join
+           end
+           @table_rows.tables[table_name].concat(joins)
          end

Rails API: `ActiveRecord::FixtureSet`(翻訳)

🔗 ActiveSupport::CurrentAttributesのキーワード引数を修正

最近のRubyで(キーワード引数周りが)変更された後にキーワード引数を引き続き使いたい人向けのシンプルな改良。
respond_to?変更は、RSpecでパーシャルのダブル(double)をチェックするときのCurrentAttributesのスタブ化の問題を解決するのが目的。
キーワード引数が動かないという前提を置く理由がないので、ドキュメントは変更していない。
同PRより大意


つっつきボイス:「CurrentAttributesmethod_missingでキーワード引数を中継できるように**kwargsが追加されてますね↓」

# activesupport/lib/active_support/current_attributes.rb#L158
-       def method_missing(name, *args, &block)
+       def method_missing(name, *args, **kwargs, &block)
          # Caches the method definition as a singleton method of the receiver.
          #
          # By letting #delegate handle it, we avoid an enclosure that'll capture args.
          singleton_class.delegate name, to: :instance

-         send(name, *args, &block)
+         send(name, *args, **kwargs, &block)
+       end
+
+       def respond_to_missing?(name, _)
+         super || instance.respond_to?(name)
+       end

「今は、受け取った引数をすべて受け取ってdelegateできるようにするには、上のようにmethod_missing(name, *args, **kwargs, &block)と書くのか」「へ〜、今はこうなんですね」「これはnameが必ず存在する前提ですが、そういうのがない場合は*args, **kwargs, &blockと書けばすべての引数を受け取れるんでしょうね」

🔗 ドキュメント更新: redirect_toの危険な利用法について


つっつきボイス:「APIドキュメントとガイドの更新です↓」「redirect_toにユーザー入力をそのまま渡すのは一般に危険、たしかに」「これはもうおっしゃるとおりとしか言いようがない」「そんなことをする人がいるんだろうかと思いますが、たぶんいたから明示的にドキュメントとガイドにも書くことにしたんでしょうね」

# guides/source/security.md#L345
-If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a permitted list approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a permitted list or a regular expression_.
+If it is at the end of the URL it will hardly be noticed and redirects the user to the `attacker.com` host. As a general rule, passing user input directly into `redirect_to` is considered dangerous. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a permitted list approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a permitted list or a regular expression_.

🔗Rails

🔗 @kamipoさん記事より


つっつきボイス:「MySQL 8.0のクライアントでMySQL 5.7のサーバーに接続、これやっちゃってました😅」「記事を見た感じではMySQLの接続のハンドシェイクでの問題なので、接続した後でSET NAMESコマンドを渡して設定する分には大丈夫そうですね: ちょうど記事にも書いてあった↓」「お〜」

一応、接続後にSET NAMES utf8mb4すればサーバー側のutf8mb4のdefault collationが設定されるが、最悪のケースをカバーするために適切に設定してるひとには必要ない処理が増えて損をすることになるのでなんとか回避したい気持ちがあるけど、現状はそういう感じ。
同記事より

「元々この41403↓で上がってたissueを解決しようとしていたんですね」

「そうそう、記事にもあるように、MySQL 8.0.1からデフォルトのコレーションがutf8mb4_general_ciからutf8mb4_0900_ai_ciに変わっているんですよ↓: 後者はMySQL 5.7の頃にはなかったから認識されなくて、MySQL 5.7サーバーのデフォルトのコレーションにフォールバックしてたのか」「え、そこ変わっちゃってたんですか!」「MySQL 5.7サーバーには新しいutf8mb4_0900_ai_ciid:255がないから、MySQL 8.0クライアントからこれを渡しても認識しようがありませんね」「このことを知らなかったら原因を見つけるのは難しそう…」

ここで表題の “MySQL 8.0のクライアントでMySQL 5.7のサーバーに接続するとcharsetが設定されないかもしれない” についてなんですが、MySQL 8.0.1からutf8mb4のdefault collationがutf8mb4_general_ci (id: 45)からutf8mb4_0900_ai_ci (id: 255)に変更されたため、MySQL 8.0のクライアントがuff8mb4でサーバーに接続するとid: 255のcs_numberを送るけどMySQL 5.7はid: 255のcs_numberを知らないのでサーバー側のデフォルトの設定が採用されるという仕組み。
同記事より

🔗 AS句で作ったカラムにDBの型情報はない


つっつきボイス:「永和システムマネジメントさんのブログです」「『AS句で作ったカラムにDBの型情報はない』、そういうカラムはスキーマに定義がありませんのでそのとおりですね」

「ただ、これはActive Recordがどこまでよしなにマジックを効かせてくれるかという流れを何となくでも把握していないと、すぐには見当がつかないと思います」「あ、そういうことですか」「Active Recordが内部的にSHOW FIELDS(MySQLの場合)を使ってスキーマの型情報を取得していることを理解していれば、AS句で取ったカラムに型情報がないことは推測できると思います」「ふむふむ」「SHOW FIELDSはテーブルに対して実行するものなんですが、テーブルがなければSHOW FIELDSを実行できないので、AS句でSELECTした結果に対してはSHOW FIELDSできません」「なるほど」

「もしやりたければ、VIEW(データベースビュー)↓を作ればやれますよ: VIEWならSHOW FIELDSが使えるので」「あ〜、なるほど」

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

参考: ビュー (データベース) - Wikipedia

「記事にも、スキーマにないカラムの型はデフォルトでActiveModel::Type::Valueになると書かれていますね↓」

その過程で、AS 句で作ったカラムに型情報がつかない理由もわかります。 スキーマにないカラムはデフォルトで ActiveModel::Type::Value 型になるから、です。
同記事より

「今つっつきの場にいませんが、たしかBPS Webチームのkazz氏が、AS句で組み立てたクエリに何らかの方法で型ヒントを渡してカラムの型を認識させるというのをやっていた覚えがありますので、何か方法はあると思います」「お〜、やれそうなんですね」「でなければVIEWを使うか、さもなければAS句を使わないことでしょうね」

「Active Recordがあまりにいろんなことをよしなにやってくれるので、その内部構造に興味を持ってない人はこういうところでハマるでしょうね」「ですね、自分はハマる自信あります😅」

🔗 Dateの比較で<>を混同しないようにする


つっつきボイス:「Boring Railsっていう名前が面白い」「Date同士を比較するときには<とか>よりもActive Supportのbefore?after?の方が便利だよという話のようですね」「この辺は自分もよく不安になるので動かして確かめてます」

# 同記事
start_date = Date.new(2019, 3, 31)
end_date = Date.new(2019, 4, 1)

start_date.before? end_date
#=> true
end_date.after? start_date
#=> true

start_date = Date.new(2020, 8, 11)
end_date = Date.new(2018, 8, 11)

start_date.before? end_date
#=> false

「英語圏的にはbefore?after?の方がわかりやすいのかな?」「記事のコード例だと、start_dateが2019, 3, 31で、end_dateが2019, 4, 1だと、start_date.before? end_dateはtrueになる」「えっ、falseになるのかなという気がしましたけど?」「パッと見に逆かと思っちゃいますよね: 先行するstart_dateが主語で、それに対してend_datebefore?と読まないといけないんでしょうね」「あ、それでちょっとわかってきたかも」「いつものようなオブジェクト指向的な語順で読まずに、普通の関数のような語順で読むとよさそう」「before?after?はRails 6で追加されたんですね↓」

参考: Rails 6 adds before? and after? to Date and Time | BigBinary Blog


「そういえば、このboringrails.comの他の記事にも反応を見つけました↓」「link_to_unless_currentって見たことなかった」

「なるほど、この図↓のようにWebページのメニューなどで現在のページだけリンクを生成しないで、それ以外のリンクを有効にできるメソッドなのか」「あ、それちょっと便利かも😋」「こんなメソッドあるの知らなかった〜」


同記事より

参考: link_to_unless_current — ActionView::Helpers::UrlHelper

🔗 Rails 6.1.3がリリース


つっつきボイス:「お、Rails 6.1.3が出たんですね」「リリース情報見落としててRuby Weeklyの見出しで知りました🙇」「今回の更新は少なそう」


前編は以上です。

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

週刊Railsウォッチ(20210222)ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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