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

週刊Railsウォッチ(20200302前編)RubyKaigi 2020は9月に延期、Railsのセキュリティパッチバージョニングが変更、dry-monadsほか

こんにちは、hachi8833です。テックカンファレンスの開催状況をまとめてくれた方がいました。ありがとうございます。


つっつきボイス:「こんなの見つけました」「お〜すげ〜😳、半分以上まっかっかじゃないですか🟥」「RubyKaigiは(つっつき時点では)まだ開催予定ですけど予断を許さない感じ...(臨時ニュース参照↓)」

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

臨時ニュース: RubyKaigi 2020は9月3日〜5日に延期

土曜日に延期のアナウンスがありました。詳しくはアナウンスをどうぞ。宿や交通の取り直しをお忘れなく。


esa-pages.ioより

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

この週はコミットリストから見繕いました。

strict_loadingを追加

関連付けからのlazy loadingを防ぐ#strict_loadingを任意のレコードに追加した。
親レコードがstrict_loadingとマークされている状態で関連付けをlazy loadしようとするとエラーを発生する。余分なクエリを出さないよう関連付けをpreloadする場所を見つけるのに便利。
Changelogより大意

# Changelogより: 使い方
>> Developer.strict_loading.first
>> dev.audit_logs.to_a
#=> ActiveRecord::StrictLoadingViolationError: Developer is marked as strict_loading and AuditLog cannot be lazily loaded.

つっつきボイス:「has_manyの方向にアクセスしたときの振る舞いなのかな🤔」「どうなんでしょう😅」「使い方のコード例だとfirst取った時点でeager loadingされてそうな気がしないでもないけど...ちょい走り書き感😆」「preloadすべき場所を見つけたいときに便利ということみたいです」「このプルリク、久しぶりに👍とか❤️がびっしり押されてるのでかなり喜ばれてる感じ😋」

SchemaCacheでMarshalオブジェクトからのシリアライズも選べるようになった

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L139
+     def dump_to(filename)
+       clear!
+       connection.data_sources.each { |table| add(table) }
+       open(filename, "wb") { |f|
+         if filename.end_with?(".dump")
+           f.write(Marshal.dump(self))
+         else
+           f.write(YAML.dump(self))
+         end
+       }
+     end

つっつきボイス:「SchemaCacheってDBのスキーマかな?」「だと思います」「今まではYAMLからdumpしていたのがMarshallからのdumpもできるようになったということか」

このプルリクによって、シリアライズ戦略にMarshalYAMLのどちらかを選べるようになった。スキーマダンプのパスファイル名は、拡張子が.dumpならMarshal、.ymlならYAMLで、データベース接続ごとに定義する。デフォルトはYAMLのまま。
同PRより大意

「なるほどこの辺↓で拡張子を見てファイルを読み込んでる」「どうやらMarshalからのダンプは廃止の流れだったのにまたMarshallが入ってきたので、デフォルトはYAMLにしつつMarshalも選べるようにしたようです」「そのためにload_fromdump_toを追加したということね😋」

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L6
+     def self.load_from(filename)
+       return unless File.file?(filename)
+
+       file = File.read(filename)
+       filename.end_with?(".dump") ? Marshal.load(file) : YAML.load(file)
+     end

参考: 【Rails】SwitchPoint利用時にSchemaCacheを設定しSHOW FULL FIELDSを防ぐ - Qiita

SchemaCacheについて
Railsではrake db:schema:cache:dumpを使うことでdb/schema_cache.ymlにテーブルやカラムの情報が書き出されます。
このキャッシュを使うことでActiveRecordが型情報などを特定する手助けになります。
同記事より

ActiveRecord::Baseサブクラスからのconnected_to呼び出しを禁止

振る舞いは変わっていないが、以前のAPI名だと「そのクラスでだけ接続を切り替えられる」かのようにミスリードする可能性がある。connected_toは現在の接続のコンテキスト(ロールなど)を切り替えるものであって、接続そのものを切り替えるのではない。
同PRより大意

# activerecord/lib/active_record/connection_handling.rb#L138
    def connected_to(database: nil, role: nil, shard: nil, prevent_writes: false, &blk)
+     raise NotImplementedError, "connected_to can only be called on ActiveRecord::Base" unless self == Base
+
      if database
        ActiveSupport::Deprecation.warn("The database key in `connected_to` is deprecated. It will be removed in Rails 6.2.0 without replacement.")
      end

つっつきボイス:「ActiveRecord::Baseのサブクラスでconnected_toを呼べなくするそうです」「connected_toって何するんだっけ?😆」「connects_toとかconnected_to?とかよく似た名前のメソッドがあってややこしいです😅」「親でしか許さないとなると継承とは一体何だったのか🤣」「🤣」「このconnected_toは接続のコンテキストを切り替えるものなのに、接続を切り替えると勘違いされやすかったのね😆」「メソッド名がイマイチなのかも😆」

テンプレートレンダリング時のハッシュアロケーションを削減


つっつきボイス:「こちらは引数を修正しつつアロケーションをちょっぴり減らしたそうです」「可読性の向上がメインみたいなのでこれでいいのかも☺️: これでカリカリチューニングで可読性落ちたとかだったら残念ですし」

# activerecord/lib/active_record/railties/collection_cache_association_loading.rb#L4
  module Railties # :nodoc:
    module CollectionCacheAssociationLoading #:nodoc:
      def setup(context, options, as, block)
-       @relation = relation_from_options(**options)
+       @relation = nil
+
+       return super unless options[:cached]
+
+       @relation = relation_from_options(options[:partial], options[:collection])

        super
      end

-     def relation_from_options(cached: nil, partial: nil, collection: nil, **_)
-       return unless cached
-
+     def relation_from_options(partial, collection)
        relation = partial if partial.is_a?(ActiveRecord::Relation)
        relation ||= collection if collection.is_a?(ActiveRecord::Relation)

        if relation && !relation.loaded?
          relation.skip_preloading!
        end
      end

「ほほぅ、relation_from_options(**options)をいきなり呼ぶのをやめてるので、**optionsの展開のタイミングを変えてますね」「たしかに」「修正後はrelation_from_options(partial, collection)となってて、前のような雑な**じゃなくてオプションをきちんと展開するようになってるので、やはり可読性向上だと思います😋」「コメコメを追放して読みやすくしてくれたんですね🎉」

スキーマキャッシュ関連修正


つっつきボイス:「2つともスキーマキャッシュの話なので冒頭の#38432と関連してるっぽい」「このatomic_write↓っていうファイル関連メソッド初めて知りました」「Active Supportの機能でしたか」

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L143
      def dump_to(filename)
        clear!
        connection.data_sources.each { |table| add(table) }
-       open(filename, "wb") { |f|
+       File.atomic_write(filename) { |f|
          if filename.end_with?(".dump")
            f.write(Marshal.dump(self))
          else
            f.write(YAML.dump(self))
          end
        }
      end

「ちなみに自分はRubyのopenメソッドって雑なのでキライ😆」「😆」「うろ覚えですけどopen使って外部APIのURLを叩くこともできた気がする」「マジで😅」「openっていろんな意味になったりするのが怖いので、atomic_writeみたいな書き方にするのはいいことだと思います😋」

Ruby 2.7で調べると、Kernel#openでURLを開くにはrequire 'open-url'する必要があるようです。昔は違ったのかも?🤔

require 'open-url'
uri = 'https://ドメイン名/'
uri = URI.parse(uri)
open(uri).read 

上でURLにアクセスできましたが、以下のwarningが表示されました。URI.openURI#openだとwarningは出なくなりました。

warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open

新しい属性に対応

以下のHTML属性が追加された:
* allowpaymentrequest
* nomodule
* playsinline

# actionview/lib/action_view/helpers/tag_helper.rb#L15
-     BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked
-                             compact controls declare default defaultchecked
-                             defaultmuted defaultselected defer disabled
-                             enabled formnovalidate hidden indeterminate inert
-                             ismap itemscope loop multiple muted nohref
-                             noresize noshade novalidate nowrap open
-                             pauseonexit readonly required reversed scoped
-                             seamless selected sortable truespeed typemustmatch
-                             visible).to_set
+     BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
+                             autoplay checked compact controls declare default
+                             defaultchecked defaultmuted defaultselected defer
+                             disabled enabled formnovalidate hidden indeterminate
+                             inert ismap itemscope loop multiple muted nohref
+                             nomodule noresize noshade novalidate nowrap open
+                             pauseonexit playsinline readonly required reversed
+                             scoped seamless selected sortable truespeed
+                             typemustmatch visible).to_set

つっつきボイス:「また新しい属性が増えたようです」「改修はビューヘルパーだけど、HTMLの属性が増えたって言ってる?えぐい😆」「allowpaymentrequestってペイメント系のリクエストに関連してる感じ💰」「BOOLEAN_ATTRIBUTESには他にもどっちゃりboolean属性入ってるな〜😅」「フレームワークはこういうのに対応しないといけないから大変そう...」

参考: HTML Standard -- whatwg.org

allowpaymentrequest
iframe要素の中でPaymentRequestインターフェイスを用いてpaymentリクエストを許可するかどうかを指定
nomodule
module scriptをサポートするuser agentの実行をscript要素で禁止するかどうかを設定
playsinline
video要素の動画コンテンツをplaybackエリアで再生するようuser agentにすすめる

Railsのセキュリティアップデートポリシーが変更

# guides/source/maintenance_policy.md#L57
-the x-y-stable branch. For example, a theoretical 1.2.3 security release would
+the x-y-stable branch. For example, a theoretical 1.2.2.1 security release would

つっつきボイス:「Railsのセキュリティメンテナンスポリシーが変わってセキュリティパッチのバージョン番号が4桁構成になったそうです」「えっとメジャーバージョンとマイナーバージョンと、3つ目は何だっけ😆」「えっと、パッチバージョン」

参考: セマンティック バージョニング 2.0.0 | Semantic Versioning

「6.2.1を6.2.2とかにするのはちょいコワイけど、6.2.1.1とかにするならセキュリティだから当てなきゃという感じが伝わってきますね❤️」「たしかに〜」「Railsのバージョンを上げたいわけじゃないけどセキュリティパッチは当てないといけないということはよくありますので☺️」

Rails

Dry-rbのモナドでServiceを改善する

# 同記事より
class Reservation::Create
  include Dry::Monads[:result]

  def initialize(user:, room:, start_date:, end_date:, notes: nil)
    @user = user
    @room = room
    @start_date = start_date
    @end_date = end_date
    @notes = notes
  end

  def call
    check_if_room_available
      .bind { create_reservation }
      .bind(method(:send_notification))
  end

  private

  attr_reader :user, :room, :start_date, :end_date, :notes

  def check_if_room_available
    Try(ActiveRecord::ActiveRecordError) { existing_reservations.exists? }.to_result.bind do |result|
      if result
        Failure('The room is not available in requested time range')
      else
        Success(nil)
      end
    end
  end

  def create_reservation
    reservation = Reservation.new(
      user: user, room: room, start_date: start_date, end_date: end_date, notes: notes
    )

    if reservation.save
      Success(reservation: reservation)
    else
      Failure('The reservation could not be created')
    end
  end

  def send_notification(reservation:)
    NotificationMailer
      .notify_room_booked(user: user, reservation: reservation)
      .deliver_later

    Success(reservation)
  end

  def existing_reservations
    Reservation.where(room: room).in_time_range(start_date: start_date, end_date: end_date)
  end
end

つっつきボイス:「kazzさんが最近Railsでパイプラインの実装で悩んでるので、Dry-rbのこの辺の記事が参考になるかと思って今ローカルで雑に翻訳してみました(許可取れたら公開します)」「自分がモナドやるときが来ようとは: 今この記事に沿ってちょっとやってみてるんですけど、Doでやるのとbindでやるところがやっと見えてきたのでモナドはこれから😆」

「今ボク関数脳🧠」「この記事で使ってるDry-monadsはDry-transactionの後継ということを翻訳してて知りました」「モナドむずい😭」「大学で数学専攻しててもむずいということは相当手ごわいんですね😳」

「そういえばdry-rbのリポジトリにdry-railsというのが作り中なのを見つけました」「dry-rbでRails的なものを作ってみる企画的な感じ☺️」「思い切りWIP😆」

「この記事では『Railsway Oriented Programming』という関数型由来の概念を援用していて、以下のスライドは割と前のですがその解説です」「どれどれ👀: successとfailという2本の線路で考えるところを絵で説明してるのがとてもいいですね👍」「😋」「エラー処理はraiseしちゃえばいいという考え方もあるんですけど、こういう世界だと単純にraiseするわけにいかなくて、かといって普通にやるとifの嵐になっちゃいますし😢」

参考: Railway Oriented Programming | F# for fun and profit
参考: notes/Railway oriented programming.md at master · yukitos/notes -- 上の日本語版

コント・モナドロジー

「なははは😆、スライドのくまの会話、今の自分に染みるわ〜」「Maybeモナドと『たぶん』が入り混じったりしてて英語圏らしい笑い😆」「モナド簡単でしょ?とかひどい🤣」「『完全に理解した』出た😆」「endfunctorって何関手だったっけ😅」

後で雑に訳を付けてみました。どことなくいしいひさいちを連想するセンスです。

くま: 関数をいくつかチェインしたいんだけど、エラーもキャプチャしたいんだくま。
ブレインズくん: そんなの簡単。モナドでやれます。
くま: モナド何だか難しそうなんだくま。
ブレインズくん: モナドは単に自己関手の圏におけるモノイドですよ。

くま: ⊂( ・∀・)ワケ ( ・∀・)つワカ ⊂( ・∀・)つラン♪
ブレインズくん: 何か問題でも?
くま: 自己関手がわかんないくま。
ブレインズくん: 簡単です。関手というのは圏と圏の準同型のことで、自己関手というのは単に圏自身に写像する関手のことです。
ブレインズくん: ほらこんなにシンプル!
くま: よし完全に理解したくま。

くま: で、ぼくはどうしたらいいくま?
ブレインズくん: モナドを全部理解する必要がないなら、Maybeを使えばいいのでは?
くま: たぶん(maybe)何をくま?
ブレインズくん: Maybeっていうモナド。
くま: だからたぶん(maybe)何て言うモナドくま?
ブレインズくん: だから、"Maybe"っていう、名前の、モナドなの。

くま: 「たぶんそのモナドの名前は...」じゃなかったのかくま?。
ブレインズくん: Maybeのモナドが名前は...ってヨーダみたいなしゃべり方しないでくれる?
くま: ヨーダみたいにしゃべってるのは君の方だくま。
ブレインズくん: とにかく話を戻すと、君に必要なのはきっとMaybe(definitely Maybe)のはず。
くま: つまりDefinitely Maybeなのかくま?

ブレインズくん: ちなみにぼくは『Definitely Maybe』より『<What's the Story> Morning Glory?』の方が好きだけど。
ブレインズくん: 君にはEitherの方が合ってるかも。
くま: 何と何の『どちらか(either)』なのかくま?
ブレインズくん: Eitherはモナド。
くま: モナドと何の『どちらか(either)』なのかくま?
ブレインズくん: だからEitherだけ(just Either)。
くま: つまりJust Eitherって言うのかくま?

※ 『Definitely Maybe』と『<What's the Story> Morning Glory?)』はどちらもオアシスのアルバムタイトル。

参考: オアシス (バンド) - Wikipedia

ブレインズくん: ああそうじゃなくって!JustはMaybeの一部なの。
くま: じゃJust Maybeって言うくまか?
ブレインズくん: 違う違う、そこはJustって言わないとだめ。JustかNothingだけ(just Nothing)。
くま: Just Nothingくま?でもさっきDefinitely Maybeって言ってなかったくまか?
ブレインズくん: いや今はEitherの話をしてるんですけど。
くま: Just NothingなのかDefinitely Maybeなのかはっきりして欲しいくま。
ブレインズくん: どっちも違います!いいからEither使えっつーの。
くま: (゜∀。)ワヒャヒャヒャヒャヒャヒャ


「あとRedditに『Dry-rbの問題はドキュメントだ』という書き込みを見つけました」「それほんに: APIの仕様もコード付きのユースケースも見当たらなくて、こんなことできるよってふわっと書いてあるぐらい😢」「さっきのブレインズくんの説明と大差なさそう😆」「今欲しいのは業務に近いユースケース😢」「『これらのpredicateはルールによってカプセル化され、述語論理を用いて互いにコンポジションできる。つまり共通の論理演算子を利用してバリデーションスキーマを構築できる』とか書かれても使い方がわからないって書き込みにありますね😅」「その意味までならわかるんですけど😆」

Dry::Structはたまに使う。Dry::StructはVirtusを置き換えるものだと思っていたが、VirtusはミュータブルなのでForm Objectで便利なのにDry::Structはそうではないのが残念。結局Dry::Structはイミュータブルなstructに使っていて、ミュータブルなstructは手作りと(Trailbrazerの)Reformの合わせ技になっている。
Dry::TypesはDry::Structや手作りForm Objectと相性がいいのでしょっちゅう使っている。しかしDry::Typesはバリデーションのライブラリなのか型強制(coercion)ライブラリなのかアサーションのライブラリなのかはっきりしない。型に対する変更の順序が微妙に重要だったりするのもあって、めちゃくちゃ好きというほどではない。Rubyに型システムを追加しようと一人相撲している感もある。
Redditより大意

dry-structのissue #48を見ると「イミュータブルは仕様」とありました。

Railsリンク集


つっつきボイス:「TechRacho記事もいくつか引用されていたQiita記事を見つけました: Railsガイドより先の設計とかの記事リンクだそうです」「ほほぅ〜頑張っていろいろ集めてる😋」

「これにjoker1007さんの例の名記事↓も含めたいですね」「Service ObjectはRailsの標準ではないというのはまあ置いとくとして😆」「あとyasaichiさんの『Railsの正体』スライドも冒頭に置きたいです」「うん、あれは本当にいいスライドですよ❤️」

WebカメラをActive Storageに直結する(Ruby Weeklyより)

# 同記事より
class VideoAttachmentService
  class << self
    def attach(model, video_path)
      model.picture.attach(
        io: File.open(video_path),
        filename: "player_video_#{unique_string}.webm"
      )
    end

    private def unique_string
      SecureRandom.urlsafe_base64(10)
    end
  end
end

つっつきボイス:「直結だそうです」「直結でがんがんストリーム配信しまくる?すげ〜😆」「記事では最初に画像のアタッチ、次に動画のアタッチやってますね」「既存の何かのプロトコルの焼き直しに見えなくもないですけど😆」「どうなんでしょう😆」

その他Rails


つっつきボイス:「お馴染みt-wadaさんのプレゼンです」「先週の『開発速度と品質』の話にも通じてそう(ウォッチ20200225)」

「中身見る前に言うと、自分の感覚では品質を下げたからといって開発速度が上がるとは限らないですね: カリッカリのところだと確かにトレードオフになるんですけど、特に開発の初期段階は質と開発速度はむしろ比例すると思ってます🧐」「おぉ」「結局、書くのが早い人は品質もいいし、書くのが遅い人に時間あげてもたいてい品質はよくならない😆」「😆」「言い換えると『開発がスタートする前に設計をどこまでしっかり固めてますか』ということ: このスライドは主に開発が進んでからの話みたいですけど☺️」

↓後でスライド見つけました。「システムを設計するための判断力をつける一番の方法は、自分で設計したシステムを長い間メンテすることだ(p59)」で考えさせられました。

参考: デブサミ2020、講演関連資料まとめ:CodeZine(コードジン)


今回は以上です。

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

週刊Railsウォッチ(20200226後編)dry-rbを使うべき理由、最近のRubyオンライン教材、AWSから乗り換えた話ほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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