Django apparently can't handle that error.

105 vues
Accéder directement au premier message non lu

Neto

non lue,
23 avr. 2016, 15:05:1723/04/2016
à Django users
Hi, I am trying to delete an Account that delete in cascade others objects, but I have a post_delete in Car model that create a Log that have relation with this Account.
The django apparently can not handle the error. How to solve this?

Models:

class Account(models.Model):
pass


class Car(models.Model):
account = models.ForeignKey('Account', on_delete=models.CASCADE)


class Log(models.Model):
account = models.ForeignKey('Account', on_delete=models.CASCADE)


class CarLog(Log):
car = models.ForeignKey('Car', null=Trueblank=True, on_delete=models.SET_NULL)
    
@receiver(post_delete, sender=Car):
def create_car_log(sender, instance, **kwargs):
try:
CarLog.objects.create(
account=instance.account,
)
except:
pass

Shell:


>>> account = Account.objects.create()
>>> car = Car.objects.create(account=account)
>>> CarLog.objects.create(account=account, car=car)
>>> account.delete()  # when delete car will try to create a log related with that account, but...

Error:
insert or update on table "myapp_log" violates foreign key constraint "myapp_log_account_id_6ea8d7a6_fk_myapp_account_id"
DETAIL:  Key (account_id)=(11) is not present in table "myapp_account".
It's happening this error rather than the exception.





Stephen J. Butler

non lue,
23 avr. 2016, 16:14:5723/04/2016
à django...@googlegroups.com
Look a little closer at the error message:

Error:
insert or update on table "myapp_log" violates foreign key constraint "myapp_log_account_id_6ea8d7a6_fk_myapp_account_id"
DETAIL:  Key (account_id)=(11) is not present in table "myapp_account".
It's happening this error rather than the exception.

The table is myapp_log, not myapp_carlog. The error isn't in the post_delete signal you're showing up. Do you have a post_delete for Account objects?

Neto

non lue,
23 avr. 2016, 16:42:2723/04/2016
à Django users
Stephen, CarLog is inheriting Log.

Stephen J. Butler

non lue,
23 avr. 2016, 17:28:0823/04/2016
à django...@googlegroups.com
Sorry, I did miss that.

I created a quick test project in 1.9 and ran your sample. It works fine for me. The delete() returns that it deleted 4 objects: the Account, Car, Log, and CarLog. There's something else in your project that is causing the error.

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users...@googlegroups.com.
To post to this group, send email to django...@googlegroups.com.
Visit this group at https://groups.google.com/group/django-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/92e72312-5678-428b-ad7f-360ad911ceaa%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Neto

non lue,
23 avr. 2016, 23:19:1423/04/2016
à Django users
Stephen, I am using Django 1.9.5, PostgreSQL 9.3
I do not know, maybe the order of the apps may be interfering in the way Django sorts the commands to be sent to the postgresql.

INSTALLED_APPS = [
'core', # here: Account, Log
'myapp', # here: Car, CarLog

What is happening is that post_delete is trying to create a log for an account that does not exist. The exception doesn't works.
This seems to be a bug.

Stephen J. Butler

non lue,
23 avr. 2016, 23:52:4623/04/2016
à django...@googlegroups.com
Ahh, Postgres is the problem. When your exception is thrown then Postgres aborts the rest of the transaction. That's how its transaction handling works. Even though you ignore the exception in the myapp code, it will still cause the transaction to abort when Django tries to call commit(). When I was testing I was using sqlite, which behaves differently.


This works for me:

@receiver(post_delete, sender=Car)
def create_car_log(sender, instance, **kwargs):
    sid = transaction.savepoint()
    try:
        CarLog.objects.create(
            account=instance.account,
        )
        transaction.savepoint_commit(sid)
    except:
        transaction.savepoint_rollback(sid)

What I don't get is that using a "with transaction.atomic()" inside the try block should do the same thing. But it's not. Maybe someone else knows?

Neto

non lue,
25 avr. 2016, 13:46:2325/04/2016
à Django users
This works for you? Not for me! This error continues to ignore the exception.
Even using exception continues to show django.db.utils.IntegrityError

Stephen J. Butler

non lue,
25 avr. 2016, 23:58:3125/04/2016
à django...@googlegroups.com
Damn. It did work once, but now I can't reproduce it. But turning on SQL logging it's clear that what's happening isn't what I said it was.

I get a sequence essentially like this at acct.delete():

-- Django selecting all the things it needs to cascade/update b/c of m2m

SELECT * FROM "myapp_car" WHERE "myapp_car"."account_id" IN (1);

SELECT * FROM "myapp_carlog" INNER JOIN "myapp_log" ON ("myapp_carlog"."log_ptr_id" = "myapp_log"."id") WHERE "myapp_carlog"."car_id" IN (1);

SELECT * FROM "myapp_log" WHERE "myapp_log"."account_id" IN (1);


-- Django doing the m2m DELETEs and UPDATEs

DELETE FROM "myapp_carlog" WHERE "myapp_carlog"."log_ptr_id" IN (1);

UPDATE "myapp_carlog" SET "car_id" = NULL WHERE "myapp_carlog"."log_ptr_id" IN (1);

DELETE FROM "myapp_car" WHERE "myapp_car"."id" IN (1);


-- This is the signal firing for Car delete

SAVEPOINT "s140735184359424_x3";

SELECT "myapp_account"."id" FROM "myapp_account" WHERE "myapp_account"."id" = 1;

INSERT INTO "myapp_log" ("account_id") VALUES (1) RETURNING "myapp_log"."id";

INSERT INTO "myapp_carlog" ("log_ptr_id", "car_id") VALUES (4, NULL);

RELEASE SAVEPOINT "s140735184359424_x3";


-- Outside the signal, more cascades

DELETE FROM "myapp_log" WHERE "myapp_log"."id" IN (1);

-- Finally, delete the account, where we blow up

DELETE FROM "myapp_account" WHERE "myapp_account"."id" IN (1);


So as you can see, the reason the signal Exception isn't caught is because it isn't thrown! The problem is that Django builds a list of every DELETE/UPDATE it needs to make on CarLog and you modify CarLog afterwards.

One way to break this race condition is to call Car.objects.filter(account=acct).delete() first, then acct.delete(). I think that should work, even if it is a little more verbose. Probably will want to wrap that in a transaction.atomic().


Neto

non lue,
26 avr. 2016, 13:02:3226/04/2016
à Django users
The right is just Account.objects.last().delete() to delete everything related to account.
My project has many models related to account, and everything has log, is unfeasible be deleting the rows of each model to the end delete the account.
Django needs to handle it.

Michal Petrucha

non lue,
26 avr. 2016, 14:18:2226/04/2016
à django...@googlegroups.com
On Tue, Apr 26, 2016 at 10:02:32AM -0700, Neto wrote:
> The right is just Account.objects.last().delete() to delete everything
> related to account.
> My project has many models related to account, and everything has log, is
> unfeasible be deleting the rows of each model to the end delete the account.
> Django needs to handle it.

Do you have any concrete suggestion what Django could do better in
this case?

So, to recap, what is happening here is that in the middle of an
attempt to delete object A, you are creating an object B referencing
object A, after a plan has been calculated (and partially executed)
that would otherwise successfully remove object A and everything
referencing it.

Have you considered altering the table for Log to include an ON DELETE
CASCADE clause instead? That would make the error go away without too
much effort, even though I still think the behavior you are
implementing is simply incorrect.

Cheers,

Michal
signature.asc

Michal Petrucha

non lue,
26 avr. 2016, 15:31:2626/04/2016
à django...@googlegroups.com
On Tue, Apr 26, 2016 at 08:17:40PM +0200, Michal Petrucha wrote:
> Have you considered altering the table for Log to include an ON DELETE
> CASCADE clause instead? That would make the error go away without too
> much effort, even though I still think the behavior you are
> implementing is simply incorrect.

Doh, there might be an even easier solution:

class Account(models.Model):
marked_for_delete = models.BooleanField(default=False)

def delete(self):
with transaction.atomic():
self.marked_for_delete = True
self.save()
super().delete()


class Car(models.Model):
account = models.ForeignKey('Account', on_delete=models.CASCADE)


class Log(models.Model):
account = models.ForeignKey('Account', on_delete=models.CASCADE)


class CarLog(Log):
car = models.ForeignKey('Car', null=True, blank=True, on_delete=models.SET_NULL)


@receiver(post_delete, sender=Car):
def create_car_log(sender, instance, **kwargs):
if not instance.account.marked_for_delete:
CarLog.objects.create(
account=instance.account,
)


Maybe, if it is guaranteed that during a delete operation, the same
instance of Account is used even when accessed from related objects,
you might be able to leave out the BooleanField, and just set an
attribute instead, but I'm not sure at the moment, so I'll leave it to
you to figure that part out.

Michal
signature.asc
Répondre à tous
Répondre à l'auteur
Transférer
0 nouveau message