Quantcast
Channel: Active Recordの記事一覧|TechRacho by BPS株式会社
Viewing all 68 articles
Browse latest View live

Rails: where.firstとfind_byの違いを知る(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: 週刊Railsウォッチ20220328 where.firstfind_by

Rails: where.firstとfind_byの違いを知る(翻訳)

Active RecordのようなORM(Object-Relational Mapper)でSQLを生成することには多くのメリットがあります。明確で再利用しやすい抽象化を手に入れられるので、時間も節約でき、可読性も向上します。

ただし、ORMの便利な抽象化機能そのものが思わぬ結果をもたらすこともあります。

データベースサーバー上で実行される「実際の」SQLはフレームワークが生成するので、気をつけておかないとたちまちパフォーマンスが低下してしまいます。

私たちは最近CoverageBookの案件でまさにこの問題を踏みました。

以下のように書くのではなく

where条件に続けてfirstを書く。

User.where(email: "andy@goodscary.com").first

訳注

この書き方はRuboCop RailsのFindByでも警告されます。

以下のように書くこと

find_byを使う。

User.find_by(email: "andy@goodscary.com")
User.find_by_email("andy@goodscary.com")

訳注

2番目のfind_by_emailのような動的なfind_by_*メソッドによる書き方は古い方法であり、RuboCop RailsのDynamicFindByでも警告されます(ただしRails 7でも一応使えます)。

そうする理由

これは、ORM(およびその周辺)に邪魔されて思わぬパフォーマンス問題をひきおこす事例のひとつです。

.whereのスコープには、主キーに対する暗黙のORDERスコープが隠れており、一見しただけではわかりません。

User.where(email: "andy@goodscary.com")
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"

User.where(email: "andy@goodscary.com").first
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
# ORDER BY "users"."id" ASC
# LIMIT 1

User.find_by(email: "andy@goodscary.com")
# SELECT "users".*
# FROM "users"
# WHERE "users"."email" = "andy@goodscary.com"
# LIMIT 1

この複雑なケースでは、データベースにインデックスがあっても無力でした。クエリでインデックスが使われていたのですが、.where().firstを使っていたのが原因で、ソート順を確定するためにインデックスを用いないスキャンが意図せず発生し、パフォーマンスが大きく低下してしまいました。

さらに書き込みが1秒あたり数千件に達しており、たった1件のレコードを取り出すだけでソートが発生していたので、データベースが極めて強力であったにもかかわらずこの問題が発生していました。

クエリを実行中の.find_by.where().firstの結果に対して.to_sqlを呼び出すことができず、生成されたSQLを正確に知るにはログ出力に頼るしかなかったので、この問題のデバッグに手こずりました。

一見同じようなことをやっていそうなメソッドであっても、Active RecordがどんなSQLを生成しているかを正確に知っておくのは「とても」重要です。

そうしない理由があるとすれば

負荷の少ない小規模なテーブルであれば、where().firstのパフォーマンス低下は無視できるでしょう。

関連記事

Railsのdefault_scopeは使うな、絶対(翻訳)

The post Rails: where.firstとfind_byの違いを知る(翻訳) first appeared on TechRacho.


Rails: JOINしたActiveRecord::Relationをlimitして件数が合わない場合の対処法

$
0
0

環境

  • Ruby: 3.0.2
  • Rails: 6.1.4.1
  • MySQL: 5.7

※ Railsのバージョンを上げたりDBMSを変えたりしたときには再現しなかった問題なので、環境依存している話かもしれません。

やりたいこと

あるモデルのレコードを、has_many の関係にあるモデルのうち最新のレコードでorderした上で、limitして件数を絞り込みたいです。

具体例として、チャット管理アプリを作成しているとします。
このアプリでは以下のようなモデルが存在します。

class ChatRoom < ApplicationRecord
  has_many :chat_messages
end

class ChatMessage < ApplicationRecord
  belongs_to :chat_room
end

ここで、チャットメッセージの最新の投稿日時の順番にチャットルームを並び替えて、先頭の10件だけ取得したい、というのが今回の話です。

データは以下のようなものが存在するとします。

chat_rooms

id name
1 ルーム1
2 ルーム2
3 ルーム3
4 ルーム4
5 ルーム5
6 ルーム6
7 ルーム7
8 ルーム8
9 ルーム9
10 ルーム10
11 ルーム11

chat_messages

id chat_room_id created_at
1 1 2022-08-19 20:00:00
2 2 2022-08-19 19:00:00
3 3 2022-08-19 18:00:00
4 4 2022-08-19 17:00:00
5 5 2022-08-19 16:00:00
6 6 2022-08-19 15:00:00
7 7 2022-08-19 14:00:00
8 8 2022-08-19 13:00:00
9 8 2022-08-19 12:00:00
10 8 2022-08-19 11:00:00
11 9 2022-08-19 10:00:00
12 10 2022-08-19 09:00:00
13 11 2022-08-19 08:00:00

ルーム1〜10が順番に取得できると嬉しいですね。

失敗例

以下のようにして取得しようとすると、ルーム1〜8までしか取れません。

class ChatRoomsController < ApplicationController
  def index
    @chat_rooms = ChatRoom.includes(:chat_messages)
                          .order('chat_messages.created_at DESC')
                          .limit(10)
  end
end

実際に発行されているSQLを確認してみます。

SQL (1.1ms)  SELECT DISTINCT chat_messages.created_at AS alias_0, `chat_rooms`.`id` FROM `chat_rooms` LEFT OUTER JOIN `chat_messages` ON `chat_messages`.`chat_room_id` = `chat_rooms`.`id` ORDER BY chat_messages.created_at DESC LIMIT 10
↳ app/views/chat_rooms/index.html.erb:15
SQL (1.3ms)  SELECT `chat_rooms`.`id` AS t0_r0, `chat_rooms`.`name` AS t0_r1, `chat_rooms`.`created_at` AS t0_r2, `chat_rooms`.`updated_at` AS t0_r3, `chat_messages`.`id` AS t1_r0, `chat_messages`.`content` AS t1_r1, `chat_messages`.`chat_room_id` AS t1_r2, `chat_messages`.`created_at` AS t1_r3, `chat_messages`.`updated_at` AS t1_r4 FROM `chat_rooms` LEFT OUTER JOIN `chat_messages` ON `chat_messages`.`chat_room_id` = `chat_rooms`.`id` WHERE `chat_rooms`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 8, 8) ORDER BY chat_messages.created_at DESC

ここで注目したいのは LIMIT 10 がどこに掛かっているかです。
10件に絞り込まれているのは、 chat_rooms ではなく chat_roomschat_messages をLEFT OUTER JOINした結果です。
つまり、以下のようなJOIN結果に対して、上から10件取得し、その10件の chat_rooms.id を使って chat_rooms をSELECTしています。

chat_rooms.id chat_messages.id chat_messages.created_at
1 1 2022-08-19 20:00:00
2 2 2022-08-19 19:00:00
3 3 2022-08-19 18:00:00
4 4 2022-08-19 17:00:00
5 5 2022-08-19 16:00:00
6 6 2022-08-19 15:00:00
7 7 2022-08-19 14:00:00
8 8 2022-08-19 13:00:00
8 9 2022-08-19 12:00:00
8 10 2022-08-19 11:00:00
9 11 2022-08-19 10:00:00
10 12 2022-08-19 09:00:00
11 13 2022-08-19 08:00:00

なので、 chat_rooms.id が1〜8のものしかSELECTされないわけですね。

ちなみに、kaminaripage / per メソッドを使っても同じことが起こります。
というより、kaminariを使っていてこの問題に気づいたのでした。
この記事では説明のため、簡略化して limit を使うようにしています。

対処法

思いつくものが2つあるので順に紹介していきます。

SQLで捌く

これが一番よさそうです。
やり方は色々あると思うのですが、ここでは極値関数を使って chat_messages.created_at を比較することにします。

class ChatRoom < ApplicationRecord
  has_many :chat_messages

  scope :order_by_latest_message, lambda {
    CONDITION = <<~SQL
      chat_messages.created_at >= (SELECT MAX(created_at) 
                                   FROM chat_messages AS latest_chat_messages 
                                   WHERE chat_messages.chat_room_id = latest_chat_messages.chat_room_id 
                                   GROUP BY chat_room_id)
    SQL
    joins(:chat_messages).where(CONDITION).order('chat_messages.created_at DESC')
  }
end
class ChatRoomsController < ApplicationController
  def index
    @chat_rooms = ChatRoom.order_by_latest_message.limit(10)
  end
end

Array化する

10件limitする際にJOIN結果を絞り込もうとするからおかしくなるのであって、ChatRoomをArrayにしてから10件絞り込めば問題は解決します。

class ChatRoomsController < ApplicationController
  def index
    @chat_rooms = ChatRoom.includes(:chat_messages)
                          .order('chat_messages.created_at DESC')
                          .to_a
                          .take(10)
  end
end

ただしこの場合、取得したレコードを全てArrayにするので、件数によっては処理が重くなります。


The post Rails: JOINしたActiveRecord::Relationをlimitして件数が合わない場合の対処法 first appeared on TechRacho.

Rails: Active Recordのコールバックを避けて「Domain Event」を使おう(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

参考: Domain Event
参考: ドメイン イベント: 設計と実装 | Microsoft Docs

  • 2018/08/03: 初版公開
  • 2022/09/27: 更新

Rails: Active Recordのコールバックを避けて「Domain Event」を使おう(翻訳)

最近Marcinの書いた「Railsアプリの最大のコードの臭いはActiveRecordのコールバックである」記事にあるように、コールバックはあっという間に制御不能になってしまう可能性があります。この記事がRedditに投稿された後、コメントで実に興味深いやりとりを見かけました。

重要なモデルがひとつあり、そこにはビジネス上重大なデータがあってめったに変更されることはないが、さまざまなコントローラに散らばった自動化ビジネスロジックによって変更されることがあるとしよう。

このデータが変更されたときに何らかのアラートや通知を送信したいとする(メールとかチャットとか別テーブルにエントリを足すなど)。理由は、非常に重要かつ変更のまれなデータであり、多くの人が変更を知っておくべきだから。

こんなとき次のAとBのどちらを選ぶ?

A: 変更のたびにそのモデルでメールを送信することを許可し、「変更時に通知」というコア機能をカプセル化して、変更が発生する実際の場所に配置する。

B: コントローラ内で指定のモデルファイルが変更されるあらゆる箇所に個別に呼び出しを挿入する。

私ならAを選ぶだろう。こちらの方が将来の変化に強く、しかも的確だからだ。さらに今後プログラマーがエラーを埋めるリスクも軽減でき、「変更時に外部に通知する」という単体の責務を僅かなコストでそのモデルファイルに追加できる。

コメ主の指摘は非常に興味深く、かつ非常に有益です。私自身、数か月前に以下のようなコールバックを使っていました。

class Order < ActiveRecord::Base
  after_commit do |order|
    Resque.enqueue(IndexOrderJob,
      order.id,
      order.shop_id,
      order.buyer_name,
      order.buyer_email,
      order.state,
      order.created_at.utc.iso8601
    )
  end
end

Elasticsearchデータベースのインデックス作成をスケジューリングするために、問題を最速の方法で解決しましたが、私たちのコードベースにそれ以上の改善がもたらされないことは承知の上でした。しかしこのときは、今後このコードを取り除くのに役立つ可能性のある作業を同時に並行してやっていたことも承知していました。

こうしたコールバックには否定し難いメリットがあると同時に、いくつもの問題を抱え込んでしまいます。それについて書きたいと思います。

コールバックを正しく書くのは簡単ではない

上ととてもよく似た、以下のようなコードがあるとします。

class Order < ActiveRecord::Base
  after_save do |order|
    Elasticsearch::Model.client.index(
      id: id,
      body: {
        id:              id.to_s,
        shop_id:         shop_id,
        buyer_name:      buyer_name,
        email:           buyer_email,
        state:           state,
        created_at:      created_at
    })
  end
end

パッと見には何の問題もなさそうですが、もしこのトランザクションがロールバックされると、(手動でオープンした巨大なトランザクションの中でOrderがsaveされて)2番目のデータベースのインデックス化されたステートに誤りが生じるかもしれません。このままにするか、after_commitに切り替える方法が考えられます。

さらに、Elasticsearchで例外が発生すればただちにDBトランザクションもロールバックされるでしょう。それをよしとする考え方(DB不整合が発生せず、ElasticsearchにもSQL DBにも何も残らない)もあれば、よくないとする考え方(重要性の低いDBエラーのせいでユーザーの発注が止まってしまい利益が損なわれる)もあります。

そこで今度はafter_commitに乗り換えてみましょう。この特定のニーズにはこちらの方が合っていそうです。そしてドキュメントには次のように書かれています。

これらのコールバックが他のシステムとのやりとりに有用なのは、データベースのステートが不変な場合にのみ実行されることが保証されるからです。たとえばafter_commitがキャッシュをクリアするフックの置き場所にふさわしい理由は、キャッシュをトランザクション内でクリアすると、データベース更新が完了する前にキャッシュの再生成がトリガされてしまう可能性があるからです。

つまり言い換えると、そうしたフックをサードパーティのシステムやAPIやDBと統合するのであればafter_commitの方がより安全性の高い選択肢となります。副作用がSQL DBにも保存されるのであれば、after_saveafter_updateでも十分です。

class Order < ActiveRecord::Base
  after_commit do |order|
    Elasticsearch::Model.client.index(
      id: id,
      body: {
        id:              id.to_s,
        shop_id:         shop_id,
        buyer_name:      buyer_name,
        email:           buyer_email,
        state:           state,
        created_at:      created_at
    })
  end
end

こうして私たちはafter_commitを使うことを覚えました。

さて、おそらくテストの大半はトランザクションで占められているので、テストはDBトランザクション内で実行するのが最も高速です(そのテストではフックが発火しないため)。現在関心のあるテストがほんの数件しかない場合、これは嬉しい点です。嬉しくないのは、Elasticsearchに保存されているデータが多くのユースケースで必要になる場合です。こうなったら、トランザクションを用いない方式でテストを実行するか、test_after_commit gemを使うか、Rails 5にアップグレードしなければなりません。

歴史的には(レガシーRailsアプリから読み取った範囲では)after_commitコールバックで発生した例外は握りつぶされて単にロガーに出力されます。すべてがコミットされてしまった後では何もできないからです。これはRails 4.2以降で修正されましたが(#14488)、スタックトレースは以前ほど良好ではないかもしれません。

ほとんどの技術的問題への対応方法はいくつもあるので、それらについてひととおり知っておく必要があります。これらの例外は最も困った問題であり、何らかの形で取り扱う必要があります。

コールバックは癒着を増やす

Railsの多くの問題は癒着から来ていると私は直感します。Railsの技術的な層はデフォルトでは不十分です。私たちにあるのはビュー(ビューは本記事と何の関係もありませんが)とコントローラとモデルです。すなわち、操作で副作用をトリガしたいと思った場合、そのコードの置き場所はコントローラかモデルが唯一のデフォルト選択肢となります。コードをどちらに置いてもそれなりに問題があります。

副作用(API呼び出し、キャッシュ、セカンダリDBの統合、メール送信)をコントローラに配置すると、テストを正しく行おうとするときに問題が生じる可能性があります。理由は2つあります。コントローラはHTTPインターフェイスと密結合しているので、副作用をトリガーするにはテストでHTTP層を用いて副作用とやりとりする必要があります。コントローラはフレームワークが管理している領域なので、テストでコントローラをインスタンス化してメソッドを直接呼び出すのは簡単ではありません。

かといって副作用をモデルに配置すると、今度は別の問題が生じます。副作用がハードコードされてしまうので、別途結合テストを(明示的に)用いない限りこのドメインモデルのテストは困難です。つまり「遅いテストで我慢する」か「テストを毎回モックやスタブで塞ぐ」かのどちらかしかありません。

RailsコミュニティでService Objectに関するおびただしい記事が見つかる理由はこれです。アプリが複雑になり始めると、開発者はメール送信やサードパーティAPIへの通知などの関心事を「after save」的な副作用のところに置きたがります。別のコミュニティやアーキテクチャではこの種のコード部品をTransaction Script(訳注: Martin Fawler氏の用語)と呼んだりAppplication/Domain/Infrastructure Service(訳注: ドメイン駆動開発(DDD)の用語)と呼んだりすることがあります。しかしどちらもデフォルトのRailsにはありません。そうした機能を必要とする開発者がこぞってブログ記事やgem(1つや2つではありません)あるいはこの層をしっかり備えている新しいフレームワーク(hanamitrailblazer)に頼ってサービスの「車輪の再発明」に走る理由はここにあります。新しいフレームワークに移行せずにこの層をアプリのコードに導入する方法については弊社のFearless Refactoring bookをぜひご覧ください。本書は、システムに高度な概念を導入する前の重要なステップです。

コールバックはデータ変更の意図がわからなくなる

コールバックが呼び出されればデータが変更されることはわかりますが、変更された理由まではわかりません。ユーザーによる発注のせいなのか、別の処理でPOSを操作した人のせいなのか、それとも支払いが原因か、返金が原因か、キャンセルが原因なのか、知りようがありません。あえてstate属性に基づいて変更をかけるのは、多くの場合アンチパターンです。変更された理由がわからなくても(コールバックで何らかのデータを送信しているという理由で)問題にならないこともありますが、それ以外の場合は問題になる可能性があります。

モバイルからのAPI呼び出しや、Webブラウザから別のエンドポイントを介してUserが登録されたときに、ユーザーに「ようこそメール」を送信したいとしましょう。さらにユーザーがFacebookから登録した場合にも送信したいとします。ただしユーザーをシステムにインポートするときには送信したくない(新しいパートナー企業が顧客を連れてこちらのプラットフォームに移籍することを決めたという理由で)とします。つまりこの4つの状況のうち3つについては(メール送信という)副作用が欲しいのですが、残りの1つではこの副作用は欲しくありません。発生したイベントに対して何を行いたいかという意図がわかる方がよいでしょう。after_createはその目的には向いていません。

Domain Eventとは

Active Recordのコールバックの代わりに私が推奨しているのは、UserRegisteredViaEmailUserJoinedFromFacebookUserImportedOrderPaidといった「Domain Event」をパブリッシュして、イベントに対して応答するハンドラをそこにサブスクライブするという方法です。

この方法ではさまざまなpub/sub gem(whisperなど)を利用できますし、rails_event_store gemを用いてデータベースへの保存や今後のインスペクション/デバッグ/ログ出力なども行えます。

このアプローチについて詳しくお知りになりたい方は、2 years after the first domain event – the Saga patternのスライドや動画をご覧いただけます。Domain Eventのパブリッシュ方法や、それを用いた副作用のトリガ方法について解説しています。このアプローチはActive Recordコールバックの代わりに用いることができます。

今後アプリで何か変更が生じれば必ずイベントがパブリッシュされるようになります。変更が、指定のモデルのどこで生じるのかを探し回ることもありません。変更が生じる場所はすべて明らかだからです。

追伸: Rails 5では更に深刻です

関連記事

Ruby: gemが生成するコードを無名モジュールとprependで動かす(翻訳)

Rails: beforeバリデーションをやめてセッターメソッドにしよう(翻訳)

The post Rails: Active Recordのコールバックを避けて「Domain Event」を使おう(翻訳) first appeared on TechRacho.

Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

  • 2018/04/18: 初版公開
  • 2023/02/14: 更新

Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳)

Railsアプリで、次のようにモデルのデータベーススキーマの内部にまで立ち入っている(コントローラ)コードをよく見かけます。

class Person < ActiveRecord::Base
  enum gender: { male: 1, female: 2 }
end
class PeopleController < ApplicationController
  def index
    @people = Person.where(gender: Person.genders[:male])
                    .where('age >= 18')
                    .where(right_handed: false)

    respond_to(:html)
  end
end

このコードにはいくつか問題点があります。

  • コントローラがモデルのデータベース構造に関して持っている知識が多すぎます。背後の詳細な情報が上位の層に漏れると、背後の構造を変更しにくくなります。
  • メソッド呼び出しをチェインしているので、モックを使ったテストが死ぬほどやりづらくなります。

このような実装の詳細はモデル内にカプセル化しなければなりません。ActiveRecordのスコープの助けを借りて何とかしてみましょう。

class Person < ActiveRecord::Base
  enum gender: { male: 1, female: 2 }

  scope :male,        -> { where(gender: 1) }
  scope :adult,       -> { where('age >= 18') }
  scope :left_handed, -> { where(right_handed: false) }
end
class PeopleController < ApplicationController
  def index
    @people = Person.male.adult.left_handed

    respond_to(:html)
  end
end

生SQLやモデル属性の知識はモデル内にカプセル化されました。これで一件落着…したのでしょうか?

テストの書きやすさはほんの少しだけましになりましたが、異なるスコープを組み合わせる長いメソッドチェインはまだ残っています。コントローラをテストするには、これまでと同様にモック軍団を出動させなければなりません。

class PeopleControllerTest < ActionController::TestCase
  def test_people_index
    adult_finder        = mock
    left_handed_finder  = mock

    Person.expects(:male).returns(adult_finder)
    adult_finder.expects(:adult).returns(left_handed_finder)
    left_handed_finder.expects(:left_handed)

    get :index
    assert_response :success
  end
end

テストコードはexpectsが多く、しかもかなり脆くなっています。たとえテスト対象コードが正常に動作していても、スコープの順序がどこかで変わればテストは失敗してしまいます。

スコープが複雑になると他にも問題が生じることがあります。スコープはいくらでも自由に組み合わせられますが、その組み合わせから正しいSQLが生成されるとは限りません。その組み合わせを全部テストしていたら心が削られてしまいます。

私は、スコープをモデルの外でチェインするのではなく、スコープの組み合わせをモデル内で単一のスコープやクラスメソッドにまとめるのが好みです。これなら処理を可能な限り内部化できますし、データベースクエリの最適化などの作業もずっとやりやすくなります。

class Person < ActiveRecord::Base
  enum gender: { male: 1, female: 2 }

  scope :male,        -> { where(gender: 1) }
  scope :adult,       -> { where('age >= 18') }
  scope :left_handed, -> { where(right_handed: false) }

  class << self
    def left_handed_male_adults
      left_handed.male.adult
    end
  end
end
class PeopleController < ApplicationController
  def index
    @people = Person.left_handed_male_adults

    respond_to(:html)
  end
end

スコープチェインはPerson.left_handed_male_adultsクラスメソッドの内部にラップされています。必要ならこのクラスメソッド自身をスコープとして定義することも可能な点にご注目ください。2つの方法の大きな違いは、スコープがActiveRecordリレーションを返すことを保証するかどうかです。

スコープの組み合わせはぐっとシンプルになり、しかもテストに対して頑丈になります。

class PeopleControllerTest < ActionController::TestCase
  def test_people_index
    Person.expects(:left_handed_male_adults)

    get :index
    assert_response :success
  end
end

関連するモデルの外でスコープをチェインするのを避ければ、コードベースの癒着も減り、それによってメンテナンスやリファクタリングもやりやすくなります。

もちろんあらゆるスコープはpublicなので、このスコープもその気になればチェインできます。話を複雑にしないために、スコープをモデルの外でチェインしたくなる衝動をこらえてください。

関連

Railsのdefault_scopeは使うな、絶対(翻訳)

Rails tips: モデルのクエリをカプセル化する2つの方法(翻訳)

The post Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳) first appeared on TechRacho.

ActiveRecordのtouchを`no_touching`で一時的に無効にする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。


  • 2018/04/18: 初版公開
  • 2023/03/16: 更新

参考: Rails API touchActiveRecord::Persistence
参考: Rails API no_touchingActiveRecord::NoTouching

Rails: Active Recordのtouchをno_touchingで一時的に無効にする(翻訳)

Active Recordモデルでのtouchは多くのRailsアプリで広く使われています。特にキャッシュの無効化で便利です。このメソッドは、デフォルトでは現在時刻でupdated_atタイムスタンプを更新します。以下はモデルでtouchを使う典型例です。

# app/models/photo.rb

class Photo < ApplicationRecord
  belongs_to :user, touch: true
end

新しい写真が作成されたり既存の写真が更新/削除されるたびに、関連付けられているユーザーのupdated_at属性が現在時刻で更新されます。多くの場合これは期待どおりの動作です(これはActive Recordの珍しいコールバックですが、それほど悪いものではありません)が、何らかの理由でtouchしたくないこともあるでしょう。何かいい組み込みメソッドはないものでしょうか?

問題の分析

touchを一時的に無効にするのは、パフォーマンス上の理由(大量のレコードを更新するとき)や、after_touchとかafter_commitの重複実行を防止または回避するうえで便利です。

しかし後者には設計上の問題が潜んでいる可能性があります。理由は、レコードの内部状態を上書きするような副作用を引き起こす重要なロジックをActive Recordのコールバックに配置すると、たちまち地獄行きになるからです(特にメール通知をトリガーする場合)。しかし現実には多くのRailsアプリでコールバックがこのような形で使われてしまっています。

解決方法

ありがたいことに、ブロック内で一時的にtouchを無効にするActiveRecord.no_touchingを利用すれば、大幅なリファクタリングや書き直しは不要です。

あるユーザーと、そのユーザーに属するすべての写真を更新する必要があるとしましょう。そしてすべての写真が更新されてからtouchする必要があるとしましょう。以下のようにできます。

user = User.find(user_id)

ActiveRecord::Base.transaction do
  User.no_touching do
    user.photos.find_each do |photo|
      # userは`touch`されない
      photo.update!(some_attributes)
    end
  end

  user.touch
end

何らかの理由で全モデルのtouchを無効にしたいのであれば、このメソッドをActiveRecord::Baseで呼ぶだけで済みます。

user = User.find(user_id)

ActiveRecord::Base.transaction do
  ActiveRecord::Base.no_touching do
    user.photos.find_each do |photo|
      # どのモデルも`touch`されなくなる
      photo.update!(some_attributes)
    end
  end

  user.touch
end

できあがりです!

まとめ

ActiveRecord.no_touchingは、トリッキーな問題が潜む可能性のある問題をまさにお手軽に解決してくれます。しかしこれはアプリの設計に潜む問題に対するダーティハックでもあり、その問題は遅かれ早かれ正すべきです。

訳注

ActiveRecord.no_touchingのソースはたった1行でした。

# File activerecord/lib/active_record/no_touching.rb, line 22
def no_touching(&block)
  NoTouching.apply_to(self, &block)
end

apply_toは以下でした。

# https://github.com/rails/rails/blob/7c70791470fc517deb7c640bead9f1b47efb5539/activerecord/lib/active_record/no_touching.rb#L28

    class << self
      def apply_to(klass) # :nodoc:
        klasses.push(klass)
        yield
      ensure
        klasses.pop
      end
...

関連記事

Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳)

Railsのdefault_scopeは使うな、絶対(翻訳)

The post ActiveRecordのtouchを`no_touching`で一時的に無効にする(翻訳) first appeared on TechRacho.

Rails: Active Recordモデルのスレッド安全性問題をインスタンス変数で解決する(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails: Active Recordモデルのスレッド安全性問題をインスタンス変数で解決する(翻訳)

原文概要

RailsのActive Recordがあまりにも強力なので、開発者はともすると、Active Recordモデルが内部では普通のRubyオブジェクトにすぎないことを忘れてしまうときがあります。普通のRubyオブジェクトであるがゆえに、標準的なインスタンス変数で振る舞いを適応させることも可能です。本記事では、この手法を解説するとともに、実際に動かせるサンプルコードも提供します。

はじめに

多くのチュートリアルや技術記事では、Railsフレームワークを紹介するときにフレームワークの振る舞いやActiveRecord::BaseクラスのAPIを熱心に解説しています(もっともな話です)。そしてActiveRecord::Baseのサブクラスがビジネスドメインにおけるモデルを表現できること、そしてモデルをデータベースに永続化する自動ORMを提供していることも解説します。
ActiveRecord::Baseは豊富なAPIを提供し(1)、データモデリングに必要となるさまざまな振る舞いをカバーしています。

なお、Active RecordパターンのRailsにおける実装では、このAPIの深みが問題視されることもあります。曰く「この巨大なAPIは単一責任の原則に違反している」「開発者が”ファットモデル”(アプリケーションのあらゆるロジックが不適切にActive Recordモデルに集中する)というアンチパターンに陥りやすい(2)」「いわゆる”神オブジェクト”に結晶化されてしまいがち(4)」といった具合です。ファットモデルというアンチパターンから脱却するために、多くの手法が議論されています(23)。

Active Recordモデルの機能を考察するうえで、Active Recordモデルが単なるRubyオブジェクトに振る舞いを追加したものに過ぎないという点は非常に見過ごされがちです。つまり、Ruby標準のオブジェクト機能を使えばモデルの振る舞いをカスタマイズできるということです。本記事では、オブジェクトのインスタンス変数を用いて成功した例を見ていくことにします。

本記事に登場するコードの完全版はGitHubで参照できます(5)。

ActiveRecordドメインモデル

ここでは、UserクラスとPostクラスがある平凡なブログアプリケーションを考えてみましょう。これらのクラスは以下のように定義されています。

# 属性: :idと:nameのみ
class User < ActiveRecord::Base
end
# 属性: :id、:user_id (FK)、:title、:body
class Post < ActiveRecord::Base
  belongs_to :user

  validates :user, :body, :title, presence: true

  after_create :enrich_body

  def copy
    Post.skip_callback(:create, :after, :enrich_body)
    Post.new(is_copy: true) do |post|
      sleep 1 # マルチスレッドで興味深い振る舞いを観察するためにsleepを追加する
      post.user = self.user
      post.body = self.body
      post.title = self.title
      post.save!
    end
  ensure
    Post.set_callback(:create, :after, :enrich_body)
  end

  private

  def enrich_body
    self.body += "\n著者: #{user.name}"
  end
end

Userモデルは、標準的に継承されるActiveRecord::Baseの振る舞いに依存しており、usersテーブルから推測されたいくつかの属性を持っています。idname属性だけの、特に面白みのないモデルです。

Postモデルはもう少し興味深いものになっていて、Postの著者を表現するUsersのインスタンスと関連付けられています。必須の属性が存在しているかどうかをチェックするバリデーションがいくつか備わっていて、:after_createフックでpostのbody属性に著者名を追加するシンプルな更新も実行します。

ActiveRecord標準の振る舞いに加えて、Postのインスタンスをcopyするカスタムメソッドも実装してあります。

単に元のpostの属性を用いてPostインスタンスを新たに作成すると、この新しいモデルを保存するときに問題が発生します(:after_createフックがもう一度実行されてbodyに著者名が2つ追加されてしまう)。

そこで、この振る舞いを回避するためにcopyメソッドでActiveRecord::Base APIのskip_callbackメソッドを呼び出すことにしました。

すなわち、このコードは、PumaやSidekiqなどのマルチスレッド環境で動かさない限り問題なく動作します。この問題は、以下のようなモデルの単体テストを書くことで再現できます。

require "test_helper"

class PostTest < ActiveSupport::TestCase
  setup do
    @user = User.create!(name: "Domhnall")
    @attrs = {
      user: @user,
      title: "ARインスタンス変数",
      body: "こんなの誰が思いつくだろう"
    }
    @post = Post.new(@attrs)
  end

  ...

  test "copyは新しいPostオブジェクトを返すこと" do
    assert @post.copy.is_a?(Post)
    assert_not_equal @post.copy.id, @post.id
  end

  test "copyはpostのbodyに元と同じものを設定すること" do
    assert_equal @post.copy.body, @post.body
  end

  test "copyはスレッドセーフであること" do
    n=2
    (0...n).map do |i|
      Thread.new do
        puts "Thread #{i}"
        post = Post.new({
          user: @user,
          title: "ARインスタンス変数 #{i}",
          body: "こんなの誰が思いつくだろう #{i}"
        })
        copy = post.copy
        puts copy.body
        assert_equal post.body, copy.body
      end
    end.each(&:join)
  end
end

冒頭の2つのテストは見ればおわかりかと思いますが、末尾のテストは少し説明が必要でしょう。
このテストはn個の独立したスレッドをセットアップし、各スレッド内でPostを新たにビルドして、インスタンス化されたPostにコピーを試みます。最後は、そのpostのbodyが元のpostと一致すべきというアサーションです。

このテストを実行すると、以下のように失敗します。

copy操作をマルチスレッド環境で実行しようとすると、単体テストが失敗します。

失敗の理由は、以下のエラー出力に示されています。

After create callback :enrich_body has not been defined (ArgumentError)

実は、:skip_callbackがスレッドセーフでないことが失敗の原因だったのです。おそらく、この:skip_callbackPostインスタンスではなくPostクラスで呼び出したらraiseすべきです。
n>1の値をいろいろ変えても同じエラーが発生し、n=1(シングルスレッドを表す)の場合は問題なくパスすることから、この問題がスレッド安全性に関係していることが確認できます。

このスレッド安全性の問題をどう解決すればよいでしょうか?

通常のインスタンス変数で解決する

私たちの解決方法は「インスタンス変数を使う」といういたってシンプルなものです。このインスタンス変数は、copy操作中にオブジェクトの新しいインスタンスに設定可能で、これでafter_createフックを実行すべきかどうかを制御します。

最初に解決済みのコード全体を示し、次に各部分を見ていくことにします。

# 属性: :id、:user_id (FK)、:title、:body
class Post < ActiveRecord::Base
  attr_accessor :is_copy
  belongs_to :user

  validates :user, :body, :title, presence: true

  after_create :enrich_body, unless: :is_copy

  def copy
    Post.new(is_copy: true) do |post|
      sleep 1
      post.user = self.user
      post.body = self.body
      post.title = self.title
      post.save!
    end
  end

  private

  def enrich_body
    self.body += "\nAuthored by #{user.name}"
  end
end

ここではattr_accessorメソッド(6を参照)でis_copyインスタンス変数を定義し、Postの各インスタンスで参照できるようにしています。これはRuby標準のインスタンス変数であり、ActiveRecordモデルの典型的な属性とは異なります。
属性と対照的に、このis_copyインスタンス変数は、メモリ上に存在するオブジェクトのステートを保持するためのものであり、データベースには永続化されません。しかし今必要なのは、まさしくこれなのです。

is_copyインスタンス変数はブーリアン値を保持し、それを元にafter_createフックを実行すべきかどうかのフラグとして利用します。

after_create :enrich_body, unless: :is_copy

これで、特定のコンテキストではafter_createを実行してはならないことを、copy操作で設定できるブーリアンフラグで示せるようになりました。後はcopy操作中にis_copyフラグを設定すれば完了です。

def copy
  Post.new(is_copy: true) do |post|
    sleep 1
    post.user = self.user
    post.body = self.body
    post.title = self.title
    post.save!
  end
end

さらに、sleep呼び出しを追加するだけで、マルチスレッドの問題を明確にテストできるようになりました。新しい実装の単体テストは問題なくパスします。

copy操作でskip_callbackの代わりにインスタンス変数を使ったことで、
単体テストがすべてパスするようになりました。

まとめ

RailsのActiveRecordモジュールは、ドメインモデリングに必要な機能を多数提供していますが、Active Recordモデルの内部を見てみれば通常のRubyオブジェクトにすぎません。場合によっては、このRuby標準機能だけで問題を解決できることもあるのです。

本記事ではそうした例を紹介し、ActiveRecordskip_callbackメソッドで起きるマルチスレッド問題について検証しました。ActiveRecordモデルでskip_callbackメソッドの代わりに通常のインスタンス変数を使えば、この問題を解決できます。


本記事で紹介した手法についてお気づきの点や感想などありましたら、ぜひ元記事末尾のコメント欄までお寄せください。

本記事が気に入った方やWeb技術やWeb開発に興味がおありの方は、元記事のサイドバーにある私たちのメーリングリストにぜひ登録してください。今後公開される記事をメールで受け取れるようになります。

参考資料

  1. 🔗 Rails APIドキュメント ActiveRecord::Base
  2. 🔗 ファットモデルを分割する方法: 肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)
  3. 🔗 Airbrakeの”ファットモデル”アンチパターン記事: Top Tips for Refactoring Fat Models in Rails
  4. 🔗 ArkencyのRailsにおける神オブジェクト記事: OOP Refactoring: from a god class to smaller objects | Arkency Blog
  5. 🔗 本記事のソースコードが置かれているGitHubリポジトリ: domhnall/active-record-instance-variables
  6. 🔗 attr_accessor利用法の解説記事: How to Use attr_accessor, attr_writer & attr_reader – RubyGuides

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

The post Rails: Active Recordモデルのスレッド安全性問題をインスタンス変数で解決する(翻訳) first appeared on TechRacho.

Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: 週刊Railsウォッチ20230314 Active Record関連付けのループはfind_each

なお、Rails 6.1.0からfind_each, find_in_batchesin_batchesメソッドでorder: :descオプションを指定可能になっています(#30590)。

参考: §2.2.1.1 find_eachのオプション — Active Record クエリインターフェイス – Railsガイド
参考: PR Support order DESC for find_each, find_in_batches and in_batches by le0pard · Pull Request #30590 · rails/rails

Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳)

オブジェクトのグループを列挙する標準的なメソッドといえば、Rubyの配列でもRailsのActive Recordモデルでも、eachです。

しかし、大量のデータをループで処理すると(あるモデルの全レコードでデータを入れ直すなど)、多数のレコードを読み込むときにも処理するときにも重大なメモリ問題やスピード低下問題が発生する可能性があります。

そういうときは、ActiveRecord::Batchesが提供する機能を検討すべきです。ここではその中からfind_eachを使ってデモを行います。

以下のように書くのではなく

多数のActive Recordオブジェクトを.eachで回す。

post.comments.each do |comment|
  # 各`comment:`に対して何かするジョブをエンキューする
end

以下のように書く

.find_eachでデータベースからレコードを効率よく読み込む。

post.comments.find_each do |comment|
  # 各`comment:`に対して何かするジョブをエンキューする
end

そうする理由

eachを使うと、「1個の」SQL呼び出しをデータベースに送信してオブジェクトのセット全体をメモリに読み込み、それからループ処理を行います。これは、最初の例で post.comments.all.eachを呼び出すのと同じです。

これは2つの点で問題になります。
第1に、データベースクエリの実行に長時間を要する可能性があり、タイムアウトの可能性もあります。
第2に、ループ処理のために全レコードをメモリに読み込むので、データを返すときに(返せたとしても)メモリを大量に消費する可能性があります。

.find_eachなら、(多くの賢いデフォルト設定を持つ)効率の良い複数のSQLクエリを作成することでデータベースからレコードを取得します。これは多くの場合、一度に全レコードをメモリに読み込むよりもずっと効率が高まります。

そうしない理由があるとすれば

.find_eachはループ中のソートに主キーしか使わないので、レコードを特定のソート順で表示する必要がある場合についてはサポートされません。モデルでUUID主キーを使っている場合は、この理由によってfind_eachが期待通りに動かなくなります(理由: UUIDはシーケンシャルではありません)。つまり、ループ中に新しいデータが追加されると、レコードがスキップされる可能性があります(lainの指摘に感謝します)。

レコードをその場で変更する必要がある場合は、このようなループ処理は理想的ではありません。各レコードに対して#updateを実行すると「大量の」クエリを実行することになります。代わりに、#update_allなどのより適切なバルク更新メソッドを検討しましょう。

ビューのコードを生成する場合は、eachfind_eachも使わないでください。レコードが何件あるかもわからない状態で、巨大なコレクションをビューのコードでループ処理すると、ページが遅くなってユーザーエクスペリエンスを損なう原因になります。ページネーションを検討しましょう。

Railsガイドによると、find_eachが必要になるのは一度にメモリに乗せきれないような大量のレコードを処理する場合だけだそうです。レコード数が1000件に満たない場合のループ処理であれば通常のメソッドで問題ありませんし、実際推奨されています。

規模がさらに拡大すると、長時間のループを克服するためにもっと高度な手法が必要になります。おそらく、データベースへの大量の読み書きを削減する、あるいは数時間かかる実行を分単位に減らすなどの対応を取りたくなるでしょう。

関連記事

Rails: SidekiqはActive Jobを経由せずに直接使おう(翻訳)

Rails: ビューのパーシャルではローカル変数だけを使うこと(翻訳)

The post Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳) first appeared on TechRacho.

ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法

$
0
0

更新情報

  • 2016/08/19: 初版公開
  • 2021/08/26: 更新
  • 2023/04/12: 更新

追記: 以下の記事もどうぞ。

シン・ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法

こんにちは、hachi8833です。

Active Recordで日付範囲を指定して読み出そうとすると、おそらく次のようなコードになるでしょう。

Pattern.where(“updated_at BETWEEN ? AND ?”, from, to)

社内のSlackチャンネルのログを遡ってて、Active Recordでwhere(updated_at: range_obj_start..range_obj_end)のように、Rangeオブジェクトを#whereの値指定として渡せるというやりとりを見つけたので、確認してみました。

範囲演算子とは

RubyのRangeオブジェクトでは、.....という範囲演算子を使えます。

条件式以外の場所では式1から式2までの範囲オブジェクトを返します。範囲オブジェクトはRangeクラスのインスタンスです。...で生成された範囲オブジェクトは 終端を含みません
範囲式 (Ruby 3.0.0 リファレンスマニュアル)より(強調は筆者)

終端を含まないのは.....のどっちだったかときどきわからなくなったりしますね。

なお、数学用語では端の値を含む範囲を「閉区間」、端の値を含まない範囲を「開区間」と呼んでいます(Wikipedia: 区間(数学))。

Range#newでオブジェクトを生成できます。ここでの範囲演算子は..なので閉区間ですね。

Range.new(Time.zone.now, Time.zone.now.tomorrow)

pry-rails gemを導入したRailsコンソールで出力しました。

range_new

.....で日時を範囲指定

追記(2023/04/12)

以下のサンプルでは当日の範囲を取るのにTime.zone.today.beginning_of_day..Time.zone.today.end_of_day)という長ったらしい書き方をしていましたが、当日の閉区間ならActive Supportのall_day(Rails 5.1以降)を使う方がずっとシンプルに書けるというご指摘をいただきました。ありがとうございます!🙏

» Date.today.all_day
#>Wed, 12 Apr 2023 00:00:00.000000000 UTC +00:00..Wed, 12 Apr 2023 23:59:59.999999999 UTC +00:00

参考: Rails API: all_dayDateAndTime::Calculations

元の書き方はRuboCop Railsにも怒られるRails/ExpandedDateRangeでした↓。

参考: Rails/ExpandedDateRange — Rails :: RuboCop Docs

範囲演算子を思い出したところで、適当なRailsプロジェクトをbundle exec rails cでコンソール起動し、Active Recordの適当なモデル(ここではPatternというモデル)のupdated_atカラムに次のクエリをそれぞれ実行してみます。両者の違いはRubyの範囲指定子.....だけです。

Pattern.where(updated_at: Time.zone.today.beginning_of_day..Time.zone.today.end_of_day).to_sql
Pattern.where(updated_at: Time.zone.today.beginning_of_day...Time.zone.today.end_of_day).to_sql

AR_range

1番目の..のSQLでは、ストレートにBETWEENを使っています。
2番目の...のSQLでは、end_of_dayに終端を含まないよう、<を使って自動展開しています。

SELECT `patterns`.* FROM `patterns` WHERE (`patterns`.`updated_at` BETWEEN '2016-08-18 00:00:00' AND '2016-08-18 23:59:59')
SELECT `patterns`.* FROM `patterns` WHERE (`patterns`.`updated_at` >= '2016-08-18 00:00:00' AND `patterns`.`updated_at` < '2016-08-18 23:59:59')

もし終端値のクラス(DateとかDatetimeとか)に応じて<<=を切り替えようとすると、クラスのチェックが必要になるので煩雑になってしまいます。

BETWEEN>=<の切り替えなら、終端値のクラスを気にせず、大小関係が定義されている値の範囲にシンプルに適用できます。ささやかですが、うまい処理ですね。

参考

関連記事

シン・ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法

ActiveRecordのRangeHandlerクラスとRubyの範囲メソッドRange#exclude_end?

The post ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法 first appeared on TechRacho.


Rails: Active Recordモデルのカラムを安全に削除する(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

参考: Rails API ignored_columns=ActiveRecord::ModelSchema::ClassMethods

Rails: Active Recordモデルのカラムを安全に削除する(翻訳)

既存のActive Recordモデルに新しいカラムを追加してデプロイするのは、多くの場合問題なくできます。通常は、最初のデプロイでマイグレーションを実行し、それが終わってから、次にデータベースに追加されたカラムを利用する新しいコードをリリースするという手順になります。

しかし、カラムを削除する場合は問題が起きやすい傾向があります。Railsアプリケーションを起動すると、Active Recordがデータベースカラムをキャッシュします。このカラムをデータベーステーブルから削除すると、アプリを再起動するか再デプロイするまで例外が発生します。

データベースからカラムを削除するときは、手順を分割する戦略が有用です。

🔗 以下のように削除すること

作業を多段階に分ける戦略を用いる。

最初に、ignored_columnsでActive Recordがそのカラムを無視するようにします。これにより、アプリケーションの他の部分はこのカラムを参照できなくなります。

class Thing < ApplicationRecord
  # ...
  self.ignored_columns = ["old_column"]
  # ...
end

カラムを使っている場所を削除する前なら、カラムがまだ使われている箇所をテストスイートで見つける方法が使えます。テストがパスしたら、そのコードをデプロイします。

次に、データベースのカラムを実際に削除するマイグレーションを作成します。

class RemoveOldColumnFromThings < ActiveRecord::Migration[7.0]
  def change
    remove_column :things, :old_column
  end
end

これをデプロイして、カラムを忘却の彼方へマイグレーションします。

それが終わったら、最後の仕上げとして冒頭で追加したignored_columns行を削除し、再度デプロイします。

🔗 そうする理由

チームやアプリケーションの規模が大きい場合は、エラーやダウンタイムが発生しないよう、安定かつ予測可能な手法でデータベースの変更作業を進めることが非常に重要です。

Railsにはデータベースを変更するツールが組み込まれていますが、稼働中のproduction環境でこれを行うときは、多くの場合いつもよりさらに注意深く作業を進めることが要求されます。

「シンプルな」カラムを削除するだけでもデプロイが3回必要になることを考えれば、この方法の前提条件として、ある程度以上のまともなテストカバレッジと、1日に数回に分けてデプロイするための健全な手順を揃えておく必要があります。

そのためのgemがあります

InstaCartで最初に開発されたstrong_migrations gemを調べてみてください。

ankane/strong_migrations - GitHub

このgemは、コードが読み書きを数秒以上ブロックする場合や、今回のようにアプリケーションで他のエラーが発生する可能性が高い場合に、危険な可能性があるマイグレーションを制止して警告し、有用な指示を表示してくれます。

🔗 そうしない理由があるとすれば

トラフィックの少ないプロジェクトや、できて間もないRailsアプリケーションの場合は、そこまで厳密に必要ではありません。このような場合は、デプロイ時に少々発生するエラーに耐えながら作業すれば何とかなるかもしれません。

それでも、このような慎重な作業は早い時期から始めておくのが良い習慣です。

関連記事

Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

The post Rails: Active Recordモデルのカラムを安全に削除する(翻訳) first appeared on TechRacho.

Rails: データベースクエリにRangeを渡してコードを明確にしよう(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: Support endless ranges in where by gregnavis · Pull Request #34906 · rails/rails
参考: Add beginless range support to clusivity by bjeanes · Pull Request #45123 · rails/rails

Rails: データベースクエリにRangeを渡してコードを明確にしよう(翻訳)

Active Recordのクエリインターフェイスには、データベースから行を取得するロジックをSQLに変換するさまざまな方法が豊富に用意されています。

Active Supportコア拡張のDateTimeの拡張の素晴らしさについては過去記事でもご紹介しました。Webアプリケーションではデータベースからレコードを取得するときに時刻と時刻による期間が主要なフィルタになります。
それでは、これらの拡張を使ってデータベースにクエリをかけてみましょう。

以下のように書くよりも

文字列ベースのSQLフラグメントで日時のフィールドにクエリをかける。

User.where("created_at > ? AND created_at < ?", 2.weeks.ago, 1.week.ago).to_sql
#=> "SELECT ... WHERE (created_at > '2022-08-31 11:29:53.945995' AND created_at < '2022-09-07 11:29:53.946280')"

User.where("created_at > ? AND created_at < ?", 2.weeks.ago.beginning_of_day, 2.weeks.ago.end_of_day).to_sql
#=> "SELECT ... WHERE (created_at > '2022-08-31 00:00:00' AND created_at < '2022-08-31 23:59:59.999999')"

User.where("created_at >= ?", 2.weeks.ago.beginning_of_day).to_sql
#=> "SELECT ... WHERE \"users\".\"created_at\" >= '2022-08-31 00:00:00'"

以下のように書こう

日時のRangeを引数として渡す。

User.where(created_at: (2.weeks.ago..1.week.ago)).to_sql
#=> "SELECT ... WHERE \"users\".\"created_at\" BETWEEN '2022-08-31 11:29:33.248193' AND '2022-09-07 11:29:33.248938'"

User.where(created_at: 2.weeks.ago.all_day).to_sql
#=> "SELECT ... WHERE \"users\".\"created_at\" BETWEEN '2022-08-31 00:00:00' AND '2022-08-31 23:59:59.999999'"

User.where(created_at: (2.weeks.ago.beginning_of_day..)).to_sql
#=> "SELECT ... WHERE \"users\".\"created_at\" >= '2022-08-31 00:00:00'"

編集部注

上述のコード例の1番目と2番目のSQLクエリは、変更前が<>になっていて両端の日時を含まないのに対し、変更後のBETWEENは両端の日時を含んでいるので、クエリの意味が変わっている点に注意が必要という点がBPS社内レビューで指摘されました。

そうする理由

Rubyの構文は、こういう場合に短く簡潔に書けます。

これによって生成されるSQLステートメントは、テキストで挿入する方法に比べて値の範囲が大きく外れにくくなるので、その分ミスも減るでしょう。

さらに、Rangeを渡されたときにフレームワークが生成するSQLステートメントは、SQLの正しいBETWEEN構文を使います(少なくともPostgreSQLでは)。
これによって振る舞いを記述する精度が高まり、背後のデータベースが統計情報やインデックスを活用して結果のクエリを高速に返すようになります。

そうしない理由があるとすれば

Active RecordのスコープにRangeを渡すのは、まだ思ったほど一般的ではないようです。これは、Railsガイドのドキュメントでは文字列ベースで書くクエリ条件が最初に紹介されていて、初期のRailsではこれが主要な構文だったからではないかと私は推測しています。

この現代的な構文を使わない理由はほとんどないでしょう。Active Recordの機能を使うなら、文字列をあれこれ組み立てて書くよりもこの構文の方が読みやすくわかりやすく書けます。普段からそのように書いておけば、まれに複雑なSQLクエリを文字列で書かなければならなくなった場所も際立つようになります。

上の最後の例では、..によるエンドレスRange構文を使っています。これは2018年にRuby 2.6で導入されたもので、Railsに降りてくるまで少し時間がかかったせいか、同じことをまだ文字列の式展開でやっている人がいるかもしれません。

訳注

mainブランチにマージされたRailsガイドの更新には、文字列と?プレースホルダによるクエリサンプルを.....によるクエリに更新しているものもあります(#47054)。Rails 7.1で反映される見込みです。

# https://github.com/rails/rails/commit/7707377ddfff3348f91cf046a7b975410fe9a08e#diff-77236751c3ab97b753f641ae6e10445de7662826f0176c68b5176feec92d0a1bL47
- scope :old, -> { where('year_published < ?', 50.years.ago )}
+ scope :old, -> { where(year_published: ...50.years.ago.year) }

参考: Update guides to use ranges instead of sql literals · rails/rails@7707377

インデックス!

同じフィールドに対して恒常的にクエリを送信することがあるなら、そのデータベーステーブルでインデックスを追加することでメリットを得られるかどうかを調べる価値があるでしょう。

関連記事

Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳)

ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法

The post Rails: データベースクエリにRangeを渡してコードを明確にしよう(翻訳) first appeared on TechRacho.

Rails 7.1: ActiveRecord::Baseにnormalizesが追加された(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

Rails 7.1: ActiveRecord::Basenormalizesが追加された(翻訳)

Rails 7.1で、Active Recordの属性値の正規化(normalization)を宣言するメソッドが新たに追加されました。これは、ユーザー入力のサニタイズ、書式の統一、外部由来のデータのクリーンアップで特に有用です。

Rails 7.1より前は、以下のようにbefore_saveコールバックで属性を正規化できます。

model User < ApplicationRecord
  before_save :downcase_email, if :email_present?

  private

    def email_present?
      email.present?
    end

    def downcase_email
      email.downcase!
    end
end

Rails 7.1では、同じコードを以下のような形にリファクタリングできます。

model User < ApplicationRecord
  normalizes :email, with: -> email { email.downcase }
end

この正規化は、属性への代入や属性の更新時に適用され、正規化済みの値はデータベースで永続化されます。この正規化は、finderメソッドで対応するキーワード引数にも適用されます。これにより、作成されたレコードについて、正規化されていない値を用いて後からクエリをかけられるようになります。

デフォルトでは、nil値に対して正規化は適用されません。nil値を正規化するには、以下のようにapply_to_nil:オプションでnilの正規化を有効にできます。

model User < ApplicationRecord
  normalizes :user_name, with:
    -> user_name { user_name.parameterize.underscore }

  normalizes :email, with: -> { _1.strip.downcase }

  normalizes :profile_image, with:
    -> profile_image {
      profile_image.present? ? URI.parse(profile_image).to_s :
      "https://source.boringavatars.com/beam" },
    apply_to_nil: true
end
# rails console
>> User.create!(user_name: "Eve Smith", email: "eveSmith@EXAMPLE.com")

#<User:0x000000010b757090 id: 1, user_name: "eve_smith", profile_image:"https://source.boringavatars.com/beam", email: "evesmith@example.com", created_at: Wed, 03 May 2023 07:49:20.067765000 UTC +00:00, updated_at: Wed, 03 May 2023 07:49:20.067765000 UTC +00:00>

>> user = User.find_by!(email: "EVESMITH@example.COM")
>> user.email # => "evesmith@example.com"

>> User.exists?(email: "EveSmith@Example.COM")          # => true

ここでユーザーのメールアドレスが、正規化ステートメントがモデルに追加される前にデータベースに保存されていた場合、そのメールアドレスは正規化済みの形では取得されません。

その理由は、データベース内でメールの大文字小文字が混在している、つまり正規化されずに保存されているためです。そのようなレガシーデータがある場合は、Normalization#normalize_attributeメソッドを明示的に利用することで正規化できます。

# rails console
>> legacy_user = User.find(1)
>> legacy_user.email  # => "adamSmith@EXAMPLE.com"
>> legacy_user.normalize_attribute(:email)
>> legacy_user.email  # => "adamsmith@example.com"
>> legacy_user.save

詳しくは#43945をご覧ください。

関連記事

Rails 7.1: 複数ジョブを一度にエンキューするperform_all_laterが追加(翻訳)

The post Rails 7.1: ActiveRecord::Baseにnormalizesが追加された(翻訳) first appeared on TechRacho.

Rails: Active Recordのfindで怖い思いをした話(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: Rails API findActiveRecord::FinderMethods
参考: Object#tap (Ruby 3.2 リファレンスマニュアル)

Rails: Active Recordのfindで怖い思いをした話(翻訳)

私は最近プロジェクトでこんなリファクタリングを行いました。identityに束縛されたコンテキストにドメインイベントをさらに追加することで、システム内のidentityに対して実行された特定のアクションから取得する監査ログを改善するというものです。手始めに、コマンドを消費する責務を持つServiceを抽出しました。当初は以下のようなものでした。

class UpdatePersonalSettings
  include Command
  attribute :email, String
  attribute :name, String
  attribute :identity_id, Integer
end
class IdentityService
  # ...

  def update_personal_settings(command)
    Identity.find(command.identity_id) do |identity|
      identity.email = command.email
      identity.name = command.name
      identity.save!
      publish(
        PersonalSettingsUpdated.strict(
          data: {
            identity_id: identity.id,
            email: identity.email,
            name: identity.name
          }
        )
      )
    end
  end

  # ...
end

一見すると何の問題もなさそうです。動作が意図通りであることを検証するテストもそこそこ書いたので、このコードをtest環境にデプロイしました。

すると、何かがおかしいことに気づきました。自分のテストアカウント名を更新しようとすると、emailフィールドでuniquenessバリデーションエラーが発生していたのです。

これは一体どういうことでしょう?デバッグを始めてみると、実際に更新されていたのはcommand.identity_idで指定したidentityではなく、1番目のidentityであったことが判明しました。テストスイートを見直してみましたが、どこにもおかしい点は見当たらず、名前とメールを更新するテストケースはパスしています。

だとすると、どこが問題なのでしょうか?
そこで、Active Recordのfindメソッドのソースコードを調べてみました1

# File activerecord/lib/active_record/core.rb, line 157
      def find(*ids) # :nodoc:
        # We don't have cache keys for this stuff yet
        return super unless ids.length == 1
        return super if block_given? ||
                        primary_key.nil? ||
                        scope_attributes? ||
                        columns_hash.include?(inheritance_column)

        id = ids.first

        return super if StatementCache.unsupported_value?(id)

        key = primary_key

        statement = cached_find_by_statement(key) { |params|
          where(key => params.bind).limit(1)
        }

        record = statement.execute([id], connection).first
        unless record
          raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
                                   name, primary_key, id)
        end
        record
      rescue ::RangeError
        raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'",
                                 name, primary_key)
      end

ここで使われているsuperがとても気になったので、コンソールで以下のスニペットを実行してみました。

>> User.find(123) do |identity|
?>     puts identity
>>   end
  User Load (3.3ms)  SELECT `users`.* FROM `users`
#<User:0x00007faf2d800c30>
#<User:0x00007faf2d800af0>
#<User:0x00007faf2d8009b0>
#<User:0x00007faf2d800870>
#<User:0x00007faf2d800730>
#<User:0x00007faf2d8005f0>
#<User:0x00007faf2d8004b0>
#<User:0x00007faf2d800370>
#<User:0x00007faf2d800230>
#<User:0x00007faf2d8000f0>
=> nil

これでやっと何が起きているのかがわかりました。私のコードはDBテーブルの全レコードをひたすらイテレートし、渡したブロックをレコードごとに評価しようとしていたのです。

この振る舞いがtest環境の段階で即座にバリデーションで検出されたからよかったようなものの、このコードがもしproduction環境で実行されてuniquenessバリデーションが発生しなかったら、DBの全レコードがごっそり更新されてしまうという大変危険な事態になったかもしれません。

もうひとつ気づいたのは、私が書いたテストケースはこの問題を検出できるほど賢くなかったということです。テストではidentityを複数作成し、更新は少なくとも2つのidentityに対して行うべきです。

どうやって解決したかですか?方法は実に明白で、要は私がfind呼び出しにtapを足し忘れていただけだったのです。

class IdentityService
  # ...

  def update_personal_settings(command)
    Identity.find(command.identity_id).tap do |identity|
      identity.email = command.email
      identity.name = command.name
      identity.save!
      publish(
        PersonalSettingsUpdated.strict(
          data: {
            identity_id: identity.id,
            email: identity.email,
            name: identity.name
          }
        )
      )
    end
  end

  # ...
end

findに引数とブロックを両方渡すと、エラーなしで引数が無視されるので、これをバグとして報告するかどうか検討中です。少なくともこのような呼び出しでは、コードが意図通りに動作していないことにすぐ気付けるように、ブロックを渡すと引数が無視されるという警告を表示すべきではないかと考えています2

関連記事

Rails: アプリケーションを静的解析で”防弾”する3つの便利ワザ(翻訳)

Rails: Active Recordモデルのスレッド安全性問題をインスタンス変数で解決する(翻訳)


  1. 訳注: このソースコードに最も近いのはRails 5.2のようです。 
  2. 訳注: 翻訳公開時点ではRailsリポジトリでこのバグ報告や修正は見当たらず、Rails 7.0.5でもこうした警告は表示されません。 

The post Rails: Active Recordのfindで怖い思いをした話(翻訳) first appeared on TechRacho.

Rails: Active Record属性のデフォルト値はコールバックよりもdefaultオプションで設定しよう(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。
以下のAPIドキュメントもどうぞ。

Rails: ActiveRecord標準のattributes APIドキュメント(翻訳)

Rails: Active Record属性のデフォルト値はコールバックよりもdefaultオプションで設定しよう(翻訳)

Active Recordモデルのインスタンスにデフォルト値を設定する必要が生じたことのある人は、おそらくコールバックでデフォルト値を設定したことがあるでしょう。

実はRails 5.0からもっとよい方法があるのですが、最近まで気づいていませんでした(Mosesに感謝します!)

参考: §2.3 Active Record属性API — Ruby on Rails 5.0 リリースノート – Railsガイド

🔗 以下のように書くよりも

デフォルト値をコールバックで設定する。

class Message
  before_validation :assign_delivered_at

  # ...

  private

  def assign_delivered_at
    delivered_at ||= Time.zone.now
  end
end

🔗 以下のように書こう

Active RecordのAttributes APIを使う。

class Message
  attribute :delivered_at, default: -> { Time.zone.now }

  # ...
end

🔗 そうする理由

コールバックを使う正当な理由があったとしても、コールバックは理解しにくくなる可能性があります。一般にコールバックを減らす方が、後になって驚かされることも少なくなります。

attribute構文は簡潔かつ明快であり、Railsではこの方法で振る舞いを実行することが推奨されています。

Attributes APIには、他にも「属性の変更トラッキング」「型キャスト」「データベースを用いない属性の追加」といった豊富な機能が揃っていますが、ここではデフォルト値の設定機能だけを使っています。

🔗 そうしない理由があるとすれば

データ整合性を理想的なものにするには、データベーススキーマでデフォルト値を設定する方がより望ましくなります。

デフォルト値をデータベースレベルで設定すれば、Active Recordはその値を保存前の新しいモデルに取り込むので、defaultオプションによる方法はおそらく不要です。

データベースレベルでデフォルト値を設定したうえで、さらに上述のdefaultオプションによる方法でActive Recordモデルにデフォルト値を設定すると、Model.newを呼び出したときにデータベースが設定したデフォルト値が上書きされる点に注意が必要です。Railsでデフォルト値を何らかの理由で意図的に変更するのでない限り、データベースレベルのデフォルト値に加えてdefaultオプションによるデフォルト値も指定する必要はないでしょう。

関連記事

Rails: ActiveRecord標準のattributes APIドキュメント(翻訳)

Rails: データベースクエリにRangeを渡してコードを明確にしよう(翻訳)

The post Rails: Active Record属性のデフォルト値はコールバックよりもdefaultオプションで設定しよう(翻訳) first appeared on TechRacho.

Rails: Active Recordのメソッドに渡す文字列を式展開してはいけない(翻訳)

$
0
0

Rails: Active Recordのメソッドに渡す文字列を式展開してはいけない(翻訳)

悪意のあるユーザーからアプリケーションを保護するのは、開発者の重要な責務のひとつです。これは、Railsのように、セキュリティ機能が組み込まれているメンテの十分なフレームワークを使う優れた理由となります。

特に大きな理由は、ユーザー入力をデータベースに保存する前にサニタイズするActive Recordの保護機能です。ただし、その気になればActive Recordのスコープに文字列を直接渡す方法がいくつもあります。しかしこれは極めて慎重かつ控えめに使う必要のある、要注意の機能です。

🔗 以下のように書いてはいけない

Active Recordに渡す引数で文字列の式展開を使う。

User.delete_by("id = #{params[:id]}")
User.where("email = #{params[:email]}")

🔗 以下のように書くこと

ハッシュ形式の構文にする。

User.delete_by(id: params[:id])
User.where(email: params[:email])

🔗 そうする理由

Railsは切れ味の鋭い刃物です。開発者にとって有用なこともたくさんありますが、ユースケース(ここではActive Recordのメソッドに文字列を渡す)に合わせてフレームワークを柔軟に捻じ曲げることも可能になっています。

Active Recordには、データベースを素直に操作できるメソッドが豊富に揃っているので、上の例のような形で文字列をActive Recordに渡すことはめったにないと思いますが、SQLクエリが長く複雑になると、ユーザー入力のサニタイズを忘れがちになります。

文字列でパラメータを式展開すると、ユーザー入力からのSQLインジェクション攻撃にさらされる可能性が生じます。

# ユーザーが入力したパラメータ
params[:id] = "1) OR 1=1--"
User.delete_by("id = #{params[:id]}")
#=> User Delete All (4.2ms)  DELETE FROM "users" WHERE (id = 1) OR 1=1--)

上のユーザー入力に含まれている1=1の部分は常にtrueになるので、データベースの全ユーザーを削除するSQLクエリが発行されてしまいます。これは良くない事態です。

値を直接引数に式展開すると、上のような悪意に基づいた破滅的な結果以外にも、想定外の振る舞いが生じる可能性があります。たとえば、以下のように想定外の情報が漏洩することも考えられます。

params[:q] = "'' OR 1=1"
User.where("email = #{params[:q]}")
#=> User Load (1.1ms)  SELECT "users".* FROM "users" WHERE (email = '' OR 1=1)

この例では、サニタイズされていない1=1がSQLのWHERE条件に渡されると常にtrueになってしまうので、データベースからすべてのユーザーを読み出すSQLクエリが発行され、情報が漏洩してしまいます。

最後に、引数を文字列ベースにするとコードが読みにくくなり、理解が難しくなります。ハッシュベースの構文を使う方がずっと理解しやすくなります。

🔗 そうしない理由があるとすれば

お見せした例はいかにも作為的ですが、ここには実際の弱点が示されています。

必要なクエリでハッシュスタイルの構文を利用できない事情がある場合は、自分がやっていることを十分に、徹底的に、確実に理解してから文字列ベースの引数を使いましょう。くれぐれも細心の注意を払ってください。

関連記事

Rails: データベースクエリにRangeを渡してコードを明確にしよう(翻訳)

保存版: Railsアプリケーションのセキュリティベストプラクティス(翻訳)

Rubyの式展開(string interpolation)についてまとめ: `#{}`、`%`、Railsの`?`

The post Rails: Active Recordのメソッドに渡す文字列を式展開してはいけない(翻訳) first appeared on TechRacho.

Rails: Active Record APIクイズで最大の難問はどれか(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。

参考: 週刊Railsウォッチ20230221: Active Record APIクイズ

Rails: Active Record APIクイズで最大の難問はどれか(翻訳)

RailsのActiveRecordモジュールは、このフレームワークの成功と普及に大きく貢献したコアコンポーネントの1つです。私たちは数か月前に、ActiveRecord APIの知識を問うクイズを公開しました。この短い記事では、このクイズの平均的な成績を確認して、どの問題が回答者にとって最も難しかったかを明らかにします。

はじめに

これまで最も人気が高かったクイズは、以下のクイズでした。

クイズは10問で、Railsエコシステムで重要な要素であるActiveRecordに関するさまざまな方面の知識をセルフテストできます。

本記事では、全回答者から寄せられた解答を集計して、どんな回答が寄せられたか、そしてどの問題が最も難しかったかを明らかにします。

成績別の内訳

本記事執筆時点では1,779件の回答が登録されており、平均点は35%でした。1人の回答者が何度も回答している可能性もありますが、結果には影響しないでしょう。得点分布を以下の棒グラフにまとめました。

点数を登録した回答者数を0~10点、10~20点、90~100点のバケットごとに振り分けた度数分布図

Rails ActiveRecord APIクイズ受験者の得点分布

平均点が35%と予想外に低かったのですが、棒グラフの最頻値が40〜49になっていることがわかります。平均値が低めになったのは、高得点者が少ないために、このデータの得点分布が低い方に偏ったからです。70%超えの方は95パーセンタイルに入っています。80%超えの方は上位1.5%入りです。

問題別の内訳

次に、個別の問題についてユーザーがどう回答したかを見てみましょう。以下の棒グラフのバーをクリックすると、その問題のスクリーンショットが表示されます(訳注: 動的な操作は元記事で行ってください)。

!問題別の内訳

最も難易度の高かった問題

最も難しかったのは問9で、正答率はわずか2.35%でした。この問9では、さまざまなプリロードやeager loadingの手法に関する知識が問われます。これらの手法について詳しくはBigBinaryの以下の記事で詳しく説明されています1

参考: Preload, Eagerload, Includes and Joins – BigBinary Blog

実際の問9は以下のようなものでした。

Screenshot of Qu 9 from Ruby ActiveRecord API quiz: Given the following models, regardless of the data in the database, which of the following patterns will issue a single SQL statement to the database?

Rails ActiveRecordクイズの最難問
DB内のデータにかかわらず、以下の中で単一のSQL文をデータベースに発行するパターンを選べ(複数回答可)

ドメインモデルは、Userクラスとhas_many :postsというかなりシンプルなものになっており、Postごとにtitle属性があります。

User.joinsパターン
User.joinsパターンは、関連付けされたレコードによってユーザーをフィルタするのに用いられますが、このとき、関連付けされたレコードを読み込まない点に注意が必要です。その結果、関連付けられるPostオブジェクトにアクセスしようとするたびにデータベースから読み込まなければならなくなります。これは、あの悪名高いN+1問題の原因になりがちです。
User.preloadパターン
User.preloadパターンでは、Userモデルと、関連付けられるPostモデルが両方ともメモリに読み込まれます。しかし、これは2つの異なるクエリによって実現されています。1つはUserレコードを読み込むクエリで、もう1つはそのユーザーに関連付けられているすべてのPostレコードを読み込むクエリです。preloadを使うと、この2段階読み込みが行われるため、関連付けられるレコードの属性でフィルタできなくなってしまいます。
User.eager_loadパターン
User.eager_loadパターンでは、Userモデルと、関連付けられるPostモデルを両方ともメモリに読み込みますが、このとき単一のLEFT OUTER JOINクエリを使います。つまりこれは正しい回答です。
User.includesパターン
User.includesパターンはUser.preloadと同じことを行いますが、関連付けられているテーブル内の値でクエリをフィルタできる点が異なります。関連付けられているテーブルをwhere句を適用する形でフィルタする場合、このincludesメソッドはUserPostのレコードを両方とも単一のLEFT OUTER JOINクエリで読み込みます。つまりこれも正しい回答です。

まとめ

本記事では、以前公開したRails ActiveRecord APIクイズに登録された結果を簡単に分析してみました。最も難易度が高かった問題は、Railsで関連付けされているレコードを読み込むときのpreloadeager_loadjoinsincludesの違いを理解しているかどうかを問われます。

短い記事ですが、お役に立てられれば幸いです。ご質問やお気づきの点がありましたら元記事末尾のコメント欄までどうぞ。本記事をお楽しみいただけましたら、新着記事をすぐ読めるようぜひ購読をご検討ください。お読みいただきありがとうございました。

参考情報

  1. 元のクイズ: Test Yourself on Rails ActiveRecord API
  2. Preload, Eagerload, Includes and Joins – BigBinary Blog
  3. preloadeager_loadjoinsincludesの違いを解説するBigBinaryの記事: preload, eager-load, join and includes

関連記事

Rubyオブジェクトモデルのクイズで最大の難問はどれか(翻訳)

Rubyの配列クイズで最大の難問はどれか(翻訳)


  1. 訳注: k0kubunさんによる記事『ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い – Qiita』もどうぞ。 

The post Rails: Active Record APIクイズで最大の難問はどれか(翻訳) first appeared on TechRacho.


TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

$
0
0

概要

元サイトの許諾を得て翻訳・公開いたします。


  • 2017/10/10: 初版公開
  • 2023/07/04: 更新

参考: TestProfのリポジトリにある図


test-prof/test-profより

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

テストを書くことは、開発における重要なプロセスであり、RubyやRailsのコミュニティには特に当てはまります。私たちはテストでgreenが点灯するまで長時間待たされていることに気づいて、初めてテストスイートのパフォーマンスというものに関心を寄せるようになるものです。

私はテストスイートのパフォーマンスの分析に多くの時間を費やし、テストを高速化するテクニックを編み出すとともにツールを開発しました。そうしたノウハウをすべてTestProfという名のメタgemに盛り込みました。TestProfはRubyのテストをプロファイリングするツールボックスです。

test-prof/test-prof - GitHub

🔗 開発の動機

テストが遅いと生産性が低下し、貴重な時間が無駄になってしまう

「テストスイートのパフォーマンスがどうしてそんなに重要なんだろうか?」とお思いの方もいらっしゃるでしょうから、議論を始める前にいくつかの統計情報をお見せしたいと思います。

今年初頭に、Ruby開発者にテストのパフォーマンスに関する簡単な聞き取り調査を行いました。

最初によいお知らせです。Ruby開発者のほとんどすべてがテストを書いています(正直、私は驚きませんが)。私はRubyコミュニティのこういうところが好きです。

質問: テストを書いていますか?

質問: テストを書いていますか?

調査の結果、テスト実行に10分以上かかっているケースは4分の1程度にとどまりました。かつ、半分近くは5分以内に収まっています。

質問: テストの実行時間は?

質問: テストの実行時間は?

これなら大きな心配はなさそうです。それでは、exampleが1000を超えるテストスイートに限定して聞いてみましょう。

質問: テストスイート全体の実行時間は?(exampleが1000以上の場合)

質問: テストスイート全体の実行時間は?(exampleが1000以上の場合)

今度はだいぶ残念な結果になりました。テストスイートの約半分は実行に10分以上を要し、ほぼ3割近くが20分以上かかっています。

ところで、私がこれまで経験した典型的なRailsアプリでは、ざっくり6000件〜10000件のexampleがありました。

もちろん、変更のたびにテストスイート全体を実行する必要などありません。通常、私が中規模の機能を手がけている場合はコミットあたり100件のexampleを実行しており、実行には1分程度しかかかりません。しかし1分といえども私のフィードバックループは結構な影響を受けますし(Justin Searlsの動画↓をご覧ください)、その間私の貴重な時間は無駄になります。

それでも私たちは、デプロイサイクル中にCIサービスですべてのテストを実行しなければなりません。テスト完了まで10分も(キューでビルドの負荷が生じれば数時間)待たされても平気ですか?私にはそうは思えません。

ビルドの並列処理化で待ち時間を軽減することもできますが、結局コストに跳ね返ります。次のグラフをご覧ください。

質問: CIでの並行ジョブ数は?

質問: CIでの並行ジョブ数は?

たとえば現在のプロジェクトで5倍の並列処理を行っているとすると、ジョブ1つあたりの平均RSpec実行時間はexample 1250件あたり2.5分を要します。つまりEPM(examples per minute)は500になります。

最適化を行う前は、800件のexampleで4分を要しました。EPMにしてわずか200です。つまり最適化によってビルドあたり3、4分を節約できたのです。

明らかに、遅いテストはあなたの貴重な時間を奪い、生産性を低下させているのです。

🔗 ツールボックス

あなたがテストスイートの遅さに気づいたとしましょう。ではテストが遅い理由をどうやって明らかにしますか?

能書きは以下の動画に任せて、RubyテストのプロファイリングツールボックスであるTestProf gemをご紹介いたします。

TestProfはテストスイートのボトルネックを突き止め、改善方法を示してくれます。

これより、私がTestProfを使ってテストスイートの分析と改善を行ってみます。

🔗 一般的なプロファイリング

テストスイートの詳細をいきなり掘り下げるよりも、まず一般的な情報を集める方が有用なことがしばしばあります。

試しに、テストスイートについて以下の質問に答えてみてください。

  1. テストスイートのどこで時間を食っているか: コントローラか、モデルか、サービスか、ジョブか?
  2. 最も時間のかかっているモジュールやメソッドはどれか

これだけでも面倒な作業ですよね。

質問1の答えが知りたい場合は、TestProfのTag Profilerを使います。これはRSpecの特定のタグ値でグループ化された統計情報を収集してくれます。RSpecはexampleにtypeタグを自動で追加するので、以下のように使えます。

TAG_PROF=type rspec


[TEST PROF INFO] TagProf report for type

          type          time   total  %total   %time           avg

    controller     08:02.721    2822   39.04   34.29     00:00.171
       service     05:56.686    1363   18.86   25.34     00:00.261
         model     04:26.176    1711   23.67   18.91     00:00.155
           job     01:58.766     327    4.52    8.44     00:00.363
       request     01:06.716     227    3.14    4.74     00:00.293
          form     00:37.212     218    3.02    2.64     00:00.170
         query     00:19.186      75    1.04    1.36     00:00.255
        facade     00:18.415      95    1.31    1.31     00:00.193
    serializer     00:10.201      19    0.26    0.72     00:00.536
        policy     00:06.023      65    0.90    0.43     00:00.092
     presenter     00:05.593      42    0.58    0.40     00:00.133
        mailer     00:04.974      41    0.57    0.35     00:00.121
        ...

これで、ボトルネックを調べる際のテストのtypeを絞り込めるようになりました。

ruby-prof/ruby-prof - GitHub
tmm1/stackprof - GitHub

RubyProfStackProfのような一般的なRubyプロファイラを使ったことがある方もいると思いますが、TestProfは面倒な設定や改造を一切行わずにテストスイートに対して手軽に実行できます。

TEST_RUBY_PROF=1 bundle exec rake test

# 以下も同じ

TEST_STACK_PROF=1 rspec

TestProfの各種プロファイラが生成するレポートを使って、最も利用頻度の高いスタックパスを突き止めることができ、質問2.にも答えられるようになります。

残念ながら、この種のプロファイリングはリソース消費が著しく、既に遅いテストスイートの実行がますます遅くなるので、テストのごく一部に狙いを絞り込んでプロファイルを実行しなければなりません。しかしどうやって絞り込めばよいのでしょうか。実は、ランダムに絞り込めるのです。

TestProfには特殊なパッチが同梱されており、RSpecのexampleグループ(またはMiniTestのスイート)をランダムに選んで実行できます。

SAMPLE=10 bundle exec rspec

後はコントローラのテストのサンプルに対してStackProfを実行し(TagProfでここが最も遅かったので)、出力結果を元に分析すればよいのです。私があるプロジェクトに対して実行した結果を以下に示します。

%self     calls  name
20.85       721   <Class::BCrypt::Engine>#__bc_crypt
 2.31      4690  *ActiveSupport::Notifications::Instrumenter#instrument
 1.12     47489   Arel::Visitors::Visitor#dispatch
 1.04    205208   String#to_s
 0.87    531377   Module#===
 0.87    117109  *Class#new

これを見ると、test環境におけるSorceryの暗号化設定がproduction環境と同じぐらい厳密な設定になっていることがわかります。

Sorcery/sorcery - GitHub

典型的なRailsアプリの場合、レポートはほとんどの場合以下のようになるでしょう。

 TOTAL    (pct)     SAMPLES    (pct)     FRAME
   205  (48.6%)          96  (22.7%)     ActiveRecord::PostgreSQLAdapter#exec_no_cache
    41   (9.7%)          22   (5.2%)     ActiveModel::AttributeMethods::#define_proxy_call
    20   (4.7%)          14   (3.3%)     ActiveRecord::LazyAttributeHash#[]

ActiveRecordが随分と多くなっていて、データベースの利用量が多いことがわかります。ではこれをどうやって改善すればよいのでしょうか。続きをご覧ください。

🔗 データベースとのやりとり

テストスイートの実行時間にデータベースが占める割合がどのぐらいあるかを把握していますか?まずは自分であたりを付けてから、TestProfであぶり出してみましょう。

私たちはRailsのInstrumentation周りを既に拡張した(ActiveSupportのNotification/Instrumentation機能)ので、基本的な概要は省略してEventProfilerを紹介します。

EventProfはテストスイート実行中にinstrumentationメトリクスを収集し、遅いグループのランキングや、特定のメトリクスに関連するexampleについての一般的な情報をレポートします。現時点では、ActiveSupport::Notificationsのみ無設定で利用できますが、自分のソリューションへの統合は簡単なはずです。

データベース利用量の情報を取得するには、sql.active_recordイベントを使います。この場合のレポートは次のようになります(rspec --profileの出力と非常に似ています)。

EVENT_PROF=sql.active_record rspec ...

[TEST PROF INFO] EventProf results for sql.active_record

Total time: 00:05.045
Total events: 6322

Top 5 slowest suites (by time):

MessagesController (./spec/controllers/messages_controller_spec.rb:3)–00:03.253 (4058 / 100)
UsersController (./spec/controllers/users_controller_spec.rb:3)–00:01.508 (1574 / 58)
Confirm (./spec/services/confirm_spec.rb:3)–00:01.255 (1530 / 8)
RequestJob (./spec/jobs/request_job_spec.rb:3)–00:00.311 (437 / 3)
ApplyForm (./spec/forms/apply_form_spec.rb:3)–00:00.118 (153 / 5)

私の現在のプロジェクトでは、DBが総時間に占める割合はおよそ20%ですが、これは既に十分最適化を行った結果です。最適化前は30%を超えていました。

どのプロジェクトでも共通でチェックに使えるような単一のメトリクス指標というものはありません。これはテストのスタイルによって大きく変わるものであり、単体テストと結合テストのどちらが多いかによって異なります。

なお私たちの場合は結合テストがほとんどだったため、20%という値は決して悪くありません(もちろんさらに改善は可能なはずですが)。

データベースがテスト時間に占める割合が高い典型的な理由とは何でしょうか。そのすべてを列挙するのは無理ですが、一部をリストアップしてみました。

  • 無意味なデータ生成
  • テストの準備が重すぎる(beforesetupのフック)
  • ファクトリーがカスケードしている

最初の項目は、有名な「Model.new vs Model.create問題」(またの名を「FactoryGirlにおけるbuild_stubbed vs create問題」)です。モデルの単体テストでデータベースを叩く必要は普通ないはずなので、データベースにはアクセスしないでくださいね。

しかし既にテストコードでデータベースにアクセスしているとしたらどうでしょう。テストで永続性が不要かどうかをどうやって突き止めればよいでしょうか。そこでFactory Doctorの登場です。

Factory Doctorは、不要なデータ作成がいつ行われたかを通知してくれます。

FDOC=1 rspec

[TEST PROF INFO] FactoryDoctor report

Total (potentially) bad examples: 2
Total wasted time: 00:13.165

User (./spec/models/user_spec.rb:3)
  validates name (./spec/user_spec.rb:8)–1 record created, 00:00.114
  validates email (./spec/user_spec.rb:8)–2 records created, 00:00.514

Factory Doctorは残念ながら魔法使いではありません(まだ学習中です)ので、偽陽性や偽陰性が生じることもあります。

2番目の問題は、ややトリッキーな点です。次の例をご覧ください。

describe BeatleSearchQuery do
  # この検索機能をテストしたいので
  # exampleごとに何らかのデータが必要
  let!(:paul) { create(:beatle, name: 'Paul') }
  let!(:ringo) { create(:beatle, name: 'Ringo') }
  let!(:george) { create(:beatle, name: 'George') }
  let!(:john) { create(:beatle, name: 'John') }

  # この後15件ほどexampleが続く
end

「そんなのfixtureでいいじゃない」とお思いかもしれません。十数個ものモデルが毎日変更されるようなかなり大きなプロジェクトでなければ悪くない方法です。

もうひとつの方法はbefore(:all)フックでデータを1度だけ生成することです。しかしこの方法には1つ注意点があります。before(:all)はトランザクションの外で実行されるので、データベースを手動でクリーンアップしなければなりません。

でなければ、グループ全体を1つのトランザクションに手動で閉じ込めるというのはどうでしょうか。TestProfのbefore_allヘルパーはまさにこれを行っているのです。

describe BeatleSearchQuery do
  before_all do
    @paul = create(:beatle, name: 'Paul')
    @ringo = create(:beatle, name: 'Ringo')
    @george = create(:beatle, name: 'George')
    @john = create(:beatle, name: 'John')
  end

  # この後15件ほどexampleが続く
end

コンテキストを別のグループ(ファイル)間でも共有したい場合は、Any Fixtureをご検討ください。これは、(ファクトリーを使って)コードからフィクスチャを生成するのに使えます。

ファクトリーのカスケード問題

ファクトリーのカスケード問題は実にありがちなのですが、めったに対処されていないので、テストスイートが遅くなる可能性があります。言ってみれば、ファクトリー呼び出しのネストによって大量のデータが生成されるという、制御不能なプロセスなのです、TestProfはこれに対処する方法を理解していて、これについて専用の記事もありますので、ぜひお読みください↓。

TestProf(2) Rubyテストの遅いfactoryを診断治療する(翻訳)

バックグラウンドジョブ

データベース以外にもさまざまなボトルネックがあります。その中からひとつを取り上げてみましょう。

テストでよく行われるのが、バックグラウンドジョブをインライン化する(Sidekiq::Testing.inline!など)という手法です。

通常、重たい作業はバックグランドに回しますので、すべてのジョブを実行すると実行時間が無条件に長くなります。

TestProfはバックグラウンドに要した時間のプロファイリングもサポートしています(現時点ではSidekiq限定)。以下のようにプロファイルにsidekiq.inlineを指定するだけでできます。

EVENT_PROF=sidekiq.inline bundle exec rspec

これで、無駄になっている時間を正確に知ることができます。次はどうすればよいでしょうか。単にインラインモードをオフにすると、テストのexampleが大量に動かなくなるかもしれません。あまりに多さにすぐには修正しきれないほどです。

解決方法は、インラインをグローバルにオフにし、必要な場合のみオンにすることです。RSpecをお使いの場合は次のようにします。

# 共有コンテキストを追加
shared_context "sidekiq:inline", sidekiq: :inline do
  around(:each) { |ex| Sidekiq::Testing.inline!(&ex) }
end

# 必要な場合にのみ使う
it "do some bg", sidekiq: :inline do
  ...
end

失敗したexampleのひとつひとつにタグを付けて回らなければならないのでしょうか?TestProfならそんな必要はありません。

ツールボックスにはRSpec Stampという特殊なツールが含まれており、特定のタグを自動で追加してくれます。

RSTAMP=sidekiq:inline rspec

ところで、RSpec Stampの内部ではソースファイルをパースしてタグを正しく挿入するためにRipperを用いています。

inline!fake!に移行する方法についてはRSpec Stampのマニュアルをご覧ください。


TestProfはGitHubrubygems.orgで公開されており、いつでもアプリに導入してテストスイートのパフォーマンス向上に役立てることができます。

本記事はTestProfの紹介のみにとどまっており、すべての機能をカバーしているわけではありません。詳しくは以下の追加リソースをどうぞ。

今年9月にモスクワで開催予定のRailsClubでもテストのパフォーマンスについてスピーチしますので、ぜひご来場ください!


スタートアップをワープ速度で成長させられる地球外エンジニアたちへ告ぐ!Evil Martiansのフォームにて待つ。

関連記事

TestProf(2) Rubyテストの遅いfactoryを診断治療する(翻訳)

FactoryGirlでtraitを使うとintegration test書くのが捗るという話

The post TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳) first appeared on TechRacho.

Rails API: ActiveRecord::NestedAttributes(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。

Rails API: ActiveRecord::NestedAttributes(翻訳)

Active RecordAttributesのネステッド版

ネステッド属性を使うと、関連付けられているレコードに親を介して属性を保存できます。ネステッド属性の更新はデフォルトでは無効になっており、accepts_nested_attributes_forクラスメソッドで有効にできます。ネステッド属性が有効になると、モデル上に属性ライターメソッドが定義されます。

この属性ライターメソッドは、関連付けに基づいて命名されます。つまり、以下の例ではモデルにauthor_attributes=(attributes)pages_attributes=(attributes)という新しいメソッドが2つ追加されます。

class Book < ActiveRecord::Base
  has_one :author
  has_many :pages

  accepts_nested_attributes_for :author, :pages
end

: このとき、accepts_nested_attributes_forの対象となるすべての関連付けで:autosaveオプションが自動的に有効になります。

🔗 関連付けが1対1の場合

以下のMemberモデルがAvatarを1つ持っている場合を考えます。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar
end

1対1関連付けでネステッド属性を有効にすると、以下のようにmemberとavatarを同時に作成できるようになります。

params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
member = Member.create(params[:member])
member.avatar.id # => 2
member.avatar.icon # => 'smiling'

以下のように、member経由でavatarを更新することも可能になります。

params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
member.update params[:member]
member.avatar.icon # => 'sad'

idを提供せずに現在のavatarを更新したい場合は、以下のように:update_onlyオプションを追加する必要があります。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar, update_only: true
end
params = { member: { avatar_attributes: { icon: 'sad' } } }
member.update params[:member]
member.avatar.id # => 2
member.avatar.icon # => 'sad'

デフォルトでできるのは、関連付けられるモデル上の属性を設定・更新することだけです。関連するモデルを属性ハッシュを経由して破棄したい場合は、最初に:allow_destroy オプションで有効にしておく必要があります。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar, allow_destroy: true
end

これで、_destroyを属性ハッシュに追加すると、その値がtrueと評価される場合は、関連付けられるモデルが削除されるようになります。

member.avatar_attributes = { id: '2', _destroy: '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil

ただし、このモデルは親がsaveされるまでは削除されない点にご注意ください。

また、更新されたハッシュ内でそのモデルのidも指定しておかないと、モデルが削除されない点にもご注意ください。

🔗 1対多

以下のMemberモデルを考えてみましょう。このモデルには多数の投稿(posts)があります。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts
end

上のようにすることで、関連付けられているposts上の属性を、そのメンバーの属性ハッシュ経由で設定・更新できるようになります。値として、post属性のハッシュの配列を持つ:posts_attributesキーを含めてください。

id持たない個別のハッシュについては、以下のようにそれぞれ新しいレコードがインスタンス化されます(ただし、trueと評価される_destroyもハッシュに含まれている場合を除きます)。

params = { member: {
  name: 'joe', posts_attributes: [
    { title: 'Kari, the awesome Ruby documentation browser!' },
    { title: 'The egalitarian assumption of the modern citizen' },
    { title: '', _destroy: '1' } # これは無視される
  ]
}}

member = Member.create(params[:member])
member.posts.length # => 2
member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
member.posts.second.title # => 'The egalitarian assumption of the modern citizen'

以下のように:reject_ifにprocを設定することで、条件を満たさない場合に新規レコードハッシュを無視することも可能です。たとえば、上述の例は以下のように書き換えられます。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
end

params = { member: {
  name: 'joe', posts_attributes: [
    { title: 'Kari, the awesome Ruby documentation browser!' },
    { title: 'The egalitarian assumption of the modern citizen' },
    { title: '' } # :reject_ifのprocによって無視される
  ]
}}

member = Member.create(params[:member])
member.posts.length # => 2
member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
member.posts.second.title # => 'The egalitarian assumption of the modern citizen'

以下のように、利用するメソッドのシンボルも:reject_ifに渡せます。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, reject_if: :new_record?
end
class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, reject_if: :reject_posts

  def reject_posts(attributes)
    attributes['title'].blank?
  end
end

既に関連付けられているレコードと一致するidキーがハッシュに含まれている場合は、一致するレコードが変更されます。

member.attributes = {
  name: 'Joe',
  posts_attributes: [
    { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
    { id: 2, title: '[UPDATED] other post' }
  ]
}

member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
member.posts.second.title # => '[UPDATED] other post'

ただし上が適用されるのは、親モデルも更新される場合です。たとえば、「joe」という名前のmemberを作成すると同時にpostsも更新すると、ActiveRecord::RecordNotFoundエラーが発生します。

デフォルトでは、関連付けられるレコードは削除から保護されています。関連付けられるレコードを属性ハッシュ経由で削除したい場合は、最初に:allow_destroyオプションで削除を有効にしておく必要があります。これによって、以下のように既存のレコードを_destroyキーでも削除できるようになります。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, allow_destroy: true
end

params = { member: {
  posts_attributes: [{ id: '2', _destroy: '1' }]
}}

member.attributes = params[:member]
member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
member.posts.length # => 2
member.save
member.reload.posts.length # => 1

関連付けられるコレクションのネステッド属性は、(ハッシュの配列の代わりに)ハッシュのハッシュという形で渡すことも可能です。

Member.create(
  name: 'joe',
  posts_attributes: {
    first:  { title: 'Foo' },
    second: { title: 'Bar' }
  }
)

上は以下と同等です。

Member.create(
  name: 'joe',
  posts_attributes: [
    { title: 'Foo' },
    { title: 'Bar' }
  ]
)

この場合、:posts_attributesの値となるハッシュのキーは無視されます。ただし、そうしたキーとして'id':idを使うことは許されません。もしそうすると、ハッシュが配列にラップされて、単一postの属性ハッシュとして解釈されてしまいます。

関連付けられるコレクションの属性を「ハッシュのハッシュ」形式で渡す方法は、HTTP/HTMLパラメータから生成されるハッシュ(ハッシュの配列を自然な形で送信する方法がない)で利用できます。

🔗 保存

モデルのあらゆる変更(削除マークが付けられたレコードの削除も含む)は、親モデルが保存されると自動的かつアトミックに保存および削除されます。これは、親のsaveメソッドで開始されたトランザクションの内部で発生します。
ActiveRecord::AutosaveAssociationを参照。

🔗 親モデルの存在をバリデーションする

belongs_to関連付けは、デフォルトで親モデルの存在をバリデーションします。この振る舞いは、optional: trueを指定することで無効にできます。これは、たとえば親モデルが存在するかどうかのバリデーションを条件付きで行う場合に利用できます。

class Veterinarian < ActiveRecord::Base
  has_many :patients, inverse_of: :veterinarian
  accepts_nested_attributes_for :patients
end

class Patient < ActiveRecord::Base
  belongs_to :veterinarian, inverse_of: :patients, optional: true
  validates :veterinarian, presence: true, unless: -> { awaiting_intake }
end

なお、:inverse_ofオプションを指定しない場合は、Active Recordがヒューリスティックに基づいて逆関連付けの自動推測を試みます。

1対1のネステッド関連付けでは、新たな子オブジェクトを自分で(メモリ上に)ビルドしてから代入する場合、以下のように、このモジュールはそれを上書きしません。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar

  def avatar
    super || build_avatar(width: 200)
  end
end

member = Member.new
member.avatar_attributes = {icon: 'sad'}
member.avatar.width # => 200

🔗 定数

🔗 REJECT_ALL_BLANK_PROC

# REJECT_ALL_BLANK_PROC
proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }

🔗 publicインスタンスメソッド

🔗 accepts_nested_attributes_for(*attr_names)

指定された1つ以上の関連付けで属性ライターを定義します。

サポートされているオプション:

:allow_destroy
trueを指定すると、_destroyキーを持ち、かつその値がtrueに評価される(例: 1、'1'、true、または'true')属性ハッシュのメンバーをすべて削除します。このオプションはデフォルトではオフになっています。
:reject_if
Proc、または特定の属性ハッシュでレコードをビルドすべきかどうかをチェックするメソッドを指すSymbolを指定できます。ハッシュは指定のProcまたはメソッドに渡され、Procまたはメソッドはtruefalseを返す必要があります。 :reject_ifが指定されていない場合は、trueと評価される_destroy値を持たないすべての属性ハッシュについてレコードがビルドされます。Procの代わりに:all_blankを渡すと、_destroyの値以外の属性がすべて空白であるレコードを却下するprocを作成します。
:limit
ネステッド属性で処理可能な、関連付けられるレコードの最大個数を指定できます。この個数は、Procまたはメソッドを指すSymbolでも指定可能で、Procまたはメソッドは数値を返す必要があります。ネステッド属性の配列サイズが指定の最大個数を超えると、NestedAttributes::TooManyRecords例外が発生します。:limitオプションを省略した場合、任意の個数の関連付けを処理できます。:limitオプションを適用可能なのは「1対多」関連付けのみである点にご注意ください。
:update_only
このプションを1対1関連付けで使うと、関連付けられるレコードが既に存在する場合にネステッド属性がどう使われるかを指定できます。一般に、既存のレコードは「新しい属性値セットで更新される」か「それらの値を含むまったく新しいレコードに置き換えられる」かのどちらかになる可能性があります。:update_onlyオプションはデフォルトではfalseであり、既存のレコードの更新にネステッド属性が使われるのは、ネステッド属性にレコードのidが含まれている場合だけです。さもなければ、新しいレコードがインスタンス化されて既存のレコードの置き換えに使われます。ただし、:update_onlyオプションをtrueに設定すると、idが存在するかどうかにかかわらず、レコードの属性更新で常にネステッド属性が使われるようになります。このオプションは、コレクションの関連付けでは無視されます。

例:

# avatar_attributes=を作成する
accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
# avatar_attributes=を作成する
accepts_nested_attributes_for :avatar, reject_if: :all_blank
# avatar_attributes=とposts_attributes=を作成する
accepts_nested_attributes_for :avatar, :posts, allow_destroy: true

GitHubコード

関連記事

Rails: ActiveRecord標準のattributes APIドキュメント(翻訳)

Rails: ActiveRecord::DelegatedType APIドキュメント(翻訳)

The post Rails API: ActiveRecord::NestedAttributes(翻訳) first appeared on TechRacho.

Rails 7.1.0 Active Record CHANGELOG(翻訳)

$
0
0

🔗 Active Record CHANGELOG(v7.1.0.rc2)

🔗 rails db:drop now removes -shm and -wal sqlite files by codergeek121 · Pull Request #49436 · rails/rails

rails db:drop実行時にSQLite3のshmファイルや-walファイルも削除するようになった。

Niklas Häusele
同CHANGELOGより

🔗 Revert "Merge pull request #49056 from joshuay03/raise-on-duplicate-a… · rails/rails@0ba14eb

同一クラス内で、ある関連付けに対する#accepts_nested_attributes_forが2回以上宣言された場合にArgumentErrorを発生する変更(#49056)を取り消し。

ここで取り消された振る舞いは、#accepts_nested_attributes_forがconcern内で定義され、そのconcernをincludeしているクラス内でオーバーライドされる場合に壊れる。

Rafael Mendonça França
同CHANGELOGより

🔗 Active Record CHANGELOG(v7.1.0.rc1)

🔗 Rename back unique keys to unique constraints by kamipo · Pull Request #49383 · rails/rails

関連: Add support for unique constraints (PostgreSQL-only). by alpaca-tc · Pull Request #46192 · rails/rails

(PostgreSQL)unique制約のネーミングを改善。

*_unique_keyという命名だと、uniqueインデックスの短縮形と誤解される。
*_unique_constraintという命名なら誤解されない。

Rails 7.1.0.beta1以前:

add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_section_position"
remove_unique_key :sections, name: "unique_section_position"

変更後:

add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_section_position"
remove_unique_constraint :sections, name: "unique_section_position"

Ryuta Kamizono
同CHANGELOGより

🔗 Fix duplicate escaping of quotes for check constraint expressions in MySQL schema by Flixt · Pull Request #42429 · rails/rails

MySQLを利用中、スキーマダンプ内のCHECK制約に含まれる一重引用符''が重複する問題を修正。

CHECK制約に渡す式に引用符が既に含まれていると、mysql2アダプタのスキーマダンプが無効になっていた。

修正: #42424

Felix Tscheulin
同CHANGELOGより

🔗 Performance tune the SQLite3 adapter connection configuration by fractaledmind · Pull Request #49349 · rails/rails

SQLite3アダプタのコネクション設定のパフォーマンスを高速化。

Write-Ahead-Log(WAL)synchronous=NORMALモードで利用するようにし、ジャーナルサイズの上限(journal_size_limit)、健全なサイズの共有メモリバッファ(mmap_size)、共有キャッシュ(cache_size)を設定したことで、Railsアプリケーションで平均して2倍改善された。

Stephen Margheim
同CHANGELOGより

参考: Enhancing your Rails app with SQLite | Fractaled Mind

🔗 Allow SQLite3 busy_handler to be configured with simple max number of retries by fractaledmind · Pull Request #49352 · rails/rails

SQLite3のbusy_handlerで、retriesをシンプルに上限回数まで行える設定を追加。

パフォーマンスが重要なアプリケーションでは、ビジーなコネクションを遅延なしで再試行するのが望ましい。database.ymlretries値(integer)で指定した回数に達するまで、busy_handler関数でビジーコネクションへの再接続を(待ち時間を指数増加せずに)試行するようになった。

Stephen Margheim
同CHANGELOGより

🔗 Add SQLite3 support for supports_insert_returning? by fractaledmind · Pull Request #49290 · rails/rails

SQLite3アダプタでsupports_insert_returning?をサポート。

supports_insert_returning?AbstractAdapter)を完全に実装することで、SQLite3アダプタでも自動生成カラム([#48241[#ar34])とカスタム主キーをサポートする。

Stephen Margheim
同CHANGELOGより

参考: Rails API ActiveRecord::ConnectionAdapters::AbstractAdapter

🔗 Ensure the SQLite3 adapter handles default functions with the || concatenation operator by fractaledmind · Pull Request #49287 · rails/rails

SQLite3アダプタで、結合演算子||を含むデフォルト関数を処理できるようになった。

従来は、デフォルト関数で"'Ruby ' || 'on ' || 'Rails'"のように静的な文字列が生成されていた。
改修によって、"Ruby on Rails"を適切にアダプタに渡して利用できるようになった。

change_column_default "test_models", "ruby_on_rails", -> { "('Ruby ' || 'on ' || 'Rails')" }

Stephen Margheim
同CHANGELOGより

🔗 dump PostgreSQL schemas as part of the schema dump by lsylvester · Pull Request #49164 · rails/rails

PostgreSQL固有のスキーマをschema.rbにダンプできるようにするcreate_schemaを追加。

Lachlan Sylvester
同CHANGELOGより

🔗 Active Record CHANGELOG(v7.1.0.beta1)

🔗 Encryption: support support_unencrypted_data at a per-attribute level by ghiculescu · Pull Request #49072 · rails/rails

属性ごとに設定されるsupport_unencrypted_dataオプションを暗号化に追加。

特定の暗号化済み属性だけをsupport_unencrypted_dataで暗号化しないように設定可能になった。
このオプションは、ActiveRecord::Encryption.config.support_unencrypted_data == trueが設定済みの場合にのみ有効。

class User < ActiveRecord::Base
  encrypts :name, deterministic: true, support_unencrypted_data: false
  encrypts :email, deterministic: true
end

Alex Ghiculescu
同CHANGELOGより

参考: 週刊Railsウォッチ20230926: 暗号化属性の平文データサポートをsupport_unencrypted_dataオプションで属性ごとに無効化できるようになった

🔗 Instrument Active Record transactions by ipc103 · Pull Request #49192 · rails/rails

Active Recordのトランザクションでinstrumentation(計測)を利用可能になった。

トランザクションイベントをサブスクライブすることでトラッキングやinstrumentationで利用できるようになる。イベントペイロードには、コネクションの他にタイミングの詳細詳細情報も含まれる。

ActiveSupport::Notifications.subscribe("transaction.active_record") do |event|
  puts "Transaction event occurred!"
  connection = event.payload[:connection]
  puts "Connection: #{connection.inspect}"
end

Daniel Colson, Ian Candy
同CHANGELOGより

参考: 週刊Railsウォッチ20230926: Active Recordのトランザクションをinstrumentationで計測できるようになった
参考: Active Support の Instrumentation 機能 - Railsガイド

🔗 Support composite foreign keys via migration helpers by fatkodima · Pull Request #47637 · rails/rails

マイグレーションヘルパーで複合主キーをサポート。

# "carts"テーブルの主キーが"(shop_id, user_id)"だとする

add_foreign_key(:orders, :carts, primary_key: [:shop_id, :user_id])

remove_foreign_key(:orders, :carts, primary_key: [:shop_id, :user_id])
foreign_key_exists?(:orders, :carts, primary_key: [:shop_id, :user_id])

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20230926: マイグレーションヘルパーで複合主キーをサポート

🔗 Adds support for if_not_exists when adding a check constraint. by ccutrer · Pull Request #49178 · rails/rails

マイグレーションにCHECK制約を追加するときのif_not_existsオプションをサポート。

add_check_constraint :posts, "post_type IN ('blog', 'comment', 'share')", if_not_exists: true

Cody Cutrer
同CHANGELOGより

参考: 週刊Railsウォッチ20230926: マイグレーションのadd_check_constraintif_not_existsオプションを渡せるようになった

🔗 [Fix #49055] Raise an ArgumentError when #accepts_nested_attributes_for is redeclared for an association by joshuay03 · Pull Request #49056 · rails/rails

注: このプルリクはその後0f7fe4aで取り消されました。

同じクラス内の関連付けに対して#accepts_nested_attributes_forが複数回宣言されるとArgumentErrorを発生するようになった。
従来は、最後の宣言が以前の宣言をエラーなしでオーバーライドしていた。サブクラスでのオーバーライドは引き続き許可されている。

Joshua Young
同CHANGELOGより

Rails API: ActiveRecord::NestedAttributes(翻訳)

🔗 Deprecate passing rewhere to merge by HParker · Pull Request #45498 · rails/rails

#mergeメソッドのrewhereオプションを非推奨化。

#mergeメソッドのrewhereオプションは置き換えなしに非推奨化され、Rails 7.2で削除される予定。

Adam Hess
同CHANGELOGより

参考: 週刊Railsウォッチ20230913: mergerewhereオプションを渡すことが非推奨化された

🔗 Fix unscope not working when where by tripe dot range by ippachi · Pull Request #48095 · rails/rails

unscopeが特定のケース(トリプルドット...)で動作しない問題を修正。

修正前:

Post.where(id: 1...3).unscope(where: :id).to_sql # "SELECT `posts`.* FROM `posts` WHERE `posts`.`id` >= 1 AND `posts`.`id` < 3"

修正後:

Post.where(id: 1...3).unscope(where: :id).to_sql # "SELECT `posts`.* FROM `posts`"

修正:#48094

Kazuya Hatanaka
同CHANGELOGより

参考: 週刊Railsウォッチ20230913: whereでトリプルドット... rangeを使うとunscopeが効かない問題を修正

🔗 Change has_secure_token default to on: :initialize by seanpdoyle · Pull Request #48912 · rails/rails

has_secure_tokenのデフォルトをon: :initializeに変更。

新しいデフォルト値をon: :createからon: :initializeに変更した。
この設定はconfig.active_record.generate_secure_token_onコンフィグで変更可能。

config.active_record.generate_secure_token_on = :create

Sean Doyle
同CHANGELOGより

参考: config.active_record.generate_secure_token_on -- Rails アプリケーションを設定する - Railsガイド

🔗 Fix change_column not setting precision for sqlite by skipkayhil · Pull Request #49090 · rails/rails

7.0以降のマイグレーションとSQLiteを利用するとchange_columndatetimeカラムがprecision: 6に設定されない問題を修正。

Hartley McGuire
同CHANGELOGより

🔗 Support composite identifiers in to_key by nvasilevski · Pull Request #48998 · rails/rails

to_keyで複合idをサポート。

#idが既に配列の場合はto_key#id値をArrayでラップしないようになる。

Nikita Vasilevsky
同CHANGELOGより

参考: Rails API to_key -- ActiveModel::Conversion
参考: Rails API to_key -- ActiveRecord::AttributeMethods::PrimaryKey

🔗 Make enums validatable without raising error by mechnicov · Pull Request #49100 · rails/rails

enumvalidateオプションを追加。

class Contract < ApplicationRecord
  enum :status, %w[in_progress completed], validate: true
end
Contract.new(status: "unknown").valid?   # => false
Contract.new(status: nil).valid?         # => false
Contract.new(status: "completed").valid? # => true

class Contract < ApplicationRecord
  enum :status, %w[in_progress completed], validate: { allow_nil: true }
end
Contract.new(status: "unknown").valid?   # => false
Contract.new(status: nil).valid?         # => true
Contract.new(status: "completed").valid? # => true

Edem Topuzov, Ryuta Kamizono
同CHANGELOGより

参考: 週刊Railsウォッチ20230913: Active Recordのenumにエラーをraiseしないvalidateオプションが追加された

🔗 Use already loaded relation when batching if possible by HParker · Pull Request #48876 · rails/rails

in_batchesメソッドが、可能な場合に読み込み済みのリレーションを利用するようになった。

既に読み込まれているリレーションでバッチメソッドを呼び出すと、データベースからレコードを再度取得せずに読み込み済みのレコードを使うようになる。

Adam Hess
同CHANGELOGより

参考: Rails API in_batches -- ActiveRecord::Batches

🔗 Deprecate read_attribute(:id) returning the primary key by adrianna-chang-shopify · Pull Request #49019 · rails/rails

主キーが:idでない場合にread_attribute(:id)が主キーを返す振る舞いを非推奨化。

Rails 7.2以後は、read_attribute(:id)はモデルの主キーに関係なくidカラムの値を返すようになる。主キーの値を取得するには、代わりに#idを使うこと。複合主キーモデルでは、read_attribute(:id)は今後もidカラムの値を返すようになる。

Adrianna Chang
同CHANGELOGより

参考: 週刊Railsウォッチ20230906: 主キーが:idでない場合に主キーを返すread_attribute(:id)を非推奨化

🔗 Fix 6.1 change_table setting datetime precision by skipkayhil · Pull Request #48974 · rails/rails

マイグレーション6.1のchange_tableが設定するdatetimeカラムの精度を修正。

Hartley McGuire
同CHANGELOGより

🔗 Fix 6.1 change_column setting datetime precision by skipkayhil · Pull Request #48969 · rails/rails

マイグレーション6.1のchange_columnが設定するdatetimeカラムの精度を修正。

Hartley McGuire
同CHANGELOGより

🔗 Use alias_attribute to provide id_value alias for id attribute by adrianna-chang-shopify · Pull Request #48930 · rails/rails

レコードのidカラムの生の値にアクセスするActiveRecord::Base#id_valueを追加。

このエイリアスは、:idカラムを宣言しているモデルに対してのみ提供される。

Adrianna Chang
同CHANGELOGより

参考: 週刊Railsウォッチ20230906: モデルのid属性をid_valueで取得できるようになった

🔗 Fix tracking previous changes for ActiveRecord::Store accessors with underlying JSON data column by rdimartino · Pull Request #43386 · rails/rails

JSONで構造化されたデータ型を使うカラムにおけるActiveRecord::Storeの直前の変更トラッキングを修正。

従来は、JSONで構造化されたデータベース型を持っているカラムに対してストアがstore_accessorとして定義されていると、最後に保存したときの変更にアクセスする以下のメソッドが動作しなかった。

  • #saved_change_to_key?
  • #saved_change_to_key
  • #key_before_last_save

Robert DiMartino
同CHANGELOGより

🔗 Support NULLS NOT DISTINCT in Postgres 15+ by grjones · Pull Request #48608 · rails/rails

PostgreSQL 15以降のインデックスでNULLS [NOT] DISTINCTを完全サポート。

前回の作業でマイグレーションでのインデックス作成は行えるようになっていたが、schema.rbではサポートされていなかった。また、NULLS [NOT] DISTINCTのマッチ順序が正しくなかったため、スキーマ検出が一定していなかった。

Gregory Jones
同CHANGELOGより

参考: 週刊Railsウォッチ20230823: PostgreSQL 15のNULLS NOT DISTINCTをサポート
参考: PostgreSQL 15.4ドキュメント CREATE INDEX

🔗 Allow escaping of literal colons in ActionRecord::Sanitization#replace_named_bind_variables by f3ndot · Pull Request #48852 · rails/rails

名前付きバインド変数が使われている場合にsanitize_sql_*メソッドでコロン:をエスケープするようになった。

Justin Bull
同CHANGELOGより

参考: 週刊Railsウォッチ20230823: ActionRecord::Sanitization#replace_named_bind_variablesでコロン:をエスケープできるよう修正

🔗 Fix #previously_new_record? on destroyed records by adrianna-chang-shopify · Pull Request #48796 · rails/rails

#previously_new_record?が削除済みレコードでtrueを返す問題を修正。

従来は、レコードを作成してから削除すると#previously_new_record?trueを返すことがあった。
修正後は、レコードに対するどのUPDATEおよびDELETEでも変更を考慮することで#previously_new_record?falseを返すようになった。

Adrianna Chang
同CHANGELOGより

参考: この修正はRails 7.0.7でリリース済みです

🔗 Specify when to generate has_secure_token by seanpdoyle · Pull Request #47420 · rails/rails

has_secure_tokenon:でコールバックを指定できるようになった。

class User < ApplicationRecord
  has_secure_token on: :initialize
end

User.new.token # => "abc123...."

Sean Doyle
同CHANGELOGより

訳注: デフォルトはon: createです。

参考: 週刊Railsウォッチ20230809: has_secure_tokenを生成するタイミングを指定可能になった

🔗 Fix counter_cache create/concat with overlapping counter_cache_column by casperisfine · Pull Request #48665 · rails/rails

関連付けのカラムがオーバーラップしている場合のインメモリカウンタキャッシュの増加を修正。

2つの関連付けに名前の似ているカウンタキャッシュカラムがある場合、Active Recordが間違った方をインクリメントする可能性があった。

Jacopo Beschi, Jean Boussier
同CHANGELOGより

🔗 Don't show secrets for Active Record's Cipher::Aes256Gcm#inspect. by p8 · Pull Request #48679 · rails/rails

Active RecordのCipher::Aes256Gcm#inspectでsecretsを表示しないようになった。

修正前:

ActiveRecord::Encryption::Cipher::Aes256Gcm.new(secret).inspect
"#<ActiveRecord::Encryption::Cipher::Aes256Gcm:0x0000000104888038 ... @secret=\"\\xAF\\bFh]LV}q\\nl\\xB2U\\xB3 ... >"

修正後:

ActiveRecord::Encryption::Cipher::Aes256Gcm(secret).inspect
"#<ActiveRecord::Encryption::Cipher::Aes256Gcm:0x0000000104888038>"

Petrik de Heus
同CHANGELOGより

参考: Rails API ActiveRecord::Encryption::Cipher::Aes256Gcm

🔗 Active Record commit transaction on return, break and throw by casperisfine · Pull Request #48600 · rails/rails

トランザクションの振る舞いを非ローカルなreturnbreakthrowでコミットする以前の振る舞いに戻した。

Model.transaction do
  model.save
  return
  other_model.save # 実行されなくなった
end

かつて、エラーが発生した場合にのみロールバックがトリガーされていた時代があったが、Ruby 2.3のtimeoutライブラリが実行を中断するためにthrowを使うようになり、オープン中のトランザクションがコミットされるという逆効果が生じていた。

これを解決するために、Active Record 6.1ではトランザクションを(コミットではなく)ロールバックするように動作を変更していた(不完全なトランザクションをコミットする可能性よりも安全性が高いため)。
Rails 6.1以降は、transactionブロック内でのreturnbreakthrowの利用は根本的に非推奨となっていた。

しかし、timeout 0.4.0のリリースにより、Timeout.timeoutで(throwではなく)再びエラーをraiseするようになった。これによって、Active Recordの振る舞いを当初の(驚きの少ない)動作に戻すことが可能になった。

Rails 6.1より前の「歴史的な」振る舞いは、以下の設定で有効にできる。

Rails.application.config.active_record.commit_transaction_on_non_local_return = true

また、Rails 7.1で作成する新規アプリケーションはデフォルトでこの振る舞いになる。

Jean Boussier
同CHANGELOGより

参考: Deprecate committing a transaction exited with return or throw by dylanahsmith · Pull Request #29333 · rails/rails
参考: config.active_record.commit_transaction_on_non_local_return -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20230809: トランザクションがreturnbreakthrowでコミットするようになった

🔗 Deprecate name argument in remove_connection by eileencodes · Pull Request #48681 · rails/rails

#remove_connectionname引数を非推奨化。

#remove_connectionname引数は置き換えなしで非推奨化された。
#remove_connectionは、コネクションを確立するクラスで直接呼び出されるべき。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API remove_connection -- ActiveRecord::ConnectionHandling

🔗 Fix has_one through singular building with inverse. by gmcgibbon · Pull Request #48674 · rails/rails

単数形の逆関連付けを経由するhas_one through:関連付けでレコードをビルド可能になった。

belongs_to through:関連付けでは、外部キーを主キーのモデルにリンクする必要はない。
has_oneの場合はこの関連付けがミュータブルではないため、レコードをビルドできなかった。

Gannon McGibbon
同CHANGELOGより

参考: 週刊Railsウォッチ20230802: belongs_to :inverse_ofを介したhas_one :through関連付けで、レコードをビルドできるように修正

🔗 Disable database prepared statements when query logs are enabled by zzak · Pull Request #48631 · rails/rails

データベースでクエリログが有効な場合はプリペアドステートメントを無効にするようになった。

クエリログは毎回一意のクエリを作成するため、プリペアドステートメントとクエリログは互換性がない。

zzak, Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20230802: クエリログが有効な場合はプリペアドステートメントを無効にするよう修正

🔗 Add a encryption option to support previous data encrypted non-deterministically with a SHA1 hash digest by jorgemanrubia · Pull Request #48530 · rails/rails

SHA1ハッシュダイジェストで非決定論的に暗号化された既存データの復号をサポート

これにより、Active Recordの新しい暗号化オプションが追加され、SHA1ハッシュダイジェストで非決定論的に暗号化されたデータを復号できるようになる。

Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true

新しいオプションは、7.0から7.1にアップグレードする際の問題に対処する。Active Record暗号化の初期化方法にバグがあったため、非決定論的暗号化に用いるキープロバイダは、RailsがRails.application.config.active_support.key_generator_hash_digest_classでグローバルに設定したものではなく、SHA-1をダイジェストクラスとして利用していた。

Cadu Ribeiro and Jorge Manrubia
同CHANGELOGより

参考: config.active_record.support_sha1_for_non_deterministic_encryption -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20230725: SHA1ハッシュダイジェストで非決定論的に暗号化された従来のデータの復号をサポート

🔗 Adding PG enum drop, rename, add value, rename value by rayfaddis · Pull Request #44898 · rails/rails

PostgreSQLのマイグレーションでenumのリネーム、値の追加、値のリネームが可能になった。

rename_enumおよびrename_enum_valueはリバース可能である。add_enum_valueはPostgreSQLの制約(enum値を削除できない)によりリバースできない。代替手段として、enum全体を削除してから再作成すること。

rename_enum :article_status, to: :article_state
add_enum_value :article_state, "archived" # will be at the end of existing values
add_enum_value :article_state, "in review", before: "published"
add_enum_value :article_state, "approved", after: "in review"
rename_enum_value :article_state, from: "archived", to: "deleted"

Ray Faddis
同CHANGELOGより

Rails: PostgreSQLアダプタでenumのリネーム、enum値の追加とリネームが可能になった(翻訳)

🔗 Allow composite primary key to be derived from schema by nvasilevski · Pull Request #47633 · rails/rails

スキーマから複合主キーを導出可能になった。

複合主キーを含むスキーマを持つアプリケーションを起動したときにwarningが発生しないようになり、ActiveRecord::Base#primary_keyの値がnilになることもなくなる。
以下のようなtravel_routesテーブル定義とTravelRouteモデルがあるとする。

create_table :travel_routes, primary_key: [:origin, :destination], force: true do |t|
  t.string :origin
  t.string :destination
end

class TravelRoute < ActiveRecord::Base; end

このTravelRoute.primary_keyの値は自動的に["origin", "destination"]に導出される。

Nikita Vasilevsky
同CHANGELOGより

参考: 週刊Railsウォッチ20230628: スキーマから複合主キーを導出可能になった

🔗 Store connection_pool in database-related exceptions by luanzeba · Pull Request #48295 · rails/rails

connection_poolにコネクションアダプタからraiseされる例外を保存するようになった。

例外を引き起こしたコネクションや、ロール、シャードなどのコンテキストがconnection_poolに追加される。

Luan Vieira
同CHANGELOGより

参考: 週刊Railsウォッチ20230628: データベース関連の例外をconnection_poolに保存するようになった

🔗 Support batching using composite primary keys and multiple column ordering by TakuyaKurimoto · Pull Request #48268 · rails/rails

複合主キーを持つテーブルのfind_eachfind_in_batchesin_batches:asc:descを指定可能になった。

複合主キーがあるテーブルでfind_eachfind_in_batchesin_batchesを実行するときに、キーごとに:asc:descを指定可能になる。

Person.find_each(order: [:desc, :asc]) do |person|
  person.party_all_night!
end

Takuya Kurimoto
同CHANGELOGより

参考: 週刊Railsウォッチ20230628: 複合主キーを持つテーブルのfind_eachfind_in_batchesin_batches:asc:descを指定可能になった

🔗 Fix polymorphic association subquery by lazaronixon · Pull Request #48362 · rails/rails

has_one/has_manyポリモーフィックリレーションの関連付けにおけるwhereの振る舞いを修正。

修正前:

Treasure.where(price_estimates: PriceEstimate.all)
#=> SELECT (...) WHERE "treasures"."id" IN (SELECT "price_estimates"."estimate_of_id" FROM "price_estimates")

修正後:

Treasure.where(price_estimates: PriceEstimate.all)
#=> SELECT (...) WHERE "treasures"."id" IN (SELECT "price_estimates"."estimate_of_id" FROM "price_estimates" WHERE "price_estimates"."estimate_of_type" = 'Treasure')

Lázaro Nixon
同CHANGELOGより

この修正は、Rails 7.0.6でリリース済みです

🔗 Assign auto populated columns on Active Record object creation by nvasilevski · Pull Request #48241 · rails/rails

Active Recordでのレコード作成時に自動生成カラムを割り当てられるようになる。

レコード作成ロジックを変更することで、auto_incrementカラムをレコード作成時に割り当て可能にする。これによって、モデルの主キーへのリレーションにかかわらず、レコード作成直後にauto_incrementカラムを割り当てられるようになる。

この変更によるメリットが最も大きいのはPostgreSQLアダプタで、RETURNINGステートメントを利用するレコードのINSERT後に、任意の個数の自動生成カラムをオブジェクトに割り当てられるようになる。

Nikita Vasilevsky
同CHANGELOGより

参考: 週刊Railsウォッチ20230621: オブジェクト作成時にデータベース側の自動入力属性を割り当て可能にする

🔗 Set default_shard from connects_to hash by eileencodes · Pull Request #48353 · rails/rails

connected_toshardsハッシュの最初のキーをdefault_shardで使うようになった。

アプリケーションによっては、コネクションモデルのシャード名に:defaultを使いたくない場合がある。残念なことに、Active Recordはプールマネージャから正しいコネクションを得るために何らかのシャードを仮定しなければならないため、:defaultシャードの存在を期待する。

アプリケーションで強制的に手動設定する代わりに、connects_toがシャードのハッシュからデフォルトシャード名を推測して、最初のシャードをデフォルトであると仮定するようになる。

たとえば以下のようなモデルがあるとする。

class ShardRecord < ApplicationRecord
self.abstract_class = true

connects_to shards: {
  shard_one: { writing: :shard_one },
  shard_two: { writing: :shard_two }
}

これで、このクラスのdefault_shardshard_oneに設定されるようになる。

修正: #45390

Eileen M. Uchitelle
同CHANGELOGより

参考: 週刊Railsウォッチ20230621: connected_toshardsハッシュの最初のキーをdefault_shardで使うようになった

🔗 Fix change_in_place? for binary serialized columns by casperisfine · Pull Request #48274 · rails/rails

背後のカラムがバイナリエンコードになっているシリアライズ属性での変更検出を修正。

Jean Boussier
同CHANGELOGより

この修正は、Rails 7.0.5でリリース済みです
参考: 週刊Railsウォッチ20230613: change_in_place?の挙動を修正

🔗 Implement ActiveRecord.disconnect_all! to close all connections by casperisfine · Pull Request #47856 · rails/rails

全コネクションプールにある全コネクションを即座にクローズするActiveRecord.disconnect_all!を追加。

Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20230613: マルチプルDBで使えるActiveRecord.disconnect_all!が追加

🔗 Discard connections which may be left in a transaction by nicholasdower · Pull Request #48200 · rails/rails

トランザクションに残っているコネクションを破棄する機能を改善。

エラーが原因で、within_new_transactionのコネクションがオープン中のトランザクションに予期せず残ってしまうことがある。そうなるとコネクションが再利用されてしまい、以下のようなエラーが発生する可能性がある。

  • 書き込みが実際は成功しているにもかかわらず失敗したように見える
  • 書き込みが実際は失敗しているにもかかわらず成功したように見える
  • 読み込んだデータが古い、またはコミットされていない

従来は以下のケースについては検出されていた。

  • トランザクション内でエラーが発生し、ロールバックを試行中に別のエラーが発生した場合

改修によって、以下のケースも検出されるようになった。

  • トランザクションの開始直後にエラーが発生した場合
  • トランザクションのコミット中にエラーが発生し、ロールバックを試行中に別のエラーが発生した場合
  • トランザクションのロールバック中にエラーが発生した場合

Nick Dower
同CHANGELOGより

🔗 Make Active Record's query cache an LRU by casperisfine · Pull Request #48110 · rails/rails

Active Recordのクエリキャッシュが直近で最も利用頻度の低い(LRU: least recently used)エントリーを削除するようになった。

デフォルトでは、最も直近で利用された100件のクエリのみを維持する。
このキャッシュサイズはdatabase.ymlで変更可能。

development:
adapter: mysql2
query_cache: 200

クエリキャッシュそのものを無効にすることも可能。

development:
adapter: mysql2
query_cache: false

Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20230607: 保持するクエリキャッシュを直近50件までに変更した

🔗 Deprecate check_pending! in favor of check_all_pending! by eileencodes · Pull Request #48134 · rails/rails

check_pending!を非推奨化。今後はcheck_all_pending!を使うこと。

check_pending!が保留中のマイグレーションをチェックする対象は、現在の(または渡された)データベースコネクションのみでマルチプルデータベースに対応していない。このcheck_pending!は非推奨化される。今後は、指定の環境にある複数のデータベース設定ですべての保留中マイグレーションをチェックするcheck_all_pending!を使うこと。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API check_all_pending! -- ActiveRecord::Migration

🔗 Make increment_counter/decrement_counter accept an amount argument by fatkodima · Pull Request #48128 · rails/rails

increment_counterdecrement_counterby:オプションで増分値を渡せるようになった。

Post.increment_counter(:comments_count, 5, by: 3)

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20230607: Active Recordのカウンタインクリメントやデクリメントに1以外の増分量を効果的に指定できるようになった

🔗 Add intersects? to Relation by john-h-k · Pull Request #47670 · rails/rails

Array#intersect?ActiveRecord::Relationに追加。

Array#intersect?はRuby 3.1以降でのみ利用可能。

これにより、ActiveRecord::RelationでRuboCopのStyle/ArrayIntersect copを利用できるようになる。

John Harry Kelly
同CHANGELOGより

参考: 週刊Railsウォッチ20230524: ActiveRecord::Relationintersects?が追加された

🔗 deferrable foreign key can be passed to references by alpaca-tc · Pull Request #47671 · rails/rails

マイグレーションでdeferrableオプション(先延ばし可能)を有効にした外部キーをt.referencesに渡せるよう修正。

Hiroyuki Ishii
同CHANGELOGより

参考: 週刊Railsウォッチ20230524: マイグレーションでreferencesdeferrableオプション付きの外部キーを渡しても無視される問題を修正

🔗 Deprecate deferrable: true option of add_foreign_key by alpaca-tc · Pull Request #47659 · rails/rails

関連: #46192

add_foreign_keydeferrable: trueオプションを非推奨化。

非推奨化されたdeferrable: trueオプションはRails 7.2で削除される予定。今後はdeferrable: :immediateが推奨される。

非推奨化の理由は、deferrable: truedeferrable: :deferredの意味がわかりにくいため(どちらの値もtruthyに見えてしまう)。

推奨されるdeferrable: :immediateの振る舞いは、#46192で追加されたadd_unique_keydeferrableオプションと同じ。

Hiroyuki Ishii
同CHANGELOGより

参考: 週刊Railsウォッチ20230524: add_foreign_keyのdeferrable: trueオプションを非推奨化する

🔗 Make Adapter#exec_query clear the query cache by casperisfine · Pull Request #48069 · rails/rails

関連: Active Record: clear query cache automatically when calling #execute by casperisfine · Pull Request #48061 · rails/rails

AbstractAdapter#execute#exec_queryでクエリキャッシュをクリアするよう修正。

リードオンリーのSQLクエリをクエリキャッシュをクリアせずに実行する必要がある場合は、AbstractAdapter#select_allを使うこと。

Jean Boussier
同CHANGELOGより

参考: Rails API select_all -- ActiveRecord::ConnectionAdapters::DatabaseStatements

🔗 Add CTE support for joins by palkan · Pull Request #46843 · rails/rails

関連: Common Table Expression support added "out-of-the-box" by vlado · Pull Request #37944 · rails/rails

.joins.left_outer_joinsもCTE(Common Table Expression)で使えるようになった。

例:

Post
  .with(commented_posts: Comment.select(:post_id).distinct)
  .joins(:commented_posts)
#=> WITH (...) SELECT ... INNER JOIN commented_posts on posts.id = commented_posts.post_id

Vladimir Dementyev
同CHANGELOGより

参考: §Common Table Expression -- Hierarchical and recursive queries in SQL - Wikipedia
参考: 週刊Railsウォッチ20230524: joinsでCTEをサポート

🔗 Add load hook for ActiveRecord::ConnectionAdapters::Mysql2Adapter by fatkodima · Pull Request #48012 · rails/rails

ActiveRecord::ConnectionAdapters::Mysql2Adapter(名前はactive_record_mysql2adapter)にloadフックを追加。これにより、ActiveRecord::ConnectionAdapters::Mysql2Adapterの一部をオーバーライド可能になり、既にloadフックを備えているPostgreSQLAdapterSQLite3Adapterと統一される。

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20230524: Mysql2Adapterにloadフックを追加

🔗 Introduce adapter for Trilogy, a MySQL-compatible DB client by adrianna-chang-shopify · Pull Request #47880 · rails/rails

Trilogyデータベースクライアント用のアダプタを導入。

TrilogyはMySQL互換のデータベースクライアント。Railsアプリケーションのconfig/database.ymlファイルを以下のように設定することでTrilogyを利用できる。

development:
adapter: trilogy
database: blog_development
pool: 5

または、以下のようにDATABASE_URL環境変数による設定でも利用できる。

ENV['DATABASE_URL'] # => "trilogy://localhost/blog_development?pool=5"

Adrianna Chang
同CHANGELOGより

trilogy-libraries/trilogy - GitHub
trilogy-libraries/activerecord-trilogy-adapter - GitHub

参考: 週刊Railsウォッチ20230502: Trilogyデータベースクライアント用のアダプタが導入される

🔗 Run after_commit callbacks defined on models in the correct order by ghiculescu · Pull Request #46992 · rails/rails

モデルで定義されているafter_commitコールバックが、定義順に正しく実行されるようになった。

class User < ActiveRecord::Base
  after_commit { puts("これが最初に呼ばれる") }
  after_commit { puts("これが次に呼ばれる") }
end

従来のコールバックは逆順で実行されていた。以下のコンフィグで新しい振る舞いを有効にできる。

config.active_record.run_after_transaction_callbacks_in_order_defined = true

新規アプリでは、これがデフォルトの振る舞いになる。

Alex Ghiculescu
同CHANGELOGより

参考: config.active_record.run_after_transaction_callbacks_in_order_defined -- Rails アプリケーションを設定する - Railsガイド

🔗 Infer foreign_key when inverse_of is present by Tiedye · Pull Request #47797 · rails/rails

has_one関連付けやhas_many関連付けでinverse_ofが指定されている場合にforeign_keyを推論するようになった。

has_many :citations, foreign_key: "book1_id", inverse_of: :book

上を以下のようにシンプルに書けるようになる。

has_many :citations, inverse_of: :book

foreign_keyは、対応するbelongs_to関連付けから読み取られる。

Daniel Whitney
同CHANGELOGより

参考: 週刊Railsウォッチ20230412: has_onehas_manyinverse_ofが存在する場合にfoerign_keyを推論するようになった

🔗 Fix Rails generated index name being too long by mscoutermarsh · Pull Request #47753 · rails/rails

自動生成されるインデックス名の長さに上限が設定されるようになった。

自動生成されるインデックス名は最大62バイトになった。この長さは、MySQL、PostgreSQL、SQLite3のインデックス名のデフォルトの最大長さに収まる。

インデックス名がこの上限を超えた場合は自動的に短縮される。

変更前(長すぎる):

index_testings_on_foo_and_bar_and_first_name_and_last_name_and_administrator

変更後(短縮形):

idx_on_foo_bar_first_name_last_name_administrator_5939248142

短縮形には、インデックス名がデータベースで一意になるようハッシュが追加される。

Mike Coutermarsh
同CHANGELOGより

参考: 週刊Railsウォッチ20230425: 自動生成されるインデックス名を上限で切り詰めるようになった

🔗 Implement marshal_dump and marshal_load on ActiveRecord::Base by casperisfine · Pull Request #47747 · rails/rails

安定性が高く最適化されたMarshalシリアライザがActive Recordモデルに導入された。

以下のコンフィグで有効になる。

config.active_record.marshalling_format_version = 7.1

Jean Boussier
同CHANGELOGより

参考: config.active_record.marshalling_format_version -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20230412: ActiveRecord::Basemarshal_dumpmarshal_loadを実装

🔗 Introduce query-by-tuple syntax by paarthmadan · Pull Request #47729 · rails/rails

複合主キー向けの"タプル"構文をwhereに導入。

whereを用いるクエリで新しいタプル(tuple)構文が使えるようになった。これは、キーに「カラムの配列」、値に「それに対応するタプルの配列」をそれぞれ渡せる。
このキーではカラムのリストを指定するが、値はそのカラムリストに適合する順序付きタプルの配列となる。
例:

# Cpk::Book => Cpk::Book(author_id: integer, number: integer, title: string, revision: integer)
# Cpk::Book.primary_key => ["author_id", "number"]
book = Cpk::Book.create!(author_id: 1, number: 1)
Cpk::Book.where(Cpk::Book.primary_key => [[1, 2]]) # => [book]
# Topic => Topic(id: integer, title: string, author_name: string...)
Topic.where([:title, :author_name] => [["The Alchemist", "Paul Coelho"], ["Harry Potter", "J.K Rowling"]])

Paarth Madan
同CHANGELOGより

参考: 週刊Railsウォッチ20230412: 複合主キー向けの"タプル"構文をwhereに導入

🔗 Allow SQL Warnings to be ignored using error codes by nickborromeo · Pull Request #47650 · rails/rails

SQL警告メッセージをエラーコードでフィルタできるようになった。

以下のActive Recordコンフィグで特定の警告コードを無視できる。

# 常に無視すべき警告の許可リストを設定する
config.active_record.db_warnings_ignore = [
  "1062", # MySQL Error 1062: Duplicate entry
]

この機能はMySQLアダプタとPostgreSQLアダプタでサポートされる。
Nick Borromeo
同CHANGELOGより

参考: 週刊Railsウォッチ20230412: SQL警告メッセージをエラーコードでフィルタできるようになった

🔗 Run a load hook when TestFixtures is included by andrewn617 · Pull Request #47690 · rails/rails

:active_record_fixtures遅延読み込みフックを導入。

この名前で定義したフックは、クラスにTestFixturesincludeされると常に実行されるようになる。

ActiveSupport.on_load(:active_record_fixtures) do
  self.fixture_paths << "test/fixtures"
end

klass = Class.new
klass.include(ActiveRecord::TestFixtures)

klass.fixture_paths # => ["test/fixtures"]

Andrew Novoselac
同CHANGELOGより

参考: 週刊Railsウォッチ20230412: TestFixturesincludeするたびにloadフックを実行するように修正

🔗 Introduce TestFixtures#fixture_paths by andrewn617 · Pull Request #47675 · rails/rails

TestFixtures#fixture_paths(複数形)を追加。

#fixture_pathsアクセサを使うことで、複数のフィクスチャパスを指定できるようになった。
アプリは引き続きデフォルトでtest/fixturesを単一のフィクスチャパスとして持つが、追加のフィクスチャパスも指定できるようになる。

ActiveSupport::TestCase.fixture_paths << "component1/test/fixtures"
ActiveSupport::TestCase.fixture_paths << "component2/test/fixtures"

TestFixtures#fixture_path(単数形)は非推奨化された。

Andrew Novoselac
同CHANGELOGより

参考: 週刊Railsウォッチ20230405: fixtureパスをRailsエンジン単位で指定可能になった

🔗 Adds support for deferrable exclude constraints in PostgreSQL. by alpaca-tc · Pull Request #47655 · rails/rails

PostgreSQLのEXCLUDE制約でDEFERRABLEをサポート。

デフォルトでは、PostgreSQLのEXCLUDE制約は個別のステートメントの後でチェックされる。
ほとんどのユースケースではこれで上手くいくが、以下のように範囲がオーバーラップする複数のステートメントを用いてレコードを置き換える場合は大きな制限となる。

exclusion_constraint :users, "daterange(valid_from, valid_to) WITH &&", deferrable: :immediate

deferrable: :immediateを指定すると、制約が個別のステートメントの後でチェックされる。
しかしトランザクション内でSET CONSTRAINTS ALL DEFERREDを用いてチェックを手動で延期すれば、トランザクション終了後にEXCLUDE制約をチェックするようになる。

これと同じことを、以下のようにデフォルトの振る舞いをimmediateチェック(=個別のステートメントの後でチェックする)からdeferredチェック(=トランザクション終了後にチェックする)に変更することでも可能になった。

exclusion_constraint :users, "daterange(valid_from, valid_to) WITH &&", deferrable: :deferred

Hiroyuki Ishii
同CHANGELOGより

参考: PostgreSQL 15ドキュメント SET CONSTRAINTS

🔗 Delegated Type supports customizeable foreign_type column by jasonkarns · Pull Request #45041 · rails/rails

delegated_typeforeign_typeオプションを渡すことで{role}_classメソッドに反映されるようになった。

foreign_type オプションを渡すことで、delegated_typeで標準でない{role}_typeカラム名を利用できるようになった。
このオプションは、delegated_type がラップしている背後のbelongs_to関連付けへforwardされるforeign_type と同じ。

Jason Karns
同CHANGELOGより

参考: 週刊Railsウォッチ20230405: Delegated Typesでカスタムカラム名を指定するforeign_typeオプションが追加された

Rails: ActiveRecord::DelegatedType APIドキュメント(翻訳)

🔗 Add support for unique constraints (PostgreSQL-only). by alpaca-tc · Pull Request #46192 · rails/rails

関連: Adds support USING INDEX for unique constraints in PostgreSQL. by alpaca-tc · Pull Request #47971 · rails/rails
関連: Deprecate deferrable: true option of add_foreign_key by alpaca-tc · Pull Request #47659 · rails/rails

UNIQUE制約のサポートを追加(PostgreSQLのみ)。

add_unique_key :sections, [:position], deferrable: :deferred, name: "unique_section_position"
remove_unique_key :sections, name: "unique_section_position"

UNIQUE制約に関しては、PostgreSQLのUNIQUE制約のドキュメントを参照。

デフォルトでは、PostgreSQLでのユニーク制約は個別のステートメントの後にチェックされる。
ほとんどの場合はこれで問題ないが、複数のステートメントを利用してレコードのuniqueカラムを置換する場合には大きな成約となる。

以下の例では、レコード間でuniqueカラムを交換している。

# このpositionはuniqueカラム
old_item = Item.create!(position: 1)
new_item = Item.create!(position: 2)

Item.transaction do
  old_item.update!(position: 2)
  new_item.update!(position: 1)
end

デフォルトの振る舞いでは、最初のUPDATEステートメントを実行する時点で失敗する。

マイグレーションで以下のようにadd_unique_key:deferrableオプションを渡すことで、このチェックを先延ばしできるようになった。

add_unique_key :items, [:position], deferrable: :immediate

deferrable: :immediate を指定しても、振る舞いは最初の例と変わらないが、トランザクション内で SET CONSTRAINTS ALL DEFERREDを使ってこのチェックを手動で先延ばしできるようになる。
これにより、UNIQUE制約がトランザクションの完了後にチェックされるようになる。

また、以下のようにdeferrable: :deferredを指定することで、個別のステートメント後にチェックするデフォルトの振る舞いを、トランザクション完了後にチェックする先延ばしチェックに変更することも可能。

add_unique_key :items, [:position], deferrable: :deferred

既存のuniqueインデックスを先延ばし可能にしたい場合は、以下のように:using_indexオプションを渡すことで先延ばし可能なUNIQUE制約を作成できる。

add_unique_key :items, deferrable: :deferred, using_index: "index_items_on_position"

Hiroyuki Ishii
同CHANGELOGより

参考: 週刊Railsウォッチ20230502: PostgreSQL: UNIQUE制約でUSING INDEXをサポート

🔗 Remove deprecated Tasks::DatabaseTasks.schema_file_type · rails/rails@049dfd4

関連: Remove deprecated methods in ``Tasks::DatabaseTasks` · rails/rails@71f61b1

非推奨化されていたTasks::DatabaseTasks.schema_file_typeを削除。

Rafael Mendonça França
同CHANGELOGより

🔗 Remove deprecated config.active_record.partial_writes · rails/rails@96b9fd6

関連: Deprecate partial_writes in favor of partial_inserts and partial_updates by casperisfine · Pull Request #42355 · rails/rails

非推奨化されていたconfig.active_record.partial_writesを削除。

Rafael Mendonça França
同CHANGELOGより

🔗 Remove deprecated ActiveRecord::Base config accessors · rails/rails@96c9db1

関連: Define deprecated delegators for the cattr that were moved out of AR::Base by casperisfine · Pull Request #42489 · rails/rails

非推奨化されていたActiveRecord::Baseコンフィグのアクセサメソッドを削除。

Rafael Mendonça França
同CHANGELOGより

🔗 Allow configs_for to accept a custom hash key by eileencodes · Pull Request #47536 · rails/rails

configs_forから:include_replicas引数を削除。今後は:include_hiddenを使うこと。

Eileen M. Uchitelle
同CHANGELOGより

アプリケーションのコンフィグをカスタムのハッシュキーで検索できるようになった。

カスタム設定を登録した場合や、ハッシュが特定のキーとマッチする設定を見つけたい場合は、configs_forconfig_keyオプションを渡せるようになった。
たとえば、キーがvitessdb_configがある場合、そのキーにマッチするデータベース設定のハッシュを以下のように検索できる。

ActiveRecord::Base.configurations.configs_for(env_name: "development", name: "primary", config_key: :vitess)
ActiveRecord::Base.configurations.configs_for(env_name: "development", config_key: :vitess)

Eileen M. Uchitelle
同CHANGELOGより

参考: 週刊Railsウォッチ20230322: データベース設定のカスタムハンドラを登録可能になった

🔗 Allow applications to register custom database configurations by eileencodes · Pull Request #47522 · rails/rails

アプリケーションでカスタムのデータベース設定ハンドラを登録できるようになった。

データベース設定がカスタムメソッドに応答するようにカスタマイズしたい場合に、カスタムハンドラを登録する仕組みを追加する。これは、Rails以外のデータベースアダプタやVitessなどのツールで、標準のHashConfigUrlConfigとは異なる方法で設定したい場合に有用。

以下のデータベースYAMLで、primary データベースを UrlConfig にしたまま、animals dbでCustomConfigオブジェクトを作成したいとする。

development:
  primary:
    url: postgres://localhost/primary
  animals:
    url: postgres://localhost/animals
    custom_config:
      sharded: 1

カスタムハンドラを登録するには、最初にカスタムメソッドを持つクラスを作成する。

class CustomConfig < ActiveRecord::DatabaseConfigurations::UrlConfig
  def sharded?
    custom_config.fetch("sharded", false)
  end

  private
    def custom_config
      configuration_hash.fetch(:custom_config)
    end
end

次にこのコンフィグをイニシャライザで登録する。

ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
  next unless config.key?(:custom_config)
  CustomConfig.new(env_name, name, url, config)
end

これで、アプリケーションが起動すると、:custom_configキーの設定ハッシュがCustomConfigオブジェクトになり、sharded?に応答するようになる。アプリケーションは、Active Recordでこのカスタムハンドラが利用されるように条件を処理する必要がある。

Eileen M. Uchitelle and John Crepezzi
同CHANGELOGより

参考: 週刊Railsウォッチ20230322: データベース設定のカスタムハンドラを登録可能になった

🔗 Stop serializing columns as YAML by default by casperisfine · Pull Request #47422 · rails/rails

ActiveRecord::Base.serializeのデフォルトがYAMLでなくなった。

YAMLのパフォーマンスは高くないうえに、注意しないとセキュリティ上の問題を引き起こす可能性がある。

残念ながら、Rubyの標準ライブラリには置き換えに適したシリアライザがない。

明らかな選択肢としてはJSONがあり、このユースケースには適したフォーマットだが、Ruby標準ライブラリのJSONシリアライザは厳密性が十分ではない(未知の型を文字列にキャストする形でフォールバックするため、データが破損する可能性がある)。

OjなどのサードパーティJSONライブラリは、これに適したstrictモードを備えている。

ユーザーは、自分たちの制約に基づいてシリアライザを選択することが望ましい。

従来のデフォルト設定に戻したい場合は、以下を設定する。

config.active_record.default_column_serializer = YAML

Jean Boussier
同CHANGELOGより

参考: config.active_record.default_column_serializer -- Rails アプリケーションを設定する - Railsガイド

ohler55/oj - GitHub

🔗 Allow to define the default column serializer by casperisfine · Pull Request #47463 · rails/rails

ActiveRecord::Base.serializeのシグネチャが変更された。

serializeに、2個の可能な値を渡せる単一の第2位置引数ではなく、2種類のキーワード引数(codertype)を渡せるように変更された。

変更前:

serialize :content, JSON
serialize :backtrace, Array

変更後:

serialize :content, coder: JSON
serialize :backtrace, type: Array

Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20230314: デフォルトのカラムシリアライザを定義可能になった

🔗 YAMLColumn: use YAML.safe_dump if available by casperisfine · Pull Request #47103 · rails/rails

YAMLカラムで可能な場合はYAML.safe_dumpを使うようになった。

psych 4.0.1以降、YAML.safe_loadと同様の型制約を利用可能で、使いやすいYAML.safe_dumpを適用可能になった。

最初にシリアライズするときは、許可された型だけをペイロードで使うことが望ましい。そうしないと、データベース内に無効なレコードが残る可能性がある。

Jean Boussier
同CHANGELOGより

ruby/psych - GitHub

🔗 ActiveRecord::QueryLogs: handle invalid encoding by casperisfine · Pull Request #47214 · rails/rails

ActiveRecord::QueryLogsにおける壊れたエンコーディングの処理を改善。

BLOBフィールドを含むクエリをビルドするときにバイナリデータが含まれることがよくある。文字列がASCII-8BITで慎重にエンコードされない限り、通常はUTF-8でエンコードされてQueryLogsが失敗する可能性があった。

この修正により、ActiveRecord::QueryLogsはクエリの正しいエンコードに依存しなくなった。

Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20230221: クエリのエンコードが壊れている場合のActiveRecord::QueryLogsの処理を改善

🔗 Model Generator Source Paths Should Allow for Customization by spencerneste · Pull Request #47181 · rails/rails

create_table_migrationテンプレートのオーバーライドがActiveRecord::Generators::ModelGeneratorで反映されないバグを修正。

rails g model create_books title:string content:text

修正により、上のジェネレータが以下の場所からこの順序でcreate_table_migration.rb.ttテンプレートを読み込むようになった。

lib/templates/active_record/model/create_table_migration.rb
lib/templates/active_record/migration/create_table_migration.rb

Spencer Neste
同CHANGELOGより

参考: 週刊Railsウォッチ20230613: モデルのジェネレータにcreate_table_migrationテンプレートのオーバーライドが正しく反映されない問題を修正

🔗 ActiveRecord::Relation#explain accepts options by reid-rigo · Pull Request #47043 · rails/rails

ActiveRecord::Relation#explainにオプションを渡せるようになった。

explain:analyzeオプションやverboseオプションを渡すことで、クエリプラン分析を詳細に行えるようになった。現在サポートされているデータベースおよびアダプタはPostgreSQLとMySQL。

Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)

Reid Lynch
同CHANGELOGより

参考: 週刊Railsウォッチ20230214: ActiveRecord::Relation#explainにオプションを渡せるようになった

🔗 Add Arel functionality for "stitching together" SQL by olefriis · Pull Request #46948 · rails/rails

複数のArel::Nodes::SqlLiteralノードを互いに追加してArel::Nodes::Fragmentsノードを形成できるようになった。これにより、多くのSQLスニペットを結合できるようになる。

Matthew Draper, Ole Friis
同CHANGELOGより

🔗 ActiveRecord::Base#signed_id: raise if called on a new record by ghiculescu · Pull Request #47027 · rails/rails

新規レコードでActiveRecord::Base#signed_idが呼び出されたらエラーを発生するようになった。

従来はid = nilを元にしていたため、利用できないIDを返すことがあった。

Alex Ghiculescu
同CHANGELOGより

🔗 Allow SQL warnings to be reported. by adrianna-chang-shopify · Pull Request #46690 · rails/rails

SQLのwarningを通知できるようにした。

以下のActive Recordコンフィグを設定することで、SQL warningの通知が有効になる。

# SQLクエリでwarningが発生した場合の操作を設定する
config.active_record.db_warnings_action = :raise

# warningの許可リストを設定する(設定したwarningは常に無視される)
config.active_record.db_warnings_ignore = [
  /Invalid utf8mb4 character string/,
  "An exact warning message",
]

この機能はMySQLアダプタとPostgreSQLアダプタでサポートされる。

Adrianna Chang, Paarth Madan
同CHANGELOGより

参考: config.active_record.db_warnings_action -- Rails アプリケーションを設定する - Railsガイド
参考: config.active_record.db_warnings_ignore -- Rails アプリケーションを設定する - Railsガイド

🔗 Add regroup method to ActiveRecord by dvisockas · Pull Request #47010 · rails/rails

#regroupクエリメソッドを追加。これは.unscope(:group).group(fields)のショートハンド。

例:

Post.group(:title).regroup(:author)
# SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`author`

Danielius Visockas
同CHANGELOGより

参考: 週刊Railsウォッチ20230207: Active Recordにregroupメソッドとregroup!メソッドが追加された

🔗 Adds schema parameter into enable_extension by lomefin · Pull Request #46894 · rails/rails

PostgreSQLアダプタのenable_extensionメソッドで、他のスキーマによってインストールされていなければならないPostgreSQL拡張をスキーマ名.拡張名形式で指定できるようになった。

例: enable_extension('heroku_ext.hstore')

Leonardo Luarte
同CHANGELOGより

🔗 Add support for :include index option by steve-abrams · Pull Request #44803 · rails/rails

マイグレーションのadd_index:includeオプションを追加(PostgreSQLのみ)。

PostgreSQLのINCLUDE(キーでないカラムをインデックスに含める)のサポートを追加。

add_index(:users, :email, include: [:id, :created_at])

上は以下を生成する。

CREATE INDEX index_users_on_email USING btree (email) INCLUDE (id, created_at)

Steve Abrams
同CHANGELOGより

参考: 週刊Railsウォッチ20230328: PostgreSQLのadd_indexincludewhereを両方使えるようにする

🔗 ActiveRecord::Relation#none?/#any?/#one?: support pattern arg by georgeclaghorn · Pull Request #46728 · rails/rails

ActiveRecord::Relation’の以下のメソッドに、同等のEnumerableにより近い形でマッチするパターン引数をオプションで渡せるようになった。

  • #any?
  • #none?
  • #one?

George Claghorn
同CHANGELOGより

# 以下の2つは同等
products.any?(MediaBlock)
products.any? { |product| MediaBlock === product }

参考: 週刊Railsウォッチ20230125: ActiveRecord::Relation#none?#any?#one?Enumerableと同様のパターン引数を渡せるようになった

🔗 Add ActiveRecord::Base::normalizes by jonathanhefner · Pull Request #43945 · rails/rails

属性の正規化を宣言するActiveRecord::Base.normalizesを追加。

属性の正規化は、属性の代入または更新時に適用され、正規化された値はデータベースに永続化される。また、この正規化はクエリメソッドの対応するキーワード引数にも適用されるので、正規化されていない値でレコードをクエリできるようになる。

例:

class User < ActiveRecord::Base
  normalizes :email, with: -> email { email.strip.downcase }
  normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
end

user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
user.email                  # => "cruise-control@example.com"

user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
user.email                  # => "cruise-control@example.com"
user.email_before_type_cast # => "cruise-control@example.com"

User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count         # => 1
User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0

User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")         # => true
User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false

User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"

Jonathan Hefner
同CHANGELOGより

Rails 7.1: ActiveRecord::Baseにnormalizesが追加された(翻訳)

🔗 Hide changes to before_committed! behaviour behind config by adrianna-chang-shopify · Pull Request #46739 · rails/rails

before_committed!コールバックの振る舞いの変更をコンフィグで戻せるようになった。

#46525before_committed!コールバックの動作が変更され、トランザクションに登録されたすべてのレコードでコールバックが実行されるようになった。この振る舞いを、config.active_record.before_committed_on_all_recordsという設定オプションで制御できるようになった(Rails 7.1ではデフォルトで有効)。

Adrianna Chang
同CHANGELOGより

参考: 週刊Railsウォッチ20221206: before_committed!コールバックをレコードの直近のコピーに対して実行するよう修正
参考: config.active_record.before_committed_on_all_records -- Rails アプリケーションを設定する - Railsガイド

🔗 Query Logs: namespaced_controller tag should match controller format by ghiculescu · Pull Request #46641 · rails/rails

クエリログのnamespaced_controllerタグがコントローラの名前空間フォーマットとマッチするよう修正。

たとえばNameSpaced::UsersControllerで処理されたリクエストは以下のようにログ出力されるようになった。

:controller # "users"
:namespaced_controller # "name_spaced/users"

Alex Ghiculescu
同CHANGELOGより

参考: Rails API ActiveRecord::QueryLogs
参考: config.active_record.query_log_tags -- Rails アプリケーションを設定する - Railsガイド

🔗 Fix: ActiveRecord::Calculations#ids returns duplicate ids by joshuay03 · Pull Request #46503 · rails/rails

ActiveRecord::Calculations#idsが一意のidリストだけを返すよう修正。

ActiveRecord::Calculations#idsが更新され、eager_loadpreloadincludesでベースモデルの一意のidリストだけを返すようになった。

Post.find_by(id: 1).comments.count
# => 5
Post.includes(:comments).where(id: 1).pluck(:id)
# => [1, 1, 1, 1, 1]
Post.includes(:comments).where(id: 1).ids
# => [1]

Joshua Young
同CHANGELOGより

参考: 週刊Railsウォッチ20221220: ActiveRecord::Calculations#idsが返すidが重複する問題を修正

🔗 Don't use lower() for citext columns by pirj · Pull Request #46592 · rails/rails

PostgreSQLのcitextカラムでは大文字小文字を区別しないクエリにlower()を追加しないよう修正。

従来はuniquenessバリデーションなどでcase_sensitive: falseを指定したときのクエリにlower()が追加されていた。
しかし、lower()が定義されていないインデックスに対してこれを行うとインデックスが効かなくなることが見落とされていた。

Phil Pirozhkov
同CHANGELOGより

参考: PostgreSQL 15ドキュメント 9.4. 文字列関数と演算子
参考: PostgreSQL 15ドキュメント F.10. citext
参考: 週刊Railsウォッチ20221213: PostgreSQLのcitext型カラムの検索時にlower()を使わないようにした

🔗 Extract #sync_timezone_changes method in AbstractMysqlAdapter by adrianna-chang-shopify · Pull Request #46604 · rails/rails

AbstractMysqlAdapter#sync_timezone_changesメソッドをMySQL::DatabaseStatementsに移動した。
これにより、サブクラスで#raw_executeをオーバーライドせずにデータベースのタイムゾーン変更を同期できるようになった。

Adrianna Chang, Paarth Madan
同CHANGELOGより

🔗 Do not write additional new lines when dumping sql migration versions by mishaschwartz · Pull Request #46454 · rails/rails

マイグレーションのSQLダンプでバージョン番号に余分な行が追加されていたのを修正。

この変更によってinsert_versions_sql関数が修正され、現在のマイグレーションバージョン番号を含む挿入文字列の末尾に余分な改行文字が2つ含まれないようになる。

Misha Schwartz
同CHANGELOGより

🔗 Fix composed_of freezing by gregnavis · Pull Request #46377 · rails/rails

composed_ofの値のfreezedupを修正。

従来のコンポジション値は、混乱を招く2通りの振る舞いを示していた。

  1. コンポジション値を読み取るときは値がfreezeされない。このため、背後のデータベースカラムとの同期がずれてしまう。
  2. コンポジション値を書き込むときは引数がfreezeされる。これによって呼び出し側が混乱する可能性がある。

修正後、データベースカラムに基づいてインスタンス化されたコンポジション値はfreezeされるようになった(問題1の修正)。代入したコンポジション値はdupされるようになり、dupしたものをfreezeするようになった(問題2の修正)。

Greg Navis
同CHANGELOGより

参考: Rails API ActiveRecord::Aggregations::ClassMethods

🔗 Fix incorrect caching of case-insensitivity · rails/rails@b39050e

大文字小文字を区別しないカラムでキャッシュが効かなかったのを修正。

カラムが大文字小文字を区別しない比較が可能かどうかをチェックすることで余分なクエリを発行しないよう修正した。

Phil Pirozhkov
同CHANGELOGより

🔗 option to disable all methods that ActiveRecord.enum generates by alfie-max · Pull Request #46490 · rails/rails

ActiveRecord.enumによるメソッド生成をinstance_methods: falseオプションで無効にできるようになった。

Alfred Dominic
同CHANGELOGより

参考: Rails 7.1に入る主要な機能まとめ(3) -- ActiveRecord#enumのメソッド生成を無効にするオプションが追加された

🔗 Avoid validating belongs_to association if it has not changed by fatkodima · Pull Request #46522 · rails/rails

belongs_to関連付けで変更が生じていない場合のバリデーションを回避するようになった。

従来のActive Recordは、レコード更新時にbelongs_to関連付けで(存在が必須と設定されている場合に)存在チェックの追加クエリを実行していたが、属性が変更されていない場合でも実行していた。

修正後は、belongs_toに関連するカラムだけが存在チェックされるようになった。ただしこの方法では孤立レコードができてしまう可能性があるので、この問題を回避するには外部キーを使う必要がある。

この振る舞いは、以下のコンフィグで制御できる。

config.active_record.belongs_to_required_validates_foreign_key = false

この設定はconfig.load_defaults 7.1ではデフォルトでfalseに設定される。

fatkodima
同CHANGELOGより

参考: config.active_record.belongs_to_required_validates_foreign_key -- Rails アプリケーションを設定する - Railsガイド

参考: Rails 7.1に入る主要な機能まとめ(3) -- 属性が変更されていないbelongs_to関連付けのバリデーションを回避するようになった

🔗 Allow resetting singular associations by georgeclaghorn · Pull Request #46165 · rails/rails

has_one関連付けとbelongs_to関連付けで、オーナーモデルにreset_関連付け名が定義されるようになった。

このメソッドは、キャッシュされた関連付けレコードがあればアンロードし、次回のアクセスでデータベースから読み込むようになる。

George Claghorn
同CHANGELOGより

参考: Rails 7.1に入る主要な機能まとめ(3) -- 単数形の関連付けをリセットできるようになった

🔗 Permit YAML classes and unsafe load per attribute · rails/rails@e313fc5

serializeyamlオプションが追加され、permitted_classessafe_load用)やunsafe_loadを属性単位で設定できるようになった。

Carlos Palhares
同CHANGELOGより

# 同APIドキュメントより
class User < ActiveRecord::Base
  serialize :preferences, yaml: { permitted_classes: [Symbol, Time] }
 end

🔗 Add a build persistence method by seand7565 · Pull Request #45696 · rails/rails

永続化を行うbuildメソッドを追加。

newのラッパーを提供し、関連付けのbuildメソッドと同じ記法で、createと同様にハッシュの配列から複数のレコードを作成する機能を提供する。

Sean Denny
同CHANGELOGより

# 同APIドキュメントより
# 単一のオブジェクトをビルド
User.build(first_name: 'Jamie')

# 新規オブジェクトの配列を渡してビルド
User.build([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }])

# 単一のオブジェクトをビルドし、ブロックで他の属性を設定
User.build(first_name: 'Jamie') do |u|
  u.is_admin = false
end

# 新規オブジェクトの配列を渡してビルド:(ブロックはオブジェクトごとに設定される)
User.build([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) do |u|
  u.is_admin = false
end

参考: 週刊Railsウォッチ20221206: buildメソッドにハッシュの配列を渡すことで複数のオブジェクトをまとめてbuildできるようになった

🔗 Raise on assignment to readonly attributes by hmcguire-shopify · Pull Request #46105 · rails/rails

attr_readonlyで生成した属性に代入するとエラーを発生するようになった。

class Post < ActiveRecord::Base
  attr_readonly :content
end
Post.create!(content: "cannot be updated")
post.content # "cannot be updated"
post.content = "something else" # => ActiveRecord::ReadonlyAttributeError

従来はデータベースに書き込まずに代入に成功してしまい、エラーを発生しなかった。

この振る舞いは以下のコンフィグで制御できる。

config.active_record.raise_on_assign_to_attr_readonly = true

config.load_defaults 7.1ではこの振る舞いがデフォルトで有効になる。

Alex Ghiculescu, Hartley McGuire
同CHANGELOGより

参考: config.active_record.raise_on_assign_to_attr_readonly -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20221129: readonlyの属性に代入すると`ActiveRecord::ReadonlyAttributeError`を発生するようになった

🔗 Allow unscoping of preload and eager_load associations by dmorehouse · Pull Request #45147 · rails/rails

unscopeで関連付けのpreloadeager_loadも指定できるようになった。

includesjoinsなどと同様に、unscopeで関連付けのpreloadeager_loadを行う機能が追加された。サポートされているunscope可能なスコープの完全なリストについては、ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUESを参照。

query.unscope(:eager_load, :preload).group(:id).select(:id)

David Morehouse
同CHANGELOGより

参考: 週刊Railsウォッチ20221129: 関連付けのpreloadeager_loadunscopeできるようになった

🔗 Add filtering of encrypted attributes in #inspect by skipkayhil · Pull Request #46453 · rails/rails

inspectで暗号化済み属性をフィルタで自動的に除外するようになった。

この機能はデフォルトで有効になるが、以下のコンフィグを設定することで無効にできる。

config.active_record.encryption.add_to_filter_parameters = false

Hartley McGuire
同CHANGELOGより

この機能により、暗号化済み属性は自動的にログからも除外されるようになります。

参考: config.active_record.add_to_filter_parameters -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20221129: #inspect実行時に暗号化済み属性を自動的にフィルタで除外する機能が追加された

🔗 Clear locking column on #dup by shouichi · Pull Request #46243 · rails/rails

レコードを#dupしたときにlocking_columnを解除するようになった。

この変更により、idやタイムスタンプなどのlocking_columnが複製されない問題が修正される。

car = Car.create!
car.touch
car.lock_version #=> 1
car.dup.lock_version #=> 0

Shouichi Kamiya, Seonggi Yang, Ryohei UEDA
同CHANGELOGより

参考: Rails API locking_column -- ActiveRecord::Locking::Optimistic::ClassMethods

🔗 Invalidate transaction as early as possible by nvasilevski · Pull Request #46367 · rails/rails

トランザクションを可能な限り早期に無効化するようになった。

TransactionRollbackError例外をrescueした後、トランザクションをフローの早い段階で無効化し、フレームワークがROLLBACKステートメントの発行をスキップするケースを増やす。
これが影響するのは、savepoint_errors_invalidate_transactions?trueを設定しているアダプタのみであり、現時点ではmysql2アダプタにのみ影響する。

Nikita Vasilevsky
同CHANGELOGより

参考: Rails API savepoint_errors_invalidate_transactions? -- ActiveRecord::ConnectionAdapters::Mysql2Adapter

🔗 Allow specifying columns to use in ActiveRecord::Base object queries by nvasilevski · Pull Request #46331 · rails/rails

ActiveRecord::Baseオブジェクトが発行するSQLクエリで複数カラムのリストを設定できるようになった。

複数カラムをリストとして設定可能になったことで、ActiveRecord::Baseオブジェクトを更新/削除/リロードしたときのSQLクエリ句のビルドで使われるようになった。

class Developer < ActiveRecord::Base
  query_constraints :company_id, :id
end

developer = Developer.first.update(name: "Bob")
# => UPDATE "developers" SET "name" = 'Bob' WHERE "developers"."company_id" = 1 AND "developers"."id" = 1

Nikita Vasilevsky
同CHANGELOGより

参考: 週刊Railsウォッチ20221115: ActiveRecord::Baseオブジェクトのクエリでカラムのリストを指定可能になった

🔗 Adds validate to foreign keys and check constraints in schema.rb by TAGraves · Pull Request #46339 · rails/rails

(PostgreSQL)schema.rbの外部キーやCHECK制約にvalidateを追加するようになった。

従来は、外部キーやCHECK制約の追加にvalidate: falseを指定したかどうかがschema.rbに記録されていなかったため、データベースをスキーマからリストアすると外部キーやCHECK制約が誤ってバリデーションされる可能性があった。

Tommy Graves
同CHANGELOGより

参考: 週刊Railsウォッチ20221115: schema.rbの外部キーやCHECK制約にvalidate: falseを追加するようになった(PostgreSQL)

🔗 Allow adapter #execute methods to take allow_retry option by adrianna-chang-shopify · Pull Request #46273 · rails/rails

データベースアダプタの#executeメソッドにallow_retryオプションを渡せるようになった。

このオプションをtrueに設定するとSQLステートメントがリトライされるようになる。リトライは、リトライ回数がデータベース設定のconnection_retries値に達するまで、またはコネクション関連のエラーが発生するまで行われる。

Adrianna Chang
同CHANGELOGより

参考: 週刊Railsウォッチ20221115: DBアダプタの#executeメソッドにallow_retryオプションを渡せるようになった

🔗 Don't trigger after_commit :destroy callback again on destroy if record previously was destroyed by bensheldon · Pull Request #46197 · rails/rails

データベース行が削除済みの場合にのみafter_commit :destroyがトリガーされるようになった。

これにより、同じレコードに対してdestroyが複数回呼び出された場合にafter_commit :destroyコールバックが複数回トリガーされるのを防止する。

Ben Sheldon
同CHANGELOGより

参考: 週刊Railsウォッチ20221101: 同一レコードでafter_commit :destroyの重複トリガーを解消

🔗 Fix ciphertext_for for yet-to-be-encrypted values by jonathanhefner · Pull Request #46284 · rails/rails

ciphertext_forが返す値が暗号化されないバグを修正。

従来のciphertext_forは、永続化されていないレコードなどから暗号化されていない平文を返していた。

Post.encrypts :body

post = Post.create!(body: "Hello")
post.ciphertext_for(:body)
# => "{\"p\":\"abc..."

post.body = "World"
post.ciphertext_for(:body)
# => "World"

修正後のciphertext_forは、暗号化属性から常に暗号化済みテキストを返すようになった。

Post.encrypts :body

post = Post.create!(body: "Hello")
post.ciphertext_for(:body)
# => "{\"p\":\"abc..."

post.body = "World"
post.ciphertext_for(:body)
# => "{\"p\":\"xyz..."

Jonathan Hefner
同CHANGELOGより

参考: 週刊Railsウォッチ20221101: ciphertext_forが暗号化前の値を返す問題を修正

🔗 Fix a bug where using groups and counts with long table names would return incorrect results. by Dooor · Pull Request #46287 · rails/rails

テーブル名が長い場合にgroupcountが誤った値を返すバグを修正。

Shota Toguchi, Yusaku Ono
同CHANGELOGより

この修正は、Rails 7.0.5でリリース済みです

🔗 Fix encryption of column default values by jonathanhefner · Pull Request #46281 · rails/rails

カラムのデフォルト値の暗号化を修正。

従来は、カラムのデフォルト値が設定されている暗号化属性がレコード作成時に暗号化されているように見えていたが、実際には暗号化されていなかった。

Book.encrypts :name

book = Book.create!
book.name
# => "<untitled>"
book.name_before_type_cast
# => "{\"p\":\"abc..."
book.reload.name_before_type_cast
# => "<untitled>"

修正後は、カラムのデフォルト値を設定した暗号化属性が暗号化されるようになった。

Book.encrypts :name

book = Book.create!
book.name
# => "<untitled>"
book.name_before_type_cast
# => "{\"p\":\"abc..."
book.reload.name_before_type_cast
# => "{\"p\":\"abc..."

Jonathan Hefner
同CHANGELOGより

🔗 Deprecate delegation to connection handler from Base by eileencodes · Pull Request #46274 · rails/rails

Baseからconnection_handlerへの委譲を非推奨化。

以下の呼び出しが非推奨化された。

  • Base.clear_all_connections!
  • Base.clear_active_connections!
  • Base.clear_reloadable_connections!
  • Base.flush_idle_connections!

今後これらのメソッドはコネクションハンドラで直接呼び出すこと。
Baseからconnection_handlerへの委譲は今後のRailsで削除される予定。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API ActiveRecord::ConnectionAdapters::ConnectionHandler

🔗 Allow ActiveRecord::QueryMethods#reselect to accept a hash by sampatbadhe · Pull Request #46253 · rails/rails

ActiveRecord::QueryMethods#reselectにもActiveRecord::QueryMethods#selectと同様にハッシュ値を渡せるようになった。

Sampat Badhe
同CHANGELOGより

# 同PRより(以下の2つは同じ)
 Post.select(:title, posts: { title: :post_title })
 Post.select(:title, :body).reselect(:title, posts: { title: :post_title })

参考: 週刊Railsウォッチ20221101: ActiveRecord::QueryMethods#reselectにもカラムやエイリアスを含むハッシュを渡せるようになった

🔗 Validate options when managing columns and tables in migration by tgxworld · Pull Request #46178 · rails/rails

マイグレーションのカラム/テーブル管理メソッドに渡されるオプションが有効かどうかをバリデーションするようになった。

create_tableadd_columnなどのマイグレーション用メソッドに無効なオプションを渡すと、従来は単に無視されていたが、修正後はエラーを発生するようになった。
オプションのバリデーションは、新規作成されたマイグレーションに対してのみ適用される。

Guo Xiang Tan, George Wambold
同CHANGELOGより

参考: 週刊Railsウォッチ20221025: 新規マイグレーションのカラム/テーブル管理で無効なオプションが指定されるとraiseするようになった

🔗 Add ability to set the tags_format for QueryLogs by iheanyi · Pull Request #45081 · rails/rails

QueryLogsのタグフォーマットにデフォルトでSQLCommenter形式が使われるようになった。
詳しくは#46179を参照。

QueryLogsでSQLCommenter形式のタグを無効にするには、config.active_record.query_log_tags_format = :legacyを設定する。
デフォルトでは:sqlcommenterが設定される。

Modulitos and Iheanyi
同CHANGELOGより

参考: Rails API ActiveRecord::QueryLogs
参考: config.active_record.query_log_tags_format -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20221018: QueryLogsでtags_formatオプションを指定可能になった

🔗 Facilitate use of any regular ERB in database.yml by eikes · Pull Request #46134 · rails/rails

rakeタスク作成時にdatabase.ymlで任意のERBを書けるようになった。

環境設定にアクセスする場合でもdatabase.ymlに任意のERBを書けるようになった。

config.active_record.suppress_multiple_database_warning設定は非推奨化された。

Eike Send
同CHANGELOGより

参考: RailsガイドのRails アプリケーションを設定するには、このconfig.active_record.suppress_multiple_database_warningはこれまで記載されていません。
参考: 週刊Railsウォッチ20221018: database.ymlのYAMLキーに任意のERBを書けるようになった

🔗 Add table name to error for duplicate column definitions by p8 · Pull Request #46117 · rails/rails

カラム定義が重複していた場合のエラーメッセージにテーブル名も出力されるようになった。

マイグレーションでテーブルのカラム定義が重複している場合に、問題が生じたテーブル名を含むエラーメッセージが表示される。

Petrik de Heus
同CHANGELOGより

# 同PRより: テーブル名'testings'が出力される
you can't define an already defined column 'testing_column' on 'testings'.

🔗 Fix erroneous nil default precision on virtual datetime columns by sambostock · Pull Request #46110 · rails/rails

仮想のdatetimeカラムがデフォルトでprecision: nilになっていたのを修正。

この修正の前は、仮想のdatetimeカラムのデフォルト精度が通常のdatetimeカラムのデフォルト精度と同じになっておらず、以下の2つが誤って同等になっていた。

t.virtual :name, type: datetime,                 as: "expression"
t.virtual :name, type: datetime, precision: nil, as: "expression"

この変更では、デフォルト精度の探索が修正され、datetimeのデフォルト精度が仮想カラムでも通常カラムでも一致するようになった。

Sam Bostock
同CHANGELOGより

この修正は、Rails 7.0.5でリリース済みです

🔗 Use #with_raw_connection in #quote_string to retry connection errors by adrianna-chang-shopify · Pull Request #46108 · rails/rails

#quote_string#with_raw_connectionのコネクションを使うようになった。

これにより、#with_raw_connectionが提供する再接続・リトライロジックによって#quote_stringがラップされるようになる。

Adrianna Chang
同CHANGELOGより

参考: Rails API quote_string -- ActiveRecord::ConnectionAdapters::Quoting

🔗 Add expires_at option to signed_id · rails/rails@364939c

失効日時を指定するexpires_atオプションをsigned_idに追加。

Shouichi Kamiya
同CHANGELOGより

# 同PRより
Account.find_signed(@account.signed_id(expires_at: 1.minute.from_now)

参考: Rails API signed_id -- ActiveRecord::SignedId

🔗 Take into account timeout limit when retrying queries by adrianna-chang-shopify · Pull Request #46046 · rails/rails

クエリのリトライ期間に上限を設定できるようになった。

#44576#44591で行われた作業を基に、データベースコネクションの自動再接続ロジックを拡張してタイムアウト制限を考慮するようになった。クエリが最初に試行されてから一定時間経過すると、クエリは再試行されなくなる。この値にはデフォルトではnilが設定されている(経過時間に関係なくすべての再試行可能なクエリが再試行される)。これはデータベース設定のretry_deadlineオプションで変更可能。

Adrianna Chang
同CHANGELOGより

# 同PRより
development:
  adapter: mysql2
  retry_deadline: 5 # 5秒経過したらリトライを停止

🔗 Dup and freeze complex types when making query attributes by tenderlove · Pull Request #46048 · rails/rails

クエリキャッシュが誤った値を返すことがあるバグを修正。

#46044を参照。

Aaron Patterson
同CHANGELOGより

🔗 Add ssl-mode option to dbconsole command and MySQLDatabaseTasks by p8 · Pull Request #46008 · rails/rails

MySQLDatabaseTasksでMySQLのSSLモードオプションをサポート。

データベースサーバーの識別情報を検証するには、--ssl-modeオプションにVERIFY_CAまたはVERIFY_IDENTITYを設定する必要がある。従来、データベース作成やstructureダンプなどのMySQLデータベースタスクでこのオプションが無視されていた。

Petrik de Heus
同CHANGELOGより

参考: 週刊Railsウォッチ20221011: dbconsoleコマンドとMySQLDatabaseTasksに--ssl-modeオプションを追加

🔗 Move InternalMetadata to an independent object by eileencodes · Pull Request #45982 · rails/rails

ActiveRecord::InternalMetadataを別オブジェクトに移動。

ActiveRecord::InternalMetadataActiveRecord::Baseを継承しなくなり、connectionを渡して初期化する独立したオブジェクトになった。
このクラスはprivateであり、アプリケーションから直接使うべきではない。スキーママイグレーションのテーブルとやりとりする必要がある場合は、ActiveRecord::Base.connection.schema_migrationのように直接コネクションにアクセスすること。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API ActiveRecord::InternalMetadata

🔗 Deprecate quoting ActiveSupport::Duration as an integer (#44341) by aramgre · Pull Request #44438 · rails/rails

ActiveSupport::Durationを整数値として式展開することを非推奨化。

SQL文字列テンプレート内でActiveSupport::Durationを式展開にバインドするパラメータとして利用することが非推奨化された。
この非推奨警告表示を回避するには、明示的にDurationをより具体的なデータベース型に変換すること。
たとえば、Durationを秒単位の整数値として利用したい場合は、以下のように書くこと。

Record.where("duration = ?", 1.hour.to_i)

DurationISO 8601形式の文字列として使いたい場合は、以下のように書くこと。

Record.where("duration = ?", 1.hour.iso8601)

Aram Greenman
同CHANGELOGより

参考: この変更には後方互換用のコンフィグはありません(#44438コメント)。

🔗 improve "in_order_of" to allow string column name by igorkasyanchuk · Pull Request #45971 · rails/rails

QueryMethods#in_order_ofによる並べ替えが、カラム名が文字列の場合にも使えるようになった。

Post.in_order_of("id", [4,2,3,1]).to_a
Post.joins(:author).in_order_of("authors.name", ["Bob", "Anna", "John"]).to_a

Igor Kasyanchuk
同CHANGELOGより

参考: Rails API in_order_of -- ActiveRecord::QueryMethods

🔗 Move SchemaMigration to an independent object by eileencodes · Pull Request #45908 · rails/rails

ActiveRecord::SchemaMigrationを別オブジェクトに移動。

ActiveRecord::SchemaMigrationActiveRecord::Baseを継承しなくなり、connectionを渡して初期化する独立したオブジェクトになった。
このクラスはprivateであり、アプリケーションから直接使うべきではない。スキーママイグレーションのテーブルとやりとりする必要がある場合は、ActiveRecord::Base.connection.schema_migrationのように直接コネクションにアクセスすること。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API ActiveRecord::SchemaMigration

🔗 Make connection_pool_list take an explicit argument by eileencodes · Pull Request #45961 · rails/rails

all_connection_poolsを非推奨化し、connection_pool_listにオプションを明示的に渡すようにした。

#45924に続いて、all_connection_poolsを非推奨化した。
connection_pool_listには明示的にrole引数を渡すようになった。この引数に:allを渡すことで、アプリケーションで新しい振る舞いを選択できるようになる。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API connection_pool_list -- ActiveRecord::ConnectionAdapters::ConnectionHandler

🔗 Fix bug in connection handler methods using all pools by eileencodes · Pull Request #45924 · rails/rails

コネクションハンドラのメソッドがすべてのコネクションプールで動作するよう修正。

以下のメソッドが、デフォルトですべてのコネクションプールで動作するようになった。

  • active_connections?
  • clear_active_connections!
  • clear_reloadable_connections!
  • clear_all_connections!
  • flush_idle_connections!

従来は、ロールを指定しない場合にデフォルトでcurrent_roleロールまたは:writingロールが使われていた。

Eileen M. Uchitelle
同CHANGELOGより

参考: 週刊Railsウォッチ20221003: コネクションハンドラメソッドのバグを修正

🔗 Allow ActiveRecord::QueryMethods#select to accept a hash by alextrueman · Pull Request #45612 · rails/rails

関連: Allow ActiveRecord::QueryMethods#reselect to accept a hash by sampatbadhe · Pull Request #46253 · rails/rails

ActiveRecord::QueryMethods#selectにハッシュ値を渡せるようになった。

従来は、selectでカラム定義やselectのエイリアス定義を行うには生SQLまたはシンボルを渡すしかなかった。

この変更によって、以下のようにhashを引数として渡せるようになる。

Post.joins(:comments).select(posts: [:id, :title, :created_at], comments: [:id, :body, :author_id])
#=> "SELECT \"posts\".\"id\", \"posts\".\"title\", \"posts\".\"created_at\", \"comments\".\"id\", \"comments\".\"body\", \"comments\".\"author_id\"
#   FROM \"posts\" INNER JOIN \"comments\" ON \"comments\".\"post_id\" = \"posts\".\"id\""

Post.joins(:comments).select(posts: { id: :post_id, title: :post_title }, comments: { id: :comment_id, body: :comment_body })
#=> "SELECT posts.id as post_id, posts.title as post_title, comments.id as comment_id, comments.body as comment_body
#    FROM \"posts\" INNER JOIN \"comments\" ON \"comments\".\"post_id\" = \"posts\".\"id\""

Oleksandr Holubenko, Josef Šimánek, Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20220926: ActiveRecord::QueryMethods#selectにハッシュを渡せるようになった

🔗 Normalize virtual attributes on ActiveRecord::Persistence#becomes by intrip · Pull Request #42650 · rails/rails

ActiveRecord::Persistence#becomesで仮想属性を使えるようになった。

ソースクラスとターゲットクラスの属性セットが異なる場合は、ターゲットクラスの属性を追加する形で属性を適応させるようになる。

class Person < ApplicationRecord
end

class WebUser < Person
  attribute :is_admin, :boolean
  after_initialize :set_admin

  def set_admin
    write_attribute(:is_admin, email =~ /@ourcompany\.com$/)
  end
end

person = Person.find_by(email: "email@ourcompany.com")
person.respond_to? :is_admin
# => false
person.becomes(WebUser).is_admin?
# => true

Jacopo Beschi, Sampson Crowley
同CHANGELOGより

参考: Rails API becomes -- ActiveRecord::Persistence
参考: 週刊Railsウォッチ20220920: ActiveRecord::Persistence#becomesをvirtual attributeに適応させる

🔗 Fix ActiveRecord::QueryMethods#in_order_of to work with nils by fatkodima · Pull Request #45670 · rails/rails

Enumerable#in_order_ofの振る舞いに合わせるため、ActiveRecord::QueryMethods#in_order_ofnilを扱えるよう修正。

たとえば、Post.in_order_of(:title, [nil, "foo"])でタイトルがnilのpostsも含まれるようになる。これはPost.all.to_a.in_order_of(:title, [nil, "foo"])の振る舞いと同じ。

fatkodima
同CHANGELOGより

参考: Rails API in_order_of -- ActiveRecord::QueryMethods
参考: Rails API in_order_of -- Enumerable
参考: 週刊Railsウォッチ20220905: ActiveRecord::QueryMethods#in_order_ofのソート対象の値がnilでも動作するよう修正

🔗 Optimize add_timestamps to use a single SQL statement when supported by ilianah · Pull Request #45723 · rails/rails

add_timestampsが発行するSQLステートメントが1つだけになるよう最適化。

add_timestamps :my_table

上によって以下のSQLが生成されるようになる。

ALTER TABLE "my_table" ADD COLUMN "created_at" datetime(6) NOT NULL, ADD COLUMN "updated_at" datetime(6) NOT NULL

Iliana Hadzhiatanasova
同CHANGELOGより

参考: §3.9 changeメソッドを使う -- Active Record マイグレーション - Railsガイド

🔗 Add drop_enum command for Postgres by ghiculescu · Pull Request #45735 · rails/rails

マイグレーションコマンドにdrop_enumを追加(PostgreSQLのみ)。

これはcreate_enumと逆の動作。enumを削除する前には、そのenumに依存しているカラムを必ず削除しておくこと。

Alex Ghiculescu
同CHANGELOGより

Rails 7: PostgreSQLのカスタムenum型が使いやすくなった(翻訳)

🔗 Add support for if_exists option when removing a check constraint · rails/rails@25f97a6

CHECK制約の削除でif_existsをサポート。

remove_check_constraintメソッドにif_existsオプションを渡せるようになった。if_exists: trueを設定すると、そのCHECK制約が存在しない場合にエラーを発生しなくなる。

Margaret Parsa and Aditya Bhutani
同CHANGELOGより

参考: §3.9 changeメソッドを使う -- Active Record マイグレーション - Railsガイド
参考: 週刊Railsウォッチ20220822: CHECK制約の削除でif_existsオプションが利用可能になる

🔗 find_or_create_by: handle race condition by finding again by casperisfine · Pull Request #45720 · rails/rails

find_or_create_byRecordNotUniqueが発生した場合はfindをリトライするようになった。

find_or_create_byは本質的に競合がつきものなので、適切なunique制約が設定されているかどうかで「重複レコードを作成する」か「ActiveRecord::RecordNotUniqueで失敗する」かが決まる。

このユースケース向けにcreate_or_find_byが導入されたが、レコードが既に存在する可能性が非常に高い場合は、INSERTはSELECTよりも多くのデータ送信が必要になり、データベース側の作業も増加するため、効率がかなり悪くなる。また、データベースによっては、主キーのインクリメント(望ましくない)が消費される可能性もある。

そのため、レコードが既に存在する可能性が非常に高いユースケースでは、createActiveRecord::RecordNotUniqueで失敗した場合はfindを再試行することで競合が発生しないようにできる。これはテーブルで適切なunique制約が設定されていることが前提となる。さもないと、find_or_create_byで引き続き重複レコードが発生する。

Jean Boussier, Alex Kitchens
同CHANGELOGより

参考: §19.1 find_or_create_by -- Active Record クエリインターフェイス - Railsガイド
参考: Rails API create_or_find_by -- ActiveRecord::Relation
参考: 週刊Railsウォッチ20220822: find_or_create_byRecordNotUniqueエラーの場合にfindをリトライするようになった

🔗 Simplify adapter construction; defer connect until first use by matthewd · Pull Request #44591 · rails/rails

Active Recordのデータベースアダプタに、よりシンプルなコンストラクタAPIが導入された。

従来は、データベースアダプタが再接続をサポートするために新しいraw_connectionのビルド方法を知っている必要があったが、最初の確立済みコネクションを渡されることも期待されていた。

改修後は、アダプタのインスタンスを手動で作成する場合は、コンフィグ用ハッシュを1個渡すだけで、必要に応じて実際の接続が確立するようになった。

Matthew Draper
同CHANGELOGより

可能な場合は、DBプールのチェックアウト中にSELECT 1による余分なコネクションバリデーション用クエリを避けるようになった。

リクエスト中の最初のクエリが冪等であることがわかっている場合は、これを用いてコネクションを直接バリデーションできるので、ネットワークのやりとりが削減される。

Matthew Draper
同CHANGELOGより

🔗 Defer verification of database connections by matthewd · Pull Request #44576 · rails/rails

コネクションが切断された場合、安全であればリクエストの途中でもデータベースコネクションを自動で再接続するようになった。

冪等であることがわかっているクエリを実行しようとしてエラーが発生した場合、かつトランザクション内でない場合は、直ちにデータベースサーバーに再接続しても安全である。今後はこれがデフォルトの振る舞いになる。

新しいデフォルトの振る舞いは常に安全でなければならない。この振る舞いをサポートするため、どのクエリを冪等と認識するかについては保守的なアプローチを取っている。ただし、この振る舞いは、データベースコネクションのconnection_retriesオプションに0を設定することで無効にできる。

Matthew Draper
同CHANGELOGより

参考: Rails API connection_retries -- ActiveRecord::ConnectionAdapters::AbstractAdapter

🔗 Avoid removing a PostgreSQL extension when there are dependent objects by fatkodima · Pull Request #45474 · rails/rails

PostgreSQL拡張に依存しているオブジェクトが存在する場合は、その拡張の削除を回避するようになった。

従来は、拡張を削除すると依存オブジェクトも暗黙で削除されていた。
改修後は、このような削除を行うとエラーを発生するようになった。

マイグレーションで以下のように拡張を強制削除することも可能。

disable_extension :citext, force: :cascade

修正: #29091

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20220801: PostgreSQL拡張機能に依存するオブジェクトがある場合は削除しないようになった

🔗 Accept nested functions in Dangerous Query Methods by siegfault · Pull Request #44010 · rails/rails

ネストしたSQL関数を安全なSQL文字列として扱えるようになった。

Michael Siegfried
同CHANGELOGより

# 同PRより
Post.pluck(Arel.sql("length(trim(title))"))

参考: 週刊Railsウォッチ20220725: ネストしたSQL関数を安全なSQL文字として許容する

🔗Defer constant loading of ActiveRecord::DestroyAssociationAsyncJob via a String instead of a class constant by bensheldon · Pull Request #45476 · rails/rails

destroy_association_async_job=に設定するクラス(定数)を、クラス名の文字列でも設定できるようになった。

ActiveRecord::BaseActiveJob::Baseの間のオートローディングを先延ばしするようにし、ActiveRecord::DestroyAssociationAsyncJobの設定をActive JobからActive Recordに移動した。

ActiveRecord::ActiveJobRequiredErrorは非推奨化された。
これにより、ジョブクラスが読み込み不能な場合はNameErrorが発生するようになった。

また、関連付けでdependent: :destroy_asyncが宣言され、かつジョブクラスが未設定の場合はActiveRecord::ConfigurationErrorが発生するようになった。

Ben Sheldon
同CHANGELOGより

参考: Rails API destroy_association_async_job -- ActiveRecord::Core
参考: config.active_record.destroy_association_async_job -- Rails アプリケーションを設定する - Railsガイド

🔗 ActiveRecord::Store encode store as a regular Hash by casperisfine · Pull Request #45591 · rails/rails

ActiveRecord::Storeにおけるハッシュのシリアライズを通常のハッシュとして行うよう修正。

従来はActiveSupport::HashWithIndifferentAccessとしてシリアライズしていたが、これは無駄が多く、YAML safe_loadで問題を生じる。

Jean Boussier
同CHANGELOGより

この修正は、Rails 7.0.4と6.1.7でリリース済みです

🔗 Add timestamptz as a time zone aware type for PostgreSQL by ghiculescu · Pull Request #44601 · rails/rails

PostgreSQLのタイムゾーン対応型timestamptzを追加。

データベース内でtimestamp with time zone値を正しく解析するにはこの型が必要。

これを使いたくない場合は、イニシャライザに以下を追加することで無効にできる。

ActiveRecord::Base.time_zone_aware_types -= [:timestamptz]

Alex Ghiculescu
同CHANGELOGより

この修正は、Rails 7.0.4でリリース済みです

🔗 Add ActiveRecord::Base::generates_token_for by jonathanhefner · Pull Request #44189 · rails/rails

ActiveRecord::Base.generates_token_for APIを新たに追加。

現在のsigned_idは、パスワードのリセットなどでトークンを生成する役割を担当している。しかしsigned_idにはレコードのステートを反映できないので、トークンを1回だけ使う場合は、少なくとも期限切れまでデータベースでトラッキングしなければならなくなる。

generates_token_forを使うことで、トークンにレコードのデータを埋め込めるようになる。このトークンを用いてレコードを取得すると、トークンのデータと現在のレコードのデータが比較される。両者が一致しない場合、トークンは無効と見なされるため、期限切れした場合と同じ扱いになる。

例:

class User < ActiveRecord::Base
  has_secure_password

  generates_token_for :password_reset, expires_in: 15.minutes do
    # `password_salt`(`has_secure_password`で定義される)は、
    # そのパスワードのsaltを返す。パスワードが変更されるとsaltも変更されるので、
    # パスワードが変更されるとこのトークンは無効になる。
    BCrypt::Password.new(password_digest).salt[-10..]
  end
end

user = User.first
token = user.generate_token_for(:password_reset)

User.find_by_token_for(:password_reset, token) # => user

user.update!(password: "new password")
User.find_by_token_for(:password_reset, token) # => nil

Jonathan Hefner
同CHANGELOGより

参考: Rails API generates_token_for -- `ActiveRecord::TokenFor::ClassMethods

🔗 Optimize Active Record batching for whole table iterations by fatkodima · Pull Request #45414 · rails/rails

Active Recordのテーブル全体をイテレーションするバッチを最適化。

従来のin_batchesでは、全idを取得したうえでバッチごとにINベースのクエリを構築していた。テーブル全体をイテレーションする場合、この方法では不要なidまで読み込まれてしまい、INクエリの項目数が増えて遅くなる。

改修後は、テーブル全体のイテレーションでは範囲指定(id >= x AND id <= y)をデフォルトで使うようになり、イテレーションが数倍高速になる。たとえば、PostgreSQLで1000万件のレコードを持つテーブルでテストした場合のクエリ時間は253s-> 30s、更新は288s->124s、削除は268s-> 83sとなった。

このイテレーションをデフォルトで使うのはテーブル全体をイテレーションする場合に限られる。この振る舞いは、use_ranges: falseオプションを渡すことで無効化できる。

テーブル全体のイテレーションで、 archived_at: nil のような条件だけを指定する場合(かつアーカイブ済みのレコードがごく一部しかない場合)、このアプローチを採用する意義がある。

Project.where(archived_at: nil).in_batches(use_ranges: true) do |relation|
  # 何かする
end

詳しくは#45414を参照。

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20220711: Active Recordのin_batchesuse_ranges: trueを指定可能になった

🔗 Common Table Expression support added "out-of-the-box" by vlado · Pull Request #37944 · rails/rails

.withクエリメソッドを追加。
CTE(Common Table Expression)を手軽に構築してActiveRecord::Relationを得られるようになった。

Post.with(posts_with_comments: Post.where("comments_count > ?", 0))
# => ActiveRecord::Relation
# WITH posts_with_comments AS (SELECT * FROM posts WHERE (comments_count > 0)) SELECT * FROM posts

Vlado Cingel
同CHANGELOGより

参考: PostgreSQL 15ドキュメント 7.8. WITH問い合わせ(共通テーブル式)
参考: 週刊Railsウォッチ20220711: ActiveRecord::RelationにCTEを利用できるwithメソッドが追加

🔗 Only remove connection for an existing pool if the config is different by eileencodes · Pull Request #45450 · rails/rails

同一のコネクションプールが既に存在する場合は新しいコネクションを確立しないようになった。

従来は、既に確立済みのコネクションを持つクラスで establish_connection が呼び出されると、設定が同じかどうかに関係なく既存のコネクションが削除された。
改修後は、新しいコネクションと同じ値を持つコネクションプールが見つかった場合、新しいコネクションではなく既存のコネクションを返すようになった。

アプリケーションのコードが、新しいコネクションが既存のコネクションと同一かどうかにかかわらず確立される振る舞いに依存している場合、振る舞いがわずかに変更されることになる。
古い振る舞いに戻したい場合は、新しいコネクションを確立する前にActiveRecord::Base#remove_connectionを呼び出す必要がある。設定を変えてestablish_connectionを呼び出す場合の振る舞いは、従来と同様になる。

Eileen M. Uchitelle
同CHANGELOGより

参考: Rails API establish_connection -- ActiveRecord::ConnectionHandling

🔗 Allow db:prepare to load schema if database already exists but is empty; also dumps schema after migrations by bensheldon · Pull Request #45464 · rails/rails

db:prepareタスクを更新。

初期化されていないデータベースが存在する場合はスキーマを読み込み、残りのマイグレーションがあれば実行して、その後スキーマをダンプするように変更した。

Ben Sheldon
同CHANGELOGより

参考: 週刊Railsウォッチ20220719: 空のデータベースが存在していてもdb:prepareでスキーマを読み込み可能にした

🔗 Fix supporting timezone awareness for tsrange array columns. by morgoth · Pull Request #45348 · rails/rails

(PostgreSQLのみ)範囲型のtsrangeカラムとtstzrangeカラムでタイムゾーンを認識するサポートを修正。

# データベースのマイグレーション内
add_column :shops, :open_hours, :tsrange, array: true

# アプリのコンフィグ内
ActiveRecord::Base.time_zone_aware_types += [:tsrange]

# このコードのtimeがアプリのタイムゾーンに正しく変換されるようになる
Shop.create!(open_hours: [Time.current..8.hour.from_now])

Wojciech Wnętrzak
同CHANGELOGより

この修正は、Rails 7.0.4でリリース済みです

参考: Rails API ActiveRecord::ConnectionAdapters::PostgreSQL::ColumnMethods
参考: PostgreSQL 15ドキュメント 8.17. 範囲型

🔗 Introduce "Execution Strategy" object for Migrations by adrianna-chang-shopify · Pull Request #45324 · rails/rails

マイグレーションの実行にStrategyパターンを導入。

デフォルトでは、メソッドをコネクションアダプタに委譲するstrategyオブジェクトをマイグレーションで使う。利用側(consumer)は、カスタムのstrategyオブジェクトを実行することでマイグレーションの実行方法を変更できるようになる。

Adrianna Chang
同CHANGELOGより

参考: config.active_record.migration_strategy -- Rails アプリケーションを設定する - Railsガイド
参考: Strategy パターン - Wikipedia
参考: 週刊Railsウォッチ20220704: マイグレーションをExecutionStrategyでカスタマイズ可能にする

🔗 Add adapter option disallowing foreign keys by promulo · Pull Request #45301 · rails/rails

アダプタ設定ファイルのオプションで外部キーを禁止できるようになった。

新しいforeign_keysオプションはdatabase.ymlに追加可能。これにより、背後のデータベースで外部キー制約がサポートされていても外部キー制約をスキップできるようになる。

利用法:

development:
    <<: *default
    database: storage/development.sqlite3
    foreign_keys: false

Paulo Barros
同CHANGELOGより

参考: 週刊Railsウォッチ20220704: database.ymlでforeign_keys: falseを指定可能になった

🔗 Add configurable deprecation warning for singular associations by HParker · Pull Request #45344 · rails/rails

単数形の関連付けに対する非推奨化警告をコンフィグで設定できるようになった。

このコンフィグは、単数形の関連付け名をwhere内で複数形で参照する非推奨の書き方(遅くなる)に警告を表示するかどうかを制御する。

以下の設定にするとパフォーマンスが改善する。

config.active_record.allow_deprecated_singular_associations_name = false

Adam Hess
同CHANGELOGより

このコンフィグは、Rails 7.1ではデフォルトでfalseになります。
参考: config.active_record.allow_deprecated_singular_associations_name -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20220704: 関連付け先の単数形の名前をwhere内から複数形で参照すると警告を出す

🔗 Run transactional callbacks on instances most likely to match DB state by cbothner · Pull Request #45280 · rails/rails

トランザクション内でレコードをsaveすると、最も新しいインスタンスでトランザクションコールバックを実行するようになった。

1つのトランザクション内で複数のActive Recordインスタンスが同じレコードを変更する場合、そのうちの1つだけがafter_commitafter_rollbackを実行する。
Railsでどのインスタンスがコールバックを受け取るかを指定できるよう、config.active_record.run_commit_callbacks_on_first_saved_instances_in_transactionコンフィグが追加された。フレームワークはデフォルトで新しいロジックを使うよう変更された。

config.active_record.run_commit_callbacks_on_first_saved_instances_in_transactiontrueの場合は、インスタンスのステートがstaleしていても(=古くなっても)、最初に保存したインスタンスでトランザクションコールバックが実行される。
これがfalseの場合は7.1からフレームワークのデフォルトになるが、トランザクションコールバックはステートが最新のインスタンスで実行される。インスタンスは以下のように選択される。

  • 一般に、トランザクションコールバックは最新のインスタンスで実行され、トランザクション内で指定のレコードを保存する。
  • ただし例外が2つある。
    • トランザクション内でレコードを作成して別のインスタンスで更新すると、after_create_commitは2番目のインスタンスで実行される。これは、インスタンスのステートに基づいてナイーブに実行されるafter_update_commitコールバックの代わりとなる。
    • レコードがトランザクション内で削除されると、after_destroy_commitコールバックは最後に削除されたインスタンスで実行される。これは、たとえstaleしたインスタンスがその後更新を行ったとしても同様で、この更新はどの行にも影響しない。

Cameron Bothner and Mitch Vollebregt
同CHANGELOGより

参考: 週刊Railsウォッチ20220620: トランザクション内に同一モデルのインスタンスが複数ある場合にどのインスタンスからコールバックを呼び出すかを変更

🔗 Add :strict option to default SQLite database.yml template by fatkodima · Pull Request #45346 · rails/rails

SQLite3Adapterで"strict strings"モードを有効にした。

SQLite3で"strict strings"モードを設定することで、二重引用符("")で囲まれた文字列リテラルが無効になった。

SQLite3では、二重引用符で囲んだ文字列リテラルにいくつかの癖がある。
最初は二重引用符で囲んだ文字列を識別子名とみなそうとするが、存在しない場合は文字列リテラルとみなす。この振る舞いのせいで、タイポがあっても通知されない。たとえば、存在しないカラムに対してインデックスを作成できてしまう。
詳しくは以下のSQLite3ドキュメントを参照。

この振る舞いを無効にしたい場合は、以下の設定で行える。

# config/application.rb
config.active_record.sqlite3_adapter_strict_strings_by_default = false

修正: #27782

fatkodima, Jean Boussier
同CHANGELOGより

参考: config.active_record.sqlite3_adapter_strict_strings_by_default -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20220620: SQLiteのdatabase.ymlにデフォルトで:strictオプションを追加

🔗 Update AR Relation method to reset cache_version by austenmadden · Pull Request #45342 · rails/rails

リレーションのcache_versionがstaleする(古くなる)可能性がある問題を修正。

従来は、リレーションオブジェクトでresetを呼び出しても@cache_versionsがリセットされていなかったため、最新の正しいデータがあるにもかかわらず、古いcache_versionの値が返されて混乱することがあった。

利用法:

developers = Developer.all
developers.cache_version

Developer.update_all(updated_at: Time.now.utc + 1.second)

developers.cache_version # cache_versionがstaleする
developers.reset
developers.cache_version # 最新の正しいcache_versionを返す

修正: #45341

Austen Madden
同CHANGELOGより

参考: 週刊Railsウォッチ20220620: Active Recordリレーションのresetcache_versionがリセットするよう修正

🔗 Add support for exclusion constraints (PostgreSQL-only) by agrobbin · Pull Request #40224 · rails/rails

(PostgreSQLのみ)EXCLUDE制約をサポート。

  • add_exclusion_constraint
  • remove_exclusion_constraint
add_exclusion_constraint :invoices, "daterange(start_date, end_date) WITH &&", using: :gist, name: "invoices_date_overlap"
remove_exclusion_constraint :invoices, name: "invoices_date_overlap"

EXCLUDE制約について詳しくはPostgreSQLドキュメントのCREATE TABLE ... EXCLUDE ...を参照。

Alex Robbin
同CHANGELOGより

参考: 週刊Railsウォッチ20220620: (PostgreSQLのみ)EXCLUDE制約のサポートが追加

🔗 change_column_null should raise if a non-boolean 3rd argument is provided by ghiculescu · Pull Request #45229 · rails/rails

change_column_nullの第3引数がブーリアンでない場合はエラーを発生するよう修正。

従来は、change_column_nullの第3引数がブーリアンでない場合にtruthyとして扱われ、カラムがnull許容になった。この振る舞いは予想に反するので、trueまたはfalseのみを渡せるよう変更された。

change_column_null :table, :column, true # good
change_column_null :table, :column, false # good
change_column_null :table, :column, from: true, to: false # raiseする(従来はnullableカラムになった)

Alex Ghiculescu
同CHANGELOGより

参考: 週刊Railsウォッチ20220620: change_column_nullにブーリアン以外の値を渡すとエラーになるように修正

🔗 Enforce limit on table names length by fatkodima · Pull Request #45136 · rails/rails

テーブル名の長さに上限を設けた。

修正: #45130

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20220620: テーブル名の長さに上限を設定

🔗 Correct minimum MariaDB version for CHECK_CONSTRAINTS by elebow · Pull Request #45326 · rails/rails

CHECK制約サポートのため、MariaDBの最小バージョン指定を10.2.22に修正。

Eddie Lebow
同CHANGELOGより

🔗 Fix Hstore deserialize regression by edsharp · Pull Request #45222 · rails/rails

PostgreSQLのHstoreデータ型のデシリアイズで再発した不具合を修正。

edsharp
同CHANGELOGより

参考: PostgreSQL 15ドキュメント F.18. hstore

🔗 Add validity for PostgreSQL indexes by fatkodima · Pull Request #45160 · rails/rails

PostgreSQLインデックスの有効性をチェックするvalid: trueオプションを追加。

connection.index_exists?(:users, :email, valid: true)
connection.indexes(:users).select(&:valid?)

fatkodima
同CHANGELOGより

参考: 週刊Railsウォッチ20220606: PostgreSQL用のindex_exists?valid:キーワード引数が追加

🔗 Fix eager loading models without primary keys by mattalat · Pull Request #43402 · rails/rails

主キーがないモデルのeager_loadが正しく行われない問題を修正。

Anmol Chopra, Matt Lawrence, and Jonathan Hefner
同CHANGELOGより

プルリクメッセージには、通常のActive Recordでも、データベースVIEWを使っている場合や、以下のようなメソッドチェインでこの問題が起きていたと書かれています。

my_instance.includes(:model_without_primary_key).order('model_without_primary_key.name')

参考: 週刊Railsウォッチ20220606: 主キーのないモデルのeager_loadを修正

🔗 Avoid validating a unique field if it has not changed and is backed by a unique index by fatkodima · Pull Request #45149 · rails/rails

uniqueインデックスのあるフィールドで変更が生じなかった場合は、フィールドのuniquenessバリデーションを回避するようになった。

従来は、レコードをsaveしたときにuniquenessバリデーションが設定されている属性でuniquenessチェックのためのクエリが余分に送信されていた。これは属性に変更がなかった場合でも発生していた。

これに対応するuniqueインデックスがデータベース側にあれば、永続化でこのバリデーションが失敗することはありえないので、このバリデーションを安全にスキップできる。

fatkodima
同CHANGELOGより

参考: §2.12 uniqueness -- Active Record バリデーション - Railsガイド
参考: 週刊Railsウォッチ20220531: uniqueness指定のフィールドが変更されていない場合のバリデーションを回避

🔗 no longer set sql_auto_is_null by HParker · Pull Request #45134 · rails/rails

MySQLアダプタでvariables["sql_auto_is_null"] = 0を設定しないようになった。

この設定はMySQL 5.5以降デフォルトでオフになったので、手動でわざわざオフにする必要はない。

Adam Hess
同CHANGELOGより

参考: PDF MySQL 5.5 Release Notes

🔗 Fix touch to raise an error for readonly columns by fatkodima · Pull Request #45125 · rails/rails

attr_readonlyカラムにtouchしたらActiveRecord::ActiveRecordErrorを発生するよう修正。

fatkodima
同CHANGELOGより

同プルリクメッセージでは以下のように書かれています。

  • update_attributeおよびupdate_attributesではエラーになる
  • 属性への代入やupdateは、readonly属性についてはsave時に単に無視される(以下のAPIドキュメントに書かれている通り)。

参考: Rails API attr_readonly -- ActiveRecord::ReadonlyAttributes::ClassMethods

🔗 Add ability to ignore tables by regexp for SQL schema dumps by fatkodima · Pull Request #45091 · rails/rails

SQLスキーマダンプで除外したいテーブルを正規表現で指定できるようになった。

ActiveRecord::SchemaDumper.ignore_tables = [/^_/]

fatkodima
同CHANGELOGより

参考: ActiveRecord::SchemaDumper.ignore_tables -- Rails アプリケーションを設定する - Railsガイド

🔗 Avoid query from calculations on contradictory relation by luanzeba · Pull Request #45030 · rails/rails

矛盾のあるリレーションで計算メソッドを実行するときにクエリ送信を回避するよう修正。

従来は、User.where(id: []).countのように矛盾のあるリレーションを渡すと計算時にクエリが送信されていた。修正により、このようなシナリオでクエリを送信しなくなった。

該当する計算メソッドは以下のとおり。

  • count
  • sum
  • average
  • minimum
  • maximum

Luan Vieira, John Hawthorn and Daniel Colson
同CHANGELOGより

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

Rails 7: リレーションの結果が空になる計算でクエリ送信を回避する(翻訳)

🔗 Allow using aliased attributes with insert_all/upsert_all by fatkodima · Pull Request #45036 · rails/rails

insert_allupsert_allでエイリアス属性も指定できるようになった。

class Book < ApplicationRecord
  alias_attribute :title, :name
end

Book.insert_all [{ title: "Remote", author_id: 1 }], returning: :title

fatkodima
同CHANGELOGより

参考: Rails API insert_all -- ActiveRecord::Persistence::ClassMethods
参考: Rails API upsert_all -- ActiveRecord::Persistence::ClassMethods

Rails 7: insert_allとupsert_allで属性のエイリアスを指定可能になる(翻訳)

🔗 Support encrypted attributes on columns with default values by jorgemanrubia · Pull Request #45033 · rails/rails

カラムの暗号化属性でデータベースのデフォルト値をサポート。

これにより、カラムに定義された暗号化属性にデフォルト値を設定できるようになる。値は作成時に暗号化される。
改修前は、config.active_record.encryption.support_unencrypted_datatrueを設定しないとエラーになっていた。

Jorge Manrubia and Dima Fatko
同CHANGELOGより

参考: §6.1.1 config.active_record.encryption.support_unencrypted_data -- Active Record と暗号化 - Railsガイド
参考: 週刊Railsウォッチ20220516: デフォルト値付きのカラムで暗号化属性をサポート

🔗 Multi database: define reading_request? in resolver by ghiculescu · Pull Request #44944 · rails/rails

DatabaseSelector::Resolverミドルウェアのreading_request?がオーバーライド可能になった。

デフォルトの実装ではリクエストがget?またはhead?かどうかをチェックしているが、この振る舞いを自由に変更できるようになった。
このメソッドがtrueを返すとResolver#readが呼び出されるようになり、リクエストがreplicaデータベースによっても配信されるようになる。

Alex Ghiculescu
同CHANGELOGより

参考: Rails API ActiveRecord::Middleware::DatabaseSelector

Rails 7: マルチプルDBのreading_request?がカスタマイズ可能になった(翻訳)

🔗 Remove legacy_connection_handling by eileencodes · Pull Request #44827 · rails/rails

Rails 6.1から非推奨化されていたActiveRecord.legacy_connection_handlingを削除。

Eileen M. Uchitelle
同CHANGELOGより

参考: §2.1 データベース単位のコネクション切り替え -- Ruby on Rails 6.1 リリースノート - Railsガイド

参考: 週刊Railsウォッチ20220411: legacy_connection_handlingを削除

🔗 rails db:schema:{dump,load} now checks ENV["SCHEMA_FORMAT"] before config by ghiculescu · Pull Request #44834 · rails/rails

rails db:schema:{dump,load}でコンフィグ前にENV["SCHEMA_FORMAT"]をチェックするようになった。

rails db:structure:{dump,load}は既に非推奨化されているため、スキーマをSQL形式とRuby形式のどちらでも手軽に(=コンフィグを変更せずに)ダンプできる方法がなかった。
この改修により、以下のように環境変数を設定することでこれを行えるようになった。

SCHEMA_FORMAT=sql rake db:schema:dump

Alex Ghiculescu
同CHANGELOGより

この変更は、Rails 7.0.4でリリース済みです

なおconfig.active_record.schema_formatのデフォルトは:rubyです。

参考: §3.8.10 config.active_record.schema_format -- Rails アプリケーションを設定する - Railsガイド

Rails 7: rails db:schema:dumpやloadのスキーマ形式を環境変数で指定可能になった(翻訳)

Rails: db:structure:loadとdb:structure:dumpタスクが非推奨化(翻訳)

🔗 Fixed MariaDB default function by kaspernj · Pull Request #44654 · rails/rails

MariaDBでのデフォルトSQL関数サポートを修正。

db/schema.rbへのダンプでデフォルト関数名が正しく書き込まれていなかったため、db:schema:loadを実行しても正しく動作しなかった。今後、より多くの関数が新規レコードの保存で文字列コンテンツとして追加されるようになるだろう。

kaspernj
同CHANGELOGより

参考: 週刊Railsウォッチ20220328: MariaDBのデフォルト関数サポートを修正

🔗 Add active_record.destroy_association_async_batch_size configuration by nholden · Pull Request #44617 · rails/rails

非同期バッチサイズを指定するactive_record.destroy_association_async_batch_sizeコンフィグを追加。

これにより、アプリケーションでの関連付けにdependent: :destroy_asyncオプションを指定して、単一のバックグラウンドジョブで削除する最大レコード数を指定できるようになる。デフォルトでは、現在の振る舞いを変えない(親レコードを削除すると、すべての依存レコードが単一のバックグラウンドジョブで削除される)。依存レコード数がこの設定を超えると、レコードの削除が複数のバックグラウンドジョブに分割されるようになる。

Nick Holden
同CHANGELOGより

参考: config.active_record.destroy_association_async_batch_size -- Rails アプリケーションを設定する - Railsガイド

Rails 7: バックグラウンドジョブで削除する最大レコード数を指定可能になった(翻訳)

🔗 Fix remove_foreign_key with :if_exists option when foreign key actually exists by fatkodima · Pull Request #44637 · rails/rails

remove_foreign_key:if_existsオプションを指定すると、外部キーが実際に存在している場合にエラーになっていたのを修正。

fatkodima
同CHANGELOGより

参考: §3.7 外部キー -- Active Record マイグレーション - Railsガイド

🔗 Remove --no-comments from Postgres structure dump command by ghiculescu · Pull Request #44633 · rails/rails

PostgreSQLのstructure dumpの--no-commentsフラグを廃止。

これにより、スキーマでカスタムコメントを使っている一部のアプリが動かなくなる。
structureダンプにコメントを含めたくない場合は、以下を設定できる。

ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ['--no-comments']

Alex Ghiculescu
同CHANGELOGより

参考: §6 Structure Dumpについて -- Active Record と PostgreSQL - Railsガイド

🔗 Reduce the memory footprint of fixtures accessors by casperisfine · Pull Request #44528 · rails/rails

フィクスチャのアクセサでメモリフットプリントを削減。

従来は、フィクスチャのアクセサをdefine_methodでeagerに定義していたため、フィクスチャやテストスイートの量が増えるとメモリ使用量に直接影響していた。

改修後は、フィクスチャのアクセサをmethod_missingで実装したことで、メモリやCPUのオーバーヘッドが大きく軽減される。

Jean Boussier
同CHANGELOGより

参考: 週刊Railsウォッチ20220308: フィクスチャのメモリフットプリントを削減
参考: Module#define_method (Ruby 3.2 リファレンスマニュアル)
参考: BasicObject#method_missing (Ruby 3.2 リファレンスマニュアル)

🔗 Fix config.active_record.destroy_association_async_job configuration by nholden · Pull Request #44309 · rails/rails

config.active_record.destroy_association_async_jobコンフィグの不具合を修正。

config.active_record.destroy_association_async_jobは、dependent: :destroy_asyncオプションを指定したhas_many関連付けに対して行う削除をバックグラウンドで実行できるようにすべき。
従来はこのdependent: :destroy_asyncオプションが無視されていたため、ActiveRecord::DestroyAssociationAsyncJobによる削除が常にバックグラウンドで実行されていた。

Nick Holden
同CHANGELOGより

参考: config.active_record.destroy_association_async_job -- Rails アプリケーションを設定する - Railsガイド
参考: 週刊Railsウォッチ20220221: Active Recordのdestroy_association_async_jobコンフィグが効くように修正

🔗 Fix change_column_comment to preserve column's AUTO_INCREMENT in the MySQL adapter by fatkodima · Pull Request #44480 · rails/rails

MySQLアダプタ: change_column_commentでカラムのAUTO_INCREMENTが失われないよう修正。

fatkodima
同CHANGELOGより

参考: Rails API change_column_comment -- ActiveRecord::ConnectionAdapters::SchemaStatements

🔗 Handle quoting of Rational numbers for MySQL by kmcphillips · Pull Request #44404 · rails/rails

MySQLアダプタで、数値がActiveSupport::DurationRationalの場合の引用符処理を修正。

Kevin McPhillips
同CHANGELOGより

参考: 週刊Railsウォッチ20220221: mysql2アダプタでActiveSupport::Durationを適切に扱うよう修正

🔗 Allow column name with COLLATE as safe SQL string by shugo · Pull Request #44384 · rails/rails

orderでカラムにCOLLATEを指定した場合(例: title COLLATE "C")にも、SQL関数を指定したときと同様に安全なSQL文字として扱えるようになった。

Shugo Maeda
同CHANGELOGより

参考: Allow column name with function (e.g. length(title)) as safe SQL string by kamipo · Pull Request #36448 · rails/rails
参考: 週刊Railsウォッチ20220221: orderCOLLATEを安全なSQL文字列として使えるようになった

参考: PostgreSQL 15ドキュメント 24.2. 照合順序サポート
参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 10.8.1 SQL ステートメントでの COLLATE の使用

🔗 Accept _ integer notation in VERSION arg to database tasks · rails/rails@ef4bf94

データベース用rakeタスクのVERSION引数でアンダースコア(_)も使えるようになった。

Eddie Lebow
同CHANGELOGより

参考: 週刊Railsウォッチ20220221: DBのrakeタスクでVERSION envの数値に_が使えるようになった

🔗 Reverse the order of INSERT statements in structure.sql dumps by ghiculescu · Pull Request #44363 · rails/rails

structure.sqlダンプのINSERTステートメントで値の並び順を逆にした。

これにより、マージで競合する可能性が減るはず。
新しいマイグレーションでは、新しい値がリストのトップに追加されるようになる。

ただし、既存のアプリでは次回structure.sqlを生成したときに大量の差分が発生することになる。

Alex Ghiculescu, Matt Larraz
同CHANGELOGより

🔗 Postgres adapter passes keywords in a deprecated way (Ruby 2.7, ActiveRecord 6.1.4) · Issue #44307 · rails/rails

Ruby 2.7とActive Record 6.1.4でPG.connectにキーワード引数を渡すと非推奨警告が表示されていたのを修正。

修正: #44307

Nikita Vasilevsky
同CHANGELOGより

この修正は、Rails 7.0.2でリリース済みです

🔗 Fix rollbacks following serialization failures or deadlocks by zarqman · Pull Request #44127 · rails/rails

関連: [Tests only] Flunk if test is not using SavepointTransaction by nvasilevski · Pull Request #44686 · rails/rails

シリアライズが失敗してデッドロックした後でデータベースコネクションが切断されるバグを修正。

6.1.4より前は、シリアライズが失敗してデッドロックすると、実際のトランザクションとsavepointの両方でロールバックが発行されていた。MySQLはデッドロック後にsavepointのロールバックを許さないため、これによって壊れてしまう。

6.1.4では実際のトランザクションとsavepointの両方でロールバックが削除されたが、これによってデータベースコネクションのステートがunknownになり、切断されてしまう。

この修正によって、MySQLのsavepointを除いてロールバックが復元されるようになった。

Thomas Morgan
同CHANGELOGより

参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.3.4 SAVEPOINT、ROLLBACK TO SAVEPOINT および RELEASE SAVEPOINT ステートメント

🔗 Fiber-safe ConnectionPool by machty · Pull Request #44219 · rails/rails

ActiveRecord::ConnectionPoolがFiberセーフになった。

ActiveSupport::IsolatedExecutionState.isolation_level:fiberを指定すると、コネクションプールからコネクションをチェックアウトする同一のThreadからの複数のFiberをサポートするようになる。

Alex Matchneer
同CHANGELOGより

Rails 7: Active RecordのConnectionPoolsがFiberセーフになった(翻訳)

🔗 Add ActiveRecord::Persistence#update_attribute! by drewtempelmeyer · Pull Request #44141 · rails/rails

ActiveRecord::Persistenceupdate_attribute!を追加。

update_attributeと同様だが、before_*コールバックで:abortがスローされた場合はActiveRecord::RecordNotSavedをraiseする点が異なる。

class Topic < ActiveRecord::Base
  before_save :check_title

  def check_title
    throw(:abort) if title == "abort"
  end
end

topic = Topic.create(title: "Test Title")
# #=> #<Topic title: "Test Title">
topic.update_attribute!(:title, "Another Title")
# #=> #<Topic title: "Another Title">
topic.update_attribute!(:title, "abort")
# raises ActiveRecord::RecordNotSaved

Drew Tempelmeyer
同CHANGELOGより

参考: Rails API update_attribute -- ActiveRecord::Persistence
参考: 週刊Railsウォッチ20220117: update_attribute!が追加

🔗 Avoid eager loading in Relation#pretty_print by BuonOmo · Pull Request #43302 · rails/rails

ActiveRecord::Relation#pretty_printで全レコードの読み込みを回避するようになった。

# 修正前
pp Foo.all # 全テーブルを読み込む

# 修正後
pp Foo.all # 10件まで表示し、残りを"..."で表示

Ulysse Buonomo
同CHANGELOGより

参考: 週刊Railsウォッチ20220117: Relation#pretty_printのeager loadingを回避

🔗 QueryMethods#in_order_of drop records not listed by kddnewton · Pull Request #44097 · rails/rails

QueryMethods#in_order_ofメソッドで、指定の値にないレコードをWHEREで除外するように変更。

Enumerablein_order_ofと振る舞いを合わせるため、QueryMethodsin_order_ofに渡した値のリストで絞り込んだものを並べ替えるようにした。

Kevin Newton
同CHANGELOGより

# 同PRのAPIドキュメントより
 User.in_order_of(:id, [1, 5, 3])
 # SELECT "users".* FROM "users"
 #   ORDER BY FIELD("users"."id", 1, 5, 3)
 #   WHERE "users"."id" IN (1, 5, 3)

注: このプルリクによって、#43916の「指定外のレコードのソート順も維持する振る舞い」が打ち消されました。

参考: この改修はRails 7.0.1でリリース済みです

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

🔗 Allow named expression indexes to be revertible. by oliverguenther · Pull Request #43333 · rails/rails

名前付きの式インデックスをロールバックできるよう修正。

従来は、リバーシブル(取り消し可能)なマイグレーションのロールバックで以下のコードがエラーになった(インデックスの削除でこのインデックス名が使われていなかったため)。

add_index(:settings, "(data->'property')", using: :gin, name: :index_settings_data_property)

修正: #43331

Oliver Günther
同CHANGELOGより

🔗 Updating the --no-comment argument to the correct --no-comments argument by aldent95 · Pull Request #44028 · rails/rails

PostgreSQLのstructure dumpタスクのヘルプ説明文にある引数名のスペルを修正。

Rails 7で追加された--no-comment引数を正しい--no-comments(複数形)に更新した。

Alex Dent
同CHANGELOGより

🔗 Fix migration compatibility to create SQLite references/belongs_to column as a integer when migration version is 6.0 by marcelolx · Pull Request #43295 · rails/rails

マイグレーションのバージョンが6.0の場合に、SQLite3のreferencesカラムとbelongs_toカラムがinteger型として作成されるよう修正。

バージョン6.0のreferencesカラムとbelongs_toカラムが、SQLite3のアダプタでinteger型ではなくbigint型で作成されていた。

Marcelo Lauxen
同CHANGELOGより

この修正は、Rails 6.1.5でリリース済みです

🔗 Fix QueryMethods#in_order_of to handle empty order list by casperisfine · Pull Request #43916 · rails/rails

QueryMethods#in_order_ofに空のリストを渡せるよう修正。

Post.in_order_of(:id, []).to_a

また、このカラムを明示的に第2ソート順として設定することで、指定以外の値のソート順を維持するようになった。

Jean Boussier

同CHANGELOGより

注: 指定以外の値のソート順を維持する振る舞いは、その後#44097でなくなりました。

この改修は、Rails 7.0.1でリリース済みです

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

🔗 Properly quote autogenerated column aliases by casperisfine · Pull Request #43911 · rails/rails

計算系メソッドによって生成されるカラムエイリアスを正しく引用符で囲むよう修正。

このエイリアスはテーブル名から導出されるので、有効な識別子を得られるという前提が成り立つとは限らない(テーブル名が数字始まりの場合など)。

class Test < ActiveRecord::Base
  self.table_name = '1abc'
end

Test.group(:id).count
# syntax error at or near "1" (ActiveRecord::StatementInvalid)
# LINE 1: SELECT COUNT(*) AS count_all, "1abc"."id" AS 1abc_id FROM "1...

Jean Boussier
同CHANGELOGより

🔗 Add authenticate_by when using has_secure_password by jonathanhefner · Pull Request #43765 · rails/rails

has_secure_passwordauthenticate_byメソッドを追加。

このauthenticate_byメソッドは、以下のようなコードを置き換えるのが目的。このコードは、メールアドレスが一致するユーザーが見つからない場合に、見つかった場合よりも早期に処理が終了してしまう。

User.find_by(email: "...")&.authenticate("...")

このようなコードは、タイミングベースの列挙攻撃に対して脆弱である。攻撃者はこの脆弱性を使って、特定のメールアドレスを持つユーザーアカウントが存在するかどうかを検索時間の違いで判定可能になる。アカウントが存在することを攻撃者が確認できたら、他のデータベースから漏洩したパスワードの中から、そのメールアドレスに関連付けられたパスワードを試せるようになる。これは、ユーザーが同じパスワードを複数のサイトで使い回すというありがちな運用で起こる可能性がある。さらに、攻撃者がアカウントのメールアドレスを手に入れれば標的型フィッシング("spear phishing")攻撃を試みることも可能になる。

authenticate_byは、メールアドレスが一致するユーザーが見つかった場合にかかる時間と、一致するユーザーが見つからない場合にかかる時間を同じにすることで、この脆弱性を修正する。

User.authenticate_by(email: "...", password: "...")

Jonathan Hefner

注: その後、パスワードが空の場合の修正も7.1.0.beta1に含まれました。

Short circuit authenticate_by on empty password by jonathanhefner · Pull Request #43958 · rails/rails

Rails 7: has_secure_password利用時のauthenticate_byメソッド(翻訳)


以前の変更については7-0-stableのCHANGELOGを参照。

関連記事

Rails 7.1に入る主要な機能まとめ(1)update_attribute!、CTEサポートほか(翻訳)

Rails 7.1に入る主要な機能まとめ(2)error_highlight対応、routes --grepほか(翻訳)

Rails 7.1に入る主要な機能まとめ(3)Docker関連ファイル導入ほか(翻訳)

The post Rails 7.1.0 Active Record CHANGELOG(翻訳) first appeared on TechRacho.

has_manyにブロック引数を渡してリレーションを拡張する

$
0
0

今日はhas_manyのブロック引数に関して取り上げます。

以下のモデルを想定します。

# attributes
#
# name: 氏名
# attendance_count: 出席回数
class Student < ApplicationRecord
  has_many :exams
end
# attributes
#
# student_id: 学生ID
# subject: 教科
# period: テスト実施回
# score:  得点
class Exam < ApplicationRecord
  belongs_to :student
end

スコープブロック

has_many のブロックと聞くとまずスコープブロックが思いつくと思います。
今回のテーマのブロック引数とは異なりますが、先にスコープブロックに関して触れていきます。

スコープブロックは has_many の第2引数に指定することで、関連リソースを取得するクエリーにwhereorderといった任意の句を追加することができます。

以下は、テストを高得点順に取得する例です。

class Student < ApplicationRecord
  has_many :exams, -> { order(score: :desc) }
end

student = Student.find(1)
studnt.exams.to_sql
#=> 
# SELECT "exams".* FROM "exams" WHERE "exams"."student_id" = 1
# ORDER BY "score" DESC

スコープブロックの引数

スコープブロックに引数を設定すると関連元オブジェクトを参照できます。

学生出席回数が5回未満の場合テストは無効とする例を示します。

class Student < ApplicationRecord
  has_many :exams, -> student { student.attendance_count >= 5 ? all : none }  
end

good_student.attendance_count #=> 10
good_student.exams.to_sql
#  => SELECT "exams".* FROM "exams" WHERE "exams"."student_id" = 1

no_good_student.attendance_count #=> 0
no_good_student.exams.to_sql
#  => SELECT "exams".* FROM "exams" WHERE "exams"."student_id" = 2 AND (1=0)

注意点として引数付きのスコープブロックを joinsincludes 等に指定することはできません。

Student.joins(:exam).to_sql
#=> 
# `check_eager_loadable!': The association scope 'exams' is instance dependent 
# (the scope block takes an argument). 
# Eager loading instance dependent scopes is not supported. (ArgumentError)

ブロック引数

has_manyにブロック引数を渡すことで、関連先リソースを取得するリレーションを拡張することができます。
第2引数に渡すスコープブロックとは全くの別物になります。

以下に学生の教科ごとの平均点を出力するメソッドを追加する例を示します。

class Student < ApplicationRecord
  has_many :exams do
    def subject_averages
      # SELECT
      #   AVG("exams"."score") AS "average_score",
      #   "exams"."subject"    AS "exams_subject"
      # FROM "exams"
      # WHERE "exams"."student_id" = $student_id
      # GROUP BY "exams"."subject"
      group(:subject).average(:score)
    end
  end
end

student.exams.pluck(:period, :subject, :score)
#=> 
# [
#  [1, "math", 80], [1, "japanese", 80], [1, "english", 66],
#  [2, "math", 56], [2, "japanese", 87], [2, "english", 99]
# ]

student.exams.subject_averages
#=> {"math"=>0.68e2, "japanese"=>0.835e2, "english"=>0.825e2}

student.exams.where(subject: 'japanese').subject_averages
#=> {"japanese"=>0.835e2}

ブロック内で定義したメソッドの self はhas_manyが生成したリレーションそのものになります。

class Student < ApplicationRecord
  has_many :exams do
    def return_self
      self
    end
  end
end

student = Student.find(1)
exams = student.exams.where(subject: 'japanese')
in_blocked_exams = exams.return_self

exams.object_id == in_blocked_exams #=> true

関連元オブジェクトの取得

has_many が生成するリレーションは ActiveRecord::Associations::CollectionProxy を継承します。
#proxy_association#owner から関連元オブジェクトが取得できます。

exams = Student.find(1).exams
exams.proxy_association.owner # => <Student id: 1, ...>

このため、ブロック引数内で定義したメソッドでは、関連元オブジェクトを用いたロジックを実装可能です。
以下は受験者ごとに試験レポートを出力する実装例です。

class Student
  has_many :exams do
    def report
      average = -> exams { Rational(exams.pluck(:score).sum, values.size).round(2).to_f }
      student = proxy_association.owner
      {
        student_id: student.id,
        student_name: student.name,
        student: records.count,
        exam_times: pluck(:period).uniq.size,
        subject_averages: records.group_by(&:subject).transform_values(&average),
      }
    end
  end

  delegate :report, to: :exams, prefix: true # define Student#exams_report
end


Student.includes(:exams).map(&:exams_report)
#=>
# [
#  {
#    :student_id=>1, :student_name=>"kazz", :exam_times=>2, 
#    :subject_averages=>{"math"=>68.0, "japanese"=>83.5, "english"=>82.5}
#  },
#  {
#   :student_id=>2, :student_name=>"tester", :exam_times=>1, 
#   :subject_averages=>{"english"=>97.0, "japanese"=>93.0, "math"=>87.0}
#  }
#]

引数付きスコープブロックでできなかった includes(:exams) を使うことができます。
ただし、実装するメソッド内ではキャッシュを使うように実装しておく必要があります

class Student
  has_many :exams do
    def total_sql_sum_scores
      sum(:score) # SELECT SUM("exams"."score")
    end
    def total_scores_array_sum
      records.pluck(:score).sum # [80, ...].sum
    end
  end

  delegate :total_sql_sum_scores, :total_scores_array_sum, to: :exams
end

SQLを用いる実装の場合N+1問題が発生します。

Student.includes(:exams).map(&:total_sql_sum_scores)
#  Student Load (0.6ms)  SELECT "students".* FROM "students"
#  Exam Load (0.5ms)  SELECT "exams".* FROM "exams" WHERE "exams"."student_id" IN ($1, $2)  [["student_id", 1], ["student_id", 2]]
#  Exam Sum (1.1ms)  SELECT SUM("exams"."score") FROM "exams" WHERE "exams"."student_id" = $1  [["student_id", 1]]
#  Exam Sum (0.4ms)  SELECT SUM("exams"."score") FROM "exams" WHERE "exams"."student_id" = $1  [["student_id", 2]]

キャッシュを利用するとN+1問題を抑制することができます。

Student.includes(:exams).map(&:total_scores_array_sum)

# Student Load (2.2ms)  SELECT "students".* FROM "students"
# Exam Load (1.4ms)  SELECT "exams".* FROM "exams" WHERE "exams"."student_id" IN ($1, $2)  [["student_id", 1], ["student_id", 2]]

ブロック引数の分離

has_manyのブロック引数が肥大化していくと、参照元クラスは本来の実装とは直接関係のないコードで埋め尽くされてしまいます。

class Student
  has_many :exams do
    def feature1
       ...
    end
    ...

  end
  # Studentの本来の実装が埋もれる
end

以下のように has_manyのオプション extend: にモジュールとして設定することで関心の分離が実現できます。

module ExamsExtension
  def feature1
     ...
  end
  ...
end

class Student
  has_many :exams, extend: ExamsExtension
end

参考


The post has_manyにブロック引数を渡してリレーションを拡張する first appeared on TechRacho.

Rails API: ActiveRecord::AutosaveAssociation(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。

訳文には適宜強調を加えています。

Rails API: ActiveRecord::AutosaveAssociation(翻訳)

AutosaveAssociationは、親がsaveされるときに、関連付けられているレコードも自動的にsaveされるようにするモジュールです。saveに加えて、mark_for_destruction済みの関連付けレコードのdestroyも行います(mark_for_destructionおよびmarked_for_destruction?を参照)。

親とその関連付けのsave、およびmark_for_destruction済みの関連付けレコードのdestroyは、すべて1個のトランザクション内で行われます。これにより、データベースの状態が不整合にならないようにしています。

関連付けでバリデーションが1個以上失敗すると、そのエラーメッセージは親に適用されます。

これは、「mark_for_destruction済みの関連付けが直接destroyされることはない」という意味でもある点にご注意ください。ただし、バリデーションが失敗したときの関連付けは引き続きmark_for_destruction済みのままになります。

また、「autosave: falseを宣言する」ことと「:autosaveを宣言しない」ことは同じでない点にもご注意ください。:autosaveオプションが存在しない後者の場合、新規の関連付けはsaveされますが、更新された関連付けはsaveされません。

🔗 バリデーション

子レコードは、validate: falseを指定しない限りバリデーションされます。

🔗 コールバック

自動保存オプションを指定した関連付けでは、さまざまなコールバック(around_savebefore_saveafter_createafter_update)をモデルに定義します。コールバックは「モデルでの定義順」で実行されることにご注意ください。また、自動保存コールバックが実行される前に関連付けの内容を変更することは避けてください。通常であれば、関連付けより後にコールバックを配置することをおすすめします。

🔗 1対1の例

class Post < ActiveRecord::Base
  has_one :author, autosave: true
end

上のように書くことで、親の変更とそれに関連付けられたモデルの変更が、自動的かつアトミックにsaveされるようになります。

post = Post.find(1)
post.title       # => "The current global position of migrating ducks"
post.author.name # => "alloy"

post.title = "On the migration of ducks"
post.author.name = "Eloy Duran"

post.save
post.reload
post.title       # => "On the migration of ducks"
post.author.name # => "Eloy Duran"

関連付けられたモデルを親のsave操作の一環としてdestroyするときは、以下のようにmark_for_destructionでマーキングするだけで簡単に行なえます。

post.author.mark_for_destruction
post.author.marked_for_destruction? # => true

ただし、上の時点ではデータベースからまだ削除されていません

id = post.author.id
Author.find_by(id: id).nil? # => false

post.save
post.reload.author # => nil

saveすると実際にデータベースから削除されます。

Author.find_by(id: id).nil? # => true

🔗 1対多の例

:autosaveオプションが宣言されていない場合、親がsaveされたときに子が新規レコードの場合にのみsaveされます。

class Post < ActiveRecord::Base
  has_many :comments # :autosaveオプションを宣言していない
end

post = Post.new(title: 'ruby rocks')
post.comments.build(body: 'hello world')
post.save # => postもcommentもsaveされる

post = Post.create(title: 'ruby rocks')
post.comments.build(body: 'hello world')
post.save # => postもcommentもsaveされる

post = Post.create(title: 'ruby rocks')
comment = post.comments.create(body: 'hello world')
comment.body = 'hi everyone'
post.save # => postはsaveされるがcommentはsaveされない

:autosave: trueを指定すると、子は新規レコードかどうかにかかわらず、すべてsaveされます。

class Post < ActiveRecord::Base
  has_many :comments, autosave: true
end

post = Post.create(title: 'ruby rocks')
comment = post.comments.create(body: 'hello world')
comment.body = 'hi everyone'
post.comments.build(body: "good morning.")
post.save # => postも2つのcommentもsaveされる

関連付けられたモデルの1つを親のsave操作の一環としてdestroyするときは、以下のようにmark_for_destructionでマーキングするだけで簡単に行なえます。

post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
post.comments[1].mark_for_destruction
post.comments[1].marked_for_destruction? # => true
post.comments.length # => 2

ただし、上の時点ではデータベースからまだ削除されていません

id = post.comments.last.id
Comment.find_by(id: id).nil? # => false

post.save
post.reload.comments.length # => 1

saveすると実際にデータベースから削除されます。

Comment.find_by(id: id).nil? # => true

🔗 注意

レコード自体が変更されたときに自動保存がトリガーされるのは、関連付けレコードが既に永続化済みの場合だけであることにご注意ください。その理由は、関連付けのバリデーションが循環することで引き起こされるSystemStackErrorから保護するためです。これには1つ例外があり、カスタムのバリデーションコンテキストが利用されている場合は、関連付けされたレコードに対して常にバリデーションが実行されます。

🔗 メソッド

🔗 インスタンスpublicメソッド

🔗 changed_for_autosave?()

このレコードが何らかの方法で変更されたかどうかを返します(ネストした自動保存関連付けが同様に変更されたかどうかについても返します)。

🔗 destroyed_by_association()

destroyされる親の関連付けを返します1

カウンタキャッシュが不必要に更新されるのを避けたいときに利用します。

🔗 destroyed_by_association=(reflection)

このレコードのdestroyをトリガーするのに使う特定の関連付けを指定します。指定した関連付けは、そのエンティティがdestroyされたときに、このレコードをmark_for_destructionでマーキングするのに使われます。

🔗 mark_for_destruction()

このレコードが、親のsaveトランザクションの一環としてdestroyされるようマーキングします。実際には、これによってレコードが即座にdestroyされるのではなく、親.saveが呼び出されたときに子レコードがdestroyされます。

このメソッドは、この関連付けられているモデルで親の:autosaveオプションが有効になっている場合にのみ有用です。

🔗 marked_for_destruction?()

このレコードが親のsaveトランザクションの一環としてdestroyされるかどうかを返します。

このメソッドは、この関連付けられているモデルで親の:autosaveオプションが有効になっている場合にのみ有用です。

🔗 reload(options = nil)

オブジェクトの属性を通常どおり再読み込みし、marked_for_destructionフラグをクリアします。

関連記事

Rails APIドキュメント: Active Recordのトランザクション(翻訳)

Rails API: ActiveRecord::NestedAttributes(翻訳)

Rails API: ActiveSupport::ConcernとModule::Concerning(翻訳)


  1. 訳注: 具体的には、destroyされる親が存在する場合は、その親との関連付けを返します。destroyされる親がない場合はnilを返します。 

The post Rails API: ActiveRecord::AutosaveAssociation(翻訳) first appeared on TechRacho.

Viewing all 68 articles
Browse latest View live