- Ruby / Rails関連
週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編)
こんにちは、hachi8833です。供給そんなにヤバいのかしら。
多方面から『お前が言うとネタかマジかわからんw』と突っ込まれ...勿論サイカノのコピペで8割ネタです。が、半導体に限らず部品全般が買えないのはまじです。納期50週とかザラ。結果偽物も結構出回ってて、身近な所での被害も発生。アニメのように世界は終わりませんが、来年もこの状態は続くでしょう
— 和蓮和尚 (@warenosyo) October 18, 2021
つっつきボイス:「電子部品の他に鉄も値上がりしてると聞いてますね」「あ〜」「給湯器の値上がりが著しいとか」「新型MacBook、部品のあるうちに買っとくのがいいのかな...」「Appleはそれなりに部品の流通を確保していると思いますけど、どれかが滞ったら詰まったりして」「欲しいときに買うのが一番」
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下の公式情報から見繕いました。
- 更新情報: Auto timestamps on bulk inserts, HTML safe translations in controllers and more | Riding Rails
🔗 insert_all
やupsert_all
でタイムスタンプを自動更新するオプションが追加
このプルリクは、
insert_all
またはupsert_all
(および関連するメソッド)でレコードが作成された場合にタイムスタンプのカラム(created_at
、created_on
、updated_at
、updated_on
)を自動設定するオプションを提供する。現時点では、これらのカラムを確実に設定するクリーンな方法は、カラム自体にデフォルトを設定するか、さもなければ属性として明示的に渡したうえでさらに既存レコードのcreated_at
を上書きしないようon_duplicate
のSQLをカスタマイズするしかなかった。
同PRより
つっつきボイス:「insert_all
やupsert_all
のタイムスタンプって自動更新なのでは?と思いましたけど、このプルリクが出たということは今までは自動更新じゃなかったんですね」「record_timestamps:
オプションをtrue
にすればinsert_all
やupsert_all
でタイムスタンプが自動更新されるようになったようです↓」
# activerecord/lib/active_record/insert_all.rb#L10
- def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+ def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil, record_timestamps: nil)
raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
@model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
@on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+ @record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps
「record_timestamps
はデフォルトがnil
か」「false
を明示的に設定すると今まで通りになるんですね」
# activerecord/test/cases/insert_all_test.rb#L410
+ def test_upsert_all_does_not_implicitly_set_timestamps_on_create_when_model_record_timestamps_is_true_but_overridden
+ with_record_timestamps(Ship, true) do
+ Ship.upsert_all [{ id: 101, name: "RSS Boaty McBoatface" }], record_timestamps: false
+
+ ship = Ship.find(101)
+ assert_nil ship.created_at
+ assert_nil ship.created_on
+ assert_nil ship.updated_at
+ assert_nil ship.updated_on
+ end
+ end
🔗 コントローラの_html
サフィックスの挙動を修正
つっつきボイス:「これはi18n関連ですね」「以下のようにキーに_html
というサフィックスを追加するとhtml_safe?
がtrueになってそのままビューに出力される機能は前からありますね」「ウォッチでも何度か話題になりました(ウォッチ20180723)」「う、知らなかった」「以下のhello
はエスケープされるけど、hello_html
はhtml_safe?
がtrue、つまりサニタイズ済みとして扱われる↓というものです」「なるほど〜」
# actionpack/test/abstract/translation_test.rb#L20
translation: {
index: {
foo: "bar",
+ hello: "<a>Hello World</a>",
+ hello_html: "<a>Hello World</a>",
+ interpolated_html: "<a>Hello %{word}</a>",
+ nested: { html: "<a>nested</a>" }
},
no_action: "no_action_tr",
},
参考: 4.4 安全なHTML変換 -- Rails 国際化 (i18n) API - Railsガイド
「そして今回のプルリクを見ると、今まではコントローラとビューで_html
サフィックスの挙動が違っていたらしい」「え、コントローラでも使えるんですか?」
これは#27872をやり直したもの。
html_safe
への変換の一般的な動作を、Active Supportのprivateなモジュールに抽出するコミットを追加して、Action ViewとAction Packで異なっている挙動を合わせ忘れることのないようにした。これにより、#39989で実現されたメモリ節約の一部が元に戻される(Action Viewの実装ではチェックが必要な可能性のあるキーごとに
html_safe
オプションをビルドするので)。Action Viewのループ内だけでオブジェクトのアロケーションをメモ化する方法が見つからなかったので、これについては妥協することにした。メモリを節約する方法についてアイデアがあれば求む。
同PRより
「AbstractController
が改修されているので↓、コントローラの実装はビューと別だったみたい: ActiveSupport::HtmlSafeTranslation.translate
に切り出して共通化したことで修正したようですね」「なるほど」「この機能をコントローラで使ったことはなかったな〜」
# actionpack/lib/abstract_controller/translation.rb
# frozen_string_literal: true
+require "active_support/html_safe_translation"
+
module AbstractController
module Translation
mattr_accessor :raise_on_missing_translations, default: false
...
i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
- I18n.translate(key, **options, raise: i18n_raise)
+
+ ActiveSupport::HtmlSafeTranslation.translate(key, **options, raise: i18n_raise)
end
alias :t :translate
# Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
def localize(object, **options)
I18n.localize(object, **options)
end
alias :l :localize
end
end
🔗 ArelにFILTER句のサポートを追加
つっつきボイス:「PostgreSQLとSQLite3の場合にfilter
メソッドが使えるようになった」「MySQLはサポートされてないのか残念」
機能リクエストの多かったrails/arel#518を再度オープンした(rails/arel#460にもある)。
以下のRubyコードを書くことで、
Model.all.pluck(
Arel.star.count.as('records_total').to_sql,
Arel.star.count.filter(Model.arel_table[:some_column].not_eq(nil)).as('records_filtered').to_sql,
)
以下のSQLが出力されるようになる。
SELECT
COUNT(*) AS records_total
COUNT(*) FILTER (WHERE some_column IS NOT NULL) AS records_filtered
FROM models
サポート対象はPostgreSQL 9.4以降(2014年12月、リリースノート)とSQLite 3.30以降(2019年10月、リリースノート)
参考:
「FILTER構文って何でしょう?」「上のModern SQLサイトによると、以下のように集計関数に条件を指定できるとありますね: 集計関数をSELECT文で条件付けするよりも簡潔に書けそう」「お〜!」
# modern-sql.comより
SUM(<expression>) FILTER(WHERE <condition>)
「以下みたいにCASE WHENでもやれますけど↓、条件が増えてくるとどんどん行数が増えてしまう」「それは読みづらそう...」「filter
メソッドはそういうときに便利でしょうね👍」
# modern-sql.comより
SUM(CASE WHEN <condition> THEN <expression> END)
🔗 ビューのplain textモードの箇条書きを改善
- PR: Better ActionText plain text output for nested lists by swanson · Pull Request #37976 · rails/rails
つっつきボイス:「to_plain_text
の改善だそうです」「箇条書きがネストしたときの書式を改善したのね」「そもそもto_plain_text
というメソッドがあったことを知らなかった」「plain textモードを使う人って少なそうですけど、いるんでしょうね」
# 同PRより
• Item 0
• Item 1
• Item A
1. Item i
2. Item ii
• Item B
• Item i
• Item 2
「今実装を見てますけど、" " * (depth - 1)
とか"\n#{text}"
のあたりが何というか生々しいですね」「自分でゴリゴリ実装したときのような感じが出てる」
# actiontext/lib/action_text/plain_text_conversion.rb#L93
+ def indentation_for_li_node(node)
+ depth = list_node_depth_for_node(node)
+ if depth > 1
+ " " * (depth - 1)
+ end
+ end
+
+ def list_node_depth_for_node(node)
+ node.ancestors.map(&:name).grep(/^[uo]l$/).count
+ end
+
+ def break_if_nested_list(node, text)
+ if list_node_depth_for_node(node) > 0
+ "\n#{text}"
+ else
+ text
+ end
+ end
🔗 CSRF防止戦略のカスタマイズをサポート
概要
このプルリクは、protect_from_forgery
でのカスタムCSRF防止戦略を渡すサポートをAPIドキュメントで公式に追加する。
その他
現在のRailsは、CSRF保護戦略のカスタマイズを偶然サポートしている。protection_method_class
にはクラスまたはシンボルオブジェクトを渡せるし、.to_s.classify
呼び出しも両方で同じように振る舞う。
そこで@rafaelfrancaに相談した結果、このメソッドの振る舞いを変更してcase
/when
文で既存のCSRF防止戦略を明示的に返すようにし、かつ早期リターンによってクラスを戦略として渡せるようにした。
同PRより
つっつきボイス:「protection_method_class
でCSRF防止の挙動を変えられるようにしたんですね: csrf-token
の生成ポリシーを修正したいことはあるかもしれないので」「どんなときに変更したいんでしょうか?」「CSRFトークンの生成をRailsサーバー側以外で行いたい時とかかなあ」「なるほど」「たぶん自分でカスタマイズすると相当複雑になると思いますが」
参考: 3 クロスサイトリクエストフォージェリ (CSRF) -- Rails セキュリティガイド - Railsガイド
🔗 has_secure_password
利用時にpassword = nil
しても値が残る問題を修正
- PR: clear secure password cache if password is set to `nil` by doits · Pull Request #43378 · rails/rails
# 更新情報より
user.password = 'something'
user.password = nil
# before:
user.password # => 'something'
# now:
user.password # => nil
つっつきボイス:「Active Modelのpassword=
セッターでnil
を代入してもpassword
リーダーで読み出すと消えていなかったのが修正されたのね」「これは修正しないといけないヤツ」「修正はinstance_variable_set
を1行追加しただけなんですね↓」
# activemodel/lib/active_model/secure_password.rb#L95
define_method("#{attribute}=") do |unencrypted_password|
if unencrypted_password.nil?
+ instance_variable_set("@#{attribute}", nil)
self.public_send("#{attribute}_digest=", nil)
elsif !unencrypted_password.empty?
instance_variable_set("@#{attribute}", unencrypted_password)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
🔗Rails
🔗 Zeitwerkにアップグレードした話(Ruby Weeklyより)
つっつきボイス:「RailsアプリのオートローダーをZeitwerkにアップグレードしたときのノウハウ記事です」「ファイルやクラスのリネームも発生したのか」「今どきはたいていZeitwerkになっていると思いますけど、アプリが大きいと後からZeitwerkに乗り換えるのは大変でしょうね」
🔗 RailsにSorbetを導入
以下はつっつき後に見つけたツイートです。
Stripe が開発してる Ruby 型チェッカの Sorbet,LLVM ベースの Ahead-Of-Time コンパイラ実装を公開してる.型注釈を書いた Ruby コードをネイティブに事前コンパイルできる.よくある質問と実装概要 | 'Sorbet Compiler: An experimental, ahead-of-time compiler for Ruby' https://t.co/qkNeot4bTR
— ドッグ (@Linda_pp) July 31, 2021
つっつきボイス:「freee会計などを手掛けているfreeeがRubyの静的型チェッカーSorbetを導入した記事です」「記事でやっているようにビジネスロジックを中心に少しづつ型を追加していくのがよさそうですね」「SorbeとYARDを両方書くのは大変、たしかに」
「Sorbetを使っている会社のリストを見ると、開発元のStripe以外にShopifyなども使ってますね」「以前から型注釈を欲しいと思っていた会社が使っているんでしょうね」「SorbetはLanguage Server Protocolに対応しているのがありがたい」
参考: Official page for Language Server Protocol
先週土曜日のKaigi on Rails 2021のクロージングキーノートでも、RafaelさんがShopifyでSorbetを導入したことを話していましたね。
参考: Keynote by Rafael França - Kaigi on Rails 2021
🔗 Arel入門
つっつきボイス:「ArelはActive Record内部のクエリ生成APIですね」「Arel職人を目指す記事なのかな」「arel_table
とかを見ると昔のトラウマが😅」
# 同記事より
Organization.where(
Organization.arel_table[:id].in(
Comment.where(
Comment.arel_table[:user_id].eq(user.id)
).distinct.pluck(:organization_id)
)
)
参考: ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs
「Arelを使うとこんなふうに書ける↓」「gt(2)
はgreater than 2なんですね」「自分は普通にwhere
とプレースホルダ?
で書きますけど、事情によってはArelで書くこともたまにあります」
# 同記事より
users[:id].in([1,2,3]).to_sql
=> "`users`.`id` IN (1, 2, 3)"
users[:id].gt(2).to_sql
=> "`users`.`id` > 2"
users[:id].eq(3).to_sql
=> "`users`.`id` = 3"
「Arelでjoin
が絡んだりコンポジションしたりするうちにだんだん複雑になりがち」「そうそう」「Arelはありがたい存在だけど、毎回Arelで書く気にはなれないな〜」
# 同記事より
users.join(photos, Arel::Nodes::OuterJoin).on(users[:id].eq(photos[:user_id]))
「BPSだとkazzさんがよくArelを使ってたかも」「Railsで汎用的なモジュールを書こうとするとArelが必要になってくることがあるんですよ」「なるほど」
追記: 今週金曜日の銀座Rails#38で、@osyoさんが『AST を使って ActiveRecord の where の条件式をブロックで記述しよう』というタイトルでお話しされるそうです。
来週10/29(金)19時~ の銀座Rails#38 のプログラムを公開しました!発表登録がまだの方はお早めにどうぞ🙇♂️https://t.co/Bdo7zb6J7Y
— 銀座Rails (@GinzaRails) October 23, 2021
🔗 Railsのビジネスロジックを「contexts」で整理(Ruby Weeklyより)
つっつきボイス:「また新しめのパターン」「contextという言葉のメタ度が高くてどうとでも解釈できそうな感じ」
「contextは、Elixir言語で動くPhoenixフレームワーク↓が由来と記事に書かれていました」「Elixir、知らない世界」「Elixirは見た目がRubyに似てて、型が書けるそうです」
phoenix framework - create rich, interactive experiences across browsers, native mobile apps, and embedded devices with real-time streaminghttps://t.co/r1gRNw9LO0https://t.co/2n7HFwExFS pic.twitter.com/sIMDdfqwIa
— Christopher Ackerman (@cackerman1) July 27, 2019
参考: Elixir (プログラミング言語) - Wikipedia
「app/contexts/
の下にファイルを作って、そこにビジネスロジックを置くスタイルなんですね」「見た感じでは、GoF本で言うところのFacade(ファサード)を普通にcontextsに置いている感じかな🤔」
# app/contexts/accounts.rb
module Accounts
def self.active_users
User.all.active
end
def self.account_details(id)
account = Account.find(id)
# ...
end
end
# app/contexts/accounting.rb
module Accounting
def self.create_invoice
# business logic magic
end
end
参考: ギャング・オブ・フォー (情報工学) - Wikipedia -- GoF
「ElixirのフォーラムにcontextsとFacadeのことが書かれている↓: まさにFacadeパターンですね」
「記事では、Service Objectを使いたくないのでcontextにしたそうです」「Service Objectはクラスがやたらと増える傾向があるので、Facadeパターンを使うのはわかる: 自分もその方が好みです」「たしかに」
「そういえばService Objectに置くのはたいていFacadeか、もうひとつ何かのパターンのどっちかだと以前おっしゃってましたね」「もうひとつはCommandパターンですね: RailsでService ObjectというとこのCommandパターンを使ったものを指すことが多いようです」「なるほど」「Commandパターンだと基本的に1クラス=1機能になるけど、Facadeパターンはそこにこだわらない感じ」
🔗 GitLabコメント欄にmermaid構文でグラフを書く
つっつきボイス:「GitLabのコメント欄でmermaidというグラフ生成構文を使ってグラフを生成できるそうです」「元記事を見るとGitLab 10.3と随分昔からあるようなので、最近これを発見したのかも」「PlantUMLも使えるのね」
参考: mermaid - Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.
参考: PlantUML: シンプルなテキストファイルで UML が書ける、オープンソースのツール
# docs.gitlab.comより: mermaidの例
graph TB
SubGraph1 --> SubGraph1Flow
subgraph "SubGraph 1 Flow"
SubGraph1Flow(SubNode 1)
SubGraph1Flow -- Choice1 --> DoChoice1
SubGraph1Flow -- Choice2 --> DoChoice2
end
subgraph "Main Graph"
Node1[Node 1] --> Node2[Node 2]
Node2 --> SubGraph1[Jump to SubGraph1]
SubGraph1 --> FinalThing[Final Thing]
end
「コメント欄でちょっぴりグラフを書くのにいいのかも」「自分はDraw.ioを開いてスクショを貼る方が早いかな」「それもそうですね」
後で調べると、draw.ioドメインはセキュリティ上の理由でdiagrams.netドメインに移行していました。
参考: diagrams.net
参考: Blog - Open source diagramming is moving to diagrams.net, slowly
前編は以上です。
バックナンバー(2021年度第4四半期)
週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)
- 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ウォッチタグ)