5

I'm using PostGres 10, Python 3.9, and Django 3.2. I have set up this model with the accompanying many-to-many relationship ...

class Account(models.Model):    
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    ...
    crypto_currencies = models.ManyToManyField(CryptoCurrency)

After generating and running Django migrations, the following table was created ...

\d cbapp_account_crypto_currencies;
                               Table "public.cbapp_account_crypto_currencies"
      Column       |  Type   |                                  Modifiers                                   
-------------------+---------+------------------------------------------------------------------------------
 id                | integer | not null default nextval('cbapp_account_crypto_currencies_id_seq'::regclass)
 account_id        | uuid    | not null
 cryptocurrency_id | uuid    | not null
Indexes:
    "cbapp_account_crypto_currencies_pkey" PRIMARY KEY, btree (id)
    "cbapp_account_crypto_cur_account_id_cryptocurrenc_38c41c43_uniq" UNIQUE CONSTRAINT, btree (account_id, cryptocurrency_id)
    "cbapp_account_crypto_currencies_account_id_611c9b45" btree (account_id)
    "cbapp_account_crypto_currencies_cryptocurrency_id_685fb811" btree (cryptocurrency_id)
Foreign-key constraints:
    "cbapp_account_crypto_account_id_611c9b45_fk_cbapp_acc" FOREIGN KEY (account_id) REFERENCES cbapp_account(id) DEFERRABLE INITIALLY DEFERRED
    "cbapp_account_crypto_cryptocurrency_id_685fb811_fk_cbapp_cry" FOREIGN KEY (cryptocurrency_id) REFERENCES cbapp_cryptocurrency(id) DEFERRABLE INITIALLY DEFERRED

How do I alter my field relation, or generate a migration, such that the cascade relationship is ON-DELETE CASCADE? That is, When I delete an account, I would like accompanying records in this table to also be deleted.

Dave
  • 15,639
  • 133
  • 442
  • 830
  • `When I delete an account, I would like accompanying records in this table to also be deleted.` -- This should already be the behaviour when you delete Account objects. – Brian Destura Nov 29 '21 at 02:36
  • ON-DELETE CASCADE does not sound logical for manytomany relationship. For e.g. there are 4 drivers and 3 cars. A car can be driven by multiple drivers, so if you delete a driver , the car should NOT be deleted from the database, because other drivers can still drive that particular car. – Neeraj Nov 29 '21 at 03:40

2 Answers2

1

Had a closer look on this. I tried to replicate your models and I also see that the intermediary table has no cascade. I have no answer on your main question on how to add the cascade, but it seems that django does the cascade behavior which already supports this:

When I delete an account, I would like accompanying records in this table to also be deleted.

To demonstrate:

a = Account.objects.create(name='test')
c1 = CryptoCurrency.objects.create(name='c1')
c2 = CryptoCurrency.objects.create(name='c2')
c3 = CryptoCurrency.objects.create(name='c3')
a.crypto_currencies.set([c1, c2, c3])

If you do:

a.delete()

Django runs the following SQL which simulates the cascade on the intermediary table:

[
    {
        'sql': 'DELETE FROM "myapp_account_crypto_currencies" WHERE "myapp_account_crypto_currencies"."account_id" IN (3)', 'time': '0.002'
    }, 
    {
        'sql': 'DELETE FROM "myapp_account" WHERE "myapp_account"."id" IN (3)', 'time': '0.001'
    }
]

I can't find in the documentation why it is done this way though. Even adding a custom intermediary like this results in the same behavior:

class Account(models.Model):
    name = models.CharField(max_length=100)
    crypto_currencies = models.ManyToManyField(CryptoCurrency, through='myapp.AccountCryptocurrencies')


class AccountCryptocurrencies(models.Model):
    account = models.ForeignKey(Account, on_delete=models.CASCADE)
    cryptocurrency = models.ForeignKey(CryptoCurrency, on_delete=models.CASCADE)
Brian Destura
  • 11,487
  • 3
  • 18
  • 34
0

When you use a ManyToManyField, Django creates a intermediary table for you, in this case named cbapp_account_crypto_currencies. What you want to do in the future is to always explicitly create the intermediary model, AccountCryptoCurrencies, then set the through attribute of the ManyToManyField. This will allow you to add more fields in the future to the intermediary model. See more here: https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.ManyToManyField.through.

What you will now need to do is so create this intermediary table:

class AccountCryptoCurrencies(models.Model):
    account = models.ForeignKey(Account)
    cryptocurrency = models.ForeignKey(CryptoCurrency)

    class Meta:
        db_table = 'cbapp_account_crypto_currencies'

class Account(models.Model):    
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    ...
    crypto_currencies = models.ManyToManyField(CryptoCurrency, through=AccountCryptoCurrencies)

You are now need to generate a migration, but do not apply it yet! Modify the migration by wrapping it in a SeparateDatabaseAndState. I havent created your migration file because I dont have the full model, but you can see here for how to do it: How to add through option to existing ManyToManyField with migrations and data in django

Now you can apply the migration and you should now have an explicit intermediary table without losing data. You can also now add additional fields to the intermediary table and change the existing fields. You can add the on_delete=models.CASCADE to the account field and migrate the change.

TaipanRex
  • 795
  • 5
  • 11
  • Thanks, althouhg I'm not understanding what part of the link applies to my answer. Can you cut and paste only the relevant portions of that answer into your post? – Dave Dec 01 '21 at 20:59
  • Step 3. in @grain's answer, shows how you should modify your migration file by wrapping it in `SeparateDatabaseAndState`. – TaipanRex Dec 03 '21 at 13:33