[Rails] Uniqueness Validationのクエリ構築時にコネクション取得を不要にする改善
Context
RailsのUniqueness Validationでは、これまでクエリのAST(抽象構文木)を構築する段階でデータベースコネクションを取得する必要がありました。これは case_sensitive オプションに応じた比較演算子を決定するために、データベース固有の比較ロジックを呼び出す必要があったためです。
この設計には問題がありました。クエリの構築段階でコネクションを取得することは、本来クエリ実行時まで遅延できるはずのリソース確保を早期に行うことを意味し、パフォーマンスとスレッドセーフティの観点から望ましくありません。
PR #51353 で一部改善されたものの、Uniqueness Validationの部分には TODO コメントとして残されていた課題を、このPRで解決します。
Technical Detail
新しいArel Nodeの追加
Arelに2つの新しいノードクラスを追加しました。
class CaseSensitiveEquality < Arel::Nodes::Binary
include FetchAttribute
def equality?; true; end
end
class CaseInsensitiveEquality < Arel::Nodes::Binary
include FetchAttribute
def equality?; true; end
end
これらのノードは、大文字小文字を区別する/しない比較演算を表現する専用のASTノードです。Arel::Nodes::Binary を継承し、equality? メソッドで等価性チェックであることを示します。
Predicate Methodsの追加
新しいノードを生成するための述語メソッドを Arel::Predications に追加しました。
def case_sensitive_eq(other)
Nodes::CaseSensitiveEquality.new self, quoted_node(other)
end
def case_insensitive_eq(other)
Nodes::CaseInsensitiveEquality.new self, quoted_node(other)
end
これにより、以下のように明示的な比較を記述できるようになります。
User.arel_table[:email].case_insensitive_eq('user@example.com')
SQL生成時の処理
新しいノードをSQLに変換する処理を Arel::Visitors::ToSql に追加しました。
def visit_Arel_Nodes_CaseSensitiveEquality(o, collector)
visit @connection.case_sensitive_comparison(o.left, o.right), collector
end
def visit_Arel_Nodes_CaseInsensitiveEquality(o, collector)
visit @connection.case_insensitive_comparison(o.left, o.right), collector
end
重要なのは、データベース固有の比較ロジック(case_sensitive_comparison / case_insensitive_comparison)の呼び出しが、クエリのAST構築時ではなく、SQL生成時(つまりクエリ実行直前)に行われるようになった点です。この段階では既にコネクションが取得されているため、追加のコネクション取得は不要です。
Uniqueness Validationの改善
最も重要な変更は、Uniqueness Validationのクエリ構築ロジックです。
変更前:
comparison = klass.with_connection do |connection|
relation.bind_attribute(attribute, value) do |attr, bind|
return relation.none! if bind.unboundable?
if !options.key?(:case_sensitive) || bind.nil?
connection.default_uniqueness_comparison(attr, bind)
elsif options[:case_sensitive]
connection.case_sensitive_comparison(attr, bind)
else
connection.case_insensitive_comparison(attr, bind)
end
end
end
変更後:
comparison = relation.bind_attribute(attribute, value) do |attr, bind|
return relation.none! if bind.unboundable?
if !options.key?(:case_sensitive) || bind.nil?
attr.eq(bind)
elsif options[:case_sensitive]
attr.case_sensitive_eq(bind)
else
attr.case_insensitive_eq(bind)
end
end
with_connection ブロックが不要になり、新しい述語メソッドを使用してASTを構築するようになりました。データベース固有のロジックは、SQL生成時に自動的に適用されます。
Impact
この変更により、Uniqueness Validationのクエリ構築がより効率的になります。
- 遅延評価の実現: コネクション取得がクエリ実行時まで遅延される
- スレッドセーフティの向上: AST構築時にコネクションプールからの取得が不要になる
-
コードの簡潔化:
with_connectionブロックが不要になり、意図が明確になる
この改善は、PR #51353 で開始された、クエリ構築時のコネクション取得を排除する取り組みの完成形となります。