デフォルト順序を定義できる `default_order` をリレーションとアソシエーションに追加
Active Recordに #default_order クエリメソッドと default_order アソシエーションオプションが追加され、他の順序指定がない場合に適用されるデフォルト順序を定義できるようになりました。これにより、#implicit_order_column が提供していた機能がスコープとアソシエーションのレベルに拡張されます。
背景
これまでActive Recordでは、モデルレベルでの順序を default_scope { order(...) } で定義するか、アソシエーションのラムダで order を指定する方法が主流でした。しかし、どちらのアプローチも「他の順序指定がある場合はデフォルト順序を無視したい」という一般的なユースケースに対応できませんでした。default_scope の order は reorder や unscope を使わなければ上書きできず、ラムダで指定した order も同様に結合されてしまいます。
#implicit_order_column はこの問題をモデルの主キー選択レベルで解決していましたが、任意のカラムや式をアソシエーションやスコープ単位で指定する手段が不足していました。本PRはRuby on Railsフォーラムでの議論(https://discuss.rubyonrails.org/t/default-order-for-relations/75554)を経て実装された変更です。
技術的な変更
default_order は MULTI_VALUE_METHODS に追加された新しい値リストであり、order_values とは独立した default_order_values として管理されます。クエリ構築時に order_values が空の場合のみ default_order_values が使用されるため、明示的な order が優先されます。
query_methods.rb に追加された #default_order と #default_order! は、既存の #order / #reorder! と同じ構造に従っています。
変更後(query_methods.rb):
def default_order(*args)
check_if_method_has_arguments!(__callee__, args) do
sanitize_order_arguments(args)
end
spawn.default_order!(*args)
end
def default_order!(*args) # :nodoc:
preprocess_order_args(args)
self.default_order_values = args
self
end
SQLへの適用は build_order メソッドの1行追加で実現されています。
変更後(build_order):
def build_order(arel)
orders = order_values.compact_blank
orders = default_order_values.compact_blank if orders.empty?
arel.order(*orders) unless orders.empty?
end
finder_methods.rb の ordered_relation メソッドも同様に拡張され、order_values が空のときに default_order_values を order として適用してからレコードを取得します。
アソシエーション側では、collection_association.rb の scope メソッドで options[:default_order] が存在する場合に default_order! を呼び出すよう変更され、has_many.rb の valid_options に :default_order が追加されました。さらに、association_scope.rb ではスコープのマージ時に default_order_values も合成されるようになっています。
def scope
scope = super
scope.none! if null_scope?
scope.default_order!(options[:default_order]) if options[:default_order].present?
scope
end
設計判断
order_values と default_order_values を分離して管理する設計 が採用されています。default_order_values を order_values に直接マージするのではなく別フィールドとして保持することで、order の存在有無を実行時に判断できます。この設計により、default_order を設定したスコープに後から order を追加した場合でも、デフォルト順序が静かに取り除かれる動作を実現しています。
アソシエーションオプションとしての利用と、クエリメソッドとしての利用の両方をサポートしている点も注目に値します。has_many :comments, default_order: :likes という宣言的な書き方と、Relation#default_order(:likes) というメソッドチェーンの書き方が同じ仕組みの上に統一されており、インターフェースの一貫性が保たれています。また、default_order_values は MULTI_VALUE_METHODS に含まれているため、unscope(:default_order) による取り消しも可能です。
まとめ
default_order の追加により、「明示的な順序指定がなければこの順序で返す」というよく求められる挙動を、default_scope のような強制的な適用なしに宣言できるようになりました。order_values と default_order_values を独立したフィールドとして管理する設計は、既存のクエリパイプラインへの影響を最小限に抑えながら、柔軟なデフォルト順序制御を実現しています。