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

週刊Railsウォッチ(20201005前編)Ruby 2.7.2がリリース、Shopifyのモジュラー化gem「packwerk」、stimulus_reflexほか

こんにちは、hachi8833です。皆さまもKaigi on Rails STAY HOME Editionをエンジョイされましたでしょうか。スポンサーおよび関係者の皆さまありがとうございました&お疲れさまでした!🙇
アーカイブ動画も今後順次配信されるようです!ありがとうございます🙇。

先ほど@koicさんの以下の記事も公開されました。

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

臨時ニュース: Ruby 2.7.2がリリース

Ruby 2.7.2ではキーワード引数のwarningがデフォルトでオフになり、以下のWEBrickの脆弱性も修正されます。

Ruby: WEBrick 1.6.1セキュリティ修正がリリース

また、Ruby 2.6.x系や2.5.x系にも同様にWEBrickを修正したバージョンが近日中にリリースされる可能性がありそうです。

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

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

新機能: PostgreSQLのintervalデータ型をActiveSupport::Durationとしてサポート


つっつきボイス:「そうそう、ぽすぐれにintervalというデータ型があるというのを思い出した」「何秒とか何日みたいな『期間』を表すデータ型ですね」

参考: PostgreSQL 12.3文書 8.5. 日付/時刻データ型

「データベース側で処理できる型の対応が増えた🎉」「微妙にDB依存しそうな機能ですけど」「RailsのActive RecordはこういうDB依存的な機能も含めて強力にサポートするところがスゴい」「@kamipoさんのご尽力でPostgreSQL独自機能の導入が進んでいると思います🙏」

「ところでMySQLにもintervalあるのかな?🤔」

後で見てみると、「PostgreSQLのintervalはデータ型として定義されていて、dateやtimestampと+-演算子で計算させることもできる」「MySQLのINTERVALキーワードはデータ型ではない↓(のでカラムとしては定義できない)」とのことです。

参考: MySQL :: MySQL 8.0 Reference Manual :: 9.5 Expressions


PostgreSQLのintervalデータ型をActiveSupport::Durationに変換するサポートを追加する。変換は、データベースからレコードを読み込むときや、save時にISO 8601形式のduration文字列にシリアライズするときに行われる。
マイグレーションでカラムを定義する機能やスキーマダンプで取得する機能をサポートする。
カラムの精度もオプションでサポートされる。
同コミットより大意

# 6.1でこの機能を使うには、以下の文字列をモデルファイルに書く必要がある:
        attribute :duration, :interval

# 6.2がリリースされるまでの古い振る舞いを維持する場合は以下のように書く

        attribute :duration, :string
# changelogよりコード例
        create_table :events do |t|
          t.string   :name
          t.interval :duration
        end

        class Event < ApplicationRecord
          attribute :duration, :interval
        end

        Event.create!(name: 'Rock Fest', duration: 2.days)
        Event.last.duration # => 2 days
        Event.last.duration.iso8601 # => "P2D"
        Event.new(duration: 'P1DT12H3S').duration # => 1 day, 12 hours, and 3 seconds
        Event.new(duration: '1 day') # Unknown value will be ignored and NULL will be written to database

新機能: 関連付けをジョブで非同期削除するdependent::destroy_async

dependent:キーをサポートする関連付けでdependent: :destroy_asyncを取れるようにする。

class Account < ActiveRecord::Base
  belongs_to :supplier, dependent: :destroy_async
end

:destroy_asyncは、関連付けられたレコードをバックグラウンドでdestroyするジョブをキューに入れる。
Changelogより大意


つっつきボイス:「お〜、dependentの削除を非同期でActive Jobキューに乗せられる機能なのか」「どんな場合に便利なんでしょう?」「dependentの削除が重くなりそうな場合じゃないかな」「なるほど」

「こういう削除はロックを取りに行ったりするでしょうし、カウンターキャッシュが効きまくっているテーブルなんかだとカウンターキャッシュの再構築でフックがフックを呼ぶという、Railsでよくあるコールバック地獄になりそうですし」「あ〜」「そういう処理を同期的にやると遅くなるので、非同期でやりたいというのはわかる気がします」

「そのdestroy_asyncで例外が発生したのをトラッキングできなくなると地獄感ありそうですけど」「あわわ、それもそうか」「そういうケースもきちんと考慮して使えばいいんでしょうけど」

参考: 4.1.2.4 :dependent — Active Record の関連付け - Railsガイド

「失敗したらロールバックしてくれるといいんですけど」「そこまでやれるかどうかはわかりませんが、Active Jobのレベルでは失敗したジョブとして出てきて、リトライを繰り返したりうまくいかないときは失敗キューに入ったりということはできそうな気はしますね」「この種の振る舞いでは、消したつもりのデータが消えてなかったというのが怖いので、自分はこの機能をすぐには使わないかも」

「これを使うユースケースとしてどんなのがあるかな…」「ユーザーテーブルに関連付けられる個人情報ログテーブルをdestroy_asyncで消したいというのはありそう: たとえばGDPR対応ではログが個人情報に該当する場合(かつ以下↓に該当しない場合)はユーザー情報を削除するときにそのユーザーのログも削除しないといけなくなるんですけど、その種のログテーブルはINSERTだけを念頭に置いた設計であることが多いでしょうし量も多いしで、消すのに時間がかかるので」「よくあるヤツですね」

【適用されない場合の例】(GDPR17条3項)
表現の自由及び情報の自由の権利の行使に取扱いが必要な場合
事業者が従うEU 法又はEU 加盟国の国内法の義務の遵守のために取扱いが必要な場合、又は公共の利益等のために取扱いが必要な場合
公共の利益の目的、科学的若しくは歴史的研究目的又は統計目的の達成のために取扱いが必要な場合
【改正個人情報保護法】「保有個人データ」の対応強化(短期保有個人データの例外廃止・開示のデジタル化・利用停止、消去、第三者提供の停止の請求に係る要件の緩和) | 弁護士法人 三宅法律事務所より

「その意味でdestroy_asyncはあると便利なのはわかるんですけど、まだ正しく使う方法が見えてない」「使うなら、失敗した場合のリカバリーも含めて、この機能を十分知り尽くしてからじゃないとちょっと怖そうではありますね」「便利そうだからとすぐに飛びつかない方がいいかも」

「このプルリクはShopifyのなんですね」「Shopifyのように大規模なサービスならこういう機能を使いたくなりそう」

Active Storageが環境を認識するようになった


つっつきボイス:「Active Storage大好きです❤️」「お〜、config/storage/の下のyamlを環境ごとに上書き読み込みできるようになったのね↓」

# activestorage/lib/active_storage/engine.rb#L115
    initializer "active_storage.services" do
      ActiveSupport.on_load(:active_storage_blob) do
        configs = Rails.configuration.active_storage.service_configurations ||=
          begin
-           config_file = Rails.root.join("config/storage.yml")
+           config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
+           config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
            raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?

            ActiveSupport::ConfigurationFile.parse(config_file)
          end
        ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
        if config_choice = Rails.configuration.active_storage.service
          ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
        end
      end
    end

「この機能って今までなかったのかな?」「これはありませんでしたね…storageのコンフィグを一律で全部読み込もうとして、存在しない環境変数があると怒られるんですけど、開発環境でしか使わない環境変数なのに本番で怒られるのもどうかと思ってました」

「その環境では使わない環境変数なのに、その定義が存在しないと失敗するということ?」「それです!この機能が入ったことでそこがちゃんとできるんですね😂」「こういうissueが出されて解決されるようになったということは、それだけActive Storageが本格的に使われ始めているんでしょうね」

rexml gemがRuby 3.0でdefault gemからbundled gemに変わるのに対応


つっつきボイス:「rexml懐かしい」「rexmlって何でしたっけ?」「XMLパーサー」

# Gemfile#L46
# Active Support
gem "dalli"
gem "listen", "~> 3.2", require: false, github: "guard/listen"
gem "libxml-ruby", platforms: :ruby
gem "connection_pool", require: false
-gem "rexml", require: false
# activesupport/activesupport.gemspec#L36
  s.add_dependency "i18n",            ">= 1.6", "< 2"
  s.add_dependency "tzinfo",          "~> 2.0"
  s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
  s.add_dependency "zeitwerk",        "~> 2.3"
  s.add_dependency "minitest",        ">= 5.1"
+ s.add_dependency "rexml"
end

「上のdiffを見ると3.0からはdefault gemではなくなるので、Rails側で対応したみたいだけど」「どういう判断で変更されたんだろう?🤔」「プルリクに貼られている#16485を見ると↓、rexmlがRubyのdefault gemからbundled gemに変わったのね」「default gemとbundled gemの違いがわかってなかった😅」

メリット: kou(Kouhei Sutou)によるメンテとリリースがやりやすくなる。
デメリット: Ruby 2.8(3.0)以降はgemのユーザーが自分のGemfileにrexmlを追加する必要がある。
#16485より抜粋・大意

「プルリクタイトルにremoveって書かれているから消されたのかと思っちゃいました」「bundled_gemsファイルにrexmlがgemとして追加されている↓ので、たしかに削除ではなく移動ですね」

# gems/bundled_gemsより
minitest 5.13.0 https://github.com/seattlerb/minitest
net-telnet 0.2.0 https://github.com/ruby/net-telnet
power_assert 1.1.5 https://github.com/k-tsj/power_assert
rake 13.0.1 https://github.com/ruby/rake
test-unit 3.3.4 https://github.com/test-unit/test-unit
xmlrpc 0.3.0 https://github.com/ruby/xmlrpc
rexml 3.2.3 https://github.com/ruby/rexml
rss 0.2.8 https://github.com/ruby/rss

後で調べると、default gemとbundled gemの違いについては@hsbtさんの以下の記事に詳しく載っていました。

参考: RubyKaigi 2017 に登壇します & GMO ペパボがスポンサードします - ペパボテックブログ

Rails

stimulus_reflex: RailsをインタラクティブにするStimulusベースのフレームワーク(Ruby Weeklyより)

hopsoft/stimulus_reflex - GitHub


expo.stimulusreflex.comより


つっつきボイス:「デモサイトを触ってみた感じでは結構よくできているように自分には思えました」「StimulusってDHHというかBasecampが推してるんですよね、その意味ではRailsでリッチなインタラクティブUIを分離せずに一元管理したいのであればStimulusにするという雰囲気をちょっと感じますね」「Rails wayとまでいかないけどBasecamp way的な位置づけをちょっと感じる」

stimulusjs/stimulus - GitHub

「管理画面ならこれで十分作れそう😋」「あとは学びの労力に見合うかどうかですね」「現代だとjQueryを新規で学ぶよりもStimulusを学ぶ方がありなのかも?🤔」「jQueryが前から入っているRailsアプリならしょうがないですけど、今の時代に新規アプリにjQueryを入れるのはちょっとためらいますし」

モダンなFlashメッセージ(Ruby Weeklyより)


つっつきボイス:「今風のFlashメッセージを表示するという記事だそうです」「これもStimulusを使ってサーバーから非同期でメッセージを取ってきて表示しようということね」


同デモサイトより

そういえばRailsのデフォルトのFlashメッセージは以下のようなスタイルですね。

「これは単なる予想ですけど、Railsチュートリアルが次のバージョンでStimulusを採用したらRailsエンジニアの間で流行るかもしれませんね」「最近は特に規模の大きいアプリでフロントエンドとバックエンドを手分けすることがかなり普及してきているので、Stimulusがどこまで食い込めるかはわかりませんけど」「完全にひとりで作るRailsアプリならStimulusはイケるかも」「既にちゃんと動くStimulusの改修案件が来たら触って練習してみようかな😋」「StimulusはTurbolinksと連携する分Railsとの相性が良さそうではありますね」

データベーストリガ関数で競合条件を解決する(Ruby Weeklyより)

# 同記事より
class CreateTriggerItemTotalCheck < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL
      CREATE OR REPLACE FUNCTION check_item_total()
        RETURNS TRIGGER 
      AS $func$
      DECLARE
        allowed_total BIGINT;
        new_total     BIGINT;
      BEGIN
        SELECT INTO allowed_total allocation_cents
        FROM budgets
        WHERE id = NEW.budget_id;

        SELECT INTO new_total SUM(price_cents)
        FROM items
        WHERE budget_id = NEW.budget_id;

        IF new_total > allowed_total
        THEN
          RAISE EXCEPTION 'Items total price [%] is larger than budget allocation [%]',
          new_total,
          allowed_total;
        END IF;
        RETURN NEW;
      END;
      $func$ 
      LANGUAGE plpgsql;

      CREATE TRIGGER item_total_trigger
      AFTER INSERT OR UPDATE ON items
          FOR EACH ROW EXECUTE PROCEDURE check_item_total();
    SQL
  end

  def down
    execute <<-SQL
      DROP TRIGGER item_total_trigger ON items;
    SQL
  end
end

つっつきボイス:「↑この辺とか、まさにストアドプロシージャ」「ストアドプロシージャ書くのは今まで避け続けてきました😅」「少なくとも自分から書くことはないかな〜」「ストアドプロシージャに手を出し始めると沼が始まりそうですし」

参考: ストアドプロシージャ - Wikipedia

「ストアドプロシージャってIFも書けるんですか!」「こうしなくてもモデルのクラスでコールバックすればよさそうですけど」「まあストアドプロシージャに便利な点があるとすれば、上のコードみたいにカスタムトリガーを作れることに尽きますね: このトリガーはデータベースサーバー上で動くので、ユースケース次第ですけどモデルのクラスでコールバックするよりめちゃくちゃ速くなることがあります」「あ〜なるほど!」

「たとえば、データベース側のキャッシュに乗っているデータだけでできる処理だけどレコード数がめちゃくちゃ多い場合、アプリケーション側で処理するためにその大量のレコードをいったん全部持ってこないといけなくなって遅くなるのが目に見えますよね」 「ふむふむ」「そういうのをストアドプロシージャで処理すると、キャッシュがよく効けば爆速になる可能性を『秘めている』んですけど、そうでもない限り自分はたぶんストアドプロシージャに手を出さないと思います」

「相当昔のことですけど、ストアドプロシージャで爆速になるはずなのにならないので調べて欲しいって3か月ほどストアドプロシージャを解読し続けたことがあったのを思い出しました😇」「それはつらそう… お疲れさまです!」「ビジネスロジックがストアドプロシージャに入っているなら調べるしかないでしょうね😆」「いや〜あれは沼でした」

「ストアドプロシージャにする必要のない箇所でストアドプロシージャが使われていることってよくありますよね」「それそれっ!」「逆にストアドプロシージャをうまく使えばトランザクションロックを活用しつつシンプルに書けることもあります」「この記事のようなケースだとストアドプロシージャはかなり光るでしょうね: これをアプリケーション層でやろうとすると大きめのトランザクションロックをかけないといけなくなる可能性もあるでしょうし」

「この記事みたいなデータベーストリガー関数が必要になったことは今のところありませんけど、必要となればこういう手法もあるということは押さえてます」「方法があることを知っておくのが大事👍」「解決方法の選択肢を複数持っているのと、それしか解決方法がないと思い込むのとでは大きな違いですし」


「なお記事ではこのfxというgemを使っているそうです↓」「データベースの関数やトリガー作成とかを支援するみたい」「こういう感じのgemは昔からちょくちょくありますね」

teoljungberg/fx - GitHub

# 同リポジトリより
% rails generate fx:function uppercase_users_name
      create  db/functions/uppercase_users_name_v01.sql
      create  db/migrate/[TIMESTAMP]_create_function_uppercase_users_name.rb

packwerk: ShopifyのRailsモジュラライズgem(Ruby Weeklyより)

Shopify/packwerk - GitHub


同リポジトリより

以下の記事で知りました。


つっつきボイス:「packwerkはZeitwerkが必要なせいか「パックヴェルク」と読むそうです」「この1分間デモ動画を見るのが早そう↓」

  • ファイルのグループをパッケージにまとめる
  • 可視性がパッケージレベルの定数(publicにアクセスできる定数)を定義する
  • パッケージ間でプライバシー(内向き)や依存(外向き)の境界線を強制する
  • 開発を妨げずに既存のコードベースをよりモジュラーにする
    同リポジトリより

「ファットになったRailsの機能を分割隔離できるgemみたい」「機能を厳密に仕切るんですね」「Shopifyのように大規模Railsアプリを大人数で開発していてメンバーの出入りが多そうなところだと、こういう機能が切実に求められてくるでしょうね」「以下の翻訳記事ではRuboCopで似たようなことをやっていたのを思い出しました」

RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)

「越境アクセスそのものをできなくするというライブラリって大事: 知らないうちにアクセスされていると、それをレビューで見逃した途端に機能の独立性が損なわれてしまいますし」「そういう境界線がないと、ある変数がどこのモジュールから参照・更新されているのかわかりにくくなるのでテストもだんだん越境して重くなってきたり」

「Railsのレベルで機能を仕切ろうとすると普通は名前空間を切るんですけど、それだと結局Rubyの同じコンテキストの中なので、その気になれば越境できてしまうんですよ」「たしかに」「昔のRubyKaigiでそれを越境できないようにするというセッションを見かけたような気がするけど何だったかな…🤔」

後で探しましたが見つけられませんでした。


なお、つっつきの後でpackwerkの日本語記事をzenn.devで見かけました↓。

参考: Packwerk でスパゲッティな Rails のコードをソーメンぐらいにさっぱりさせる | Zenn

その他Rails

以下はつっつき後に見かけたツイートです。


前編は以上です。

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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