`reverse_order` が `query_constraints` に基づく逆順に変更
reverse_order が暗黙的な並び順を逆にする際、query_constraints を定義したモデルでも主キーだけで並び替えていた問題を解消し、first/last と同様のカラムで逆順が生成されるようになりました。
背景
reverse_order が無序の relation に対して主キー順の逆転だけを行っていた ため、query_constraints を持つモデルで first と last の結果が一致しませんでした。first/last は暗黙的に query_constraints_list のカラムで順序付けを行う一方、reverse_order は常にプライマリキーでのみ逆転していたため、ClothingItem.all.reverse_order.first == ClothingItem.last が false になるケースがありました。この不整合は、Rails が提供する API の期待する「逆順は元の順序の逆」とは相反する動作でした。
技術的な変更
reverse_order の実装が _order_columns を利用するよう変更されました。reverse_sql_order メソッド内部で、order_query が空の場合に _order_columns(暗黙的な order カラムの集合)を基に DESC 句を生成するロジックへ置き換えられ、主キーだけに依存していた旧実装が除去されました。
@@ -2087,8 +2087,8 @@ def table_name_matches?(from)
def reverse_sql_order(order_query)
if order_query.empty?
- if !_reverse_order_columns.empty?
- return _reverse_order_columns.map { |column| table[column].desc }
+ if !_order_columns.empty?
+ return _order_columns.map { |column| table[column].desc }
end
raise IrreversibleOrderError, <<~MSG.squish
_reverse_order_columns ヘルパーが削除され、代わりに既存の _order_columns が再利用されました。この変更により、暗黙的な order カラム(query_constraints_list が存在すればそれ、なければプライマリキー)が一貫して利用され、追加のロジックや重複したデータ構造が排除されました。
- def _reverse_order_columns
- roc = []
- roc << model.implicit_order_column if model.implicit_order_column
- roc << model.primary_key if model.primary_key
- roc.flatten.uniq.compact
- end
テストスイートに query_constraints を持つモデル向けの検証が追加されました。ClothingItem モデルを用いた新しいケースで、reverse_order が期待通りに query_constraints のカラムで逆順を生成し、first/last と同等の結果になることが確認されています。
@@ -27,6 +27,7 @@
require "models/category"
require "models/categorization"
require "models/edge"
+require "models/clothing_item"
require "models/wheel"
require "models/subscriber"
require "models/cpk"
@@ -399,6 +400,19 @@ def test_default_reverse_order_on_table_without_primary_key_with_implicit_order_
assert_equal edge_2.source_id, ordered_edge.all.reverse_order.first.source_id
end
+ def test_reverse_order_uses_query_constraints
+ ClothingItem.delete_all
+ # Insert so that the id order is the reverse of the query_constraints order,
+ # to catch a reverse_order that falls back to ordering by the primary key.
+ zzz = ClothingItem.create!(clothing_type: "zzz", color: "red")
+ aaa = ClothingItem.create!(clothing_type: "aaa", color: "blue")
+
+ assert_equal aaa.id, ClothingItem.all.first.id
+ assert_equal zzz.id, ClothingItem.all.last.id
+ assert_equal zzz.id, ClothingItem.all.reverse_order.first.id
+ assert_equal aaa.id, ClothingItem.all.reverse_order.last.id
+ end
+
def test_order_with_hash_and_symbol_generates_the_same_sql
assert_equal Topic.order(:id).to_sql, Topic.order(id: :asc).to_sql
end
設計判断
reverse_order が既存の _order_columns を再利用する方針 が採用された背景には、一貫性と後方互換性の維持 があります。query_constraints が定義されている場合はそのカラム列が _order_columns に含まれるため、first/last と同一基準で逆順が生成され、モデル間で振る舞いが統一されます。一方、query_constraints が無いモデルでは _order_columns が空となり、従来どおりプライマリキーにフォールバックするため、既存アプリケーションへの影響はありません。
まとめ
今回の変更により、reverse_order が query_constraints を考慮した逆順クエリを生成し、first/last と整合性が取れるようになりました。モデルが持つ暗黙的な順序付けロジックを活用する設計は、機能追加による破壊的変更を防ぎつつ、API の一貫性を高める選択と言えます。