Skip to content

Alternative primary_key not respected in has_many through associations used in the union #1

@josephyancey-apptegy

Description

@josephyancey-apptegy

When using a has_many through association on a model that has a custom primary_key, it defaults to using id as the primary key.

has_many :enrollments, inverse_of: :course, dependent: :restrict_with_error
has_many :roster_enrollments, through: :rosters, class_name: "Enrollment", primary_key: :uuid, source: :enrollments
has_many :all_enrollments, class_name: 'Enrollment', union_of: %i[
    enrollments
    roster_enrollments
]

Course.first.all_enrollments generates sql that looks like

SELECT "enrollment".* FROM "enrollment" WHERE "enrollment"."uuid" IN (SELECT "enrollment"."uuid" FROM ( (SELECT "enrollment"."uuid" FROM "enrollment" WHERE "enrollment"."course_id" = $1) UNION (SELECT "enrollment"."id" FROM "enrollment" INNER JOIN "rosters" ON "enrollment"."roster_id" = "rosters"."id" INNER JOIN "course_rosters" ON "rosters"."id" = "course_rosters"."roster_id" WHERE "course_rosters"."course_id" = $2) ) "enrollment")

Please notice the SELECT "enrollment"."id" in the second UNION.

It appears that this is caused by .select(association.reflection.active_record_primary_key) in scope.rb:17.

Inspecting association.reflection for the direct enrollments looks like this:

{"class_name"=>"Enrollment", "counter_cache_column"=>nil, "inverse_of"=>{"class_name"=>"Course", "counter_cache_column"=>nil, "inverse_of"=>nil, "inverse_which_updates_counter_cache_defined"=>false, "inverse_which_updates_counter_cache"=>nil, "name"=>"course", "scope"=>nil, "options"=>{"optional"=>true}, "active_record"=>"Enrollment", "klass"=>"Course", "plural_name"=>"courses", "join_table"=>nil, "foreign_key"=>"course_id", "association_foreign_key"=>nil, "association_primary_key"=>nil, "inverse_name"=>nil}, "inverse_which_updates_counter_cache_defined"=>false, "inverse_which_updates_counter_cache"=>nil, "name"=>"enrollments", "scope"=>nil, "options"=>{"inverse_of"=>"course", "dependent"=>"restrict_with_error", "autosave"=>true}, "active_record"=>"Course", "klass"=>"Enrollment", "plural_name"=>"enrollments", "join_table"=>nil, "foreign_key"=>nil, "association_foreign_key"=>nil, "association_primary_key"=>nil, "inverse_name"=>"course"}

Inspecting it for the has_many through roster_enrollments looks like this:

{"class_name"=>nil, "counter_cache_column"=>nil, "inverse_of"=>nil, "inverse_which_updates_counter_cache_defined"=>false, "inverse_which_updates_counter_cache"=>nil, "delegate_reflection"=>{"class_name"=>nil, "counter_cache_column"=>nil, "inverse_of"=>nil, "inverse_which_updates_counter_cache_defined"=>false, "inverse_which_updates_counter_cache"=>nil, "name"=>"roster_enrollments", "scope"=>nil, "options"=>{"through"=>"rosters", "class_name"=>"Enrollment", "primary_key"=>"uuid", "source"=>"enrollments"}, "active_record"=>"Course", "klass"=>nil, "plural_name"=>"roster_enrollments", "join_table"=>nil, "foreign_key"=>nil, "association_foreign_key"=>nil, "association_primary_key"=>nil, "inverse_name"=>nil}, "klass"=>nil, "source_reflection_name"=>"enrollments"}

Given that the reflection is not properly reflecting the class type of the association (even though it's specified in the association definition) it looks like active_record_primary_key is just falling back to a default of id.

Simply using .select(primary_key) appears to resolve the issue but I suspect that is only because both unions are returning the same model.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions