Rollback all saved models if a IntegrityError is raised

I have a view that saves multiple forms on three related models. One of the models that has to be saved later can fail due IntegrityErrors. I want to make sure that that if those later models fail, the entire transaction is rolled back. Should I do this with a try: ... except: transaction.rollback() or with transaction.atomic():. I’ve read several post but it’s not clear what should I do to make sure that the entire transaction succeeds or fails, without dangling operations.

The excerpt of the models are like follow:

class Bins(models.Model):
    serial_range = IntegerRangeField()
    parent_transaction = models.ForeignKey(
        Transactions,
        on_delete=models.PROTECT)
    class Meta:
        constraints = [
            ExclusionConstraint(
                name='bin_history_ranges_overlap',
                expressions=[
                    (models.F('document_type'), RangeOperators.EQUAL),
                    ('series', RangeOperators.EQUAL),
                    ('serial_range', RangeOperators.OVERLAPS),
                ])]

The view:

class DocumentCreationView(View):
    def post(self, request):
        transaction_form = DocumentCreationForm(request.POST)
        bins_formset = BinsFormset(
            request.POST,
            prefix='bins_set')
        if transaction_form.is_valid():
            transaction_instance = transaction_form.save(commit=False)
            transaction_instance.sending_warehouse = self.creating_warehouse
            # Change for user
            transaction_instance.started_by = transaction_instance.sending_contact
            transaction_instance.save()
        if bins_formset.is_valid():
            bins = bins_formset.save(commit=False)
            for bin_instance in bins:
                bin_instance.parent_transaction = transaction_instance
                bin_instance.located_at = transaction_instance.recieving_warehouse
# This can fail if the serial_range already exist for the series and type.
                bin_instance.save()

Right now the results is that the transaction model is saved, while the bin model fails.

Either version will work assuming it’s set up correctly in the code.

Many people prefer the with version.

Also see the docs at Database transactions | Django documentation | Django for two more options. (The atomic decorator and ATOMIC_REQUESTS)

They’re all valid, they’ll all do what you need it to do. Which one you choose is more a style-choice than one of function.

I decided to pass the forms to a helper function like this:

    @transaction.atomic
    def handle_forms(self,
                     transaction_form,
                     bins_formset,
                     bins_history_formset):
        if transaction_form.is_valid():
            transaction_instance = transaction_form.save(commit=False)
            transaction_instance.sending_warehouse = self.creating_warehouse
            # Change for user
            transaction_instance.started_by = transaction_instance.sending_contact
            transaction_instance.save()
            if bins_formset.is_valid():
                bins = bins_formset.save(commit=False)
                for bin_instance in bins:
                    bin_instance.parent_transaction = transaction_instance
                    bin_instance.located_at = transaction_instance.recieving_warehouse
                    bin_instance.save()
            if bins_history_formset.is_valid():
                bins_history = bins_history_formset.save(commit=False)
                for bin_history in bins_history:
                    bin_history.parent_transaction = transaction_instance
                    bin_history.save()

            return transaction_instance, bins, bins_history

    def post(self, request):
        transaction_form = DocumentCreationForm(request.POST)
        bins_formset = BinsFormset(
            request.POST,
            prefix='bins_set')
        bins_history_formset = BinsHistoryFormset(
            request.POST,
            prefix='bins_set')
        transaction_instance, bins, bins_history = self.handle_forms(
            transaction_form,
            bins_formset,
            bins_history_formset)

That will error me out. How can I capture the integrityerror to inform the user about the problem?

I wouldn’t wrap that function with the decorator.

In this situation, I would use a try / except block with the with statement inside it in the post method where you’re calling this function.

See the third example at Controlling transactions explicitly.

Is that a preference or is there a reason?

Also that would indeed allow me to prevent the user from getting the integrity error message, but won’t allow me to inform the user what’s wrong and how they can fix it. The returned IntegrityError doesn’t have any structured info that would allow me to tell them “fix these fields, they overlap”.

My reason is that you moved the logic for performing the updates out to a different function. That tends to make it more awkward to coordinate the efforts between the two. You could do this in that function and return the details, but then you have to check the return values you get back to figure out what that function has returned to you. (Or, you could update values in self and check those.)

I don’t actually know to what level of detail that sort of information is even available. There are many different reasons why the transaction may fail, and not all of them are necessarily under the user’s control. (e.g. a dropped database connection)

If you need that degree of “fine-grained” control, you might end up needing to control the transaction yourself. Create your own transaction, and catch any exception at each SQL statement. If an error occurs, save the details of the error, rollback the transaction, and return.

I’ve been reading about personalizing the violation_error_message of ExclusionConstraint which would be more desirable since I would only need to handle that one. Maybe I should expand it to include something I could read from my view. I’m just trying to figure out how to get the original names/values used in the default message and add some decorators using those. Found this but it isn’t straight forward for me to manage it.