Nested Changeset's changes order on insert action

Hello,

I faced with an issue when the order of nested assocs during insert matters. It matters because I have custom check constraint which excludes rows by complex rules. What are the options? I think only the work around here is to use a transaction and make each insert by hand?

Thanks.

UPDATE

I think that I am ignored because it could not be clear. Sorry about that. So I decided to add extra cases to show my problem.

Let’s assume I have the next table

CREATE TABLE date_ranges (
  id SERIAL PRIMARY KEY,
  user_id INTEGER,
  start_at DATE,
  end_at DATE,
  is_exception BOOLEAN
  is_day_off BOOLEAN
);

The use schema has next fields:

has_many :date_ranges, DateRange, on_replace: :delete
has_many :except_ranges, DateRange, on_replace: :delete
has_many :day_off_ranges, DateRange, on_replace: :delete

When I do my insert the order of date_ranges, except_ranges, day_off_ranges is not frozen. The log shows that except_ranges insert before date_ranges. And I would like to specify the priority to insert date_ranges first and then the rest.

I have PGSQL functions to check every case to exclude ranges according to the table:

user_id     start_at        end_at      is_exception    is_day_off
1           2019-03-01      2019-03-05  FALSE           FALSE       // is fine
1           2019-03-01      2019-03-05  FALSE           FALSE       // FAIL. Range intersects with range above
1           2019-03-02      2019-03-03  TRUE            FALSE       // is fine since exception is inside "regular range above"
1           2019-03-04      2019-03-04  TRUE            FALSE       // is fine since exception is inside "regular range above" & does not intersect another exception
1           2019-03-03      2019-03-03  FALSE           TRUE        // is fine since day off is inside "regular range above"
1           2019-03-05      2019-03-05  FALSE           TRUE        // is fine since day off is inside "regular range above" & does not intersect another day off

1           2019-03-02      2019-03-03  TRUE            FALSE       // FAIL. Exception should NOT intersect with another one
1           2019-03-03      2019-03-03  FALSE           TRUE        // FAIL. Day off should NOT intersect with another one

1           2019-03-15      2019-03-15  TRUE            FALSE       // FAIL. Exception is NOT inside "regular range above"
1           2019-03-10      2019-03-10  FALSE           TRUE        // FAIL. Day off is NOT inside "regular range above"

At the end I get {:error, changeset} because excepted_ranges is inserted first.

I’m pretty new at ecto and phx but would ecto multi help?

I’m thinking with multi you can dictate the order and I’m thinking you can do multiple insert line and order how you like.

Yeah, it is an option, but I hope maybe there is another way to avoid manual separation of nested models, using Multi and then merge them back. The question is a very simplified version of the real schema.

@vlad.grb Can you post the code that builds the changeset with nested associations? (Probably has cast_assoc or similar)

I haven’t dug into how Ecto does it, but the order those associations save in is likely established either by the order of the has_manys in the schema or the order in cast_assoc.

Sure.

    has_many :date_ranges, Schedule.DateRange, on_replace: :delete
    has_many :except_ranges, Schedule.DateRange, on_replace: :delete
    has_many :day_off_ranges, Schedule.DateRange, on_replace: :delete

Them in my changeset/2

  def changeset(struct, attrs) do
    struct
    |> cast(attrs, [:is_restricted])
    |> validate_required([:is_restricted])
    |> cast_assoc(:date_ranges, required: true)
    |> cast_assoc(:except_ranges, with: &Schedule.DateRange.exception_changeset/2)
    |> cast_assoc(:day_off_ranges, with: &Schedule.DateRange.day_off_changeset/2)
  end

And then in test case log the first one after parent is

INSERT INTO "ranges" ("end_at","is_day_off","is_exception","start_at","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id" [{2019, 2, 24}, false, true, {2019, 2, 24}, {{2019, 3, 3}, {20, 49, 13, 478451}}, {{2019, 3, 3}, {20, 49, 13, 478458}}]

I just ran into a similar issue. I tried to play around with @al2o3cr’s suggestion. Reordering the cast_assoc didn’t work for me. But reordering the has_many’s in the schema did, weirdly enough.

For my case, it consistenly inserts the associated records in the reverse order of which they are defined in the schema.
It seems to be an implicit order as I couldn’t find any documentation on it. An ordering that could very easily break with a new Ecto update (currently using version 3.5.0). But it may help you, 3 years after date, or anyone else running into this issue.

1 Like

Yes, if I’m interpreting the code correctly in v3.7.2, the ordering is reversed in pop_assoc/2 (called from do_insert/4 line 346) where it builds a reversed list of children in:

%{^assoc => {:assoc, %{relationship: :child} = refl}} ->
                {changes, parent, [{refl, value} | child]}

It’s implicit and should probably be documented. This code has been there since 2015.

1 Like