ID of parent record is not filled in inside associated record with cast_assoc when trying to update

I am trying to update a Course 's CourseTierGroups, which is a join table with an extra property, order.

This is the structure of the update:

%{
  "course_tier_groups" => [
    %{
      "id" => 1,
      "order" => 1,
      "tier_group" => %{
        "description" => "Fuga...",
        "id" => 1,
        "tag" => "tgatque_et",
        "tiers" => [],
        "title" => "TG-Hic aliquid quas cupiditate accusantium nam et quos minus tempora."
      }
    },
    %{
      "id" => 2,
      "order" => 0,
      "tier_group" => %{
        "description" => "Nihil...",
        "id" => 2,
        "tag" => "tgharum_maxime_vero_animi_ad",
        "tiers" => [],
        "title" => "TG-Et sunt perferendis consequatur."
      }
    }
  ]
}

This is the CourseTierGroup schema:

schema "course_tier_group" do
  belongs_to(:course, Course)
  belongs_to(:tier_group, TierGroup)
  field(:order, :integer)
  timestamps()
end

And changeset:

def changeset(%CourseTierGroup{} = struct, params \\ %{}) do
  struct
  |> Repo.preload([:tier_group])
  |> cast(params, [:order])
  |> validate_required([:order])
  |> cast_assoc(:tier_group, required: true, with: &TierGroup.changeset/2)
  |> unique_constraint([:course_id, :tier_group_id])
  |> unique_constraint([:course_id, :order])
end

This is the relationship in the Course schema:

schema "course" do
...
    has_many(:course_tier_groups, CourseTierGroup, on_replace: :delete)
...
end

And this is the Course changeset:

def changeset(%Course{} = struct, params \\ %{}) do
    struct
    |> Repo.preload([:scopes, :type, :course_tier_groups, :topic_pills])
    |> cast(params, [:title, :description, :tag, :type_metadata])
    |> put_scopes
    |> put_type
    |> put_topic_pills
    |> cast_assoc(:course_tier_groups, with: &CourseTierGroup.changeset/2)
    |> validate_required([:title, :description, :tag])
    |> unique_constraint([:tag])
  end

In the original data, the order is switched around (id = 1 has order 0 and id = 2 has order 1).

The tier group itself is not changed, which is picked up correctly by Ecto. It also detects that I am trying to update the order. However, from what I’ve read, I was expecting the course’s ID in course_tier_groups to be filled in by Ecto, but it doesn’t do it.

It gives me the following error due to the fact that course_id is null and there already exists a record with course_id = nil and order = 1 (The one before the update):

#Ecto.Changeset<
  action: :update,
  changes: %{
    course_tier_groups: [
      #Ecto.Changeset<
        action: :update,
        changes: %{course_id: nil, order: 1},
        errors: [
          course_id: {"has already been taken",
           [
             constraint: :unique,
             constraint_name: "course_tier_group_course_id_order_index"
           ]}
        ],
        data: #LLServer.Schema.CourseTierGroup<>,
        valid?: false
      >,
      #Ecto.Changeset<
        action: :update,
        changes: %{order: 0},
        errors: [],
        data: #LLServer.Schema.CourseTierGroup<>,
        valid?: true
      >
    ]
  },
  errors: [],
  data: #LLServer.Schema.Course<>,
  valid?: false
>

I am expecting course_id to be filled in with the ID of the course that is being updated.

How should I deal with this? Should I also pass course_id in my update map?

The issue here is that cast_assoc/3 attempts to create a new associated record. If you already have the record created you’ll want to use put_assoc/4 Instead. Alternatively you can just set the id of the associated record if you don’t have it loaded from the database already and want to avoid the extra query.

1 Like