- Ruby / Rails関連
週刊Railsウォッチ: Kaigi on Rails発表「Simplicity on Rails」を見るほか(20231107)
こんにちは、hachi8833です。Kaigi on Railsの動画が正式に公開されました🎉。
Kaigi on Rails 2023の発表アーカイブを公開しました。
当日に見逃したトーク、もう一度聞きたいあのトークも、ぜひゆっくりご覧ください! 💻 #kaigionrailshttps://t.co/N4EX6ERVut— Kaigi on Rails (@kaigionrails) November 3, 2023
発表資料まとめ記事を再録します↓。現時点では1本を除いて全スライドが公開されています。まとめありがとうございます!
参考: 【Kaigi on Rails 2023】発表資料まとめ #Rails - Qiita
🔗Rails: 先週の改修(Rails公式ニュースより)
参考: 7.1.2 Milestone(10件)
参考: 7.2.0 Milestone(8件)
なお、マイルストーンの#49371を見ていて、GitHubのプルリクでファイルリストのURLに?w=1
を付けると、インデントしか違いのないdiffを除外してくれることを知りました。
参考: GitHub の Pull Request で出ている差分のスペースやタブを無視して見やすくする方法
🔗 ActiveSupport::Callbacks
のメモリ使用量を削減
従来、サブクラスと共有されているコールバックは、
Filters::Before
やFilters::After
で生成されたプロシージャをサブクラス間で共有していなかった。しかもこれは遅延生成されていたので、アプリケーションの起動後にメモリ増加が発生する (Copy on Writeを介してワーカー間で共有できない)。原因は、コールバックの呼び出しに使われるオブジェクトのリビルドを
CallbackChain#compile
経由で行っていたため。そのため、コールバックチェインに違いが生じるとすべてのコールバックproc
がリビルドされることになる。このコミットは、before系コールバックやafter系コールバック(ただしbeforeとafter両方で呼ばれるコールバックは含めない)が、それが定義された場所のあらゆるサブクラスで共有されるようにする。これは、
Filters::Before
とFilters::After
を(proc
を生成するのではなく)call
に応答する素のクラスに変更することで実現する(厳密には必ずしもそうしなければならないわけではないが、この方が実装が楽であり、よりシンプルでメモリ消費の少ないオブジェクトも得られるようもなる)。これらのオブジェクトは参照を回避し、特定のコールバックシーケンスに結び付けられることで、メモ化可能かつ再利用可能になる。これが最も大きく影響するのは、コントローラが多数あり、かつ
ApplicationController
などで多数のコールバックが使われているアプリケーション。また、この機会に、生成されるさまざまな形式のproc(
halting
、halting_and_conditional
、conditional
、simple
)をif
文で1つの形式に統一した。以前行われた特殊化は、さほどパフォーマンスが向上しない。今回は、呼び出し可能なフィルタオブジェクトをeagerにビルドするようにはしなかったが、今後のフォローアップで行う価値がありそう。
たぶん@byrootに喜んでもらえそう😊
再現方法
これは自己完結型アプリになっていて、作成した100個のコントローラはすべて、50個のbefore系コールバックを持つ
ApplicationController
から継承している(この数はGitHubにある最大級アプリに匹敵する)。個別のコントローラにリクエストを投げると、スタック全体を通過する。このアプリでライブオブジェクトとRSSを観察してみて、次のことがわかった。benchmark_controller_callback.rb
mainブランチの場合:
$ be ruby benchmark_controller_callback.rb live objects before: 252506 RSS before: 70044 live objects after: 594812 RSS after: 126052 object delta: 342306 RSS delta: 56008
このブランチの場合:
$ be ruby benchmark_controller_callback.rb live objects before: 252517 RSS before: 70036 live objects after: 339032 RSS after: 97756 object delta: 86515 RSS delta: 27720
アプリ起動からリクエスト配信後までのメモリ増加が半分程度にまで下がった(残りのほとんどはコールキャッシュだと思う)。
同PRより
参考: Rails API ActiveSupport::Callbacks
参考: コピーオンライト - Wikipedia
つっつきボイス:「これは最適化ですね」「親コントローラにコールバックがたくさんある場合の子クラスでのコールバックでメモリ使用量を削減したということかな: たしかに子のコントローラごとにコールバックのオブジェクトが生成されてたらメモリがもったいない」「改修後のオブジェクト数増加もかなり減っていますね」
「修正されたファイルはActive Supportのcallbacks.rbだけなのか!」「コールバック処理がここに凝縮されているからこのファイルだけ修正すればいいということでしょうね: diffを見るとほとんど全体を書き換えてますけど」「ほんとだ、ほぼ作り直し」「こういうコールバック処理そのものはそんなに複雑にならないはずですし、改修がこの範囲で収まったのもそのおかげでしょうね」
「ちなみにコールバックが使えるおかげでコントローラとかのコードをかなりきれいに書けるので便利: もちろんやりすぎ注意ですが」
🔗 enum
でカラムのない属性のサポートが復活
- PR: Support non-column-backed attributes for
enum
by jonathanhefner · Pull Request #49769 · rails/rails
#45734のフォローアップ。
このプルリクによって、
enum
属性名のタイポを引き続き防ぎながら、カラムのない属性をenum
で利用可能にするサポートが再度追加される。カラムのない属性をenum
で使う場合は、以下のように事前にその属性で型を明示的に宣言しなければならない。class Post < ActiveRecord::Base attribute :topic, :string enum topic: %i[science tech engineering math] end
修正: #49717
同PRより
つっつきボイス:「このプルリクの前に#45734が2022年にマージされていて、データベースカラムのないenumを定義するとタイポとみなしてraiseするようになっていたそうです」「ほ〜、データベースカラムのないenum属性が定義できなくなっていたけど、サポートが再度追加されたとあるので、それを復活させたということですね」「その代わりに、そういう属性を定義するときはattribute :topic, :string
みたいに型を明示的に指定する必要がある、なるほど」
🔗 number_to_human_size
で負数を扱えるよう修正
動機/背景
このプルリクを作成した理由は、
number_to_human_size
で負の数値を扱えなかったため。# 従来の振る舞い helper.number_to_human_size(-1234567) # => "-1234567 Bytes" # 新しい振る舞い helper.number_to_human_size(-1234567) # => "-1.18 MB"
詳細
number_to_human
メソッドは既に負の数値をサポートしているので、このプルリクではそれを使う2つのメソッドが修正される。
同PRより
つっつきボイス:「プルリクメッセージには書かれていませんでしたが、振る舞いが修正されたのはAction Viewのnumber_to_human_size
と、Active Supportの#to_fs(:human_size)
でした」「#to_fs
はto_formatted_s
のエイリアス」「たしかto_s
に引数を渡すのは非推奨化されてて、to_fs
でやるようになってましたね(ウォッチ20221025)」「abs
で絶対値を取るシンプルな修正↓」
# activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb#L44
def exponent
max = STORAGE_UNITS.size - 1
- exp = (Math.log(number) / Math.log(base)).to_i
+ exp = (Math.log(number.abs) / Math.log(base)).to_i
exp = max if exp > max # avoid overflow for the highest unit
exp
end
def smaller_than_base?
- number.to_i < base
+ number.to_i.abs < base
end
参考: § 10.4.2 to_fs
-- Active Support コア拡張機能 - Railsガイド
🔗 マルチDBのrollback/up/downで指定していないDBスキーマをダンプしないよう修正
現在の
rails db:migrate:primary
(およびrails db:up:primary
やrails db:down:primary
)は、必要ない場合にも全データベースのスキーマをダンプしていた。修正: #49351(問題についてはここの議論を参照)
同PRより
つっつきボイス:「issue #49351を以下に抜き出してみました↓」
再現手順
新規Railsアプリを作成する。
rails new --minimal rails-rollback-bug cd rails-rollback-bug
database.yml
のdevelopment
セクションを以下で置き換える。development: primary: <<: *default database: db/primary.sqlite3 secondary: <<: *default database: db/secondary.sqlite3
ロールバックタスクを実行する。
rails db:rollback:primary
このタスクが
primary
データベースだけに対して実行されるはずだったにもかかわらず、両方のデータベースでスキーマがダンプされていることを確認する。ls db | grep schema schema.rb secondary_schema.rb # これがあってはいけない
期待される振る舞い
primaryデータベースのスキーマだけがダンプされるべき、つまりschema.rbファイルだけが作成されるべき。
実際の振る舞い
2つのデータベースのスキーマが両方ともダンプされる、つまりschema.rbとsecondary_schema.rbファイルが作成される。同PRより
「rakeファイルの修正を見ると、3箇所で名前が渡されてなかったのね↓」「普通にバグでしたね」
# activerecord/lib/active_record/railties/databases.rake#L207
- db_namespace["_dump"].invoke
+ db_namespace["_dump:#{name}"].invoke
🔗 PostgreSQLのmoney型へのキャストが特定の値でエラーになる問題を修正
説明はコミットメッセージの方に書かれていました。なおこの修正は7-1-stableブランチと7-0-stableブランチにもバックポートされました。
ガード内の
/^-?\D*+[\d.]+,\d{2}$/
という正規表現は、後続のgsub!
とマッチすることが保証されない(例:"3,50"
をキャストする場合)。
つまりgsub!
がnil
を返す可能性があり、その場合、チェインされているsub!
呼び出しがNoMethodError
エラーを発生する可能性がある。このコミットは、この呼び出しチェインを分割する。また、このようなケースでは正規表現は不要なので、
gsub!
をdelete!
に、sub!
をtr!
にそれぞれ置き換える。共著: Jonathan Hefner jonathan@hefner.pro
同PRのcommit bcd3589より
# activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb#L16
def cast_value(value)
return value unless ::String === value
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
# Negative values are represented as follows:
# (3) -$2.55
# (4) ($2.55)
value = value.sub(/^\((.+)\)$/, '-\1') # (4)
case value
when /^-?\D*+[\d,]+\.\d{2}$/ # (1)
- value.gsub!(/[^-\d.]/, "")
+ value.delete!("^-0-9.")
when /^-?\D*+[\d.]+,\d{2}$/ # (2)
- value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
+ value.delete!("^-0-9,")
+ value.tr!(",", ".")
end
super(value)
end
# activerecord/test/cases/adapters/postgresql/money_test.rb#L55
def test_money_type_cast
- type = PostgresqlMoney.type_for_attribute("wealth")
- assert_equal(12345678.12, type.cast(+"$12,345,678.12"))
- assert_equal(12345678.12, type.cast(+"$12.345.678,12"))
- assert_equal(12345678.12, type.cast(+"12,345,678.12"))
- assert_equal(12345678.12, type.cast(+"12.345.678,12"))
- assert_equal(-1.15, type.cast(+"-$1.15"))
- assert_equal(-2.25, type.cast(+"($2.25)"))
- assert_equal(-1.15, type.cast(+"-1.15"))
- assert_equal(-2.25, type.cast(+"(2.25)"))
+
+ {
+ "12,345,678.12" => 12345678.12,
+ "12.345.678,12" => 12345678.12,
+ "0.12" => 0.12,
+ "0,12" => 0.12,
+ }.each do |string, number|
+ assert_equal number, type.cast(string)
+ assert_equal number, type.cast("$#{string}")
+
+ assert_equal(-number, type.cast("-#{string}"))
+ assert_equal(-number, type.cast("-$#{string}"))
+
+ assert_equal(-number, type.cast("(#{string})"))
+ assert_equal(-number, type.cast("($#{string})"))
+ end
参考: String#gsub!
(Ruby 3.2 リファレンスマニュアル)
参考: String#delete!
(Ruby 3.2 リファレンスマニュアル)
参考: String#tr!
(Ruby 3.2 リファレンスマニュアル)
つっつきボイス:「通貨の値がたとえば"3,50"
というフランス式表記(後述)になっているとエラーになっていた」「修正前だとgsub!
とsub!
がチェインされていたけど、これだと"3,50"
はgsub!(/[^-\d,]/, "")
で何も置き換えられないのでgsub!
がnil
を返し、そのnil
に対してsub!
を行うとNoMethodErrorになっていたんですね」
「修正はチェインをやめて2行に分ける形で行ったのね」「実際、修正前のチェインを手元で以下のように2行に分けてみると正常に動作しました↓」
value = "3,50"
value.gsub!(/[^-\d,]/, "")
value.sub!(/,/, ".") #=> "3.50"
「それとは別にgsub!
をdelete!
に、sub!
をtr!
にそれぞれ置き換えていますけど、これはバグとは無関係で、単に正規表現を使わなくても書ける高速なメソッドに置き換えたということですね(正規表現だと遅くなりがちなので): ちなみに修正前後の[^-\d.]
と^-0-9.
、[^-\d,]
と^-0-9,
は、書き方が違うだけで内容は同じです」
「以前ローカライズの仕事をやっていたときに知ったんですが、通貨の(に限らず)ピリオドとカンマの書き方って国によって違っているんですよ」
参考: 小数点 - Wikipedia
「たとえばテストデータの1番目の12,345,678.12
は日本でも使われている記法で、小数点はピリオド.
で桁区切りはカンマ,
というイギリス式なんですけど、2番目の12.345.678,12
という数値はフランス式の記法です」「あ〜、そういえばフランスでは書き方が違うみたいな話があった気がする」「はいそうなんです: 実はヨーロッパの一部の国(後で調べるとフランス、ドイツ、イタリア、スペイン、ロシア)ではピリオドとカンマが入れ替わっているんですよ」「これは面倒くさいヤツ...」
「さらにフランスでは、小数以下の桁も0.168 588 478
みたいにnbsp(ノーブレークスペース)で3桁ずつ区切ることになっています」「数学記号や記法が国によって違う、あるあるですね」「数値のパースは以前やったことありますけど、なかなか言う事聞いてくれなかったな〜」
「ところで話は逸れますけど、自分たちが小学校のときは、桁区切りは一万円を1,0000円
みたいに4桁区切りで習った覚えがあるんですよ」「あ、私もそうでした!」「えぇ、それ知らない...」「日本の貨幣の単位だと4桁区切りの方が万とか億とかと合うからわかりやすかったのに」「それがいつの間にか3桁区切りになってて戸惑った」「3桁区切りは英語で読む場合はthousandとかmillionと合うからわかりやすいんですけどね」「昭和のどこかの時点で教育現場が4桁区切りから3桁区切りに移行したんですかね」「当時は自分の知らないところでいつの間にか変更された気分でした」「もしかするとデータ交換との兼ね合いで3桁区切りになったのかも?」
後で調べてみましたが、学習指導要領が当時変わったのかどうかはわからずじまいでした。
どっちでもいいんだけど、昭和40年代に小学校で教わったのは間違いなく四桁区切り
.@ito3com さんの「最近の学校ではカンマを3桁づつじゃなく4桁づつと教えてるらしい。えっ!!」https://t.co/MHFKzqb99C をお気に入りにしました。— あ〜る菊池誠(反緊縮)公式 (@kikumaco) October 24, 2018
🔗 BroadcastLogger#deep_dup
を追加
動機/背景
このプルリクを作成した理由は、自分のアプリで
BroadcastLogger
のインスタンスを手軽に複製したかったため。単にdup
を呼んでもbroadcasts
が複製されないのでよくない。なのでdeep_dup
が使えてもよいのではと思っている。詳細
このプルリクは、
ActiveSupport::BroadcastLogger
に#deep_dup
を追加する。deep_dup
を呼ぶとブロードキャストロガーが複製され、個別のブロードキャストをイテレートして複製する。同PRより
つっつきボイス:「BroadcastLogger
はRails 7.1リリース直前に駆け込みで追加された機能ですね(ウォッチ20231011)」「dup
だといわゆるshallow copyになってしまうやつですね」「deep_dup
欲しいから足した、わかりやすい」
# activesupport/lib/active_support/broadcast_logger.rb#L221
+ def initialize_copy(other)
+ @broadcasts = []
+ @progname = other.progname.dup
+ @formatter = other.formatter.dup
+
+ broadcast_to(*other.broadcasts.map(&:dup))
+ end
🔗 レコードの第2主キー以降が空文字列""
の場合にurl_helpers
が無効なURLを生成する問題を修正
修正: #49695(議論についてはこちらを参照)
同PRより
つっつきボイス:「これだけだとわからないので、リンク先のissue #49695にある再現手順の最後を取り出してみました」
期待される振る舞い
生成されるURLは、複合主キーの一部が空であっても正しく処理されるべき。空のキーをサポートしないのは設計上の選択かもしれないが、少なくともURLヘルパーでは無効なURLを警告すべき。
実際の振る舞い
マージされた複合主キーを含む生成URLは、第2主キーが空だと常に404エラーを返す。
#49695より抜粋
「URLヘルパーは、url_for
とか、ルーティングのリソースから動的に生えてくる何とか_path
などのことでしょうね」「ここで言う空(blank)というのは何を指すのかな?」「テストコードを見ると空文字列""
ですね↓」
# actionpack/test/controller/parameters/accessors_test.rb#L425
test "#extract_value splits param by delimiter" do
params = ActionController::Parameters.new(
id: "1_123",
- tags: "ruby,rails,web"
+ tags: "ruby,rails,web",
+ blank_tags: ",ruby,,rails,"
)
assert_equal(["1", "123"], params.extract_value(:id))
+ assert_equal(["ruby", "rails", "web"], params.extract_value(:tags, delimiter: ","))
+ assert_equal(["", "ruby", "", "rails", ""], params.extract_value(:blank_tags, delimiter: ","))
assert_nil(params.extract_value(:non_existent_key))
end
🔗 ActiveRecord::Persistence.delete
に主キーとして無効な値を渡した場合にクエリを発行しないよう修正
ActiveRecord::Persistence.delete
メソッドに何らかの無効な主キーが渡されると、Active Recordは本来不要な場合であっても以下のようにDELETE
を実行してしまう。irb(main):001> User.delete([]) User Delete All (0.6ms) DELETE FROM "users" WHERE 1=0 => 0
つまり、よく行われているように
User.delete(ids) if ids.compact.any?
やUser.delete(id) if id
のように呼び出しの前後に条件を追加したり、あるいは条件を付け忘れたりすると、不要なクエリが実行されていた。しかしユーザーからこの重荷を取り除くことは可能。
以下は最近の実例:しかし
NULL
は果たして有効な値なのだろうか、実際には無視しても大丈夫なのだろうか?自分がチェックしたところ、主キーはNULL
にできないことがわかった。ANSI標準でもそうなっているし、PostgreSQL、MySQL、Microsoft SQL Server、Oracle Serverもこれに準拠している。例外はSQLiteで、ドキュメントによると「歴史的な理由により、いくつかの条件が満たされる場合を除いて
NULL
許容になる」(そうした条件の中には、明示的なNOT NULL
や、テーブルをSTRICT
モードで作成した場合などがある: 詳しくはリンク先のドキュメントを参照)。しかしRailsでは明示的にNOT NULL
を設定している。| | primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL", |
そして#45346では
strict
モードが設定されている。つまり、nil
は#delete
に提供する値として安全だと思う。同PRより
つっつきボイス:「最初見たときに、無効な主キーを渡すと実際にレコードが削除されていたのかと思ったけど、WHERE 1=0
という絶対成立しない条件が設定されているから、DELETEは発行するけど何も行われてなかったということなんですね」「何もしないクエリだから害はありませんけど、発行するのは無駄なので、主キーが無効な場合はそもそもクエリを発行しないように早期脱出を追加したということですね」「delete
は削除した件数を返すから、キーが無効なら0
を返して終了、なるほど」
# activerecord/lib/active_record/persistence.rb#L565
def delete(id_or_array)
+ return 0 if id_or_array.nil? || (id_or_array.is_a?(Array) && id_or_array.empty?)
+
delete_by(primary_key => id_or_array)
end
「ところで、コード例のUser.delete([])
を見てて思い出したんですけど、空配列だとループを回さない、つまり何もしないというメソッドってたまにありますよね: そういうメソッド呼び出しの後ろにif !ary.empty?
みたいなのを書いてあって、それ空だったら何もしないヤツだからif
文いらないでしょっていうコードを人生で3回ぐらい見かけたことがあります」「あ〜、ありそう」
🔗 @rails/ujs
を7.1.0にしたときの読み込みエラーを修正
- PR: Fix rails-ujs auto start() in bundled environments by skipkayhil · Pull Request #49668 · rails/rails
これは7-1-stableにもバックポートされました(#49773)。
動機/背景
@rails/ujs
を7.1.0以上にアップグレードすると以下のエラーが発生するという問題が最近持ち上がっている。Uncaught Error: rails-ujs has already been loaded!
一般に、この問題はrails-ujsの振る舞いが「純粋なsprockets環境(JSを追加する)」と「バンドル環境(webpackやesbuildなど)」とで異なるために発生する。Sprockets環境では
start()
を自動的に呼び出すことが前提になっているが、バンドル環境ではユーザーがそれらをimport
してstart()
を呼び出すことが要求される。CoffeeScriptからJavaScriptへの移行の一環として、現在の環境が(JS)バンドラー環境であるかどうかを検出する条件が追加され、sprocketsの場合にのみ
start()
を呼び出すようになっている。しかし、この条件は期待通りに動かない。自分はimportmapとesbuildでrails-ujsを使った場合にこのエラーを再現することに成功した。興味深いことに、webpackを(JS)バンドラーにした場合はこの問題は発生しなかった。詳細
修正方法としては、条件をうんと明確にすることがベストだと思う。sprocketsユーザーはesm(ESモジュール)バージョンのrails-ujsを使うべきではないので、rollupのreplaceプラグインを使ってesmバンドルでの
start()
自動呼び出しを常にオプトアウトできる。これが機能する理由は、terserがreplaceプラグインより後に実行され、条件全体が(true == false
のような)デッドコードとして削除されるからである。追加情報
自分は以下のアプリを使ってsprockets、importmap、esbuild、webpackをテストした。
cc @tricknotes: この問題で作業してもらったので共著者に加える
cc @Earlopain: これをテストしてもらえると非常に助かる
同PRより
つっつきボイス:「CTOのbabaさんもRailsを7.1にアップグレードしたときにこのエラーを見たそうです」「rails-ujsの振る舞いがSprocketsの場合とバンドルの場合で違うの、ややこしそう」「esbuildとかrollupとかwebpackとか、フロントエンド関連は登場するものが多くて大変」「terserって何だろうと思ったらJavaScriptのminifyツールなんですね」
terser: terse(そっけない、簡素な)の比較級
参考: Terser
🔗Rails
🔗 特別企画: Kaigi on Railsの@moroさんの発表を見る
つっつきボイス:「今日はちょっと趣向を変えて、Kaigi on Rails 2023で大評判だった@moroさんこと諸橋さんの発表(参加レポート記事)を少し早回しで視聴してみたいと思います」「ほほ〜、これは楽しみ」「Railsを長くやっている人ほど絶賛していたので、強い人の感想を詳しく聞いてみたいと思います」(しばらく視聴)
「これはいい発表!👍: この内容が、"え、どうしてこんな当たり前の話をするの?"と思われるぐらいの世界になってくれたら理想ですね」「こういう設計が普通になる世界!」「もちろん簡単にはいかないでしょうけど」
「このE/R図↓ではconferences
テーブルとusers
テーブルを中間テーブルを介して多対多のリレーションにしていて、それ自体はリレーショナルデータベースではよく行われることなんですが、何も考えずに中間テーブルを作ると、中間テーブルの名前がregistrations
じゃなくて単なるconferences_users
になっちゃうんですよ」「あ、たしかにRailsのhas_and_belongs_to_many
(HABTM)でやるとテーブル名はそうなりますね」
参考: やさしい図解で学ぶ ER図 表記法一覧 #Rails - Qiita
「そうではなくて、中間テーブルにregistrations
という意味のある名前を明示的に付けて概念を与える、そしてこれは"コト"としてリソースにできる: この考え方はとてもよくできていると思います」
「その一方で、このように抽象化するのは、特に最初のうちは実はなかなか勇気が要るんですよ」「自分がやろうとすると、本当にこんなふうに設計していいんだろうかという気持ちになりそうです」「この"コト"が要件に潜んでいることに、最初はなかなか気づきにくいんですね」
「Webアプリは何もリレーショナルデータベースをそのままRESTの形にするのが仕事ではありませんし、conferences_users
もregistrations
も、それだけならWebアプリの画面には直接出てこないものなので、なおさら気づきにくい: 要件からこういう"コト"をリソースとして見い出して使いこなせるようになったら、設計の語彙と表現力がとても豊かになると思います、素晴らしい」「見えにくいコトをうまく見い出せたらきっと快感でしょうね」
参考: Representational State Transfer - Wikipedia -- REST
「RESTを真面目に設計していれば、どこかでこういうふうにコトをリソースとして扱うようになるはずなんですよ: 逆に"動く機能がありさえすればいい"とばかりに、モデルも作らずにコントローラのアクションを生やして機能を書きまくっていると、肥大化してfatコントローラになってしまう昔ながらのアンチパターンになる」
「そうやってfatコントローラにコードを押し込めてしまうと再利用性もなくなるので、後でお客さんから"この機能を他の場所でも使いたい"というリクエストが来たときに詰む: 業務開発でそこまで極端なことはそうありませんけど、それに近いことは割りと起きがちですね」
「スライド↓にあるConferenceController
に#register
アクションを定義してしまうの、つい自分もやってしまいそう...」「そうそう、こんなふうに自分が書いているものがRESTじゃなくなったときに、"あれ、これでいいんだろうか?"と立ち止まれるかどうかが大事」「ルーティングのリソースにpost :register
みたいなのを書いたら負け、と」「もちろん何が何でもRESTにしなければならないという話ではないんですが、まずはRESTで自然に書く方法があるんじゃないかと十分考えてからにするのが大事」
「そういえば私はこういうのを"縛りプレイ"とよく呼んでいますね: Railsでいいとされていることしかやらない、野良アクションを生やさない、みたいに、RESTという縛りの中でどこまで自然に抽象化できるかを頑張っていると、コントローラがほぼscaffoldテンプレートと変わらないものになったりする」「野良アクションを生やさない、なるほど」「みんな縛りプレイしないのかな、もっとやればいいのにって思ったりします😆」
「RESTという概念にはこういう"コト"とか"イベント"の概念も含められるんですね: RESTわかってなかった...」「RESTはリレーショナルデータベースと1対1対応する必要はまったくありませんし、極端に言えばRESTはデータベースとつながっていなくてもいいんですよ: @moroさんが引き合いに出していた、ログインやログアウトを"セッションのcreate
やdelete
で表す"↓というのがまさにそれで、自分も大好きですし、いい例えだと思います」
「逆に言えば、RailsはRESTから外れても書けるので、コントローラにアクションを書いてルーティングを足せばそれだけで動くんですよ」「それもそうですね、だからこそ小さなアプリなら初心者がとりあえず作って動かせる」
「でもそれだけだと、ともすれば@moroさんの発表で説明しているような"コトの抽出"みたいなことを見落としたり忘れたりしがちなんですね: そして特に業務アプリを開発するうえでは、こういう"コトの抽出"のような部分こそがRailsの設計のキモなんだということを、ぜひ多くの人に知っていただきたいですね」
「そういう話がとてもシンプルにまとまっていて、本当にいいスライド👍」「話の持って行き方もうまいですよね」「キーノートスピーチにしてもいいくらい」「英語字幕付けて海外で普及させてもいいかも」
「こういう発表を若いうちに見られる人が本当にうらやましいですね: このあたりの概念を早いうちに把握して設計の練習を重ねれば、コードのコピペに頼らない、まっとうなRailsエンジニアとして成長できると思います」
「この設計を体現した実際のRailsアプリのコードを見てみたいです」「たぶん有名どころのオープンソースRailsアプリなら大体そうなっているんじゃないかな: 少なくともfatコントローラのような悪いことはしてないと思います」
「has_many :through
とHABTMの使い所の違いもよくわかって嬉しいです」「has_many :through
などのthrough系リレーションは自分も大好きで使いまくっています: よくできていますし、チェインした1個先や2個先のリソースがどんどん手に入るようになるのが楽しくてしょうがないですヨ」
参考: §2.8 has_many :through
とhas_and_belongs_to_many
のどちらを選ぶか -- Active Record の関連付け - Railsガイド
「こういう設計を理解して会得するにはやっぱり練習が必要でしょうね」「そうそう、特に業務アプリだと最初は勇気も要りますね」「業務で最初にそういう良質な設計のコードに触れる機会があると、アプリの構造もわかりやすいし、設計の学びも大きいでしょうね」
「理解や練習ももちろん大事ですけど、こういう設計についてどしどし話をしたり質問ができるような場があることが一番重要なんじゃないかなって、聞いてて思いましたね」「あ〜、たしかに!」「こういうものを一人で理解するのは大変ですし、一人で理解するものでもない気がするんですよ: Registrationみたいな概念を最初にたった一人で見つけられる気がしないし、それこそ議論の賜物じゃないかと思うんですよ」「わかる」「私は自分が慣れ親しんでいた大学の研究室のゼミのような場を思い浮かべたんですけど、勉強会でもいいし、つっつき会のような雑談の場でもいいので、そういう場があるといいですよね」
スライドで@moroさんが学んだという書籍を挙げておきます↓。
今週は以上です。
バックナンバー(2023年度第4四半期)
- 20231024前編 7.1アップグレードガイドにActive Record暗号化設定の注意事項が追加ほか
- 20231018後編 Kaigi on Rails 2023関連イベント情報公開、複合主キーのlocality解説記事ほか
- 20231017前編 Active Storageのしくみを詳しく解説するDiscussion投稿ほか
- 20231011 Rails 7.1.0リリース、YARPがprismにリネームほか
- 20131004 Rails 7.1.0.rc1と7.1.0.rc2がリリース、SQLite3コンフィグの最適化ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)