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

週刊Railsウォッチ(20210412前編)Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

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

🔗 Cache-Controlヘッダーにprivate, no-storeを指定できるようになった


つっつきボイス:「Cache-Controlヘッダーは前にも改修がありましたね」「あ、そういえばありました(ウォッチ20201012)」「以前はno-storeを指定したら他の設定を含めないように変えられてたけど、今回の修正でprivate, no-storeを指定できるようになったということは、この組み合わせに意味があるということなんでしょうね」

#39461ではCache-Controlヘッダーのno-storeディレクティブで他の指定を含まないようにした(つまりCache-Controlヘッダーに'private, no-store'を指定しても'no-store'だけになる)。privateは不要であることが多いが、常に不要とは限らない。
たとえば、Fastlyドキュメントには「現在はno-storeno-cacheディレクティブを尊重しない」かつ「FastlyとWebブラウザの両方でキャッシュを行わないようにする必要がある場合は、privateディレクティブにmax-age=0no-storeを組み合わせることを推奨する」とある。
#39461で変更されたディレクティブを減らす振る舞いはオーバーライドできないので、FastlyユーザーがRailsをアップグレードできなくなっている。
このプルリクは、privateを指定した場合はprivate, no-storeヘッダーを設定できるように変更する。no-cacheの場合にpublicを指定できるのと同様だが、デフォルトではない。
修正されるissue: #40798

「FastlyのドキュメントにCache-Control: private, no-storeには意味があると書かれてた↓」「使いたい組み合わせを使えるようにしたという修正ですね」

参考: Configuring caching | Fastly Help Guides
参考: Cache-Control - HTTP | MDN

「変更点を見ると、privateno-storeを同時に指定できるように変わった↓」「今までは両方指定してもno-storeだけになってたのか」

# actionpack/lib/action_dispatch/http/cache.rb#L198
+         options = []
+
          if control[:no_store]
-           self._cache_control = NO_STORE
+           options << PRIVATE if control[:private]
+           options << NO_STORE
          elsif control[:no_cache]
-           options = []
            options << PUBLIC if control[:public]
            options << NO_CACHE
            options.concat(control[:extras]) if control[:extras]

-           self._cache_control = options.join(", ")
          else
            extras = control[:extras]
            max_age = control[:max_age]
            stale_while_revalidate = control[:stale_while_revalidate]
            stale_if_error = control[:stale_if_error]

-           options = []
            options << "max-age=#{max_age.to_i}" if max_age
            options << (control[:public] ? PUBLIC : PRIVATE)
            options << MUST_REVALIDATE if control[:must_revalidate]
            options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
            options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
            options.concat(extras) if extras

-           self._cache_control = options.join(", ")
          end

🔗 ActiveSupport::TimeWithZone.nameが非推奨化


つっつきボイス:「今までのnameメソッドは"Time"という文字列をそのまま返してたの?↓」「えぇ?😳」「プルリクにも何でこういう実装になったのかわからないって書かれてますね」「文字列リテラルの"Time"を返す実装、マジで謎」

# activesupport/lib/active_support/time_with_zone.rb#L43
    def self.name
+     ActiveSupport::Deprecation.warn(<<~EOM)
+       ActiveSupport::TimeWithZone.name has been deprecated and
+       from Rails 7.1 will use the default Ruby implementation.
+     EOM
+
      "Time"
    end

「機能をdeprecatedにするときはこういうふうに書くのか↑」「この書き方は定番ですね」

c00f2d2でnameメソッドが実際のクラス名"ActiveSupport::TimeWithZone"ではなく"Timeを"返すようオーバーライドされていた。このようにした理由が不明だし、nameが実際のクラス名を返すことを期待するる開発者が混乱する可能性がある。この変更が追加された理由がわからないので、ひとまず非推奨化して開発者からのフィードバックを待ち、issueが上がったら適宜修正することにする。
同PRより大意

ActiveSupport::TimeWithZone.nameが非推奨化されてRails 7.1でデフォルトの実装に戻される予定なのか」「次のRailsメジャーバージョンは7だから、リリースごとに非推奨化->削除が最短で行われれば7.1で削除されるでしょうね」


「ところで、Railsのメジャーバージョンが7に上がる理由のひとつに、おそらくRailsで必要な最小限のRubyバージョンが上がるからというのもあるんじゃないかな」「Rails 7だとRuby 3.0が最小バージョンになるんでしょうか?」「Rails 7で最小バージョンをいきなり3.0にしたら追従できない機能やgemが続出すると思うので、そこまではやらないでしょうね」「たしかに」

後でedgeガイドを見ると以下のように書かれていました↓。と思ったら以前のウォッチでも言及していました(ウォッチ20210208)。

  • Rails 7 requires Ruby 2.7.0 or newer.
  • Rails 6 requires Ruby 2.5.0 or newer.
  • Rails 5 requires Ruby 2.2.2 or newer.

以下はつっつき後に教わったツイートです。

🔗 Active Record属性暗号化がついにマージされた


つっつきボイス:「少し前のウォッチ↓ではマージ前だったActive Record属性暗号化がついにマージされました」「お、きたか🎉」「これは嬉しい😋」「思ったよりスムーズにマージされましたね」

週刊Railsウォッチ(20210330後編)Active Recordモデル属性暗号化が標準で入る可能性、Flipper Cloud、awesome_printほか

「Changelogがコンパクトにまとまっててありがたい↓」「HEYでセキュリティ監査を経て実績を積んだ属性暗号化機能が入るのはいいですね👍」

属性暗号化のサポート
暗号化された属性はモデルレベルで宣言される。これらは背後に同じ名前のカラムを備えた正式なActive Record属性である。「データベース保存前の属性暗号化」や「値読み出し前の解読」はシステムが透過的に行う。

class Person < ApplicationRecord
  encrypts :name
  encrypts :email_address, deterministic: true
end

詳しくは以下のガイドを参照。
Jorge Manrubia
Changelogより大意

「属性暗号化のガイドもまるまる追加されたんですね」「edgeガイドには実用的なコード例も丁寧に書かれていますし、このまま使えそうなくらい充実してる👍」「key_providerの指定や暗号化キーのローテーションもちゃんとサポートしてる↓」「お〜優秀!」「キーのローテーションは後付けでやろうとするとつらいんですよ...」「Rails 7の目玉機能のひとつですね」

# edgeガイドより
config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
# edgeガイドより
active_record
  encryption:
    master_key:
        - bc17e7b413fd4720716a7633027f8cc4 # Active, encrypts new content
        - a1cc4d7b9f420e40a337b9e68c5ecec6 # Previous keys can still decrypt existing content
    key_derivation_salt: a3226b97b3b2f8372d1fc6d497a0c0d3

参考: 鍵のローテーション  |  Cloud KMS ドキュメント  |  Google Cloud


「ところでRails 7、いつ出るのかな」「時が来たらとしか言いようがない😆」「マイルストーン↓を見るとまだ10件しかプルリクがないので、しばらくは出なさそうですね」

「お、以前話題になったRails Conductorが7に持ち越されてる↓」「そういえばRails Conductorってありましたね(ウォッチ20190311)」

🔗 ActiveSupport::Cachewritefetchexpires_at:で絶対時刻のTTLを設定できるようになった

ActiveSupport::Cachewritefetchに、キャッシュエントリのTTLを絶対時刻で設定するexpires_at:引数を追加。

Rails.cache.write(key, value, expires_at: Time.now.at_end_of_hour)

Jean Boussier
同Changelogより大意


つっつきボイス:「キャッシュを絶対時刻で期限切れにするexpires_at:オプションが追加された、なるほど」「何月何日の何時というふうに指定できるようになったんですね」「今までだと『あと60分』みたいな相対指定しかできなかったので、絶対時間にするなら自分で計算して渡すことになる」「と思ったら改修も絶対時間を計算してますね↓」「あると嬉しい機能👍」

# activesupport/lib/active_support/cache.rb#L805
-     # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
-     def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
+     # +:compress+, +:compress_threshold+, +:version+, +:expires_at+ and +:expires_in+.
+     def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, expires_at: nil, **)
        @value      = value
        @version    = version
-       @created_at = Time.now.to_f
-       @expires_in = expires_in && expires_in.to_f
+       @created_at = 0.0
+       @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)

        compress!(compress_threshold) if compress
      end

🔗 update_alldelete_alldestroy_allの後で@cache_keysをクリアするようになった

calculatedなキャッシュキーはリレーションにキャッシュされるが、リレーションでコレクションに改変が発生した場合に再計算する方法がない。
@cache_keysは少なくともupdate_alldelete_alldestroy_allではクリアすべき。
関連issue: #41784
同PRより大意


つっつきボイス:「以下みたいに1リクエストの処理中でrelationを再利用する場合に、最後のscoped.destroy_allで消されたときのkeyがキャッシュに残ってしまう問題に対応したようですね」「なるほど」

scoped = Hoge.where(name: 'piyo')
scoped.destroy_all
pp scoped.map(&:name)

「今までこれを回避するために何かトリッキーなことしてた覚えがありますけど、それをやらなくてよくなった😂」「少なくとも*_all系のメソッドでは@cache_keysがクリアされるようになったので、その種の対応が少し楽になりそう👍」

「1レコードを指すActive Recordオブジェクトは何度も参照されることが多いので速度の関係からキャッシュヒットさせたいけど、複数レコードを対象とするRelationオブジェクトの更新系メソッドに対して、更新前に参照していたキャッシュを参照させたいというようなニーズはまずあり得ないだろうということですね、わかる」

🔗Rails

🔗 Tailwind CSS JITでCSSコンパイルを20倍高速化


つっつきボイス:「TechRachoのRails系翻訳記事でお世話になっているEvil Martiansの記事です」「Evil Martiansさんは最近Tailwind CSSがお気に入りみたい」

tailwindlabs/tailwindcss - GitHub

「Tailwind CSSって何だっけと思ったらBootstrapみたいなCSSフレームワークなんですね」「はい」「最近CSSフレームワークを選ぶ機会があったんですけど、これも検討しとけばよかったかな...」「CSSフレームワークみたいなものは作業者が慣れてないとよさを発揮しにくい面もあるので、Bootstrapみたいに長く使われているものが今も幅広く使われていますね」「それもそうか」

「ちなみにCSSフレームワークはBootstrap 5にしちゃいました」「お、5がもう出たんですか?」「実は5-beta3でした(後で正式版にする前提で)」「公式サイト↓のバージョンがv5.0.0-beta1と表示されているので正式版まであともう少しですね」

「そういえばBootstrap 5はIEサポートやめたってどこかに書かれてました」「IEは今やTwitterすら満足に表示できないのでサポート打ち切られても仕方ないでしょうね」「あ、そういえばそんな話もあったかも」「今どきのIE対応は特別に指定がない限り基本的にはサポートしない流れでいいと思います」

Internet Explorer はサポート外です。 Internet Explor のサポートが必要な場合は Bootstrap v4 を使ってください。
ブラウザとOS · Bootstrap v5.0より

🔗 RailsライブラリなしでRubyのWebアプリを作る(Ruby Weeklyより)


つっつきボイス:「Railsライブラリを使わずにRubyでWebアプリを作るという企画か」「おぉ、TCPServerを立ち上げるところから始めている↓」「何とプリミティブな😳」「ここはソケットプログラミングで最初にやるところですね」「Rackサーバーもない状態から始めるのが徹底してる」「without Rails libraryどころか、Ruby付属のgem以外使わないぞという勢いですね」

# 同記事より
#Use Rubys Socket Library
require 'socket'

server = TCPServer.new(1337)

参考: class Socket (Ruby 3.0.0 リファレンスマニュアル)

「なかなか長い記事ですね」「真ん中過ぎぐらいでやっとRackが登場↓」「文明が来た」

# 同記事より
require 'rack/handler/puma'

class HelloWorld
  def call(environment)
    status  = 200
    headers = { 'Content-Type' => 'text/plain' }
    body    = ['Hello', ' world!']

    [status, headers, body]
  end
end

Rack::Handler::Puma.run(HelloWorld.new)

rack/rack - GitHub

「最終的にこういう感じになったんですね↓」

「Webアプリをスクラッチで作ったことがない人やソケットプログラミングをやったことのない人にとっては、かなりいい勉強になりそう👍」「たしかに!」「Railsのようなフレームワークで覆い隠されている各種レイヤをこうやって実際に触ってみると何かしら学びになると思います」

参考: ソケットプログラミング

🔗 Railsでビューのテストを書く理由(Ruby Weeklyより)

# 同記事より
# spec/views/books/index.html.erb_spec.rb

require 'rails_helper'

RSpec.describe "books/index", type: :view do
  before(:each) do
    assign(:books, [
      Book.create!(
        title: "Title",
        description: "MyText",
        download_url: "Download Url",
        status: "Status"
      ),
      Book.create!(
        title: "Title",
        description: "MyText",
        download_url: "Download Url",
        status: "Status"
      )
    ])
  end

  it "renders a list of books" do
    render
    assert_select "tr>td", text: "Title".to_s, count: 2
    assert_select "tr>td", text: "MyText".to_s, count: 2
    assert_select "tr>td", text: "Download Url".to_s, count: 2
    assert_select "tr>td", text: "Status".to_s, count: 2
  end
end

つっつきボイス:「ビューのテストを書く理由を自分なりに考えてみた記事のようです」

「たしかにビューのテストは実際には書かないことも多いんですが、view specが必要な場合なら書くのはありだと思います」「どんな場合でしょう?」「たとえば外部からクローリングされることが前提のビューだと、ユーザーにとっての見た目よりもHTMLのDOM構造自体が仕様になるので、そういう場合はview specでDOM構造をテストしたいですよね」「あ、そうか」「あるいはSEO要件で特定のHTML構造が必要な場合とか」「なるほど」「必要な理由はいろいろ考えられると思うので、頭の体操的に考えてみるのも楽しいですよ」

🔗 Railsのアレを生成するWebサイト2つ


つっつきボイス:「ruby-jp Slackで見かけました」「1つ目のrails.helpというサイトは、Railsのモデルやマイグレーションのジェネレータ文字列をGUIで生成できるんですね↓」「こういうの地味に嬉しいかも」「いつもテキストにメモしてからやってました」「textフィールドにlimitを指定できるとは知らなかった」


rails.helpより

「主にRailsを学び始めたばかりの人が、ジェネレータのタイプミスで失敗してやる気をくじかれるのを防ぐのに便利そう」「たしかに」「マイグレーションでどのフィールドタイプにどんなオプションを指定できるかをGUIで確認できるのも教育向けによさそう: decimalを指定したときはprecisionとscaleも必要になる、とか」「なるほど」「なお、自分は空のマイグレーションファイルを作ってそこに書く派です」


「2つ目のrailsnew.ioはrails newコマンドのオプションを生成するジェネレータサイトです」「omakaseオプションなんてのがあるのね」


railsnew.ioより

「へ〜、--skip-gemfileなんてオプションがあるとは」「Gemfileなしってどんなときに使うんでしょうね」「システムインストールされるgemを使う前提のときとかかな?」「minitestをスキップできない、と思ったらminimalにすると全部スキップされました」

「railsnew.ioは慣れた人にとっても便利そう👍」「rails newは使う頻度が少ないので、turbolinksをオフにするのとか忘れがちですよね」「あ、それ自分もこないだ忘れてました😆」

🔗 devise-two-factor: Deviseで二要素認証(Ruby Weeklyより)

tinfoil/devise-two-factor - GitHub


つっつきボイス:「Deviseで二要素認証が使えるgemだそうです」「ちょっと嬉しいかも: 今度使ってみようかな」

参考: 多要素認証 - Wikipedia

「だいぶ昔ですけど、sshに二要素認証を付けてみたのを思い出しました」「sshでですか?」「すぐ飽きてやめちゃいましたけど😆」

参考: sshの二段階認証(二要素認証)設定の方法3個 | 俺的備忘録 〜なんかいろいろ〜


前編は以上です。

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

週刊Railsウォッチ(20210407後編)エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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