- Ruby / Rails関連
週刊Railsウォッチ(20201005前編)Ruby 2.7.2がリリース、Shopifyのモジュラー化gem「packwerk」、stimulus_reflexほか
こんにちは、hachi8833です。皆さまもKaigi on Rails STAY HOME Editionをエンジョイされましたでしょうか。スポンサーおよび関係者の皆さまありがとうございました&お疲れさまでした!🙇
アーカイブ動画も今後順次配信されるようです!ありがとうございます🙇。
先行して @toshimaru_e さんの動画をアップしました!他のセッションも順次公開していきますのでお楽しみに! #kaigionrailshttps://t.co/duhoF1Xu1S https://t.co/y3PRYVRlO8
— Kaigi on Rails (@kaigionrails) October 3, 2020
先ほど@koicさんの以下の記事も公開されました。
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
⚓ 臨時ニュース: Ruby 2.7.2がリリース
- リリース情報: Ruby 2.7.2 Released
Ruby 2.7.2ではキーワード引数のwarningがデフォルトでオフになり、以下のWEBrickの脆弱性も修正されます。
また、Ruby 2.6.x系や2.5.x系にも同様にWEBrickを修正したバージョンが近日中にリリースされる可能性がありそうです。
⚓Rails: 先週の改修(Rails公式ニュースより)
以下のコミットリストのChangelogを中心に見繕いました。
- コミットリスト: Comparing master@{2020-09-23}...@{2020-10-01} · rails/rails
- 6.1マイルストーン: 6.1.0 Milestone -- 26件オープン
⚓新機能: 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に変わるのに対応
- PR: rexml is no longer default gem in Ruby 3.0 by kamipo · Pull Request #40281 · rails/rails
-
Ruby API: library rexml (Ruby 2.7.0 リファレンスマニュアル)
つっつきボイス:「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の違いがわかってなかった😅」
- Feature #16485: Make rexml, rss to the bundled gems - Ruby master - Ruby Issue Tracking System
- PR: Remove rexml and rss by hsbt · Pull Request #2832 · ruby/ruby
メリット: 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より)
- ドキュメント: Welcome - StimulusReflex
- デモサイト: StimulusReflexExpo
つっつきボイス:「デモサイトを触ってみた感じでは結構よくできているように自分には思えました」「StimulusってDHHというかBasecampが推してるんですよね、その意味ではRailsでリッチなインタラクティブUIを分離せずに一元管理したいのであればStimulusにするという雰囲気をちょっと感じますね」「Rails wayとまでいかないけどBasecamp way的な位置づけをちょっと感じる」
「管理画面ならこれで十分作れそう😋」「あとは学びの労力に見合うかどうかですね」「現代だとjQueryを新規で学ぶよりもStimulusを学ぶ方がありなのかも?🤔」「jQueryが前から入っているRailsアプリならしょうがないですけど、今の時代に新規アプリにjQueryを入れるのはちょっとためらいますし」
⚓モダンなFlashメッセージ(Ruby Weeklyより)
- 元記事: Modern Rails flash messages (part 1): ViewComponent, Stimulus & Tailwind CSS - DEV Community 👩💻👨💻
- 元記事: Modern Rails flash messages (part 2): The undo action for deleted items - DEV Community 👩💻👨💻
- デモサイト: Modern Rails Flash Messages
つっつきボイス:「今風の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
つっつきボイス:「↑この辺とか、まさにストアドプロシージャ」「ストアドプロシージャ書くのは今まで避け続けてきました😅」「少なくとも自分から書くことはないかな〜」「ストアドプロシージャに手を出し始めると沼が始まりそうですし」
「ストアドプロシージャってIF
も書けるんですか!」「こうしなくてもモデルのクラスでコールバックすればよさそうですけど」「まあストアドプロシージャに便利な点があるとすれば、上のコードみたいにカスタムトリガーを作れることに尽きますね: このトリガーはデータベースサーバー上で動くので、ユースケース次第ですけどモデルのクラスでコールバックするよりめちゃくちゃ速くなることがあります」「あ〜なるほど!」
「たとえば、データベース側のキャッシュに乗っているデータだけでできる処理だけどレコード数がめちゃくちゃ多い場合、アプリケーション側で処理するためにその大量のレコードをいったん全部持ってこないといけなくなって遅くなるのが目に見えますよね」 「ふむふむ」「そういうのをストアドプロシージャで処理すると、キャッシュがよく効けば爆速になる可能性を『秘めている』んですけど、そうでもない限り自分はたぶんストアドプロシージャに手を出さないと思います」
「相当昔のことですけど、ストアドプロシージャで爆速になるはずなのにならないので調べて欲しいって3か月ほどストアドプロシージャを解読し続けたことがあったのを思い出しました😇」「それはつらそう... お疲れさまです!」「ビジネスロジックがストアドプロシージャに入っているなら調べるしかないでしょうね😆」「いや〜あれは沼でした」
「ストアドプロシージャにする必要のない箇所でストアドプロシージャが使われていることってよくありますよね」「それそれっ!」「逆にストアドプロシージャをうまく使えばトランザクションロックを活用しつつシンプルに書けることもあります」「この記事のようなケースだとストアドプロシージャはかなり光るでしょうね: これをアプリケーション層でやろうとすると大きめのトランザクションロックをかけないといけなくなる可能性もあるでしょうし」
「この記事みたいなデータベーストリガー関数が必要になったことは今のところありませんけど、必要となればこういう手法もあるということは押さえてます」「方法があることを知っておくのが大事👍」「解決方法の選択肢を複数持っているのと、それしか解決方法がないと思い込むのとでは大きな違いですし」
「なお記事ではこのfxというgemを使っているそうです↓」「データベースの関数やトリガー作成とかを支援するみたい」「こういう感じのgemは昔からちょくちょくありますね」
# 同リポジトリより
% 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より)
以下の記事で知りました。
つっつきボイス:「packwerkはZeitwerkが必要なせいか「パックヴェルク」と読むそうです」「この1分間デモ動画を見るのが早そう↓」
- ファイルのグループをパッケージにまとめる
- 可視性がパッケージレベルの定数(publicにアクセスできる定数)を定義する
- パッケージ間でプライバシー(内向き)や依存(外向き)の境界線を強制する
- 開発を妨げずに既存のコードベースをよりモジュラーにする
同リポジトリより
「ファットになったRailsの機能を分割隔離できるgemみたい」「機能を厳密に仕切るんですね」「Shopifyのように大規模Railsアプリを大人数で開発していてメンバーの出入りが多そうなところだと、こういう機能が切実に求められてくるでしょうね」「以下の翻訳記事ではRuboCopで似たようなことをやっていたのを思い出しました」
「越境アクセスそのものをできなくするというライブラリって大事: 知らないうちにアクセスされていると、それをレビューで見逃した途端に機能の独立性が損なわれてしまいますし」「そういう境界線がないと、ある変数がどこのモジュールから参照・更新されているのかわかりにくくなるのでテストもだんだん越境して重くなってきたり」
「Railsのレベルで機能を仕切ろうとすると普通は名前空間を切るんですけど、それだと結局Rubyの同じコンテキストの中なので、その気になれば越境できてしまうんですよ」「たしかに」「昔のRubyKaigiでそれを越境できないようにするというセッションを見かけたような気がするけど何だったかな...🤔」
後で探しましたが見つけられませんでした。
なお、つっつきの後でpackwerkの日本語記事をzenn.devで見かけました↓。
参考: Packwerk でスパゲッティな Rails のコードをソーメンぐらいにさっぱりさせる | Zenn
⚓ その他Rails
以下はつっつき後に見かけたツイートです。
David Heinemeier Hansson氏の基調講演での質問を募集 | RubyWorld Conference 2020 https://t.co/8QW7x2Jltx
— Yukihiro Matsumoto (@yukihiro_matz) October 5, 2020
前編は以上です。
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。