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

週刊Railsウォッチ: RailsでGDPRに対応する、stateful_enum gem、rubyzip 3.0ほか(20211214前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今週はRails 7 RC1が出たこともあってか公式の更新情報がなかったので、以下の残りおよび差分より拾いました。リリースが近いせいか、ガイドの更新が増えています。

🔗 自動で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レコードとインメモリレコードの重複を解消

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して使うといった工夫が必要になるでしょうね」「ふむふむ」

参考: BIツール - Wikipedia

「日本の個人情報保護法などもそうですが、削除せよと指示を受けたら削除できるようにしておく必要があります: もちろんECサイトの注文履歴情報のような、ユーザー自身の個人情報ではない企業側の持つ情報は残してもよいことになっているので、そういう情報と個人情報はなるべく切り離して別テーブルなどに置くなどするのが肝心でしょうね」「たしかに」「正規化も個人情報がログに出ないようにうまくやる必要がありそう」

参考: 個人情報の保護に関する法律 - Wikipedia
参考: 関係の正規化 - Wikipedia

「記事を書いた会社ではこういうライフサイクルでユーザーデータを扱っているらしい↓」「データの保持をactive database/intermediate archiving/deletionというフェイズに分けているけど、GDPRの定義とは別に自分たちで定義しているみたいですね」


同記事より

🔗 rubyzipが3.0に

rubyzip/rubyzip - GitHub


つっつきボイス:「最近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すると壊れます」「ありゃ〜」

fjg/zipruby - GitHub

「ところで、普段は気軽にzipを使いますけど、要件によってはzipの仕様がすごく細かく指定されることがありますよね: EPUBだとmimetypeは非圧縮zipにしないといけないとか」「非圧縮だとtarみたいな感じですね」

参考: .NETの System.Io.Compression で非圧縮のZIPファイルを作れなかったが対象が固定だったので直接バイナリファイル作って強引に解決した話 - 木俣ロバート久の覚書 - Hatena Blog
参考: EPUB - Wikipedia

🔗 stateful_enum gem

amatsuda/stateful_enum - GitHub


つっつきボイス:「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 - GitHub

# 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

参考: 有限オートマトン - Wikipedia

「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より)

stimulusreflex/cable_ready - GitHub

stimulusreflex/stimulus_reflex - GitHub


つっつきボイス:「記事には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後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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