Tech Racho エンジニアの「?」を「!」に。
  • 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" />でボタンを作れること自体知らないかも」「そういう時代になったんですね...」

参考: <BUTTON> -HTMLタグリファレンス

バリデーションの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éみたいな名前が出てきたらちょっとびっくりしそう...」

新機能: solefind_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_bysole_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な書き方と取り違えられずに済むでしょうから、それもあってこの名前にしたのかもしれませんね」

その後調べると、古い動的ファインダーメソッドの一部はRails 4.0で以下のgemに切り出されたようです↓。

rails/activerecord-deprecated_finders - GitHub

また、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/rbs_rails - GitHub

「Pockeさんのこの記事ぐらい新しければ大丈夫ですけど、RubyのRBSや型推論周りはここ数か月でだいぶ動いたので、ちょっと前の記事だともう現状に追いついてないでしょうね」「あ、たしかに」「少なくともRuby 3.0リリース後の記事を見つけるようにしたいですね: もちろんRBSは今後も変わるかもしれませんが、正式リリース後はそうそう破壊的な変更にならないだろうと予想しています」「steepやsorbetは今後も変わるのかな?」

soutaro/steep - GitHub

sorbet/sorbet - GitHub

activerecord-importの:on_duplicate_key_ignoreオプション


つっつきボイス:「@kamipoさんの記事1本目です」「activerecord-import gem自体はお馴染みのものですけど、@kamipoさんの記事にはおぉ〜っと思いましたね」

zdennis/activerecord-import - GitHub

「記事を読んでて、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さんのツイートより


つっつきボイス:「こちらも@kamipoさんのツイートです」「そうそう、Rails 6.1からこのあたりのクエリがちょっとキレイになったんですよね: それまではorを重ねていくとwhere文のネストがものすごく深くなって#39032のようにStack level too deepになったりしてた」「こういうのを修正する@kamipoさんはやっぱりすごい人」

その他Rails


つっつきボイス:「アルゴリア!」「これ何でしたっけ?」「いい感じのインクリメンタル検索を実現するAPIサービスですね」「あ、思い出しました」「TechRachoでもAlgoliaの作者インタビューを翻訳したことあります↓」「インクリメンタル検索をやりたいときには便利ですよね」

インタビュー: 超高速リアルタイム検索APIサービス「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 - GitHub

「見つけたこのリポジトリ↑がalgolia/autocomplete.jsになっているから、この記事のはAlgoliaが出しているAutocomplete.jsの方か」「AlgoliaのAutocomplete.jsもやっぱりjQueryでした」

参考: jQuery - Wikipedia


前編は以上です。

バックナンバー(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など)です。

Rails公式ニュース


CONTACT

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