- Ruby / Rails関連
週刊Railsウォッチ: RailsでGDPRに対応する、stateful_enum gem、rubyzip 3.0ほか(20211214前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今週はRails 7 RC1が出たこともあってか公式の更新情報がなかったので、以下の残りおよび差分より拾いました。リリースが近いせいか、ガイドの更新が増えています。
- Automated shard swapping middleware, standardised error reporting interface and more! | Riding Rails
- 差分: Comparing @{2021-11-21}...main@{2021-12-09} · rails/rails
🔗 自動でshardをスワップするミドルウェアが追加
自動shardスワップ用ミドルウェアを追加。
shardの自動スワップを行う基本的なミドルウェアを提供する。アプリケーションは個別のリクエストに対してどのシャードを使うべきかを決定するリゾルバを提供する。例:
config.active_record.shard_resolver = ->(request) {
subdomain = request.subdomain
tenant = Tenant.find_by_subdomain!(subdomain)
tenant.shard
}
詳しくはガイドを参照。
Eileen M. Uchitelle, John Crepezzi
同Changelogより
つっつきボイス:「shardのリゾルバをlamda(->
)でコンフィグに書けるようになった」「ミドルウェアでできるようになったとありますね」「このコンフィグに書いておくことで、自分が決めたロジックでshardをミドルウェアレベルでスワップできるようになるということでしょうね」「なるほど」
「上のコンフィグの例がまさにそうですが、マルチテナントのデータベースはテナントごとにアクセスの多さがかなり違う、つまりテナントごとにlocality(局所性)が大きく違うことが多い」「ふむふむ」「上のコンフィグのように、たとえばIPアドレスやドメイン名でshardを分けると、キャッシュが乗りやすいような形できれいに分散できるでしょうね」「あ〜たしかに」「Active Recordのコードでconnected_to
を書いて自分でshardを選ばなくてもミドルウェアレベルでresolveできるようになって便利になったということだと思います」「なるほど!」「ごく一部をconnected_to
でやるならともかく、広範囲でやると構成の変更も難しくなるので、ミドルウェアでできるのはいいですね👍」
🔗 merge_target_lists
のpersistedレコードとインメモリレコードの重複を解消
- PR: Do not consolidate persisted in-memory records by kevinsjoberg · Pull Request #43517 · rails/rails
merge_target_lists
は2つの引数(persistedレコードのコレクション、インメモリレコードのコレクション)で呼び出される。persistedレコードがインメモリコレクションにも含まれていたとしても、persisted
コレクションはすでに最新なので安全にreject
できる。
修正: #43516
同PRより
つっつきボイス:「merge_target_lists
はprivateメソッドなので内部の修正ですね」「merge_target_lists
はおそらく、データベース上で既に永続化されている(persisted)レコードと、まだメモリ上にあるレコードをマージするメソッドらしい」「メモリ上のレコードとpersistedレコードに重複があると#43516の問題が起きるので、以下のようにreject
で重複を取り除いだということのようですね」
# activerecord/lib/active_record/associations/collection_association.rb#L338
- persisted + memory
+ persisted + memory.reject(&:persisted?)
🔗 レコード削除のエラーメッセージにレコードのクラスも表示
つっつきボイス:「お〜、_raise_record_not_destroyed
のときに、削除に失敗したクラス名とキーも表示するようになった」「たしかに"Failed to destroy the record"だけだと、どのレコードで失敗したのかわからないですね」「そういうときにRailsのログを探し回るというのを何度となくやってきました」「これは出る方が便利👍: ユーザーへのエラーメッセージで見せたくない場合には注意が必要かもしれませんが」
# activerecord/test/cases/associations/has_many_associations_test.rb#L2787
test "raises RecordNotDestroyed when replaced child can't be destroyed" do
car = Car.create!
original_child = FailedBulb.create!(car: car)
error = assert_raise(ActiveRecord::RecordNotDestroyed) do
car.failed_bulbs = [FailedBulb.create!]
end
assert_equal [original_child], car.reload.failed_bulbs
- assert_equal "Failed to destroy the record", error.message
+ assert_equal "Failed to destroy FailedBulb with #{FailedBulb.primary_key}=#{original_child.id}", error.message
end
🔗 in_order_of
の修正
データベースにintegerとして保存されたenumに
in_order_of
を使った場合の動作が期待どおりにならない。
class User < ApplicationRecord
enum role: [:normal_user, :admin, :super_user]
end
# 現時点では期待どおりの結果が返らない
User.in_order_of(:role, %w[super_user normal_user admin])
これは"super_user"、"normal_user"、"admin"がデータベース内のintegerと比較されているのが原因。データベース内のintegerとこれらの文字列を比較できるようにするには、文字列を
type_cast_for_database
で変換してデータベース内のintegerとマッチさせる必要がある。
同PRより
つっつきボイス:「in_order_of
のバグが修正」「type_cast_for_database
でキャストすることでenumを正しく比較できるようにしたんですね」
# #L
def in_order_of(column, values)
klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
references = column_references([column])
self.references_values |= references unless references.empty?
+ values = values.map { |value| type_caster.type_cast_for_database(column, value) }
column = order_column(column.to_s) if column.is_a?(Symbol)
spawn.order!(connection.field_ordered_value(column, values))
end
🔗 Railsガイドでレコード削除の例をlink_to
からbutton_to
に変更(現在オープン)
つっつきボイス:「まだマージされていないプルリクですが、Rails 7 RC1を試していて自分もこれを踏んでしまったので取り上げてみました」「お、そういえばRails 7からUJSのサポートがなくなることになっていますね」「UJSの代わりに今後はTurbo Driveになるんですが、Railsガイドもそれに合わせて整合性を取らないといけないという流れです」「こういう部分も試してくれているのはいいですね👍」
参考: 控えめなJavaScript - Wikipedia -- Unobtrusive JavaScript(UJS)
# guides/source/getting_started.md#L1288
<ul>
<li><%= link_to "Edit", edit_article_path(@article) %></li>
- <li><%= link_to "Destroy", article_path(@article),
- method: :delete,
- data: { confirm: "Are you sure?" } %></li>
+ <li><%= button_to "Delete Article", { id: @article.id}, method: :delete %>
</ul>
「今まではrails-ujsのJavaScriptコードがDestroyのlink_to
に自動で<form>
タグを付けてくれましたけど、Turbo Driveに変わるとそのあたりの挙動も変わるようですね: ちなみにDestroyはHTTPのPOSTメソッドを使います」「Destroy操作はリンクよりボタンがよいのではということでbuttun_to
が提案されていました」「言われてみれば本来<a>
タグは文書間を移動するハイパーリンクのためのものなので、その<a>
タグでDestroyやCreateのような改変操作を行うのはいかがなものかと考えることもできますね」「どこに落ち着くかが気になってます」
なおrails-ujsは以前はgemでしたが、現時点ではRailsに取り込まれています↓。
参考: Add rails-ujs to Action View · rails/rails@ad3a477
🔗Rails
🔗 RailsでGDPRに対応する(Ruby Weeklyより)
つっつきボイス:「GDPRとアカウント削除、これはちゃんとやろうとするとなかなか大変なヤツ: この種の対応でよくあることですが、たとえばログに出力されたものも適切に処理しないといけなくなる」「GDPRは最近Webサイトの通知でよく表示されていますね」
参考: EU一般データ保護規則(GDPR) - Wikipedia
参考: GDPR |個人情報保護委員会
「S3とかに保存したユーザーログも必要に応じて消さないといけなくなるんでしょうか?」「本来的にはそうなるだろうと思います: その意味では、ログを設計するときには個人情報になりうるものはなるべくログに出さないようにしておいて、使うときだけBIツールなどでその都度JOINして使うといった工夫が必要になるでしょうね」「ふむふむ」
「日本の個人情報保護法などもそうですが、削除せよと指示を受けたら削除できるようにしておく必要があります: もちろんECサイトの注文履歴情報のような、ユーザー自身の個人情報ではない企業側の持つ情報は残してもよいことになっているので、そういう情報と個人情報はなるべく切り離して別テーブルなどに置くなどするのが肝心でしょうね」「たしかに」「正規化も個人情報がログに出ないようにうまくやる必要がありそう」
参考: 個人情報の保護に関する法律 - Wikipedia
参考: 関係の正規化 - Wikipedia
「記事を書いた会社ではこういうライフサイクルでユーザーデータを扱っているらしい↓」「データの保持をactive database/intermediate archiving/deletionというフェイズに分けているけど、GDPRの定義とは別に自分たちで定義しているみたいですね」
🔗 rubyzipが3.0に
つっつきボイス:「最近Rails 7 RC1でrails new
していたら以下のメッセージが表示されたことでrubyzipが更新されたことを知りました: Railsではロックされるので基本的に大丈夫ですが、3.0でインターフェイスがモダンになったので新しい方を使うときは注意だそうです」「breaking changesが入るんですね」
RubyZip 3.0 is coming!
The public API of some Rubyzip classes has been modernized to use named
parameters for optional arguments. Please check your usage of the
following classes:
*Zip::File
*Zip::Entry
*Zip::InputStream
*Zip::OutputStream
Please ensure that your Gemfiles and .gemspecs are suitably restrictive
to avoid an unexpected breakage when 3.0 is released (e.g. ~> 2.3.0).
See https://github.com/rubyzip/rubyzip for details. The Changelog also
lists other enhancements and bugfixes that have been implemented since
version 2.3.0.
「rubyzipは定番として長く使われていますけど、もうひとつziprubyというgemも昔からあるんですよ↓」「ネーミングが逆なんですね」「実はziprubyもrubyzipも名前空間がZip
で始まるので、両方require
すると壊れます」「ありゃ〜」
「ところで、普段は気軽にzipを使いますけど、要件によってはzipの仕様がすごく細かく指定されることがありますよね: EPUBだとmimetypeは非圧縮zipにしないといけないとか」「非圧縮だとtarみたいな感じですね」
参考: .NETの System.Io.Compression で非圧縮のZIPファイルを作れなかったが対象が固定だったので直接バイナリファイル作って強引に解決した話 - 木俣ロバート久の覚書 - Hatena Blog
参考: EPUB - Wikipedia
🔗 stateful_enum gem
つっつきボイス:「amatsudaさんの作ったgemで、DiscordのRubyチャンネルでamatsudaさんがこのgemのことをつぶやいていたのを見て知りました」
# 同リポジトリより
class Bug < ApplicationRecord
enum status: {unassigned: 0, assigned: 1, resolved: 2, closed: 3} do
event :assign do
transition :unassigned => :assigned
end
event :resolve do
before do
self.resolved_at = Time.zone.now
end
transition [:unassigned, :assigned] => :resolved
end
event :close do
after do
Notifier.notify "Bug##{id} has been closed."
end
transition all - [:closed] => :closed
end
end
end
「aasmというステートマシンgem↓のようなことをRailsのenumでやる感じに見えますね: aasm do
で書く代わりにenum do
で書くのが違うけど他は似ているかな」「aasm gemは超定番ですよね」「aasmは昔からあってよく使いますけど、stateful_enumはRailsの機能を使っている分よさそうに見える❤️」
# aasm/aasmより
class Job
include AASM
aasm do
state :sleeping, initial: true
state :running, :cleaning
event :run do
transitions from: :sleeping, to: :running
end
event :clean do
transitions from: :running, to: :cleaning
end
event :sleep do
transitions from: [:running, :cleaning], to: :sleeping
end
end
end
「stateful_enumはActive Recordのenumを使うんですね」「aasmはより一般的なつくりになっていますが、aasmもActive Recordで使えますよ↓」「あ、そうでしたか」「enum
はあくまでRailsのDSLですね」
# aasm/aasmより
class Job < ActiveRecord::Base
include AASM
aasm do # default column: aasm_state
state :sleeping, initial: true
state :running
event :run do
transitions from: :sleeping, to: :running
end
event :sleep do
transitions from: :running, to: :sleeping
end
end
end
🔗 CableReadyがRuby以外の言語にも対応を表明(Ruby Weeklyより)
つっつきボイス:「記事にはRailsのAction Cable的なJSONやコードが出てきてますね↓: CableReadyはAction Cableのメッセージや多重化周りを扱いやすくしてくれる感じかな」「CableReadyが来年にGoやPythonなどの言語もサポートするとも書かれていました」
# 同記事より
[{\"message\":\"Hello!\",\"operation\":\"consoleLog\"}]
# 同記事より
cable_ready[:foo].operation(options).broadcast
参考: Action Cable の概要 - Railsガイド
参考: Rails: StimulusReflexとCableReadyでチャット機能を作ってみる - Qiita
「CableReady v5.0 pre-releaseのサイトを見るとこんなことが書かれてる↓」「複雑なSPAフレームワークなしにリアクティブにできるのね」「でも機能が足りなくて結局ReactとSPAを使ったりしているのをよく見かけますけどね😆」「StimulusJSやTurboはSPAに走らない方向で頑張っている印象あります」
CableReady offers 36 different operations that let you create reactive user experiences without the need for complex SPA frameworks.
cableready.stimulusreflex.comより
参考: Welcome - CableReady
参考: シングルページアプリケーション(SPA) - Wikipedia
前編は以上です。
バックナンバー(2021年度第4四半期)
週刊Railsウォッチ: 改訂2版『プロを目指す人のためのRuby入門』、『研鑽Rubyプログラミング β版』ほか(20211207後編)
- 20211206前編 sanitize_sql_likeは重要、X-XSS-Protectionヘッダーのデフォルト変更、kredis gemほか
- 20211201後編 Railsで「Read Model」を使う、Ruby Prize 2021受賞者決定、pru gemほか
- 20211129前編 フォームヘルパーの改修、Railsの監査ログgem比較、DHHとimport-mapほか
- 20211116後編 Ruby Struct入門、書籍『進化的アーキテクチャ』、AWS Web問題集ほか
- 20211115前編 Rails 7がRuby 3.1のClass#descendantsに対応、GitHub Issue風ファイルアップローダほか
- 20211110後編 JSON.parseの機能、Opal 1.3、async gem、Linuxコマンドチートシートほか
- 20211102後編 2021年度Rubyアソシエーション開発助成、Rails REST APIレベルで楽観的ロックほか
- 20211101前編 Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか
- 20211026後編 YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか
- 20211025前編 insert_allやupsert_allのタイムスタンプ自動更新、rails/contextsにロジックを置くほか
- 20211019後編 ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか
- 20211018前編 Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか
- 20211012後編 Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか
- 20211011前編 ServerTimingミドルウェア追加、paramsで数値キーを許可、Railsで多要素認証ほか
- 20211006後編 ruby/debug 1.2.0リリース、Railsにはthorが入っている、tendejitほか
- 20211004前編 Rails 7でbyebugがruby/debugに変更、GitHub Codespacesをサポートほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)