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

Rails: Active RecordでRepositoryパターンを実装する(翻訳)

$
0
0

概要

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

Rails: Active RecordでRepositoryパターンを実装する(翻訳)

Repositoryは本質的に、ドメインオブジェクトをその永続化方法から切り離して、それらにアクセスするための限定的なインターフェイスを提供します。Repositoryは戦術的なパターンの一種であり、本記事の導入部で私が説明したい内容よりずっと詳しくMartin FowlerEric Evansが解説しています。Repositoryパターンが推し進めるものは、いわゆるActive Recordパターンと真逆です。なぜパターンをわざわざ別のものに変換するのでしょうか?

Active Recordパターンは両刃の剣であり、その問題はまさに最大の長所から発しています。Active Recordパターンは、「個人起業家」が短期間で製品のプロトタイプを作り上げるときには大変有用であり、規律を守れる精鋭チームに適した柔軟性を提供してくれます。しかし、比較的大規模なレガシーアプリケーションに複数のチームが組織横断的に取り組んでいる場合は制御不能になってしまいます。

素のActiveRecord::Baseでは、350個のインスタンスメソッドがpublicインターフェイスで公開されます。ここに、よく使われるActiveRecord::Relationのメソッドが496個追加されます。このように大規模なActive Recordモデルで可能なすべての利用パターンをカバーする大規模なリファクタリングを実行するとなると悪夢です。以下はリファクタリングで最初に行うチェックリストの一部です。

  • 膨大なクエリAPIの調査
  • さまざまなコールバックの調査
  • リレーション、およびその拡張機能と従来の振る舞いの調査
  • GemfileでActiveRecord::Baseを拡張しているgemの調査
  • 追加されたメソッドや、変更された振る舞いの調査

これはカバーすべき重要な範囲です。お金を稼いでくれるproductionシステムに手を加えるには、それなりの時間とエネルギー、そして自信を必要とします。

以前私の同僚が、大規模なコードベースで表面化したActive Recordのカバー範囲を制御しようと何度か試みていたことが思い出されます。Active Recordの境界表現を支援するnot_activerecord gemや、読み取りに対応するさまざまなQuery Objectパターン(記事1記事2)がありました。

以前Adam PohoreckiがDRUG meetupで述べていた、20%の労力をかけてActive Recordを以下のような形に整えれば、Repositoryで80%のメリットを得られるという話も何となく思い出されます。

class Transaction
  def self.of_id(id)
    find(id)
  end

  def self.last_not_pending_of_user_id(user_id)
    where.not(status: "pending").where(user_id: user_id).order(:id).last
  end
end

この方法、すなわちActiveRecord::Baseのメソッドを"private"として扱い、アプリケーション固有のクラスメソッドだけを使ってモデルにアクセスするという方法は、チームの規律の高さに大きく依存しています。

以下は今回私が作成するRepositoryです。ここには、既にフレームワークにある外部依存関係は含まれていません。

class TransactionRepository
  class Record < ActiveRecord::Base
    self.table_name = "transactions"
  end
  private_constant :Record

  Transaction = Data.define(Record.attribute_names.map(&:to_sym))

  class << self
    def of_id(id)
      as_struct(Record.find(id))
    end

    def last_not_pending_of_user_id(user_id)
      as_struct(Record.where.not(status: "pending").where(user_id: user_id).order(:id).last)
    end

    private

    def as_struct(record)
      Transaction.new(**record.attributes.symbolize_keys)
    end
  end
end

このサンプルを少し分析してみましょう。

  1. このAPIを形成しているのは、 TransactionRepositoryとそのpublicメソッドです。ここには依存関係はなく、ライフサイクル内でステートを保持することもないので、メソッドはシングルトン上に存在します。これらはデータにアクセスする唯一の方法であり、露出は極めて限定的となっています。
  2. TransactionRepository::RecordはActive Recordのクラスになっています。この名前空間はRailsフレームワークの仕組みから「かけ離れている」ので、データベーステーブルをself.table_nameで指定しなければなりません。このRepository内ではRecordを利用することと、そこに機能を実装することが許されています。この定数はRepositoryの外からはアクセスできません。つまりカプセル化が達成されています。

  3. Repositoryクエリからの戻り値はイミュータブルな構造体になっています。ActiveRecord::RelationでもなければActiveRecord::Baseのインスタンスでもありません。

この方法に欠点はあるでしょうか?もちろんあります。他のどんな方法でもそうですが、何を選択し、何を捨てるかが腕の見せ所です。ある分野における利便性を犠牲にする代わりに、予測可能性とメンテナンス性を手に入れることになるので、効果があるかどうかは場合によりけりです。

Active Recordの膨大なAPI露出面積が最も重宝される場所は、ビューレイヤ、そしてその上に構築される多数のヘルパーですが、私たちの構成ではこうしたメリットを得られなくなります。これについては、ActiveModel::Namingの振る舞いをincludeすることで多少取り戻せるでしょう。

他にも方法はあるでしょうか?場合によっては、実行可能な選択肢にCQRS1、すなわち「モデルへの書き込みと読み出しを切り離す手法」も入ってくるかもしれません。書き込みと読み出しを異なる形で実装するときを考慮すると、読み出しをActive Recordに担当させるのが万全です。私の好みは、Railsの非正規化されたSQLデータベース上にRead Modelを実装する方法です。

関連記事

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

The post Rails: Active RecordでRepositoryパターンを実装する(翻訳) first appeared on TechRacho.


Rails 7.2: ActiveRecord::Core#inspectの修正とattributes_for_inspectの便利な使い方(翻訳)

$
0
0

概要

CC BY-NC-SA 4.0 Deedに基づいて翻訳・公開いたします。

CC BY-NC-SA 4.0 Deed | 表示 - 非営利 - 継承 4.0 国際 | Creative Commons

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

参考: ウォッチ20231122: ActiveRecord::Core#inspectの出力をカスタマイズ可能になった

Rails 7.2: ActiveRecord::Core#inspectの修正とattributes_for_inspectの便利な使い方(翻訳)

Active Recordモデルのinspectメソッドが返すのは、「モデルのクラス」と「すべての属性と値のリスト」です。Rails 7.2では、inspectの出力にどの属性を含めてよいかを指定可能になりました。

本記事では、この機能を実装するきっかけとなったパフォーマンス問題について解説するとともに、この機能を活用して開発者のエクスペリエンスを快適にする方法についても説明します。

🔗 to_sinspectについて

Rubyのオブジェクトには、to_sメソッドもinspectメソッドも定義されます。to_sはオブジェクトの文字列表現を返しますが、inspectはデバッグに利用可能なオブジェクトの情報を返します。

Active Recordモデルではto_sinspectの動作がどう違うかを見てみましょう。

Book.first.to_s #=> "#<Book:0x000000017765e6b8>"

Book.first.inspect #=> "#<Book id: 1, title: "The Rings of Saturn", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"

見ての通り、toはモデルのクラス名だけを含むモデル名の短い文字列表現を返しますが、inspectはモデルの属性とその値をすべて含む文字列を返します。

しかしこれは、inspect呼び出しがto_s呼び出しよりもずっと低速になってしまう可能性があるということです。inspectを呼び出すときは、モデルの全属性をイテレーションして値を取得しなければならず、しかも、値の1つ1つをActiveRecord::ParameterFilterで定義されているフィルタリストと照合して、いわゆる個人情報(Personal Identifiable Information: PII)が出力で露出しないように秘匿化する必要もあります。

つまり、inspectの呼び出しコストは、モデルにある属性の個数とパラメータフィルタの個数に応じて増加します。一方、to_sの呼び出しコストは一定です。

inspectは一般にデバッグ目的で利用されますが、呼び出されるモデルによってはパフォーマンスが低下する可能性があるため、production環境でinspectがどんなタイミングで呼び出されるかについて注意を払うべきです。しかし、今から説明するように、うっかりinspectを呼び出して問題を引き起こすのは驚くほど簡単なのです。

🔗 inspectは"フットガン"

Shopifyの開発者たちは、アプリケーションのパフォーマンスボトルネックを特定するために定期的にコードのプロファイリングを実施しています。昨年、あるチームが、Active Recordのinspectメソッドの実行が特定のリクエストで8秒近くかかってしまっていることを発見しました。8秒はさすがに長すぎますね。

Profile showing >7s spent in ActiveRecord::Core#inspect

上の図はプロファイルから切り出したもので、Active Recordのinspectメソッドの実行時間が7秒を超えていることを示しています。図の表示は左が最も値が大きいので、このリクエストではinspectがトータル7秒以上を消費していることを意味します(消費時間は連続しているとは限りません)。このプロファイルについて詳しくはspeedscopeを参照してください。

さらに調べを進めると、このコードは配列に対してto_sを呼び出していたのですが、その配列には数百個ものActive Recordモデルが含まれていることが判明しました。to_sは、配列やハッシュなどのenumerable(列挙可能)なオブジェクトでは、単なるinspectのエイリアスとして振る舞います(つまり、enumerableな要素をイテレーションして、要素ごとにinspectを呼び出します)。
to_sはさまざまな場面で呼び出される可能性があり、文字列の式展開でもto_sが暗黙で呼び出されます。つまり、1個もしくは数百個のモデルに対してinspectが呼び出される可能性があったとしても、すぐに見分けがつくとは限らないということです。

開発者は、この種の問題を「フットガン(footgun)」と呼ぶことがあります1。たしかにinspectはデバッグで重宝しますが、使い方を間違えればproduction環境でパフォーマンス低下を簡単に引き起こしてしまう方法でもあるのです。Active Recordモデルがぎっしりつまった巨大配列に対してto_sを呼び出すのはたいていの場合誤りの可能性が高いのですが、間違いはいつか起きるものであり、間違えたときにこれほどパフォーマンスを損なう問題を引き起こすべきではありません。

🔗 フットガンを「武装解除する」

アプリケーションのコードをリファクタリングして大量のinspect呼び出しを回避すれば、この種のパフォーマンス低下の修正は一応可能です。しかし、このような悲惨な状況を回避するためには、Railsがproduction環境でもう少しうまく頑張って欲しいものです。

どんな解決方法が可能かを検討するときは、inspectのユースケースがproduction環境に存在していることを思い出すのが重要です。したがって、私たちの解決方法は、単にproduction環境のinspectメソッド呼び出しを再定義したり削除したりするだけでは終わりません。私たちの理想の解決方法は、inspectのパワーを失わずに、意図しない誤用に対する安全性を提供することです。

いくつかの方法を検討した結果、以下の2つのソリューションに落ち着きました。

  1. inspectでどの属性を出力してよいかを開発者が設定可能にする
  2. production環境では、inspectidのみを出力するようモデルを設定する

このソリューションについてもう少し詳しく見ていきましょう(#49765もチェックしてみてください)。

まず、ActiveRecord::Baseattributes_for_inspectという新しいクラス属性を作成しました。モデルのインスタンスでinspectが呼び出されると、attributes_for_inspectのリストに含まれている属性だけが出力に含まれるようになります。

Book.first.inspect #=> "#<Book id: 1, title: \"The Rings of Saturn\", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"

Book.attributes_for_inspect = [:id, :title]

Book.first.inspect #=> "#<Book id: 1, title: \"The Rings of Saturn\">"

次に、production環境ではすべてのモデルでattributes_for_inspectの値を[:id]に設定しました。これにより、production環境のRailsコンソールで以下の結果が表示されるようになります。

Book.first.inspect #=> "#<Book id: 1>"

これで、潜在的なパフォーマンス低下の問題は大幅に軽減されました。production環境では、モデルでinspectを呼び出しても、:id属性だけが表示されます。この場合、属性に対するパラメータフィルタも引き続きチェックされるので、to_sを呼び出すよりは遅くなりますが、重要なのは、モデルの属性数が増えてもinspectの呼び出しコストが増加しなくなったことです。

inspectのフルパワーを取り戻す「脱出ハッチ」も2種類用意されています。
1つは、attributes_for_inspect:allを渡すことで、これによって再びすべての属性が出力されるようになります。この振る舞いはdevelopmentモードとtestモードのデフォルトなので、従来通りinspectをデバッグに利用可能になります。

Book.attributes_for_inspect = :all

Book.first.inspect #=> "#<Book id: 1, title: \"The Rings of Saturn\", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"

もう1つは、新しいfull_inspectを呼び出す方法です。これは特定の場面でのみすべての属性を出力する必要が生じた場合に利用できます。

Book.attributes_for_inspect = [:id]
Book.first.inspect #=> "#<Book id: 1>
Book.first.full_inspect #=> "#<Book id: 1, title: "The Rings of Saturn", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"

🔗 ボーナス: attributes_for_inspectで快適に開発しよう

attributes_for_inspectを導入した動機は、もともとproduction環境でinspectが使われた場合にパフォーマンスが低下する可能性のある問題を軽減することでした。しかしattributes_for_inspectはアプリケーションの開発エクスペリエンスを快適にするのにも利用できるのです。

大規模なRailsアプリケーションでは、Active Recordモデルに何十個もの属性があり、場合によっては属性の値が非常に長くなることも珍しくありません。

inspectは手動で呼び出して使うとは限りません。たとえば以下のようにRailsコンソールでオブジェクトが返されるときにもinspectが自動で呼び出されますが、inspectの表示量が増えると邪魔になることがあります。

myapp(dev)> Book.first
=>
  #<Book:0x000000017f9a9d88 id: 1,
  title: "The Rings of Saturn",
  author: "W.G. Sebald",
  published: 1998,
  pages: 306,
  language: "English",
  type: "fiction",
  format: "hardcover",
  translated: true,
  original_language: "German",
  original_name: "Die Ringe des Saturn",
  translator: "Michael Hulse",
  isbn-13: 9780811213783,
  isbn-10: "0811213781",
  publisher: "New Directions",
  created_at: "2024-03-19 10:16:20.229686000 -0400",
  updated_at: "2024-03-19 10:16:20.229686000 -0400">

inspectの出力量が増えた場合やターミナルウィンドウが狭い場合は、ある時点で上のように複数行に分割されます。ターミナルが出力結果でうずまってしまうと上下にスクロールしなければならなくなったり、デバッグセッションをいったん終了してからでないとコマンドもろくに入力できなくなったりすることがあります。inspectの出力結果が多すぎると、テストが失敗したときの結果表示が崩れたりすることもあります。

おそらく皆さんも私と同様に、出力が多すぎて結果の解析で苦労したり、デバッグ中に画面に表示された大量の文字を理解するのに手間取ったりした経験がおありかと思います。私の場合、モデルの全属性について値を把握しなければならなくなることは、まずありません。むしろ、モデル属性の1つか2つについて値がわかれば、次の作業を決定するのに十分です。

そこで、attributes_for_inspectを使えば、デバッグ時に最も有用な属性だけを出力できます。これは、特にモデルの配列を操作するときに便利で、数百行の出力をわずか数行に削減できます。

Rails 7.2のデフォルトでは、既存の振る舞いを維持するために、developmentモードとtestモードですべてのモデルに対してattributes_for_inspect:allに設定されます。
しかし私としては、Railsコンソールを快適にするためにattributes_for_inspect をぜひ皆さんにもお試しいただきたいと思います。full_inspectを呼び出せばいつでもモデルの全属性を表示できることもお忘れなく。

🔗 まとめ

よく言われているように、Railsは切れ味のよい刃物も提供していますが、こうした強力な機能が思わぬ問題を引き起こすこともあります。
本記事では、開発者がうっかり大量のActive Recordモデルに対してinspectメソッドを呼び出してパフォーマンス低下につながる可能性があることを見てきました。

Rails 7.2からは、inspectで出力してよい属性をコンフィグで指定可能になりました。これは、inspectが誤って呼び出されたときにパフォーマンスが低下する潜在的な問題を防ぐのに役立ちます。

さらに、この機能を活用すれば開発エクスペリエンスを快適にできることも見てきました。Railsアプリケーションを7.2にアップグレードする機会があったら、ぜひattributes_for_inspectをお試しください。

関連記事

Rails: アサーションが動いていないテストを効果的に発見する方法(翻訳)


  1. footgunは「自分の足を撃ち抜くために作られたかのようなピストル」のことで、強力だが痛い目に遭う可能性がある機能というニュアンスで英語圏の技術記事でよく使われます。参考: Urban Dictionary: footgun 

The post Rails 7.2: ActiveRecord::Core#inspectの修正とattributes_for_inspectの便利な使い方(翻訳) first appeared on TechRacho.

Rails 7.2: ActiveRecord::Base#pluckにハッシュ値を渡せるようになった(翻訳)

$
0
0

概要

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

Rails 7.2: ActiveRecord::Base#pluckにハッシュ値を渡せるようになった(翻訳)

グラフやレポートで構成されたダッシュボードを目にすることは普段からよくあります。このようなグラフやレポートを素早く作成するには、さまざまなデータベーステーブルから特定のデータを取得するクエリを高速化する必要があります。

ActiveRecord::Base#pluckメソッドは、レコード全体を読み込まずに、データベースから1個以上の属性をクエリするのに使われます。pluckの結果は属性値の配列として返されます。

User.pluck(:id, :name, :email)

上のコードから以下のクエリが生成されます。

SELECT users.id, users.name, users.email FROM "users"

上のクエリの結果から以下の配列を得られます。

#=>
  [
    [1, "David Heinemeier Hansson", "dhh@hey.com"],
    [2, "Rafael França", "rafael@franca.dev"],
    [3, "Vipul Amler", "vipul@saeloun.com"]
  ]

🔗 改修前

クエリでフィールドを取得するときは、単一のテーブルからの取得に限定されず、複数テーブルからも取得できます。

カラム名はシンボル形式で指定可能ですが、従来は単一のテーブルからカラムをフェッチするときしかシンボル指定を利用できません。
複数テーブルをJOINしたい場合は、テーブル名を指定するために以前の文字列形式を使わなければなりませんでした。

Raila 7.2より前は、以下の構文で特定のフィールドをフェッチできました。

Employee
  .joins(:companies)
  .pluck("employees.id, employees.name, companies.id, companies.name")

上のコードから以下のクエリが生成されます。

SELECT employees.id, employees.name, companies.id, companies.name
FROM "employees"
INNER JOIN "companies"
ON companies.id = employees.company_id

上のクエリの結果から以下の配列が得られます。

#=>
  [
    [1, "David Heinemeier Hansson", 1, "37signals"],
    [2, "Jason Fried", 1, "37signals"],
    [3, "Rafael França", 2, "Shopify"],
    [4, "Vipul Amler", 3, "Saeloun"]
  ]

🔗 改修後

Rails 7.2から、ActiveRecord::Base#pluckにハッシュ値を渡せるようになりました(#51565)。これにより、生SQLを使わずに同じクエリが可能になりました。

改修後の同じコードを見てみましょう。

Employee
  .joins(:companies)
  .pluck(employees: [:id, :name], companies: [:id, :name])

上のコードから以下のクエリが生成されます。

SELECT employees.id, employees.name, companies.id, companies.name
FROM "employees"
INNER JOIN "companies"
ON companies.id = employees.company_id

pluckを用いて実装されているpickメソッドにも同じことが適用されます。

pickメソッドは、relation.limit(1).pluck(*column_names).firstのショートハンドであり、リレーションが既に1行にまで絞り込まれている場合に便利です。

Employee
  .joins(:companies)
  .where(id: 1)
  .pick(employees: [:id, :name], companies: [:id, :name])

上のコードから以下のクエリが生成されます。

SELECT employees.id, employees.name, companies.id, companies.name
FROM "employees"
INNER JOIN "companies"
ON companies.id = employees.company_id
WHERE employees.id = 1
LIMIT 1

上のクエリの結果から以下の配列が得られます。

#=>
[1, "David Heinemeier Hansson", 1, "37signals"]

なお、以下の過去記事で既に取り上げたように、selectreselectにも同様にハッシュ値を渡せます。

関連記事

Rails 7.1: 自動生成されるインデックス名の長さが上限を超えないようになった(翻訳)

Rails: Active Recordメソッドのパフォーマンス改善とN+1問題の克服(翻訳)

The post Rails 7.2: ActiveRecord::Base#pluckにハッシュ値を渡せるようになった(翻訳) first appeared on TechRacho.

Rails 7.2: strict_loadingがn_plus_one_onlyモードで子の関連付けをeager loadingしないよう修正(翻訳)

$
0
0

概要

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

参考: 4.1.2.14 :strict_loading -- Active Record の関連付け - Railsガイド

日本語タイトルは内容に即したものにしました。
strict loading、eager loading、lazy loadingは原則として英ママとしました。

Rails 7.2: strict_loadingがn_plus_one_onlyモードで子の関連付けをeager loadingしないよう修正(翻訳)

Railsのstrict_loadingモードは、関連付けがlazy loading(遅延読み込み)されるのを防ぐためのものです。

strict_loadingモードを利用すると、関連付けられるレコードはincludesでeager loadingされなければならなくなります。さもないとActiveRecord::StrictLoadingViolationErrorエラーが発生します。

このstrict_loadingモードは、特定の関連付けをeager loadingすることでN+1クエリ問題を特定および修正し、パフォーマンスのボトルネックを回避するうえで有用です。

class Client < ApplicationRecord
  has_many :projects
end
Client.strict_loading.first.projects

# ActiveRecord::StrictLoadingViolationErrorが発生
class Client < ApplicationRecord
  has_many :projects, strict_loading: true
end
Client.first.projects

# ActiveRecord::StrictLoadingViolationErrorが発生
Client.includes(:projects).first.projects

  Client Load (0.7ms)  SELECT "clients".* FROM "clients" ORDER BY "clients"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Project Load (2.4ms)  SELECT "projects".* FROM "projects" WHERE "projects"."client_id" = $1  [["client_id", 1]]

# =>
[#<Project:0x00000011133aaa48
  id: 1,
  client_id: 1,
  name: "Miru",
  description: "Time tracking">,
 #<Project:0x0000001111f150b0
  id: 2,
  client_id: 1,
  name: "Azure.com",
  description: "Cloud Computing">
]

🔗 改修前

:n_plus_one_onlyモードを指定したstrict_loadingは、ネステッド関連付けの奥深くにアクセスしたときに発生する可能性のあるパフォーマンス低下問題に対処する目的で設計されています。

これにより、関連付けを深くトラバースすることを制限しつつ関連付けを直接読み込めるようになり、潜在的なN+1クエリ問題や順序の不一致に関連する予期せぬ自体を防げるようになります。

client = Client.find(1)
client.strict_loading!(mode: :n_plus_one_only)
client.projects.first

# SELECT "projects".* FROM "projects" WHERE "projects"."client_id" = $1  [["client_id", 1]] -- non-deterministic order

ただし、ネステッド関連付けを深くたどるとエラーになります。

client.projects.first.timesheets # ActiveRecord::StrictLoadingViolationErrorが発生

🔗 改修後

改修後は、:n_plus_one_onlyモードを指定したstrict_loadingが、子の関連付けをeager loadingしないようになりました。

#48785の変更によって子の関連付けがeager loadingされなくなって意図通りに振る舞うようになり、firstlastなどのメソッドが呼び出されたときに順序が一定しない問題も防止されます。firstlast自体はN+1問題を発生しないので、子の関連付けを呼び出してもエラーは発生しません。

つまり、関連付けはeager loadingされますが、子の関連付けはlazy loadingされるということです。

client = Client.find(1)
client.strict_loading!(mode: :n_plus_one_only)
client.projects.first

# SELECT "projects".* FROM "projects" WHERE "projects"."client_id" = $1  [["client_id", 1]] -- non-deterministic order

client.projects.first.timesheets # no longer raises error

🔗 まとめ

strict loadingを利用すると、関連付けられたレコードがeager loadingされていない場合はActiveRecord::StrictLoadingViolationErrorが発生するので、関連付けがlazy loadingされることを防止できます。

ただしstrict_loading!(mode: :n_plus_one_only)を利用すると、関連付けはeager loadingされ、子の関連付けはlazy loadingされます。

関連記事

Rails 7.2: ActiveRecord::Base#pluckにハッシュ値を渡せるようになった(翻訳)

Rails 7.2: ActiveRecord::Core#inspectの修正とattributes_for_inspectの便利な使い方(翻訳)

The post Rails 7.2: strict_loadingがn_plus_one_onlyモードで子の関連付けをeager loadingしないよう修正(翻訳) first appeared on TechRacho.

Rails 7.2.0 Active Record CHANGELOG(全項目リンク付き)

$
0
0

概要

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

Rails 7.2.0リリース後に必要であれば更新します。

なお、機能削除のエントリについては、原則として7.2での削除プルリクではなく、その機能が非推奨化されたときの過去のプルリクへリンクしています。

Rails 7.2.0 Active Record CHANGELOG(全項目リンク付き)

更新103件(うち非推奨削除29件)

🔗 Include the current transaction in sql.active_record event payloads · rails/rails@ee3d663

  • sql.active_recordのActive Support通知のペイロードが、:transactionキー内の現在のトランザクションを含むようになった

Xavier Noria

🔗 Pass the transaction object to transaction.active_record subscribers by fxn · Pull Request #51955 · rails/rails

  • transaction.active_recordののActive Support通知のペイロードが、:transactionキーに関連するイベントのトランザクションを含むようになった

Xavier Noria

🔗 Lazily generate UUIDs for AR transactions by fxn · Pull Request #51949 · rails/rails

  • データベーストランザクションのUUIDを返すActiveRecord::Transaction#uuidを定義した

これは、データベースの活動をトレースするときに役立つ可能性がある。これらのUUIDはオンデマンドでのみ生成される。

Xavier Noria

🔗 [Fix #51720] Infer association klass as top level if model has same demodularized name by joshuay03 · Pull Request #51721 · rails/rails

  • 名前空間ありのモデルに関連付けられる、名前空間のない同じ名前のモデル名を正しく推論できるよう修正

以下のセットアップ例で考える。

class Nested::Post < ApplicationRecord
  has_one :post, through: :other
end

修正前は、この#postNested::Postと推論されてしまっていたが、修正後は正しくPostと推論されるようになった。

Joshua Young

🔗 Fix PostgreSQL Cidr#change? to compare with address prefix by taketo1113 · Pull Request #51633 · rails/rails

  • PostgreSQLのCidr#change?でIPアドレスのプレフィックスの変更を検出するよう修正。

Taketo Takashima

🔗 Change BatchEnumerator#destroy_all to return the total number of affected rows by fatkodima · Pull Request #51788 · rails/rails

  • BatchEnumerator#destroy_allが、影響を受ける行の総数を返すよう変更。

修正前は常にnilを返していた。

fatkodima

🔗 Support touch_all in batches by fatkodima · Pull Request #51785 · rails/rails

in_batchestouch_allをサポート。

Post.in_batches.touch_all

fatkodima

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

🔗 Add support for :if_not_exists and :force options to create_schema by fatkodima · Pull Request #51790 · rails/rails

  • (PostgreSQL)create_schema:if_not_existsオプションと:forceオプションをサポート。

fatkodima

🔗 Fix index_errors and provide :nested_attributes_order mode by lulalala · Pull Request #48727 · rails/rails

  • index_errorsが関連付けのバリデーションエラーでインデックスが無効になっていたのを修正。

lulalala

  • index_errors: :nested_attributes_orderモードを追加。

この変更によって、ネステッド属性セッターで受け取った順序に基づく関連付けバリデーションエラーのインデックスが作成され、reject_if設定を尊重するようになる。これにより、バリデーションエラーをフォームの個別のフィールドにマッピングするのに十分な情報をAPIからフロントエンドに提供可能になる。

lulalala

🔗 Add active_record.postgresql_adapter_decode_dates config by JoeDupuis · Pull Request #51763 · rails/rails

  • Rails.application.config.active_record.postgresql_adapter_decode_datesコンフィグを追加。これはPostgreSQLアダプタでdatesの自動デコードをオプトアウトする(デフォルトはtrue)。

Joé Dupuis

🔗 Warn about changing query_constraints: behavior by nvasilevski · Pull Request #51571 · rails/rails

  • 関連付けのquery_constraintsオプションが非推奨化。今後はforeign_keyを使うこと。

Nikita Vasilevsky

🔗 Add ENV["SKIP_TEST_DATABASE_TRUNCATE"] flag to speed up multi-process test runs by dhh · Pull Request #51686 · rails/rails

参考: 週刊Railsウォッチ20240514: Active Recordで再帰的CTEをサポート

  • ENV["SKIP_TEST_DATABASE_TRUNCATE"]フラグを追加。すべてのテストがデフォルトのトランザクション内で実行される場合に、大規模DBでのマルチプロセステストの実行を高速化する。

これにより、HEYのテストが178個のテーブルに対して24個のプロセスで実行され、最大4,000個のテーブルでtruncate_tablesがスキップ可能になり、テスト実行が約10秒短縮される。

DHH

🔗 Add support for recursive CTEs in ActiveRecord by ClearlyClaire · Pull Request #51601 · rails/rails

  • Active Recordで再帰的CTE(common table expressions)のサポートを追加。
Post.with_recursive(
  post_and_replies: [
    Post.where(id: 42),
    Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id'),
  ]
)

上によって以下のSQLが生成される。

WITH RECURSIVE "post_and_replies" AS (
  (SELECT "posts".* FROM "posts" WHERE "posts"."id" = 42)
  UNION ALL
  (SELECT "posts".* FROM "posts" JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id)
)
SELECT "posts".* FROM "posts"

ClearlyClaire

🔗 Pass validate(_check)_constraint through change_table by ccutrer · Pull Request #51707 · rails/rails

  • マイグレーションのchange_tablevalidate_constraintを呼び出し可能になった。

例:

change_table :products do |t|
  t.check_constraint "price > discounted_price", name: "price_check", validate: false
  t.validate_check_constraint "price_check"
end

Cody Cutrer

🔗 Add a Date decoder to the pg adapter by JoeDupuis · Pull Request #51483 · rails/rails

参考: 週刊Railsウォッチ20240514: PostgreSQLアダプタにDateへのデコード機能が追加

  • PostgreSQLAdapterがdate型のカラムを文字列ではなくDateにデコードするようになった。

例:

ActiveRecord::Base.connection
     .select_value("select '2024-01-01'::date").class #=> Date

Joé Dupuis

🔗 Fix child association loading in :n_plus_one_only mode by reid-rigo · Pull Request #48785 · rails/rails

  • strict_loading!mode: :n_plus_one_onlyオプションを指定することで、子の関連付けをeager loadingしないようになった。

この変更によって子の関連付けがeager loadingされなくなって振る舞いが期待通りになり、firstlastなどのメソッド呼び出しで順序が一定にならない問題を防止する。firstlastそれ自体はN+1問題を引き起こさないので、子の関連付けを呼び出してもraiseしなくなる。
修正: #49473

改修前:

person = Person.find(1)
person.strict_loading!(mode: :n_plus_one_only)
person.posts.first
# SELECT * FROM posts WHERE person_id = 1; -- non-deterministic order
person.posts.first.firm # ActiveRecord::StrictLoadingViolationErrorをraiseする

改修後:

person = Person.find(1)
person.strict_loading!(mode: :n_plus_one_only)
person.posts.first # これは1+1(N+1ではない)
# SELECT * FROM posts WHERE person_id = 1 ORDER BY id LIMIT 1;
person.posts.first.firm # raiseしなくなる

Reid Lynch

参考: Rails API strict_loading! -- ActiveRecord::Core

🔗 dep: allow sqlite3 gem to float to version 2 by flavorjones · Pull Request #51592 · rails/rails

  • Sqlite3Adaptersqlite3 gemの2.xを利用可能になった。

Mike Dalessio

🔗 Allow ActiveRecord::Base#pluck to accept hash values by fatkodima · Pull Request #51565 · rails/rails

  • ActiveRecord::Base#pluckにハッシュ値を渡せるようになった。
# 改修前
Post.joins(:comments).pluck("posts.id", "comments.id", "comments.body")

# 改修後
Post.joins(:comments).pluck(posts: [:id], comments: [:id, :body])

fatkodima

Rails 7.2: ActiveRecord::Base#pluckにハッシュ値を渡せるようになった(翻訳)

🔗 Raise named exception in AbstractMysqlAdapter when DB reports an invalid version by kmcphillips · Pull Request #51478 · rails/rails

週刊Railsウォッチ20240416: AbstractMysqlAdapterで適切なエラーがraiseされるよう修正

  • MySQLデータベースが返すバージョン文字列が無効な場合はActiveRecord::ActiveRecordErrorをraiseするよう修正。

Kevin McPhillips

🔗 Allow to register transaction callbacks outside of a record by casperisfine · Pull Request #51474 · rails/rails

参考: 週刊Railsウォッチ20240416: トランザクションブロックの中で当該トランザクションのコールバックを登録可能になった

  • ActiveRecord::Base.transactionActiveRecord::Transactionオブジェクトをyieldするようになった。
Article.transaction do |transaction|
  article.update(published: true)
  transaction.after_commit do
    PublishNotificationMailer.with(article: article).deliver_later
  end
end

Jean Boussier

  • ActiveRecord::Base.current_transactionが追加された。

現在のトランザクションを返すことで、そこにコールバックを登録可能にする。

Article.current_transaction.after_commit do
  PublishNotificationMailer.with(article: article).deliver_later
end

Jean Boussier

🔗 Automatically delay Active Job enqueues to after commit by casperisfine · Pull Request #51426 · rails/rails

参考: 週刊Railsウォッチ20240416: ジョブのエンキューをトランザクション完了時まで自動先延ばしするようになった

  • ActiveRecord.after_all_transactions_commitコールバックを追加。

トランザクションの内部か外部のどちらかで実行される可能性のあるコードで、ステートの変更が適切に永続化されてから処理を実行しなければならない場合に有用。

def publish_article(article)
  article.update(published: true)
  ActiveRecord.after_all_transactions_commit do
    PublishNotificationMailer.with(article: article).deliver_later
  end
end

上のコード例のブロックは、トランザクションの外で呼び出されるとただちに実行されるか、オープントランザクションがコミットされた後に呼び出されるか、のどちらかになる。

トランザクションがロールバックした場合、ブロックは呼び出されない。

Jean Boussier

🔗 Add the ability to ignore counter cache columns while they are backfilling by fatkodima · Pull Request #51453 · rails/rails

参考: 週刊Railsウォッチ20240416: カウンタキャッシュからの読み出しをcounter_cache: { active: false }で制御できるようになった

  • カウンタキャッシュがバックフィルされるまでカウンタキャッシュカラムを無視できる機能を追加。

既存の巨大なテーブルでカウンタキャッシュを使い始めると厄介になることがある。カラムの追加とは別に、 :counter_cacheを使う前にはカラムの値をバックフィル(埋め戻し)しておかなければならない(テーブルが長時間ロックされないようにするために必要)。さもないと、内部カウンタキャッシュを利用しているsizeany?などのメソッドが誤った結果を生成する可能性がある。関連付けにカウンタキャッシュ構成を導入する前には、バックフィル中に子関連付けでデータベーストリガーまたはコールバックを使うことが多い。

改修の結果、子レコードの追加や削除のときに次のように書いておくことで、カラムを更新済みのままカラムを安全にバックフィルできるようになった。

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: { active: false }
end

カウンタキャッシュが"active"でない場合、sizeany?などのメソッドはカウンタキャッシュを使わなくなり、データベースから直接結果を取得する。カウンタキャッシュのカラムでバックフィルが完了した後は、カウンタキャッシュの定義から{ active: false }を削除するだけで、sizeany?などのメソッドでカウンタキャッシュが使われるようになる。

fatkodima

🔗 Retry known idempotent SELECT queries on connection-related exceptions by adrianna-chang-shopify · Pull Request #51336 · rails/rails

参考: 週刊Railsウォッチ20240409: 特定のクエリメソッドやArelでallow_retryオプションを指定可能になった

  • コネクションに関連する例外で、冪等(べきとう: idempotent)であることがわかっているSELECTクエリをリトライできるようになった。

自分たち開発者がArelツリーをたどってビルドしたSELECTクエリや、モデルの既知の属性を用いてビルドしたSELECTクエリは冪等であり、コネクションエラー時でも安全にリトライできる。従来は、リクエスト中にコネクションエラーが発生するとTrilogyAdapterActiveRecord::ConnectionFailed: Trilogy::EOFErrorが発生することがあった。

Adrianna Chang

🔗 Allow association foreign_key to be an Array by nvasilevski · Pull Request #51334 · rails/rails

  • 関連付けのforeign_keyが複合可能になった。

従来、複合外部キーを設定するには、query_constraintsオプションにArrayを渡すしか方法がなかった。
この改修によって、Array値をforeign_keyとして渡すことで関連付けで同じ振る舞いを得られる。

Nikita Vasilevsky

🔗 Allow primary_key: association option to be composite by nvasilevski · Pull Request #51345 · rails/rails

  • 関連付けのprimary_keyが複合可能になった(配列を渡せるようになった)。

関連付けのprimary_keyは、関連付けられるモデルのprimary_keyまたはquery_constraintsから導出されたときに複合可能になる。
改修によって、関連付けのprimary_keyに明示的に複合として設定可能になった。

Nikita Vasilevsky

🔗 Add config.active_record.permanent_connection_checkout setting by casperisfine · Pull Request #51349 · rails/rails

参考: 週刊Railsウォッチ20240409: config.active_record.permanent_connection_checkoutが追加

config.active_record.permanent_connection_checkout設定が追加された。これは、ActiveRecord::Base.connectionが「エラーを発生させる」「非推奨警告を表示する」「どちらも行わない」のどれにするかを制御する。

ActiveRecord::Base.connectionは、そのコネクションのプールからデータベースコネクションをチェックアウトして、リクエストやジョブが完了するまでリース中(貸出中、専有中)のままにする。この振る舞いは、利用可能なコネクション数よりも多くのスレッドやfiberが使われる環境では望ましくない場合がある。

このActiveRecord::Base.connectionコンフィグを使うと、ActiveRecord::Base.connectionをトラッキングして削除し、ActiveRecord::Base.with_connectionを代わりに使う形で移行しようと思えばできるようになる。

このコンフィグのデフォルトの振る舞いは変更されておらず、現時点ではデフォルトの振る舞いを変更する予定はない。

Jean Boussier

🔗 Add dirties option to Model.uncached by djmb · Pull Request #51204 · rails/rails

参考: 週刊Railsウォッチ20240312: uncachedメソッドにdirtiesオプションを追加

  • uncachedメソッドにdirtiesオプションを追加。

true(デフォルト)に設定すると、書き込みで現在のスレッドに属するすべてのクエリキャッシュがクリアされる。
falseに設定すると、影響を受けるコネクションプールへの書き込みでクエリキャッシュがクリアされなくなる。

これは、Solid Cacheでキャッシュ書き込みによってクエリキャッシュがクリアされないようにするために必要。

Donal McBreen

🔗 Deprecate ConnectionPool#connection by casperisfine · Pull Request #51230 · rails/rails

参考: 週刊Railsウォッチ20240312: RailsのテストスイートにあるBase.connectionwith_connectionlease_connectionに置き換えた

  • ActiveRecord::Base.connectionが非推奨化、今後は.lease_connectionを利用すること。

メソッド名がlease_connectionにリネームされ、返されたコネクションがリクエストやジョブの処理中は維持されることがより明確になった。

これはソフトな非推奨化であり、警告は表示されない。また、このメソッドを削除する予定も今のところない。

Jean Boussier

  • ActiveRecord::ConnectionAdapters::ConnectionPool#connectionを非推奨化。

メソッド名がlease_connectionにリネームされ、返されたコネクションがリクエストやジョブの処理中は維持されることがより明確になった。

Jean Boussier

🔗 Expose a generic fixture accessor for fixture names that may conflict with Minitest by casperisfine · Pull Request #51213 · rails/rails

参考: 週刊Railsウォッチ20240312: 汎用のfixtureアクセサを追加

  • minitestと競合する可能性のあるfixture名に対応するため、汎用のfixtureアクセサを公開する。
assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name

Jean Boussier

🔗 [Fix #51164] Model.query_constraints with single non-primary-key column raises incorrect error by joshuay03 · Pull Request #51165 · rails/rails

参考: 週刊Railsウォッチ20240312: 主キーでない単一のカラムでModel.query_constraintsのエラーメッセージを修正

  • 主キーでない単一のカラムでModel.query_constraintsをraiseされるのは期待通りだが、そのエラーメッセージが正しくなかった。

適切なエラーメッセージを表示するよう修正した。

Joshua Young

🔗 [Fix #50897] Autosaving has_one sets foreign key attribute when unchanged by joshuay03 · Pull Request #50901 · rails/rails

参考: 週刊Railsウォッチ20240306: has_one関連付けの自動保存のバグを修正

外部キー属性が変更されていない場合に設定されるhas_one関連付けの自動保存を修正。

この振る舞いはbelongs_toの自動保存とも一貫しておらず、外部キー属性がread-onlyとマーキングされている場合にActiveRecord::ReadOnlyAttributeErrorなどの意図しない副作用が発生する可能性がある。

Joshua Young

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

参考: Rails 7.1.0 Active Record CHANGELOG(翻訳)
参考: 週刊Railsウォッチ20230809: トランザクションがreturn、break、throwでコミットするようになった

  • トランザクションブロックをreturnbreak、またはthrowで終了したときにロールバックする非推奨の振る舞いを削除。

Rafael Mendonça França

  • Rails.application.config.active_record.commit_transaction_on_non_local_returnを非推奨化。

Rafael Mendonça França

🔗 (削除)Deprecate passing rewhere to merge by HParker · Pull Request #45498 · rails/rails

  • ActiveRecord::Relation#mergerewhereオプションを渡す非推奨化サポートを削除。

Rafael Mendonça França

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

🔗 (削除)Deprecate deferrable: true option of add_foreign_key by alpaca-tc · Pull Request #47659 · rails/rails

  • add_foreign_keydeferrable: trueを渡せる非推奨化サポートを削除。

Rafael Mendonça França

参考: 週刊Railsウォッチ20230524: add_foreign_keydeferrable: trueオプションを非推奨化する

🔗 (削除)Deprecate quoting ActiveSupport::Duration as an integer (#44341) by aramgre · Pull Request #44438 · rails/rails

参考: Rails 7.1.0 Active Record CHANGELOG(翻訳) -- ActiveSupport::Durationを整数値として式展開することを非推奨化

  • ActiveSupport::Durationを整数値として式展開する非推奨化サポートを削除。

Rafael Mendonça França

🔗 (削除)Bring back quote_bound_value & deprecate instead by matthewd · Pull Request #47247 · rails/rails

  • 非推奨化されていた#quote_bound_valueを削除。

Rafael Mendonça França

🔗 (削除)Fix granular swapping for primary_abstract_class by eileencodes · Pull Request #45621 · rails/rails

  • 非推奨化されていたActiveRecord::ConnectionAdapters::ConnectionPool#connection_klassを削除。

Rafael Mendonça França

🔗 (削除)Make connection_pool_list take an explicit argument by eileencodes · Pull Request #45961 · rails/rails

  • role引数が指定されていない場合に以下を現在のロールのコネクションプールに適用する非推奨サポートを削除。
    • #connection_pool_list
    • #active_connections?
    • #clear_active_connections!
    • #clear_reloadable_connections!
    • #clear_all_connections!
    • #flush_idle_connections!

Rafael Mendonça França

  • 非推奨化されていた#all_connection_poolsを削除。

Rafael Mendonça França

🔗 (削除)Refactor Active Record Schema Cache to not hold a connection by casperisfine · Pull Request #48716 · rails/rails

  • 非推奨化されていたActiveRecord::ConnectionAdapters::SchemaCache#data_sourcesを削除。

Rafael Mendonça França

  • 非推奨化されていたActiveRecord::ConnectionAdapters::SchemaCache.load_fromを削除。

Rafael Mendonça França

🔗 (削除)Show reason for foreign key error when loading fixtures by ghiculescu · Pull Request #47990 · rails/rails

参考: 週刊Railsウォッチ20230502: fixture読み込み時の外部キーエラーの理由を表示するようになった

  • 非推奨化されていた#all_foreign_keys_valid?をデータベースアダプタから削除。

Rafael Mendonça França

🔗 (削除)Allow to define the default column serializer by casperisfine · Pull Request #47463 · rails/rails

参考: 週刊Railsウォッチ20230314: デフォルトのカラムシリアライザを定義可能になった

  • serializeの第2引数にコーダーやクラスを渡せる非推奨化サポートを削除。

Rafael Mendonça França

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

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

  • ActiveRecord::Base#read_attribute(:id)がカスタム主キーの値を返す非推奨化サポートを削除。

Rafael Mendonça França

🔗 (削除)Introduce TestFixtures#fixture_paths by andrewn617 · Pull Request #47675 · rails/rails

参考: 週刊Railsウォッチ20230405: fixtureパスをRailsエンジン単位で指定可能になった
参考: 週刊Railsウォッチ20230412: TestFixturesincludeするたびにloadフックを実行するように修正
参考: 週刊Railsウォッチ20230613: fixture_pathsにエンジンのtest/fixturesを自動追加するようになった

  • 非推奨化されていたTestFixtures.fixture_path(単数形)を削除。

Rafael Mendonça França

🔗 (削除)Add configurable deprecation warning for singular associations by HParker · Pull Request #45344 · rails/rails

参考: 週刊Railsウォッチ20220704: 関連付け先の単数形の名前をwhere内から複数形で参照すると警告を出す

  • 単数形の関連付け名を複数形の名前で参照できる非推奨化された振る舞いを削除。

Rafael Mendonça França

  • Rails.application.config.active_record.allow_deprecated_singular_associations_nameを非推奨化。

Rafael Mendonça França

🔗 (削除)Minor refactoring to schema migration and internal metadata by eileencodes · Pull Request #46010 · rails/rails

  • ActiveRecord::MigrationContextの第2引数にSchemaMigrationクラスやInternalMetadataクラスを渡せる非推奨サポートを削除。

Rafael Mendonça França

🔗 (削除)Deprecate check_pending! in favor of check_all_pending! by eileencodes · Pull Request #48134 · rails/rails

  • 非推奨化されていたActiveRecord::Migration.check_pendingメソッドを削除。

Rafael Mendonça França

🔗 (削除)Fix Active Record :db_runtime metric by casperisfine · Pull Request #46041 · rails/rails

  • 非推奨化されていたActiveRecord::LogSubscriber.runtimeメソッドを削除。

Rafael Mendonça França

  • 非推奨化されていたActiveRecord::LogSubscriber.runtime=メソッドを削除。

Rafael Mendonça França

  • 非推奨化されていたActiveRecord::LogSubscriber.reset_runtimeメソッドを削除。

Rafael Mendonça França

🔗 (削除)ActiveRecord::Relation#explain accepts options by reid-rigo · Pull Request #47043 · rails/rails

  • コネクションアダプタ内でexplainに2つの引数を渡せる非推奨化サポートを削除。

Rafael Mendonça França

🔗 (削除)Defer constant loading of ActiveRecord::DestroyAssociationAsyncJob via a String instead of a class constant by bensheldon · Pull Request #45476 · rails/rails

  • 非推奨化されていたActiveRecord::ActiveJobRequiredErrorを削除。

Rafael Mendonça França

🔗 (削除)Deprecate delegation to connection handler from Base by eileencodes · Pull Request #46274 · rails/rails

  • 非推奨化されていたActiveRecord::Base.clear_active_connections!を削除。

Rafael Mendonça França

  • 非推奨化されていたActiveRecord::Base.clear_reloadable_connections!を削除。

Rafael Mendonça França

  • 非推奨化されていたActiveRecord::Base.clear_all_connections!を削除。

Rafael Mendonça França

  • 非推奨化されていたActiveRecord::Base.flush_idle_connections!を削除。

Rafael Mendonça França

🔗 (削除)Deprecate name argument in remove_connection by eileencodes · Pull Request #48681 · rails/rails

  • ActiveRecord::Base.remove_connectionで非推奨化されていたname引数を削除。

Rafael Mendonça França

🔗 (削除)Call proxy methods from alias_attribute generated methods by nvasilevski · Pull Request #48533 · rails/rails

  • alias_attributeに存在しない属性名を渡して呼び出せる非推奨化サポートを削除。

Rafael Mendonça França

🔗 (削除)Facilitate use of any regular ERB in database.yml by eikes · Pull Request #46134 · rails/rails

  • 非推奨化されていたRails.application.config.active_record.suppress_multiple_database_warningを削除。

Rafael Mendonça França

🔗 Add MessagePackMessageSerializer for binary data by djmb · Pull Request #51102 · rails/rails

  • ActiveRecord::Encryption::MessagePackMessageSerializerを追加。

データをMessagePack形式にシリアライズする(バイナリカラムに効率よく保存できるため)。

このバイナリエンコーディングで必要な容量は、デフォルトのシリアライザで使われるBase64エンコーディングと比較して30%少なく済む。

Donal McBreen

🔗 Support encrypting binary columns by djmb · Pull Request #50920 · rails/rails

参考: 週刊Railsウォッチ20240228: バイナリカラムの暗号化をサポート

  • バイナリカラムの暗号化をサポート

暗号化と復号でバイナリデータをType::Binary::Dataで受け渡しするようにする。

従来は、バイナリカラムをActiveRecord::Encryption::MessageSerializerで暗号化すると、MySQLやSQLiteでは動作するがPostgreSQLでは動作しなかった。

Donal McBreen

🔗 Remove initializer that was eager loading the schema cache dump by rafaelfranca · Pull Request #51034 · rails/rails

  • データベース設定でのENV["SCHEMA_CACHE"]の利用が非推奨化された。今後は schema_cache_pathを使うこと。

Rafael Mendonça França

🔗 Add ActiveRecord::Base.with_connection as a shortcut by casperisfine · Pull Request #51083 · rails/rails

参考: 週刊Railsウォッチ20240228: ActiveRecord::Base.with_connectionを追加

  • コネクションを短い時間だけリースするショートカットとしてActiveRecord::Base.with_connectionを追加する。

リースされたコネクションがyieldされると、ブロックの実行中はActiveRecord::Base.connectionで同じコネクションがyieldされる。

これは、リクエストやジョブの継続中コネクションをリースし続けることなく、複数のデータベース操作を実行したい場合に有用。

Jean Boussier

🔗 Deprecate config.active_record.warn_on_records_fetched_greater_than by hundredwatt · Pull Request #51007 · rails/rails

  • config.active_record.warn_on_records_fetched_greater_than を非推奨化(sql.active_recordの通知に:row_countフィールドが含まれるようになったことを受けて)。

Jason Nochlin

🔗 Fix override existing join types in the query in the where.associated method by saleh-alhaddad · Pull Request #51078 · rails/rails

参考: 週刊Railsウォッチ20240306: where.associatedのバグを修正

この修正によって、スコープ内の既存のJOINに基づいて、適切なJOIN種別(INNER JOINまたはLEFT OUTER JOIN)を用いて関連付けがJOINされるようになる。

これにより、既存のJOIN種別が意図せずオーバーライドされることを防ぎ、一貫したSQLクエリが生成されるようになる。

例:

# `associated`では`JOIN`ではなく`LEFT JOIN`が使われる
Post.left_joins(:author).where.associated(:author)

Saleh Alhaddad

🔗 [Fix #50604] Restore compatibility of Active Record Encryption configs with eager loading mode by maximerety · Pull Request #50606 · rails/rails

アプリケーションがeager loadingされるときに、Active Recordモデルが読み込まれる前にActiveRecord::Encryptionコンフィグが準備されない問題を修正。このために暗号化属性が誤って設定されるケースが生じていた。

Maxime Réty

🔗 Deprecate defining enums with keywords args by skipkayhil · Pull Request #50987 · rails/rails

  • enumの定義でキーワード引数を利用することが非推奨化された。
class Function > ApplicationRecord
  # BAD
  enum color: [:red, :blue],
       type: [:instance, :class]

  # GOOD
  enum :color, [:red, :blue]
  enum :type, [:instance, :class]
end

Hartley McGuire

🔗 Add active_record.config.validate_migration_timestamps config option. by adrianna-chang-shopify · Pull Request #50400 · rails/rails

  • config.active_record.validate_migration_timestampsオプションを追加(マイグレーションのタイムスタンプをバリデーションするかどうかを指定)

このオプションが設定されると、マイグレーションのタイムスタンプのプレフィックスが、現在の時刻に関連付けられたタイムスタンプから1日以内に収まることをバリデーションする。これは、マイグレーションのプレフィックスを手動で未来のタイムスタンプに編集したときに、マイグレーションの生成などのマイグレーションコマンドに影響するのを防ぐために設計されている。

Adrianna Chang

🔗 Properly synchronize Mysql2Adapter#active? and TrilogyAdapter#active? by casperisfine · Pull Request #51012 · rails/rails

参考: 週刊Railsウォッチ20240221: Mysql2Adapter#active?TrilogyAdapter#active?が正しく同期するよう修正

  • Mysql2Adapter#active?TrilogyAdapter#active?が正しく同期するよう修正

disconnect!verify!についても同様。

コネクションはスレッド間で共有されないはずなので、これは一般に大きな問題ではないが、トランザクションテストやシステムテストを実行する場合にはこれが要求されるため、SEGVが発生する可能性がある。

Jean Boussier

🔗 Support :source_location tag option for query log tags by fatkodima · Pull Request #50969 · rails/rails

参考: 週刊Railsウォッチ20240221: クエリログで:source_locationをサポート

  • クエリログのタグで:source_locationタグオプションをサポート
config.active_record.query_log_tags << :source_location

呼び出し元の計算はコストの高い操作なので、基本的にdevelopment環境で使う(同じ目的を果たすconfig.active_record.verbose_query_logsもあることに注意)べき。production環境でデバッグ目的に使う場合は短期間にとどめるべき。

fatkodima

🔗 Allow encryption without compression by djmb · Pull Request #50876 · rails/rails

参考: 週刊Railsウォッチ20240221: ActiveRecord::Encryptionの暗号化で圧縮をオフにできるようになった

  • 圧縮を無効にするオプションをActiveRecord::Encryption::Encryptorに追加する。

compress: falseを設定することで、圧縮を無効にできる

class User
  encrypts :name, encryptor: ActiveRecord::Encryption::Encryptor.new(compress: false)
end

Donal McBreen

🔗 Unify the logic to determine the default schema cache path for a database configuration by rafaelfranca · Pull Request #50893 · rails/rails

  • ActiveRecord::Tasks::DatabaseTasks.https://github.com/marvin-bitterlichcache_dump_filenameに文字列を渡すことを非推奨化

ActiveRecord::DatabaseConfigurations::DatabaseConfigオブジェクトを渡すべき。

Rafael Mendonça França

🔗 Add row_count field to sql.active_record notification by marvin-bitterlich · Pull Request #50887 · rails/rails

参考: 週刊Railsウォッチ20240221: ActiveSupport::Notificationssql.active_recordrow_countフィールドを追加

  • sql.active_record通知にrow_countフィールドを追加

このフィールドは、通知を発行したクエリによって返された行数を返す。

このメトリクスは、大きな結果セットを含むクエリを検出したい場合に有用。

Marvin Bitterlich

🔗 [Fix #50803] Consistently raise an ArgumentError when passing an invalid argument to a nested attributes association writer by joshuay03 · Pull Request #50804 · rails/rails

参考: 週刊Railsウォッチ20240215: NestedAttributesで、渡した引数の種類によってエラーメッセージが異なっていたのを修正

  • ネステッド属性関連付けライター(writer)に無効な引数を渡すと、常にArgumentErrorが発生するよう修正

従来、このエラーはコレクション(複数形)の関連付けでしか発生せず、単数形の関連付けでは一般的なエラーを発生していた。

修正後は、コレクションと単数形の両方の関連付けでArgumentErrorが発生するようになった。

Joshua Young

🔗 Fix single quote escapes on default generated MySQL columns by yash · Pull Request #50733 · rails/rails

  • デフォルトで生成されるMySQLカラムの一重引用符が二重エスケープされていたのを修正。

MySQL 5.7.5 以降では、式から計算されるカラム作成に利用可能な生成カラム(generated column)がサポートされている。

修正前のスキーマダンプでは、デフォルトの式で生成カラムが一重引用符''を含んでいると、二重エスケープされた文字列を出力していた。

これにより、MySQLデータベースの新しいインスタンスにスキーマをインポートするときに問題が発生していた。

修正後は文字列がエスケープされず、スキーマのインポート時に有効な Rubyコードになる。

Yash Kapadia

🔗 Fix add_reference options validated on < 7.1 by skipkayhil · Pull Request #50680 · rails/rails

注: この修正はRails 7.1.3で既にリリースされていますが、このChangelogに修正が加えられています。

  • マイグレーションのバージョンが7.1よりも古い場合にadd_referencet.referencesで指定されるオプションがバリデーションされていたのを修正

Hartley McGuire

🔗 Define a class method to introspect valid delegatable types so they can by jprosevear · Pull Request #50662 · rails/rails

参考: 週刊Railsウォッチ20240215: ActiveRecord::DelegatedType<role>_typesクラスメソッドが追加

  • DelegatedTypeのイントロスペクションを可能にするため、<role>_typesクラスメソッドを ActiveRecord::DelegatedTypeに追加する

JP Rosevear

🔗 Make schema_dump, query_cache, replica and database_tasks configurable via DATABASE_URL by casperisfine · Pull Request #50757 · rails/rails

参考: 週刊Railsウォッチ20240215: schema_dumpなどをDATABASE_URLでコンフィグ可能になった

  • DATABASE_URLschema_dumpquery_cachereplicadatabase_tasksを設定可能になった

従来は、ブーリアン値が文字列として解釈されたせいで設定できない場合があった。

改修後は、DATABASE_URL=postgres://localhost/foo?schema_dump=falseのように書けばスキーマキャッシュのダンプを正しく無効にできるようになった。

Mike Coutermarsh, Jean Boussier

🔗 Fix [#50260] Support :on option in #set_callback by joshuay03 · Pull Request #50261 · rails/rails

参考: 週刊Railsウォッチ20240206: after_commitコールバックやafter_rollbackコールバックでon: :updateなどを指定可能になった

  • ActiveRecord::Transactions::ClassMethods#set_callbackを導入

このクラスメソッドは、after_commitコールバックやafter_rollbackコールバックで:onオプションをサポートする他は、ActiveSupport::Callbacks::ClassMethods#set_callbackと同一。

Joshua Young

🔗 [Fix #48685] Make the encryptor agnostic of the type of data to decrypt · rails/rails@d0f3b00

  • ActiveRecord::Encryption::Encryptorが、暗号化データで使われるシリアライズ形式に影響されないようになった

修正前の暗号化インスタンスでは、Stringとしてシリアライズされた暗号化済み値でなければメッセージシリアライザに渡せなかった。

修正後は、でサポートされる暗号化済みシリアライズ値の型が、指定したmessage_serializerによって決定されるようになった。これにより、カスタムシリアライザがString以外の型を指定してActiveRecord::Encryption::Messageオブジェクトをシリアライズ可能になった。

既にデフォルトのActiveRecord::Encryption::MessageSerializerでは、デシリアライズでStringオブジェクトだけが渡されるようになっている。

Maxime Réty

🔗 [Fix #48922] Honour encrypted attribute context in encrypted_attribute? by maximerety · Pull Request #48923 · rails/rails

注: この修正はRails 7.1.3で既にリリースされています。

  • encryptsに渡したコンテキストプロパティがencrypted_attribute?で考慮されるよう修正。

Maxime Réty

🔗 Add explain support for methods like last, pluck and count by p8 · Pull Request #50482 · rails/rails

参考: 週刊Railsウォッチ20240206: lastpluckcountなどでもexplainを使えるようになった

  • explainが返すオブジェクトがpluckfirstlastaveragecountmaximumminimumsumに応答するようになった

これらの新しいメソッドは、対応するクエリでEXPLAINを実行する。

User.all.explain.count
# EXPLAIN SELECT COUNT(*) FROM `users`
# ...

User.all.explain.maximum(:id)
# EXPLAIN SELECT MAX(`users`.`id`) FROM `users`
# ...

Petrik de Heus

🔗 Pass validation_context to validates_associated by austenmadden · Pull Request #46238 · rails/rails

-validates_associatedが使われる場合は:onオプションでバリデーションするよう修正

関連付けられるレコードのバリデーションでvalidates_associated :onオプションが考慮されていなかった問題を修正。

Austen Madden, Alex Ghiculescu, Rafał Brize

🔗 Allow overriding SQLite defaults from database.yml by fractaledmind · Pull Request #50460 · rails/rails

  • database.ymlでSQLiteのデフォルト設定をオーバーライド可能になった

database.yml設定ファイル内のpragmasキー以下に書いたPRAGMA設定がRailsのデフォルトより優先されるようになり、追加のPRAGMAも設定されるようになった。

database: storage/development.sqlite3
timeout: 5000
pragmas:
  journal_mode: off
  temp_store: memory

Stephen Margheim

🔗 Remove SQLite production warning but leave production config disabled by byroot · Pull Request #50463 · rails/rails

  • SQLiteをproduction環境で実行するときの警告メッセージを表示するのをやめ、SQLiteを未設定のままにするようになった

SQliteをproduction環境で実行する有効なユースケースはいくつもあるが、利用は慎重に行わなければならない。そこで、ほとんどのユーザーが見ることのない警告を表示するよりも、SQLiteコンフィグをコメントアウトしたままにすることで、データベースを何らかの永続化ボリュームに置くことをユーザーに強制的に考えさせる方が望ましい。

Jacopo Beschi, Jean Boussier

🔗 Add support for generated columns in SQLite3 adapter by fractaledmind · Pull Request #49346 · rails/rails

参考: 週刊Railsウォッチ20240117: SQLite3アダプタに生成カラム(generated columns)のサポートを追加

  • SQLite3アダプタに生成カラムのサポートを追加

生成カラムは(storedおよびdynamicの両方について)SQLite 3.31.0以降でサポートされている。
このプルリクは、SQLite3アダプタにそうしたサポートを追加する。

create_table :users do |t|
  t.string :name
  t.virtual :name_upper, type: :string, as: 'UPPER(name)'
  t.virtual :name_lower, type: :string, as: 'LOWER(name)', stored: true
end

Stephen Margheim

🔗 TrilogyAdapter: ignore host if socket is set by casperisfine · Pull Request #50349 · rails/rails

参考: 週刊Railsウォッチ20240117: TrilogyAdapterでDATABASE_URLにUNIXソケットを指定可能になった

  • TrilogyAdapterでsocketパラメータが設定済みの場合はhostを無視するようになった

これにより、DATABASE_URL経由でUNIXソケット上にコネクションを設定可能になる。

DATABASE_URL=trilogy://does-not-matter/my_db_production?socket=/var/run/mysql.sock

Jean Boussier

🔗 Expose assert_queries_match and assert_no_queries_match assertions by fatkodima · Pull Request #50373 · rails/rails

参考: 週刊Railsウォッチ20240123: assert_queries_countassert_no_queriesassert_queries_matchassert_no_queries_matchアサーションを公開

以下のアサーションをpublicにした:

  • assert_queries_count
  • assert_no_queries
  • assert_queries_match
  • assert_no_queries_match

期待される件数のクエリが作成されるというアサーションのため、Rails 内部で assert_queries_countassert_no_queriesが使われている。特定のSQLクエリが作成されたというアサーションには、assert_queries_matchassert_no_queries_matchが使われている。これらのアサーションがアプリケーションでも利用可能になった。

class ArticleTest < ActiveSupport::TestCase
  test "queries are made" do
    assert_queries_count(1) { Article.first }
  end

  test "creates a foreign key" do
    assert_queries_match(/ADD FOREIGN KEY/i, include_schema: true) do
      @connection.add_foreign_key(:comments, :posts)
    end
  end
end

Petrik de Heus, fatkodima

🔗 [Fix #49874] has_secure_token calls the setter method on initialize by abeidahmed · Pull Request #50251 · rails/rails

注: この修正はRails 7.1.3でリリース済みです。

  • 初期化時にhas_secure_tokenがセッターメソッドを呼び出すよう修正。

Abeid Ahmed

🔗 Add a ActiveRecord.protocol_adapters configuration to map DATABASE_URL protocols to adapters at an application level by kmcphillips · Pull Request #50140 · rails/rails

参考: 週刊Railsウォッチ20231222: MySQLでDATABASE_URLを変えずにconfig.active_record.protocol_adapters.mysqlでアダプタを指定できるようになった

DATABASE_URL環境変数を使う場合、URLに含まれるプロトコル名を特定のデータベースアダプタにマッピングする設定を有効にした。これにより、アプリケーションで利用するために選択するアダプタを、デプロイ環境で設定されるデータベースコネクションの詳細から分離できるようになる。

# ENV['DATABASE_URL'] = "mysql://localhost/example_database"
config.active_record.protocol_adapters.mysql = "trilogy"
# will connect to MySQL using the trilogy adapter

Jean Boussier, Kevin McPhillips

🔗 Handle case in MySQL where the ActiveRecord.db_warnings_action is not called even when a DB query has warnings by kmcphillips · Pull Request #50049 · rails/rails

参考: 週刊Railsウォッチ20231213: MySQLクエリでwarningが発生してもActiveRecord.db_warnings_actionが呼び出されないケースを修正

MySQLが返すwarning_countがゼロより大きく、SHOW WARNINGSクエリを実行しても警告が表示されない場合に、ActiveRecord.db_warnings_actionを呼び出しても警告を無視せずに一般的な警告メッセージを表示するよう修正。

Kevin McPhillips

🔗 DatabaseConfigurations#configs_for can accept a symbol in the name param by andrewn617 · Pull Request #50061 · rails/rails

  • DatabaseConfigurations#configs_fornameパラメータにシンボルを渡せるようになった

Andrew Novoselac

🔗 [Fix #48535]: fix behavior of proc_for_binds in Arel::Nodes::HomogenousIn by JohnAnon9771 · Pull Request #49050 · rails/rails

参考: 週刊Railsウォッチ20231122: Arelのproc_for_bindsの振る舞いを修正

  • where(field: values)クエリで、fieldがシリアライズド属性の場合(fieldActiveRecord::Base.serializeを使っている場合や、fieldがJSONカラムの場合など)の振る舞いを修正した。

João Alves

🔗 Make the output of ActiveRecord::Core#inspect configurable. by andrewn617 · Pull Request #49765 · rails/rails

参考: 週刊Railsウォッチ20231122: ActiveRecord::Core#inspectの出力をカスタマイズ可能になった

  • ActiveRecord::Core#inspectの出力をカスタマイズ可能になった

デフォルトでは、あるレコードでinspectを呼び出すと、idのみを含むフォーマット済み文字列が生成される。

Post.first.inspect #=> "#<Post id: 1>"

inspectの出力に含めたい属性があれば、ActiveRecord::Core#attributes_for_inspectで設定できる。

Post.attributes_for_inspect = [:id, :title]
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"

attributes_for_inspect:allに設定すると、inspectでレコードの全属性が出力される。

Post.attributes_for_inspect = :all
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"

attributes_for_inspectは、development環境とtest環境ではデフォルトで:allに設定される。
full_inspectを呼び出すことで全属性をinspectすることも可能。
attribute_for_inspectで設定した属性はpretty_printでも使われる。

Andrew Novoselac

🔗 Don't mark Float::INFINITY as changed if didn't by MaicolBen · Pull Request #49904 · rails/rails

参考: 週刊Railsウォッチ20231122: 変更されていないFloat::INFINITY値をchangedとして保存しないよう修正

  • Float::INFINITY-Float::INFINITYに再代入したときに属性をchangedにしないようになった

Maicol Bentancor

🔗 Support RETURNING clause for MariaDB by fatkodima · Pull Request #49840 · rails/rails

参考: 週刊Railsウォッチ20231114: MariaDBでRETURNINGをサポート

  • MariaDBでRETURNINGをサポート

fatkodima, Nikolay Kondratyev

🔗 The SQLite3 adapter now implements the supports_deferrable_constraints? contract by fractaledmind · Pull Request #49376 · rails/rails

参考: 週刊Railsウォッチ20231114: SQLite3にsupports_deferrable_constraints?を追加

  • SQLite3アダプタでsupports_deferrable_constraints?コントラクトを実装

これにより、foreign_keyオプションに:deferrableキーを追加すれば外部キーをdeferredにできる。

add_reference :person, :alias, foreign_key: { deferrable: :deferred }
add_reference :alias, :person, foreign_key: { deferrable: :deferred }

Stephen Margheim

🔗 Add set_constraints helper for PostgreSQL by ccutrer · Pull Request #49187 · rails/rails

参考: 週刊Railsウォッチ20231114: PostgreSQL向けのset_constraintsヘルパーを追加

  • PostgreSQLコネクションに set_constraintsヘルパーを追加
Post.create!(user_id: -1) # => ActiveRecord::InvalidForeignKey

Post.transaction do
  Post.connection.set_constraints(:deferred)
  p = Post.create!(user_id: -1)
  u = User.create!
  p.user = u
  p.save!
end

Cody Cutrer

🔗 Include ActiveModel::API in ActiveRecord::Base by seanpdoyle · Pull Request #49680 · rails/rails

  • ActiveModel::APIActiveRecord::Baseincludeするようになった

Sean Doyle

🔗 Ensure #signed_id outputs url_safe strings by terracatta · Pull Request #49507 · rails/rails

  • #signed_idが出力する文字列がurl_safeになるよう修正

Jason Meller

🔗 Arel nulls first/last implementation Mysql by tttffff · Pull Request #50079 · rails/rails

参考: 週刊Railsウォッチ20240117: MySQLでArelのnulls_firstとnulls_lastが使えるようになった

  • MySQLにnulls_lastを追加し、desc.nulls_firstも動作するようにした

Tristan Fellows

🔗 ActiveRecord::Relation#order supports hash like ActiveRecord::Relation#where by mylesboone · Pull Request #50000 · rails/rails

  • ActiveRecord::Relationorderで、whereと同様のハッシュ渡しをサポート
Topic.includes(:posts).order(posts: { created_at: :desc })

Myles Boone

関連記事

Rails 7.1.0 Active Record CHANGELOG(翻訳)

The post Rails 7.2.0 Active Record CHANGELOG(全項目リンク付き) first appeared on TechRacho.

複雑なクエリーをActive Recordのモデルとして定義する方法

$
0
0

概要

以下の accountsprofiles というテーブルから、WITH句で users テーブルを作り、この users テーブルを Active Record のモデルとして利用できるようにします。
Railsのバージョンは7を用いましたが6でも動作すると思います。

  • accounts テーブル
列名 概要
id int PK
email string ログインメールアドレス
password string ログインパスワード
  • profiles テーブル
列名 概要
id int PK
account_id int FK
first_name string
last_name string
  • users 仮想テーブル
列名 概要
id int アカウントIDと同じ
email int ログインメールアドレス
first_name string
last_name string

WITH句

SQLではWITH句を用いて任意のサブクエリーに対して名前をつけることができます。
SELECT * FROM (複雑なサブクエリー)WITH any_name AS (複雑なサブクエリー) SELECT * FROM any_name のように記述でき、可読性の向上かが期待でき、再利用も可能となります。
WITH句では、(複雑なサブクエリー)の部分を CTE (Common Table Expression) と呼びます

今回は users 仮想テーブルでは以下のSQLの実行を目標とします

WITH "users" AS (
  SELECT
    "accounts"."id",
    "accounts"."email",
    "profiles"."first_name",
    "profiles"."last_name"
  FROM "accounts"
         INNER JOIN "profiles" 
         ON "profiles"."account_id" = "accounts"."id"
)
SELECT
  "users".*
FROM "users"

Arel::Nodes::With

Arelには Arel::Nodes::With が用意されていて、以下のような実装で With句 を生成できます

users = Arel::Table.new('users')
Arel::Nodes::With.new([Arel::Nodes::As.new(users, cte)])

WITH "users" AS ( cte で定義したSQL )

ただしWITH句単体を生成する必要はなく、通常は Arel::SelectManager#with を用いることになります

users = Arel::Table.new('users')
Arel::SelectManager
  .new(users)
  .project(users[Arel.ster])
  .with(Arel::Nodes::As.new(users, cte))

WITH "users" AS ( cte で定義したSQL ) SELECT "users".* FROM "users"

cte は Account とProfile のRailsWayな内部結合となりますので

accounts = Account.arel_table
profiles = Profile.arel_table
cte = Account.joins(:profile).select(
  accounts[:id],
  accounts[:email],
  profiles[:first_name],
  profiles[:last_name]
).arel

とすれば目標とするSQLが生成できます

ApplicationRecord.relation

このメソッドをオーバーライドしたモデルでは任意のSQL実行結果を得ることができます。

以下の例では(まるで不適切ですが) Profile.allAccount.all の結果を返す例になります

class Profile < ApplicationRecord  
  def self.relation
    Account.all
  end
end

Profile.all
#=> [#<Account:0x00000001094c66e8 id: 1, email: "taro-yamada@email.com", ...

ActiveRecord::Relation.build_arel

このメソッドで(正確には ActiveRecord::Relationincludeする ActiveRecord::QueryMethods で実装されている)発行するSQLの元となる Arel::SelectManager が生成されます。

このメソッドをオーバーライドすることで任意のSQLを実行することができます。
以下の例では(こちらも不適切ですが) 任意のリレーションが常に Account.all の結果を返します

profiles = Profile.where(id: 0)
profiles.class_eval do
  def build_arel(...)
    Account.all.arel
  end
  self
end
profiles.all
#=> [#<Account:0x00000001094c66e8 id: 1, email: "taro-yamada@email.com", ...

存在しないテーブルの偽装

Active Record はモデルロード時にDBのカラム定義から自動的に属性を生成するという強力なライブラリであるため、裏返せば対応するテーブルが存在しないとActive Recordは使えないことになります。

詳しい解説は割愛しますが、 ApplicationRecord.table_exists?ApplicationRecord.load_schema! をオーバーライドすることでモデルにテーブルが存在することを偽装することができます。筆末のコードをご参照ください

User仮想テーブルモデル

これまでで以下の技術を見てきました。

  • With句を用いてサブクエリーに名前をつける
  • Arel::SelectManager に WITH句 を付加する
  • モデルが生成するSQLを任意に変更する
  • モデルにテーブルがあることを偽装する

これらを組み合わせてusers仮想テーブルモデルが完成となります
以下がコード例となります

class User < ApplicationRecord
  # 必須ではないが属性を定義する
  attribute :id, :integer
  attribute :email, :string
  attribute :first_name, :string
  attribute :last_name, :string

  # 更新はしないのでreadonlyにしておく
  def readonly?
    true
  end

  # CTEを定義する
  def self.cte
    accounts = Account.arel_table
    profiles = Profile.arel_table

    Account.joins(:profile).select(
      accounts[:id],
      accounts[:email],
      profiles[:first_name],
      profiles[:last_name]
    ).arel
  end

  class << self
    # テーブルが存在することにする
    def table_exists?
      true
    end

    # スキーマロード時にカラム情報の格納場所を確保しておく
    def load_schema!
      connection.schema_cache.instance_exec(table_name) do |table_name|
        @columns[table_name] = [] unless @columns.key?(table_name)
      end

      super
    end

    # 生成されるSQLにWITH句をつける
    def relation
      super.tap do |relation|
        relation.class_eval do
          def build_arel(...)
            super.with(Arel::Nodes::As.new(arel_table, klass.cte))
          end
        end
      end
    end
  end
end

The post 複雑なクエリーをActive Recordのモデルとして定義する方法 first appeared on TechRacho.

Rails 7.2: counter_cacheをactiveオプションでオンオフ可能になった(翻訳)

$
0
0

Rails 7.2: counter_cacheをactiveオプションでオンオフ可能になった(翻訳)

カウンタキャッシュは、Railsアプリケーションのパフォーマンスを最適化するときの要となります。カウンタキャッシュは、モデルに関連付けられた多数のレコードを効果的にトラッキングし、データベースクエリを頻繁に発行する必要性を排除します。ただし既存のアプリケーション、特にテーブルが巨大なアプリケーションにカウンタキャッシュを追加するのは、多くの場合困難です。Rails 7.2では、まさにその問題に対処する素晴らしいアップデートが提供されています。

🔗 カウンタキャッシュを統合するときの課題

開発者が巨大なデータセットにカウンタキャッシュを導入しようとすると、主に以下の2つの課題に直面することになります。

  • データのバックフィル(backfilling: 埋め戻し)をいかに効率よく行うか。
    大量のデータを含んでいる既存のテーブルにカウンタキャッシュのカラムを追加すると、問題が発生することがあります。テーブルが長時間ロックされるとアプリケーションのパフォーマンスに重大な影響を及ぼす可能性が生じますが、これを防ぐには、カラムの追加とは別にカウンタキャッシュの値をバックフィルする必要もあります。バックフィル中は、ダウンタイムを最小限に抑えてユーザーエクスペリエンスが中断されないようにしつつ、データの整合性が損なわれない方法を慎重に検討しなければなりません。
  • データ整合性をいかに確保するか。
    カウンタキャッシュをひとたび導入した後は、データ整合性をいかに損なわないようにするかが最も肝心です。カウンタキャッシュを内部で用いるsizeany?などのメソッドは、いかなる場合も正確な結果を返さなければなりません。しかしバックフィル処理中は、全レコードが正しく更新完了するまでの間、カウンタキャッシュから不正確なカウントが生成される可能性があります。

🔗 Rails 7.2でより安全なカウンタキャッシュが実装された

Rails 7.2の新しいアップデートによって、特に既存の巨大データセットが絡んでくるシナリオでのカウンタキャッシュをより効率的に管理できる機能が導入されました。カウンタキャッシュの設定にactiveオプションを導入することで、カウンタキャッシュをアクティブにするタイミングを開発者が制御できるようになります。

これによって、カウンタキャッシュの追加作業とは別にカウンタキャッシュのバックフィル作業を行えるようになり、テーブルのロックや潜在的なパフォーマンス問題の発生を最小化できます。バックフィル作業が完了したら、このオプションでカウンタキャッシュを有効にすることで、アプリケーションのパフォーマンスを犠牲にせずに関連付けの正確なカウントを得られるようになります。

Railsにおけるこのアップデートの実装を説明するために、記事(articles)とコメント(comments)を扱うシンプルなブログアプリケーションを例にすることにします。
このブログアプリケーションでは、1件の記事に複数のコメントを付けられます。ここで、個別の記事に何件のコメントが関連付けられているかをトラッキングするために、カウンタキャッシュを追加したいとします。
ただし、データベースには既に大量のデータがあるため、従来の方法でカウンタキャッシュを実装するのは困難だとします。

🔗 実装の手順

  • 1. 関連付けを定義します。
    最初にArticleモデルとCommentモデルの間に関連付けを定義し、counter_cacheactive: falseオプションを指定して、初期セットアップ中はカウンタキャッシュを無効にしておきます。
class Comment < ApplicationRecord
  belongs_to :article, counter_cache: { active: false }
end
  • 2. カウンタキャッシュをバックフィルします。
    関連付けの設定が完了したら、articlesテーブルのカウンタキャッシュ用カラムのバックフィルに取りかかります。この作業中はカウンタキャッシュが無効になっており、sizeany?などのメソッドは(カウンタキャッシュを使わずに)データベースから直接結果を取得します。これによって、バックフィル作業中に誤った値が表示されることを防ぎます。
  • 3. カウンタキャッシュを有効にします。
    バックフィル作業が完了したら、counter_cache定義からactive: falseオプションを削除することで、カウンタキャッシュを有効にします。

class Comment < ApplicationRecord
  belongs_to :article, counter_cache: true
end

有効になったカウンタキャッシュは関連付けに統合されるようになり、1記事ごとに関連付けられているコメントの件数が効果的にトラッキングされるようになります。

activeオプションは#51453で導入されました。この機能に関するすべての議論は以下のdiscussをご覧ください。

参考: New feature to make introducing counter caches safer and easier - rubyonrails-talk - Ruby on Rails Discussions

関連記事

Rails: Active Recordコールバックを使わずにカウンタキャッシュを更新する(翻訳)

Rails向け高機能カウンタキャッシュ gem「counter_culture」README(翻訳)

The post Rails 7.2: counter_cacheをactiveオプションでオンオフ可能になった(翻訳) first appeared on TechRacho.

Rails 7.2: SQLクエリ数がログに出力されるようになった(翻訳)

$
0
0

概要

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

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

Rails 7.2: SQLクエリ数がログに出力されるようになった(翻訳)

データベースクエリのデバッグは、Rails開発者にとって頻度の高いタスクです。悪名高いN+1クエリ問題に対応するときや、キャッシュ戦略を微調整するときにも、SQLクエリ数を精査するためにログを調べることがよくあります。

従来、SQLクエリ数のチェックといえば、ログを手動で検査してクエリの件数を数える必要がありました。言うまでもありませんが、この作業は面倒なうえに、数十〜数百件にのぼる大量のクエリが生成されるアクションでは数え間違いが起きやすくなります。

ありがたいことに、Rails 7.2から便利な改善が導入されました。ログ出力が拡張されて、既存の情報に加えてクエリ件数も含まれるようになりました。

改善されたログ出力のActive Recordセクションには、上のようにクエリ件数も表示されています。3 queries, 1 cachedと表示されているので、データベースクエリが3件実行され、うち1件はキャッシュから取得されたことがわかります。

小さな追加に思われるかもしれませんが、これによってクエリの量や潜在的な最適化の余地を手早く特定できるようになります。キャッシュが効果的に効いているか、クエリ件数が予想通りかどうかを手軽に確認できるようになったことで、開発者の貴重な時間や労力を節約できます。

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

関連記事

Rails 8で基本的な認証ジェネレータが導入される(翻訳)

The post Rails 7.2: SQLクエリ数がログに出力されるようになった(翻訳) first appeared on TechRacho.


Viewing all 68 articles
Browse latest View live