7

I am using Django, Python 3.7, and PostgreSQL 9.5. How do I mark up my model such taht a cascading foreign key constraint gets generated? I currently have this in my models.py file ...

class ArticleStat(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, )

When I run make migrations my_project in the management console, it produces a file that contains this ...

    migrations.CreateModel(
        name='ArticleStat',
        fields=[
            ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ...
            ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='my_project.Article')),
        ],
    ),

However, when I run the migration (using migrate my_project 0001), the resulting foreign key does not contain a cascade delete constraint. This is what the description looks like in PostgreSQL ...

"my_project_articlesta_article_id_a02a5add_fk_my_project" FOREIGN KEY (article_id) REFERENCES my_project_article(id) DEFERRABLE INITIALLY DEFERRED

How else can I get my models.py file to output a cascading delete foreign key constraint?

Ahtisham
  • 9,170
  • 4
  • 43
  • 57
Dave
  • 15,639
  • 133
  • 442
  • 830

1 Answers1

9

As described in the django documentation of ForeignKey django merely emulates this behaviour instead of deferring it to the database.

Django emulates the behavior of the SQL constraint ON DELETE CASCADE and also deletes the object containing the ForeignKey.

So to answer your question: This is expected behavior and will still work just like you would expect it to work on the database level.

How else can I get my models.py file to output a cascading delete foreign key constraint?

You can't as django currently does not support this feature. However there is a ticket discussing to add it: https://code.djangoproject.com/ticket/21961


Edit for further clarification on how to enforce this at database-level

While I highly recommend to just let django handle this for you there might be reasons not to do so.

To opt out of database table creation or deletion operations you can set Options.managed to False in the Meta class of ArticleStat. That would however also mean that you are now responsible to do the migrations manually e.g. writing the CREATE TABLE statement to define the table including the foreign key constraint (which you thus have full control over now). Another consideration to take into account is that you should instruct django to not do anything on deletion of a referenced Article object anymore (as your database is now responsible for that). That can be ensured by setting on_delete to models.DO_NOTHING.

Put together your ArticleStat would now look like this:

class ArticleStat(models.Model):
    article = models.ForeignKey(Article, on_delete=models.DO_NOTHING)

    class Meta:
        managed = False

A comment about signals prompted me to revisit this and list some pitfalls to consider.

  • opting out means opting out of django signals as well. In particular pre_delete and post_delete won't be fired for cascaded objects anymore.

  • As mentioned in the ticket description mixing database- and django-cascading won't play nicely together.

    If a model A references a model B using CASCADE_DB, but model B references model C using regular CASCADE, a deletion of A won't cascade all the way to C.

That being said I couldn't find any definite proof of why django is handling this the way it does currently.

Fynn Becker
  • 1,278
  • 2
  • 18
  • 21
  • 1
    I want my database to be generated with a cascading on-delete foreign key constraint, which is not happening now. What changes do I need to make to get this to happen or is the behavior I'm looking for not supported by Django? – Dave Jan 15 '19 at 21:31
  • 1
    Django currently does not offer support for database-level cascading options. However there is a ticket discussing to add this option: https://code.djangoproject.com/ticket/21961 – Fynn Becker Jan 15 '19 at 21:51
  • Ok, thanks for that clarification. So if I wanted this I would just have to create the migration independent of my model? – Dave Jan 15 '19 at 23:42
  • I added some further information about how one might opt out of the "django way" of cascade deleting and instead enforcing it on the database-level. Note that I have not fully tested this approach so you might wanna experiment with this in a small testing environment first. – Fynn Becker Jan 16 '19 at 00:19
  • 2
    I'd add that the reason why Django implements cascade deletion at the application is to support model deletion signals (`pre_delete` and `post_delete`). If you use database level cascade deletion paired with `DO_NOTHING` then these signals be won't fired. – Simon Charette Jan 22 '19 at 17:34