- Ruby / Rails関連
週刊Railsウォッチ(20210112前編)Active Recordの範囲指定バリデーション改善、soleとfind_sole_byメソッド、AlgoliaとRailsほか
こんにちは、hachi8833です。今年も週刊Railsウォッチをよろしくお願いします🎍🙇。
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)
⚓Rails: 先週の改修(Rails公式ニュースより)
12/25以降のコミットリストのうち、Changelogに記載されているものから見繕いました。
⚓ button_to
が常にHTMLの<button>
をレンダリングするようになった
ActionView::Helpers::UrlHelper#button_to
の第1引数やブロックでコンテンツを渡さない場合も「常に」<button>
要素をレンダリングするように変更された。
<%= button_to "Delete", post_path(@post), method: :delete %>
<%# => <form method="/posts/1"><input type="_method" value="delete"><button type="submit">Delete</button></form>
<%= button_to post_path(@post), method: :delete do %>
Delete
<% end %>
<%# => <form method="/posts/1"><input type="_method" value="delete"><button type="submit">Delete</button></form>
Sean Doyle, Dusan Orlovic
changelogより大意
つっつきボイス:「今まで<input type="submit" />
で作っていたボタンが<button>
タグになるのね」「<input type="submit" />
で作るボタン、懐かしい👴」「今まで<button>
じゃない部分が残ってたとは知らなかった」「button_to
メソッド、使ったことなかったかも」「button_to
なんてメソッドがあったんですね、今度使ってみよう」
「もしかするとbutton_to
はあまり使われてなかったのかもしれませんが、もしふんだんに使っている人がJavaScriptとボタンを連携させていたりしたらHTMLタグが変わるのでbreaking changeになるかもしれませんね」「あ、たしかに」「button_to
をオーバーライドすればいいと思います」「どんなにマイナーな機能でも使っている人がいる可能性はあると思った方がよいでしょうね」
「<button>
タグがある今、ボタンを作るのに<input type="submit" />
を使うこともあまりやらなくなりましたよね」「Webの歴史を感じてしまいました」「若い人だと<input type="submit" />
でボタンを作れること自体知らないかも」「そういう時代になったんですね...」
⚓ バリデーションのnumericality
にrange..
で値を渡せるようになった
数値バリデーション(パーセント値など)の範囲指定方法を簡潔にするプルリク。
validates :percentage, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
# ↓
validates :percentage, numericality: { in: 0..100 }
length: { in: x..y }
バリデーションがインライン化される。
同PRより大意
つっつきボイス:「お〜なるほど、in:
でrangeリテラル..
を使って書けるようになったのか!」「これいいじゃないですか!」「これはよい👍」
「今までなかったのがちょっと不思議なぐらいですね」「自分も今までgreater_than_or_equal_to: 0, less_than_or_equal_to: 100
みたいに書いてたけど、言われてみれば..
で書ける方が楽ですよね」「greater_than_or_equal_to
って長い...」「長い長い」
「かといってgteq
とかlteq
みたいに詰めるのもちょっと考えてしまう」「Perlはeq
とか使う文化ですね」「Bashも-gt
とか-eq
とか使います」「-ge
とか-le
もあった」「やっぱり詰めると読みづらいですよね...」「やむを得ずPerlのソースを読むことになったときに最初に戸惑ったのがその辺の表記でした😢」「Perlのように歴史の長い言語だと、前方互換のために後から導入する記号のやりくりで苦心しがちですよね」
参考: Perl - Wikipedia
⚓ ActiveModel::Name
の初期化でlocaleを渡せるようになった
概要
とある理由のため、モデル名の複数形化を言語に合わせた形で行いたいと思った。調べてみるとpluralize
メソッドはlocale
を引数に取れるが、それをActiveModel::Name
の初期化に渡す方法がなかった。
同PRより大意
# activemodel/test/cases/naming_test.rb#161
class NamingWithSuppliedLocaleTest < ActiveModel::TestCase
def setup
ActiveSupport::Inflector.inflections(:cs) do |inflect|
inflect.plural(/(e)l$/i, '\1lé')
end
@model_name = ActiveModel::Name.new(Blog::Post, nil, "Uzivatel", :cs)
end
def test_singular
assert_equal "uzivatel", @model_name.singular
end
def test_plural
assert_equal "uzivatelé", @model_name.plural
end
end
つっつきボイス:「ActiveModel::Name.new
にロケールを渡せるようになった」「単数形や複数形は言語によっていろいろ違っていますね」
「↓以下のように元々pluralize
にはlocale
を渡せるようになっていて、ActiveModel::Name.new
がそれに対応してなかったのをできるようにしたようですね: これができるようになったときのことをちょっと覚えてます」「その下で単数形用のsingularlize
にもロケールを渡すようになってる」
# activemodel/lib/active_model/naming.rb#L170
@unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
@klass = klass
@singular = _singularize(@name)
- @plural = ActiveSupport::Inflector.pluralize(@singular)
+ @plural = ActiveSupport::Inflector.pluralize(@singular, locale)
@element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
@human = ActiveSupport::Inflector.humanize(@element)
@collection = ActiveSupport::Inflector.tableize(@name)
@param_key = (namespace ? _singularize(@unnamespaced) : @singular)
@i18n_key = @name.underscore.to_sym
- @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
- @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
+ @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key, locale) : @plural.dup)
+ @singular_route_key = ActiveSupport::Inflector.singularize(@route_key, locale)
@route_key << "_index" if @plural == @singular
「pluralize
したものやsingularlize
したものをさらにconstantize
することもあると思いますけど、そうやってできたクラス名にUzivatelé
みたいな名前が出てきたらちょっとびっくりしそう...」
⚓ 新機能: sole
とfind_sole_by
つっつきボイス:「sole
と言っても靴の裏のことじゃなくて、solely(単独で)のsoleだそうです」「どことなくラテン語風味を感じる言葉」「たしかにsolelyって単語ありますね」「onlyやaloneあたりを固い言葉で言い換えるときに使う印象あります」「たしかに技術ブログではたまに見ますね」「話し言葉ではあまり聞かない印象もあります」
「で、このsole
はレコードが1個しかない場合だけそれを返すメソッドのようです」「該当レコードが複数あったらどうなるんだろう?」「複数の場合や1件もない場合はエラーにするそうです」「あ、ActiveRecord::SoleRecordExceeded
っていうのがそれですね」
レコードが1個きっかり存在することを調べたりアサーションしたりする
FinderMethods#sole
および#find_sole_by
を追加。
用途としては、レコードを1行だけ取りたいが、その条件にマッチするレコートが他に複数存在しないこともアサーションしたい場合(特にデータベースの制約が不十分だったりちゃんと効いてない場合)。同Changelogより大意
「プルリクメッセージを見ると、Django(PythonベースのWebフレームワーク)にはそういうメソッドがあるということみたい↓」
参考: Proposal + patch: FinderMethods#only! for asserting there's only one result row - rubyonrails-core - Ruby on Rails Discussions
参考: Django - Wikipedia
「Railsのfind_by
で結果が複数ある場合はどうなるんだったかな...普段そういう使い方してないからな〜」「私もしません😆」「find_by
は1件目だけを返すとAPIドキュメントにありますね↓」
Finds the first record matching the specified conditions.
api.rubyonrails.orgより
「条件に合致するレコードが2件以上あったり1件もなかったりしてはいけない場合にこれらのメソッドが使えるということのようですね」「find_by
で実はレコードが複数ある場合を排除したいときとか」「アサーションを書くのに便利そう」「こういうコードを書くのはテストコードが多いでしょうね」「レコードが1件しかあってはならないことをこうやってメソッドで明示するのは好きです」
「find_sole_by
か...このメソッド名でいいのかな?」「プルリクメッセージを見ると、find_sole_by
というメソッド名について議論されてますね」「あれ、Changelogにはfind_by_sole
って書かれてる↓」「これはドキュメントの変更漏れかな?」「ホントだ」「来週のウォッチ公開日にもし修正されてなかったら、Railsにプルリクするチャンスですね」
# 同Changelogより
Product.where(["price = %?", price]).sole
# => ActiveRecord::RecordNotFound (if no Product with given price)
# => #<Product ...> (if one Product with given price)
# => ActiveRecord::SoleRecordExceeded (if more than one Product with given price)
user.api_keys.find_by_sole(key: key) # 編集部注: find_by_soleは修正前の誤りです
# as above
「メソッド名の議論を見るとこんなコメントがあった↓」「find_sole_by
の方がsoleで探し方を示していてよさそうだということですね」「DHHがfind_sole_by
かsole_first
がいいと言ってる」「こういうやりとりの間にfind_by_sole
がドキュメントに残ってしまったのかもしれませんね」
May I propose a slight tweak:
find_sole_by
. "Find solely by" seems to describe the way to find, rather than what to find. For example, "I tracked him down solely by date of birth" means "I used only his date of birth to track him down", rather than "He was the only person with that date of birth."
#40768のコメント(by jonathanhefner)より
なお、その後調べてみると、現時点でのRailsのmasterブランチ↓では既に修正済みでした(2717b08)。
Product.where(["price = %?", price]).sole
# => ActiveRecord::RecordNotFound (if no Product with given price)
# => #<Product ...> (if one Product with given price)
# => ActiveRecord::SoleRecordExceeded (if more than one Product with given price)
user.api_keys.find_sole_by(key: key)
# as above
「find_sole_by
なら昔からあるfind_by_XXX
な書き方と取り違えられずに済むでしょうから、それもあってこの名前にしたのかもしれませんね」
- API: query_attribute.rb
その後調べると、古い動的ファインダーメソッドの一部はRails 4.0で以下のgemに切り出されたようです↓。
また、RuboCopのRailsスタイルガイド↓ではfind_by_XXX
のような書き方は警告されると社内で教わりました🙇。
参考: rubocop-hq/rails-style-guide: A community-driven Ruby on Rails style guide
⚓ ActiveRecord::AttributeMethods::Query
のgetterメソッドをオーバーライドできるようにした
# 同PRより
# 修正前
class User
def admin
false # getterをオーバーライドして常にfalseを返すようにする
end
end
user = User.first
user.update(admin: true)
user.admin # false (getterのオーバーライドによる期待どおりの結果)
user.admin? # true (DBカラムの値が返った: 期待どおりでない)
修正後は
user.admin?
が期待どおりfalseを返すようになる。
同PRより大意
つっつきボイス:「ああ、やりたいことはわかりました: こういうことはあまりやって欲しくない気持ちがありますが」「どういう改修でしょうか?」「Userテーブルにadminカラムがあるときに、Active Recordが生成するadmin
メソッドを上のようにオーバーライドして、たとえば常にfalseを返すようにするというのは、たまに見かける書き方ではあります」「わかります😆」「で、admin
メソッドはオーバーライドできるけど、admin?
メソッドがオーバーライドされてなくてデータベースの値を読み込んで返していた、それをオーバーライドされるように改修したということですね」「なるほど!」
オーバーライド(override)
Ruby では上位クラスや include したモジュールで定義されているメソッドを再定義することを「オーバーライドする」という。オーバーライドしたメソッドからはsuper
によって元のメソッドを呼び出すことができる。
Ruby用語集 (Ruby 3.0.0 リファレンスマニュアル)より
「これが欲しい気持ちはわかるんですけど、admin
をオーバーライドしたときにadmin?
もオーバーライドする機能って果たして必要なんだろうかって思う気持ちもありますね」「自分もこれは要らない気がします...」
「むしろwarningを出して欲しいですよね」「たしかに!」「『admin
がオーバーライドされたけど、admin?
はオーバーライドされてないよ』という具合に」「現場ではその方が嬉しいかも」「知らないうちにadmin?
もオーバーライドされることで何か起きるんじゃないかとちょっと心配」「たぶん自分は使わないかな」
「RuboCopが注意してくれるといいかも」「言われてみれば、attributeを直接オーバーライドするメソッドの警告はRuboCopにありそうですね」
後でrubocop-railsを探してみましたが、Active Recordの組み込みメソッドを直接オーバーライドしたときの警告は見つかったものの、それ以外の警告は見つけられませんでした。
参考: Rails/ActiveRecordOverride
Rails Cops - A RuboCop extension focused on enforcing Rails best practices and coding conventions.
⚓Rails
⚓ rbs_railsでRailsアプリにSteepを導入
つっつきボイス:「昨年末のRuby 3.0リリースイベントの↓中でこの記事の話題が出ていたことで知りました」「Pockeさんは最近RBSとRails関連の記事をいろいろ書いてますね」
参考: Ruby 3.0 release event - connpass -- 終了
「Pockeさんのこの記事ぐらい新しければ大丈夫ですけど、RubyのRBSや型推論周りはここ数か月でだいぶ動いたので、ちょっと前の記事だともう現状に追いついてないでしょうね」「あ、たしかに」「少なくともRuby 3.0リリース後の記事を見つけるようにしたいですね: もちろんRBSは今後も変わるかもしれませんが、正式リリース後はそうそう破壊的な変更にならないだろうと予想しています」「steepやsorbetは今後も変わるのかな?」
⚓ activerecord-importの:on_duplicate_key_ignore
オプション
つっつきボイス:「@kamipoさんの記事1本目です」「activerecord-import gem自体はお馴染みのものですけど、@kamipoさんの記事にはおぉ〜っと思いましたね」
「記事を読んでて、MySQLでそんなことができるとは、そう言えばどこかで聞いたような、と思ったのがINSERT IGNORE
でした」「おぉ?」「MySQLにはDB制約を無効にする機能があって、セッションレベルで無効にすることもできれば、この記事のようにINSERT IGNORE
を使ってそのINSERT文だけで制約をオフにすることもできます」「へ〜!」
参考: MySQL :: MySQL 8.0 Reference Manual :: 13.2.6 INSERT Statement
「これはデータベースへのインポートでよく使われる機能で、外部キーが相互参照しているようなデータをmysqldump
すると、インポートするときに制約を無効にする必要が生じることがあるんですよ」「なるほど!」「制約を無効にしないと整合性が『タマゴが先かニワトリが先か』のような状態になってしまうので、バッチでデータをインポートするときなどにこの機能が必要になります」「今思えばINSERT IGNORE
どこかで使ったことあったかも」
「そんなときはRails 6.0からinsert_all
↓というそれ用のメソッドがあるからそっちを使ってねというお話で締めくくられています」「記事の『MySQLチョットデキル』、こんなこと自分も言ってみたいです〜」「この言い回しカッコいいですよね」
⚓ @kamipoさんのツイートより
Rails 6.1ではorを重ねてもネストが深くならなくなりました🆕 https://t.co/JaaedR3ebS
— Ryuta Kamizono (@kamipo) December 20, 2020
つっつきボイス:「こちらも@kamipoさんのツイートです」「そうそう、Rails 6.1からこのあたりのクエリがちょっとキレイになったんですよね: それまではor
を重ねていくとwhere
文のネストがものすごく深くなって#39032のようにStack level too deep
になったりしてた」「こういうのを修正する@kamipoさんはやっぱりすごい人」
⚓ その他Rails
つっつきボイス:「アルゴリア!」「これ何でしたっけ?」「いい感じのインクリメンタル検索を実現するAPIサービスですね」「あ、思い出しました」「TechRachoでもAlgoliaの作者インタビューを翻訳したことあります↓」「インクリメンタル検索をやりたいときには便利ですよね」
「AlgoliaはよくAPIドキュメントサイトなどで使われてますね」「そうそう、単純なインクリメンタル検索じゃなくて、部分一致の複数検索とか重要度の高いものから上に出すとか」「記事をざっと眺めた限りでは、Railsで特殊なことをあまりせずにAlgoliaを使えるような感じですね」
以下はAlgolia自身のインクリメンタルなAPIドキュメントサイトですが、他にもAlgoliaを使っているドキュメントサイトを見かけます。
参考: REST API | API Reference | Algolia Documentation
「ところで記事の中に出ているAutocomplete.jsってjQueryじゃなかったっけ?」「あ、そうかも」「記事のサンプルコードの書き方↓がどことなくjQueryの匂いを感じたけどやっぱりjQueryみたい」
「特に上の1行目の('#search-input', { hint: true }
とか、下から3行目の.on
あたりにjQueryフレーバーを感じました↑」「あ〜わかる気がします」
「見つけたこのリポジトリ↑がalgolia/autocomplete.jsになっているから、この記事のはAlgoliaが出しているAutocomplete.jsの方か」「AlgoliaのAutocomplete.jsもやっぱりjQueryでした」
前編は以上です。
バックナンバー(2020年度第4四半期)
週刊Railsウォッチ(20201222後編)TypeProfプレイグラウンド、Ruby 3リリースイベント、Ruby 3は3倍速くなったかほか
週刊Railsウォッチ(20201221前編)aws-sdk-rails gemの機能をチェック、RubyWorld Conference 2020のDHHインタビューほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。