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

週刊Railsウォッチ(20200831前編)GitHubがRuby 2.7にアップグレード、Durationに変換メソッドが追加、hair_triggerでデータベーストリガほか

こんにちは、hachi8833です。RubyKaigi Takeout 2020はもう今週の金曜土曜ですね。YouTubeのRubyKaigiチャンネルでリマインダーを設定できるそうです。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

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

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


つっつきボイス:「そうそう、GitのコミットにはAuthorDateCommitDateの2つがありますね」「そういえばあった」「たしか普通にコミットでオプションを付けるとAuthorDateを変えられるんですけど、CommitDateは自動的にシステムの日付になるので、CommitDateを変えようとすると何か一工夫必要だった覚えがあります」「そうそう、--dateオプションはAuthorDateを変えるとき」「CommitDateを任意に変えようとすると面倒だった気がする」

参考: gitのコミット時間を変更する - Qiita
参考: Git のコミットのタイムスタンプには author date と committer date の 2 種類があるという話 - ひだまりソケットは壊れない

新機能: ActionView::Helpers::TranslationHelper#translateがブロックを受け取って訳文と解決済み訳文キーを取れるようになった

このコミットはActionView::Helpers#translateヘルパーメソッド(ちなみにエイリアスは#t)がブロックを受け取れるよう拡張する。
このメソッドをブロック付きで呼び出すと、translate呼び出しが訳文を第1ブロック引数として、解決済み訳文キーを第2ブロック引数としてyieldする。

<%= translate(".key") do |translation, resolved_key| %>
  <span data-i18n-key="<%= resolved_key %>"><%= translation %></span>
<% end %>

相対的な訳文キーが完全修飾キーより先行する場合や、呼び出し側が解決済みキーに関心がない場合は、第2ブロック引数を省略可能。

<%= translate("action.template.key") do |translation| %>
  <p><%= translation %></p>
  <p><%= translation %>, but a second time</p>
<% end %>

訳文をyieldするメリットは、テンプレートローカルな変数がこれによって再利用できること。RubyのObject#tapも使える。

ただしこのコミットより先んじて、訳文キーの解決がActionView内部で行われているために、呼び出し側から利用できなかった(解決済みキー自体を明示的に決定するつもりがない限り限り)。これをブロックパラメータとして利用できるようにしたことで、生成する要素内で翻訳された値をアノテートできるようになる。
同コミットより大意

参考: translate -- ActionView::Helpers::TranslationHelper


つっつきボイス:「なるほど、tヘルパーに機能が追加された」「translateにブロックを付けると複雑になるのであんまりやりすぎない方がいいかなとは思いますけど」「できるようになったのはいいこと👍」「想像ですけど、ブロック渡しにするとブロックをラムダとして解釈することになってキャッシュの高速化が効かなくなったりするのかな?それほど影響しないでしょうけど」

「Rubyのコードはこういうふうにブロックを付けるといろいろ融通を利かせられる実装が多いですよね」「デバッグでも便利なことがありますし」

STI以外の型でもdemodulizeされたクラス名の保存をサポート

ポリモーフィックな型でdemodulizeされたクラス名の保存をサポートする。
Rails 6.1より前は、STI型でしかstore_full_sti_classクラス属性を用いてdemodulizeされたクラス名を保存できなかった。
このプルリクによって、STI型でもポリモーフィック型でもstore_full_class_nameクラス属性を扱えるようになった。
同PRより大意


つっつきボイス:「でもじゅらいず?」「どう日本語にしようか悩んでます」「ポリモーフィックなクラスでも動くようになったということかな」「関連するプルリクを見てみる方がいいかも↓」

「#29601が関連しているあたり、eager loadingがらみなのかも」「この辺はどう動いてるのかよくわからない...」

STIよもやま話

「STIの挙動ってすぐには予測がつきにくいことがありますよね、親クラスに対してeachしたときにそのクラスが返ってくるのか親クラスが返ってくるのかとか: まあinstance_ofでチェックすればいいんですけど」

「この辺の記事↓を見てみると、モデルのインスタンスをnewするとサブクラスがあればサブクラスとして、それ以外は普通のActiveRecord::Baseとしてnewするのね」「find_sti_classはどっかで調べたことあるかも」

参考: ActiveRecord では STI をどう実装しているかを調べたメモ - Qiita
参考: find_sti_class (ActiveRecord::Inheritance::ClassMethods) - APIdock

「STIはたまにうまく設計にはまるときがありますけど、それ以外ではあんまり使わないかな」「自分もそうかも」「継承がとてもうまく作用するケースみたいに、STIがキレイに合うときはたしかにある」「STIのいい点は1個のテーブルに入るところですよね」「横断しているtypeをSQLのクエリで一発で引っ張りたい、しかもインデックスも効かせたいというときは、結果としてSTIがデータ構造的にもうまく当てはまりやすい気がします」「ふむふむ」

参考: STI -- Active Record の関連付け - Railsガイド

「以前よく作ったのが通知機能でのSTIなんですけど、通知にはいろんなタイプがあるけどメッセージはだいたい共通していて、でもクリックしたときのジャンプ先は通知のタイプによって違う、みたいな機能」「ふむふむ」「Notificationみたいな親クラスを作ってそれぞれの通知タイプを作る感じでSTIにしたんですけど、理由は通知が軽く数千万レコードぐらいに増えてしまって毎回複数テーブルから引っ張ってくるのがまったく現実的でなくなってしまうから」「なるほど〜」

新機能: ActiveSupport::Durationに各種変換メソッドを追加

以下が追加されたそうです。

  • in_seconds
  • in_minutes
  • in_hours
  • in_days
  • in_weeks
  • in_months
  • in_years

つっつきボイス:「Active SupportのDurationは便利ですし、よく作ったと思いますね」「in_*で変換を表すのか」「たとえば『1日は何分か』を1.day.in_minutesって書けるのね」「12.hours.in_daysは0.5だからたしかに半日」

# activesupport/lib/active_support/duration.rb#L352
+   alias :in_seconds :to_i
+
+   # Returns the amount of minutes a duration covers as a float
+   #
+   #   1.day.in_minutes # => 1440.0
+   def in_minutes
+     in_seconds / SECONDS_PER_MINUTE.to_f
+   end
+
+   # Returns the amount of hours a duration covers as a float
+   #
+   #   1.day.in_hours # => 24.0
+   def in_hours
+     in_seconds / SECONDS_PER_HOUR.to_f
+   end
+
+   # Returns the amount of days a duration covers as a float
+   #
+   #   12.hours.in_days # => 0.5
+   def in_days
+     in_seconds / SECONDS_PER_DAY.to_f
+   end
+
+   # Returns the amount of weeks a duration covers as a float
+   #
+   #   2.months.in_weeks # => 8.696
+   def in_weeks
+     in_seconds / SECONDS_PER_WEEK.to_f
+   end
+
+   # Returns the amount of months a duration covers as a float
+   #
+   #   9.weeks.in_months # => 2.07
+   def in_months
+     in_seconds / SECONDS_PER_MONTH.to_f
+   end
+
+   # Returns the amount of years a duration covers as a float
+   #
+   #   30.days.in_years # => 0.082
+   def in_years
+     in_seconds / SECONDS_PER_YEAR.to_f
+   end

「カレンダー的な処理では便利そう」「このぐらいささやかだと自分で書いてもいいでしょうけど」「こういうメソッドがあることに気づかずに実装しちゃいそう😆」「そうかも😆」

rails secretsを非推奨化、5.2〜のrails credentialsを推奨


つっつきボイス:「rails secretsはちょっと前からrails credentialsへの移行が進んでた気がする」「rails secretsはとりあえずsoft deprecateされたんですね」

  • Rails::Applicationにcredential用のattr_writerを追加
  • ドキュメントからsecrets.ymlへの参照を削除
  • Railsのsecretsが非推奨化されたことを示すヘッダーをsecretsコマンドのUSAGEに追加
    同PRより大意

参考: 【Rails5.2】秘匿情報はsecret.ymlではなくcredentials.yml.encで管理する【初心者】 - Qiita

Railsガイド「Active Recordクエリガイド」のサンプルのモデルをBookstoreに変更

現実に即したサンプルにしたとのことです。

# guides/source/active_record_querying.md#L32
-class Client < ApplicationRecord
-  has_one :address
-  has_many :orders
-  has_and_belongs_to_many :roles
+class Author < ApplicationRecord
+  has_many :books, -> { order(year_published: :desc) }
+end

+class Book < ApplicationRecord
+  belongs_to :supplier
+  belongs_to :author
+  has_many :reviews
+  has_and_belongs_to_many :orders, join_table: 'books_orders'
+
+  scope :in_print, -> { where(out_of_print: false) }
+  scope :out_of_print, -> { where(out_of_print: true) }
+  scope :old, -> { where('year_published < ?', 50.years.ago )}
+  scope :out_of_print_and_expensive, -> { out_of_print.where('price > 500') }
+  scope :costs_more_than, ->(amount) { where('price > ?', amount) }
end

つっつきボイス:「こちらはドキュメントの更新」「Active Recordクエリインターフェイスガイドのサンプルコードで使うモデルを書店ベースのものに書き換えたのね」「ClientAddressAuthorCustomerになったりBookSupplierが追加されたりと前より複雑になった感」「図も更新されてますね」「もっと複雑で現実的な事例をサポートできるようにしたと」「説明により適したサンプルになるならいいと思います👍」

参考: Active Record クエリインターフェイス - Railsガイド

Rails

GitHubがRubyを2.7に上げた

Upgrading GitHub to Ruby 2.7 - The GitHub Blog

環境変数でRuby 2.6とRuby 2.7を切り替えられるデュアルブートにしてアップグレードを進めたそうです。


つっつきボイス:「ついにGitHubがRubyを2.7に!」「これは偉大!🏔」「2.7は昨年クリスマスのリリースだったから、半年以上かかったということか」「やっぱりこのぐらいはかかりますよね」「アップグレードしたことでいろいろ速くなったらしいことが書かれてる↓」


github.blogより: 起動時間(単位は秒)


github.blogより: オブジェクトのアロケーション数

「GitHubがRubyを使い続けていることでRubyがいろいろよくなっているのはいいですよね」「GitHub向けの修正もちょいちょい入ったりしてるようですし」


なお、今度のRubyKaigi Takeout 2020では2.7での最適化の話↓も聞けるそうなので楽しみにしています。


rubykaigi.orgより

hair_trigger: データベース側でトリガするマイグレーションを生成

jenseng/hair_trigger - GitHub

hair-trigger: (形)(引き金が軽いことから)すぐカッとなりやすい


つっつきボイス:「ヘアトリガー?」「なるほど、モデルにtrigger.afterとかでトリガーを書いてマイグレーションを生成するとデータベース側のトリガにできるのか↓」「あら、そうみたい」「なかなかアグレッシブなgem」

# 同リポジトリより
class AccountUser < ActiveRecord::Base
  trigger.after(:insert) do
    "UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"
  end

  trigger.after(:update).of(:name) do
    "INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);"
  end
end
# 同リポジトリより
rake db:generate_trigger_migration
-- 同リポジトリより: MySQLの場合
CREATE TRIGGER account_users_after_insert_row_tr AFTER INSERT ON account_users
FOR EACH ROW
BEGIN
    UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;
END;

CREATE TRIGGER account_users_after_update_on_name_row_tr AFTER UPDATE ON account_users
FOR EACH ROW
BEGIN
    IF NEW.name <> OLD.name OR (NEW.name IS NULL) <> (OLD.name IS NULL) THEN
        INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);
    END IF;
END;

「整合性制約をデータベース側に置くというのはデータベース的には本来あるべき姿ではありますね」「たしかに」「基本的にRDBMSは整合性制約を保証する機能があるものですし」

参考: 参照整合性 - Wikipedia

「こういうgemは割と好きかも」「整合性制約をRailsでやりたい人はそうじゃないかもしれませんね」

「ただこのトリガーにはあくまでSQLしか書けないので、Rubyで処理したデータをブロックに渡すみたいな処理はそのままでは書けないことになりますよね」「あ〜そうなっちゃうのか」「なにしろトリガーなので」「どうしてもやりたければストアドプロシージャでRubyでやるのと同じ処理を書くとかになるでしょうし: その意味でこのgemはSQL脳の人じゃないと使いこなすのが大変かも」「Rubyでトリガーのロジックを書きたい人には悩ましいですね...」「最近のRailsエンジニアだとデータベーストリガーを使ったことない人もいるかも」

参考: ストアドプロシージャ - Wikipedia
参考: データベーストリガ - Wikipedia

「限定的な状況では有用だと思いますけど、いつもこれでやれるとは限らないかな: まあ自分は好きですけど❤️」

カーソルベースとオフセットベースのページネーション


つっつきボイス:「たしかにカーソルベースとオフセットベースのページネーションは全然違う」「カーソルでやれるならその方がめちゃくちゃ大きいページのページネーションで明らかに有利ですけど」「ですよね」「そういえばOracleなんかだとセッションの範囲で使えるテンポラリテーブルを作ったりできますね」

参考: ページネーションとは|Web用語(意味・説明) | プロモニスタ

「実際、カーソルにしないと遅くてどうしようもないというケースはたしかにありますし」「使わざるを得ないときはありますね」「カーソルを自力で操作するのはちょっとしんどいですけど」

参考: カーソル (データベース) - Wikipedia

「このネタはruby-jp Slackでも見かけましたね」「ところでページングと書いてあると違うものを想像しません?」「OSのメモリ管理の方のページング?」「はい😆」「まあSQLの文脈なので意味はわかりますけど」「記事にはページャーという言葉も出てきてますけどlessとかmoreみたいなページャーコマンドを思い出しちゃいます」

参考: ページング方式 - Wikipedia
参考: ページャとは - IT用語辞典 e-Words


追いかけボイス: 「一応MySQLにもCREATE TEMPORARY TABLEというのがあります: PostgreSQLでもできる気がすると思ったら普通にありました↓」「temporary table、WITH句だと書ききれない複雑な処理結果とかをtemporary tableに突っ込んで処理する、みたいな用途に使えるのでたまに使いたいことあるんですけど、CREATE TABLEなpriviledgeが必要なのでそこがやや使いにくいと思うことはありますね」

参考: 第107回 CREATE TEMPORARY TABLEによる一時テーブルの利用:MySQL道普請便り|gihyo.jp … 技術評論社
参考: TEMPORARY TABLE(一時テーブル)を探る - Qiita

その他Rails

つっつきボイス:「ところでWindows + WSL2でやるときって、Windowsのファイルシステム側でソースコードを変更してそれをWSL2側で動かそうとすると面倒なことになりがちですよね」「やろうとしたらできなかったり、できたとしてもトラブルが多くて大変だったな〜という印象あります」「ファイルシステムが両者で違うのでもうしょうがない」「わかってはいるんですけどね...」「8ビットで表現されるパーミッションですら両者で違ってますし」「WindowsとWSL2もいろいろすごく頑張ってるのはわかるんですけど」「ファイルアクセスはもうしょうがない」


つっつきボイス:「なるほど、例のrack-mini-profiler↓をproduction環境で動かすべきと」「APMツールでうんと詳細な情報を取ろうと思ったらrack-mini-profilerを動かしておかないと取れませんし」

MiniProfiler/rack-mini-profiler - GitHub

「rack-mini-profilerをすべてのインスタンスで動かすと全体のパフォーマンスが落ちてしまうので、全体のうち数パーセントのインスタンスだけでrack-mini-profilerを走らせてそこだけAPMで取るというのもよく行われますね」「なるほど!」「New Relicが高いので💸、5つのインスタンスのうち1つだけで取るなんてことも割とやりますし」

参考: APM(アプリケーション性能管理)ツール7選 | ニーズが高まる理由・重要性を解説 - アプリケーション性能管理 | ボクシルマガジン


前編は以上です。

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

週刊Railsウォッチ(20200825後編)Rubyクラスライブラリをgem化、Rubyテストフレームワークrr、ChromebookでWindowsが動くほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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