B. Pleshakov
B. Pleshakov

Reputation: 116

Django admin inline unique constraint violation on edit

I have a form with inline. A related model has a unique together constraint (parent_id, number). I have an entity with two children

parent_id | number |
        1 |      1 |
        1 |      2 |

and I am trying to edit these children in one operation to the state

parent_id | number |
        1 |      2 |
        1 |      3 |

When I am saving I have an error on the first entry: Child with this parent_id and number already exists.

However, if I firstly edit the second entry:

parent_id | number |
        1 |      1 |
        1 |      3 |

and then the first

parent_id | number |
        1 |      2 |
        1 |      3 |

in two separate actions, it works fine.

ParentAdmin definition

class ParentAdmin(admin.ModelAdmin):
    form = BaseForm

    inlines = [LevelExerciseInline]

ChildInline definition

class ChildInline(admin.StackedInline):
    form = BaseForm

    model = Child
    extra = 3

Upvotes: 1

Views: 1331

Answers (1)

Risadinha
Risadinha

Reputation: 16666

As you are using Django Admin inlines, the following will not be the case:

and I am trying to edit these children in one operation to the state

Inlines are part of the complete form, they will be processed in order of their appearance in the form. Thus, even if you change them "in one action", meaning that you change them during one form POST, Django will still save these related objects one by one:

The first inline in the list is saved, and it tries to save parent_id 1 with number 2 while there is still parent_id 2 with number 2 in the database.

Specifically, formset.save() (https://github.com/django/django/blob/master/django/forms/models.py#L655) is called, with the following code.

Note: here, "form" is a part of the actual HTML form (it's just the name of the variables, do not confuse it).

def save(self, commit=True):
    """Saves model instances for every form, adding and changing instances
    as necessary, and returns the list of instances.
    """
    if not commit:
        self.saved_forms = []

        def save_m2m():
            for form in self.saved_forms:
                form.save_m2m()
        self.save_m2m = save_m2m
    return self.save_existing_objects(commit) + self.save_new_objects(commit)

As you can see, each inline is saved in a loop once save_m2m is called. The array is sorted in order of appearance in the form.

This is less about the Django Admin and more a question on whether it is even possible on a database level, though. The only possiblity that could work would be a transaction but even then - postgresql for example would not allow it, unless the constraint is set to DEFERRED:

https://dba.stackexchange.com/questions/104987/avoid-unique-violation-in-atomic-transaction

Postgresql documentation: https://www.postgresql.org/docs/current/sql-set-constraints.html

So, to change the behavior, you would need:

Upvotes: 2

Related Questions