From 9e1f7bf62185f1121b455d3d5994de8d64326ed2 Mon Sep 17 00:00:00 2001 From: Evan Huus <109987149+eapache-opslevel@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:10:21 -0500 Subject: [PATCH] Fix eager loading of hierarchies by defining the primary key When preloading an association via join (e.g. by using `eager_load`, or by referencing the associated table in a `where` condition), rails uses the primary key of the associated table in order to deduplicate the rows and build up the correct association. But the default generated hierarchies table doesn't have an `id` column or anything else that rails can automatically detect as a primary key. This leads rails to incorrectly treat all the records as equal, and return only one at random. For example, calling `MyModel.eager_load(:ancestor_hierarchies).map(&:ancestor_hierarchies)` currently returns incorrect data - only one hierarchy per model, no matter how deep the hierarchy actually is. This PR tells rails to use the combination of the three columns (which do have a unique index on them already in the generator!) as the primary key, and allows the example code to correctly return all the hierarchies for each model. It also would presumably fix https://github.com/ClosureTree/closure_tree/issues/294 though I haven't tested that. --- lib/closure_tree/support.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/closure_tree/support.rb b/lib/closure_tree/support.rb index 3a0a8ae..1b21739 100644 --- a/lib/closure_tree/support.rb +++ b/lib/closure_tree/support.rb @@ -36,6 +36,10 @@ def hierarchy_class_for_model # Rails 8.1+ requires an implicit_order_column for models without a primary key self.implicit_order_column = 'ancestor_id' + # Rails uses the primary key to correctly match associations when using a join to preload (e.g. via `eager_load`). + # The migration generator adds a unique index across these three columns so this is safe. + self.primary_key = [:ancestor_id, :descendant_id, :generations] + belongs_to :ancestor, class_name: model_class_name belongs_to :descendant, class_name: model_class_name def ==(other)