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

週刊Railsウォッチ: AR::Relation#destroy_allがバッチ分割に変更、Active Record暗号化解説、sidekiq-unique-jobsほか(20210712前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は以下の更新情報から見繕いました。

🔗 ActiveRecord::Relation#destroy_allの処理をデフォルトでバッチに分割

ActiveRecord::Relation#destroy_allが処理をバッチに分割するようになった。
destroy_allは実際には全リレーションを読み込んでからレコードのdestroyを1件ずつ繰り返すのでメモリがすぐ吹っ飛びがち。これを正しく行うために、デフォルトでは100件ずつのバッチに小分けするようにし、#destroy_all(batch_size: 100)のようにバッチサイズも指定できるようにする。
アプリを7.0にアップグレードするときにdeprecation warningが表示される。Rails 7.1までにdestroy_allはdestroy対象のオブジェクトのコレクションを返さなくなる。
新しい振舞いに移行するには以下のイニシャライザを設定する。

config.active_record.destroy_all_in_batches = true

このオプションは、今後新規作成するRailsアプリではデフォルトでオンになる。イニシャライザで設定しておくことで環境ごとの違いを生じないようにできる。
Genadi Samokovarov, Roberto Miranda
同Changelogより


つっつきボイス:「find_in_batches的なバッチ分割処理がRails 7.1のdestroy_allに取り入れられるようですね」「今後はdestroy_allがデフォルトでバッチ分割されるようになるのか: 割と大きな変更かも」「コンフィグでオフにできるんですね」

find_in_batchesのようなバッチ分割は、途中に別のトランザクションがはさまると結果が変わる可能性があるんですよ: destroy_allもその点は同じだと思いますが、心配な人はdestroy_allをトランザクションで囲むだろうし、destroy_allが遅いと思う人はActiveRecord::Relationdelete_allを使うと思うので、気にする人は少ないのかも」「なるほど」

destroy_allが途中でコケたらどうするんでしょう?」「destroy_allはデフォルトでorder: :ascが付いているので、コケた場合には順序を頼りに追うことになるでしょうね」

🔗 ActiveSupport::TimeZone.iso8601でordinal date値をサポート

Date._iso8601'21087'のようなordinal dateの文字列の値をパースしようとすると「28th March 2021」になる。
Rubyの標準ライブラリでDate._iso8601をサポートしているように、ActiveSupport::TimeZone.iso8601でもordinal date値をサポートすべき。
「年」と「年日数」({ year: 2021, yday: 87 }など)の値のパースをサポートし、Date.ordinalで有効となる日付の生成を試みる。このとき、パースされた値のうち、対応する年の:ydayがサニタイズされる。
同PR Summaryより

参考: Date._iso8601 (Ruby 3.0.0 リファレンスマニュアル)


つっつきボイス:「ordinal dateって初めて聞きました」「'21087'が2021年の87日目を表す、つまりその年の日付を3桁の日数で表せるのか、へ〜!」「あ、1月1日が001みたいな感じでカウントするんですね」「YYYY-DDD形式って...」

参考: ISO 8601 - Wikipedia

ordinal dateが「年間通算日(年日付)」と訳されているケースを見つけましたが、正式な名前かどうかは不明です。

「ordinal dateって日本だと見かけないかも」「普通あんまり使わなさそう」「ordinal dayの表を見つけた↓」「面白いけど使いたくないな〜😅」「うるう年は数字がズレるんですって」

# https://www.atmos.anl.gov/ANLMET/OrdinalDay.txtより
   TABLE OF ORDINAL DAY NUMBER FOR VARIOUS CALENDAR DATES.
          (After February, add 1 on leap years).

    JAN  FEB  MAR  APR  MAY  JUN  JUL  AUG  SEP  OCT  NOV  DEC

 1    1   32   60   91  121  152  182  213  244  274  305  335
 2    2   33   61   92  122  153  183  214  245  275  306  336
 3    3   34   62   93  123  154  184  215  246  276  307  337
 4    4   35   63   94  124  155  185  216  247  277  308  338
 5    5   36   64   95  125  156  186  217  248  278  309  339

 6    6   37   65   96  126  157  187  218  249  279  310  340
 7    7   38   66   97  127  158  188  219  250  280  311  341
 8    8   39   67   98  128  159  189  220  251  281  312  342
 9    9   40   68   99  129  160  190  221  252  282  313  343
10   10   41   69  100  130  161  191  222  253  283  314  344

11   11   42   70  101  131  162  192  223  254  284  315  345
12   12   43   71  102  132  163  193  224  255  285  316  346
13   13   44   72  103  133  164  194  225  256  286  317  347
14   14   45   73  104  134  165  195  226  257  287  318  348
15   15   46   74  105  135  166  196  227  258  288  319  349

16   16   47   75  106  136  167  197  228  259  289  320  350
17   17   48   76  107  137  168  198  229  260  290  321  351
18   18   49   77  108  138  169  199  230  261  291  322  352
19   19   50   78  109  139  170  200  231  262  292  323  353
20   20   51   79  110  140  171  201  232  263  293  324  354

21   21   52   80  111  141  172  202  233  264  294  325  355
22   22   53   81  112  142  173  203  234  265  295  326  356
23   23   54   82  113  143  174  204  235  266  296  327  357
24   24   55   83  114  144  175  205  236  267  297  328  358
25   25   56   84  115  145  176  206  237  268  298  329  359

26   26   57   85  116  146  177  207  238  269  299  330  360
27   27   58   86  117  147  178  208  239  270  300  331  361
28   28   59   87  118  148  179  209  240  271  301  332  362
29   29  *60   88  119  149  180  210  241  272  302  333  363
30   30        89  120  150  181  211  242  273  303  334  364

31   31        90       151       212  243       304       365

* Feb 29 exists only on a leap year.

「ordinal dateはISO 8601でも定義されていますし、この機能がActive Supportに入るということは使いたい人がいるということでしょうね」「ordinal dateは月替りを考えたくないときに使うのかな?」「週を数字にして2021年の何週目みたいに表すのは英語圏のアプリとかでたまに見かけますけど」

「ordinal dateを使うことがあるとすれば、組み込みのように少しでも桁数を減らしたい分野かもしれませんね」「あ〜たしかに」「後何日で保証が切れるみたいなタイマーが作りやすそう」


Ordinal dates
異なるカレンダーの日付を比較するなど、週や月の定義が任意だと障害になりやすい場合のためのシンプルなフォーマットです。(中略)このフォーマットは、日付システムを必要とするが完全なカレンダー計算ソフトウェアを含めるのが難しい単純なハードウェアシステムで使われます。
ISO 8601 - Wikipedia(英語版)より

🔗 remove_foreign_keyadd_foreign_keyif_exists:if_not_exists:オプションをサポート

remove_foreign_key/add_foreign_keyif_exists:if_not_exists:オプションをサポートする。以下のようにアプリケーションのマイグレーション中に、既に存在する外部キーを追加したときの例外や、存在しない外部キーを削除したときの例外を無視できるようになる。

class AddAuthorsForeignKeyToArticles < ActiveRecord::Migration[7.0]
  def change
    add_foreign_key :articles, :authors, if_not_exists: true
  end
end
class RemoveAuthorsForeignKeyFromArticles < ActiveRecord::Migration[7.0]
  def change
    remove_foreign_key :articles, :authors, if_exists: true
  end
end

同Changelogより


つっつきボイス:「Railsのマイグレーションで使うことのあるremove_foreign_keyadd_foreign_keyif_exists:オプションとif_not_exists:オプションがサポートされたんですね: 個人的にはマイグレーションにこういうオプションをあまり付けたくない気はしますけど」

参考: Active Record マイグレーション - Railsガイド

「たしかschema.rbには最終的にDDLの形で抽出したスキーマが反映されるはずだと思うので、このオプション部分で成功しても失敗してもschema.rbの一貫性は保てそうかな」「自分もたしかそうだったと認識してます」「マイグレーションの蓄積を直接schema.rbに反映していたらschema.rbが不定になる可能性があるので、そういう方法ではやっていないはず」

参考: データ定義言語 - Wikipedia -- DDL

「普段は使わないと思いますが、たとえば外部キーを追加した後に何らかの理由でマイグレーションに失敗したときのリトライにはこういうオプションがあるといいかも」「あ〜たしかに」「マイグレーションが途中で失敗すると再実行でもコケるので、それをリカバリーしたいというニーズに応えるための改修だとしたら理解できる👍」「そういう状況はあって欲しくないけど、そうなったときには欲しいかも」「外部キーのマイグレーションにif_exists:が書かれているのを見かけたら不安な気持ちになりそうですけどね」

🔗 Action Mailboxで使うデフォルトのActive Storageサービスをカスタマイズ可能に

生メールソースの保存に使うActiveStorageサービスをコンフィグできる機能を追加。

# config/storage.yml
incoming_emails:
  service: Disk
  root: /secure/dir/for/emails/only
config.action_mailbox.storage_service = :incoming_emails

Yurii Rashkovskii
同Changelogより


つっつきボイス:「今まではAction Mailboxの保存先のActive Storageサービスを選べなかったのがyamlに記述することで選べるようになった: これは必要ですね👍」「Action Mailboxまだ使ったことなかったな〜」「そもそもメールを普段使っていません😆」

参考: Action Mailbox の基礎 - Railsガイド

🔗 Active Storageのコントローラでstrict_loading_by_defaultコンフィグをサポート


つっつきボイス:「これもコンフィグ追加ですね」「今までActive Recordでstrict loadingをデフォルトでオンにするとActive Storageのコントローラでエラーになっていたのを修正したらしい」「ActiveStorage::Representationsというコントローラがあったとは知りませんでした」「ActiveStorage::RepresentationsActiveStorage::Previewと関連しているみたい: そのコントローラがビューの中でstrict loading違反していたんでしょうね」

新しくedgeで生成したアプリケーションでactive_record.strict_loading_by_default = trueを設定すると、ActiveStorage::Representationsコントローラでstrict loadingエラーが発生する。このプルリクではその問題を修正し、Active Storageのモデルでstrict loadingを無効にせずに済むようにした。
同PR Summaryより

🔗 uglify-jsをterserに置き換え

terser/terser - GitHub


つっつきボイス:「ターサー?」「terserが一瞬teaser(いじめっ子)に見えてしまいましたが、terserはterse(簡潔な)の比較級だそうです」「JSコードのminifyや難読化などに使うuglify-jsを新しいterserライブラリに置き換えたんですね: 改修の差分を見てもgemの差し替えぐらいしかやっていない↓」「ホントだ」「こんなにキレイに移行できるとは」「後発なだけにインターフェースも互換性があるんでしょうね」

# Gemfile#L26
gem "uglifier", ">= 1.3.0", require: false
gem "terser", ">= 1.1.4", require: false

参考: 難読化コード - Wikipedia

「uglifier、そんなgemもありましたね(遠い目)」「この記事は2019年だけどterserが伸びているらしい↓」「JSのライブラリは移り変わりが激しいので、こういう置き換えもケアしているんですね」

参考: 2019年のJavaScript minifier "terser" - Qiita

後で現在の比較を見るとterserが上回っています↓。

参考: terser vs uglify-js | npm trends

🔗Rails

🔗 Active Recordの暗号化機能解説(RubyFlowより)


つっつきボイス:「Rails 7に標準で搭載されるActive Record暗号化(ウォッチ20210412)の解説記事が出たんですね」

extend_queriesコンフィグをオンにすると以下ができるらしい: uniquenessバリデーションは、まさにこの間話したdeterministic encryptionに関連するヤツ(ウォッチ20210628)」「なるほど」「暗号化方式がdeterministicでない場合は、暗号化データを復号しないとuniquenessバリデーションができなくなります」

  • 暗号化カラムで暗号化なしの平文データもクエリできる(config.active_record.encryption.support_unencrypted_dataもオンにする必要あり)
  • 暗号化スキームを複数利用可能になる
  • uniquenessバリデーションのサポートが有効になる
    同記事より

「検索はこういう感じになるのね↓」

# 同記事より
> dog = Dog.find_by!(toy_location: 'top secret')
  Dog Load (2.1ms)  SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" = ? LIMIT ?  [["toy_location", "{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"], ["LIMIT", 1]]
#=> #<Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">

「記事末尾のlimitation解説もよさそう」「以下のmultiple keysはたぶんキーのローテーションを指していると思います」「なるほど」

Deterministic searching does not support multiple keys - Something good to be aware of going in - if using deterministic encryption/searching, we won't have the ability to use more than one key at a time. If we need to change keys, we'll likely need to do something fancy.
同記事より

「Railsコンソールだと生データが見えるそうです」「当然そうなりますね」「deterministic encryptionだとセキュリティが下がる、これもごもっとも」

「コード例も載っていて要点を押さえた記事、よさそう👍」「この記事翻訳してくださ〜い」「はい、聞いてみます」


その後Honyebadgerより翻訳を許可いただきました🙇。

🔗 Railsアンチパターンシリーズ記事最終回


つっつきボイス:「AppSignalブログのRailsアンチパターンシリーズ記事の最終回だそうです」

「最初はデメテルの法則↓: このコード例は、song下のlabelに直接アクセスさせるとlabelで何でもできてしまうので、Railsのdelegateヘルパーを使って隠蔽しましょうという話のようですね」「なるほど」「コンポジションしたオブジェクトを直接公開するべきではない、たしかに」

# 同記事より
# Bad
song.label.address

# Good
song.label_address

参考: Module#delegate
参考: デメテルの法則 - Wikipedia

「次は"そのGem、本当に必要?"的なトピック」「あまりに簡単な機能ならgemより直接実装する方が早いしgemのバージョンアップとかも気にしなくて済むので、自分もよくそう思います」「たしかに」

「このグラフのnpmのモジュールの増え方がヤバい↓」「自動生成してそうな勢い」「大半は使われていないモジュールでしょうけど、勢いがあるのは言語としては望ましいんですよね」「そうですね」「欲しいモジュールを検索するのは大変ですけど」


同記事より

「次は"例外を握りつぶすな"トピック」「以下のコード例を見ていて思ったんですが、returnで最後に返す値をあまり考慮していないrescueはたまに問題になりますね」「あ〜」「Rubyは最後に評価したものを値として返す仕様なので、rescueをメソッドの最後に書くときはちょっと気をつけておかないと、どこからreturnするかで想定外の値が返される可能性もあります」「なるほど」

# 同記事より
begin
  song.upload_lyrics
rescue
  puts 'Lyrics upload failed'
end

「なかなかよさそうな記事👍」「これも翻訳してみたいです」

🔗 sidekiq-unique-jobs(Ruby Weeklyより)

mhenrixon/sidekiq-unique-jobs - GitHub


つっつきボイス:「Sidekiqに重複したジョブが入らないように一意性を確保できるgemだそうです」「Sidekiqで全く同じパラメータのジョブが複数実行されるのを排除できるようですね」「ふむふむ」「割と欲しい機能に見えるので後でチェックしてみようかな」

mperham/sidekiq - GitHub

lock: :until_executeを指定できる↓:、実行完了するまでは同じジョブを投入できないけど実行が終われば投入できるということか、へ〜!」

# 同リポジトリより
class UntilExecuted
  include Sidekiq::Workers

  sidekiq_options lock: :until_executed

  def perform(id)
    # Do work
  end
end

lock: :until_expiredの場合はジョブが期限切れになれば同じジョブを再投入できる: これ賢い!」「おぉ〜」

# 同リポジトリより
class UntilExpired
  include Sidekiq::Workers

  sidekiq_options lock: :until_expired, lock_ttl: 1.day

  def perform
    # Do work
  end
end

「Webアプリの注文ボタンにたとえると、注文ボタンを押してもすぐに反応がないとつい何回も連打してしまうことがありますよね」「そうそう」「この場合なら:until_executeを指定すれば、実行中は同じジョブをキューに入れられないようにできる」「なるほど!」「READMEをさっと見た限りではロックやexpireなどをいろいろ制御できるみたい」

「このgemを使えば、ジョブの重複制御を自分でやらなくても投機的にジョブを投入できるということになりますね」「それすごいじゃないですか」

「思いついた範囲だと、たとえば時間課金の外部コンピューティングリソースを使いたいときにこのgemが合いそう: 投機的にジョブを積んでおくだけでCPU時間を効果的に使い切れるようになる」「あ〜なるほど」

「ジョブのステータスをチェックして重複を排除したりリトライしたりする機能は自分で実装すると複雑になりがちなので、今度機会があったら使ってみよう👍」「意識することが減るのはありがたい🙏」

🔗 Hanami Mastery: Hanami Frameworkのブログ

Hanami公式かどうかは確かめきれませんでした。


つっつきボイス:「Hanamiフレームワークのブログサイトを立ち上げたそうです」「Hanami Masteryというタイトルがなかなかポイント高いですね」「花見名人的な」

🔗 その他Rails


つっつきボイス:「Gmailなどで見かける2文字のアバターSVGをERBで生成する記事です」「SlackのWorkspaceもデフォルトでこういう2文字アバターが表示されますね」「たしかにアバターがないと識別が面倒」


前編は以上です。

バックナンバー(2021年度第3四半期)

週刊Railsウォッチ: GitHub CopilotのAI補完、Pure Ruby実装のRuby JIT rhizome、PostgreSQLのPG-Strom拡張ほか(20210706後編)

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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