PROTECT vs RESTRICT for on_delete (Django) - python

I read the django documentation about PROTECT and RESTRICT to use with "on_delete".
PROTECT
Prevent deletion of the referenced object by raising ProtectedError, a
subclass of django.db.IntegrityError.
Example:
class MyModel(models.Model):
field = models.ForeignKey(YourModel, on_delete=models.PROTECT)
RESTRICT
Prevent deletion of the referenced object by raising RestrictedError
(a subclass of django.db.IntegrityError). Unlike PROTECT, deletion of
the referenced object is allowed if it also references a different
object that is being deleted in the same operation, but via a CASCADE
relationship.
Example:
class MyModel(models.Model):
field = models.ForeignKey(YourModel, on_delete=models.RESTRICT)
To some degree, I could understand the difference between PROTECT and RESTRICT but not exactly so what is the difference between PROTECT and RESTRICT exactly? and when should I use them?

Based on Django documentation RESTRICT allows you to delete your referenced object in some special situations. For instance:
class Artist(models.Model):
name = models.CharField(max_length=10)
class Album(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
class Song(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
album = models.ForeignKey(Album, on_delete=models.RESTRICT)
As you can see, if you create an album instance and after that create a song instance with the same artist (now you have a song and also an album with the same artist), then you can simply delete that artist without any problem (since in this deleting operation you're also deleting related objects. Also note that artist has CASCADE on song and album deletion). But if you have defined PROTECT instead of RESTRICT, like:
class Song(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
album = models.ForeignKey(Album, on_delete=models.PROTECT)
you couldn't have deleted your artist instance because that artist is referenced by this song. If you ask me, I would say RESTRICT is another version of PROTECT with less limitation on object deletion. If this explanation is not clear so far I would recommend you Django example itself:
Artist can be deleted even if that implies deleting an Album which is referenced by a Song, because Song also references Artist itself through a cascading relationship. For example:
artist_one = Artist.objects.create(name='artist one')
artist_two = Artist.objects.create(name='artist two')
album_one = Album.objects.create(artist=artist_one)
album_two = Album.objects.create(artist=artist_two)
song_one = Song.objects.create(artist=artist_one, album=album_one)
song_two = Song.objects.create(artist=artist_one, album=album_two)
album_one.delete()
Raises RestrictedError.
artist_two.delete()
Raises RestrictedError.
artist_one.delete()
(4, {'Song': 2, 'Album': 1, 'Artist': 1})
Will successfully delete your object
Using different types of on_delete is really related to your design and your constraints on deleting your objects. So, basically when you want to just protect your object from deletion (without any dependencies), using PROTECT is your best solution because with using RESTRICT in this case, you force Django to look in every related object (a nested for loop) for checking if other relations will be deleted in this process or not and it might have bad impact on your deletion performance.

Based on the real world applications requirement, we use both for different purpose.
PROTECT never deletes and raises error. But, RESTRICT (introduced from Django 3.1) deletes in some cases, not all.
PROTECT example:
According to how to prevent deletion,
class Employee(models.Model):
name = models.CharField(name, unique=True)
class Project(models.Model):
name = models.CharField(name, unique=True)
employees = models.ForeignKey(Employee, on_delete=models.PROTECT)
PROTECT explanation: Think from real worlds perspective. There will be many Employees and an Employee can have multiple Projects. If we delete an Employee if he has multiple Projects associated with it, the project objects in Project model will be remained. This is wrong. If Employee has done any Projects, he (Employee object) can't be deleted. Hence we used PROTECT. This would work to prevent the deletion of any Employee object that has one or more Project object(s) associated with it.
You need to understand CASCADE first before understanding RESTRICT:
CASCADE example:
class Artist(models.Model):
name = models.CharField(max_length=10)
class Album(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
CASCADE explanation: Think from real worlds perspective. There will be many Artists and an Artist can have multiple Albums. If we want to delete an Artist and his/her related Albums, we will use CASCADE. Remember, CASCADE deletes. It always deletes.
RESTRICT example:
class Artist(models.Model):
name = models.CharField(max_length=10)
class Album(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
class Song(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
album = models.ForeignKey(Album, on_delete=models.RESTRICT)
RESTRICT explanation: Now, think once again from the real world perspective. An Artist will have zero or more Albums. An Album can have zero or more Songs. There is no problem in deleting if an Artist have zero Albums and an Album have zero Songs. In fact, there is no relation since Artist doesn't have any Albums at all.
The deletion problem arises and the scenario starts when an Artist has multiple Albums and an Album has multiple Songs. Here's how:
RESTRICT and PROTECT works the same way.
But, PROTECT is of two steps. Parent and Child. If we shouldn't delete a Child (Album), we shouldn't delete a Parent (Artist). In other words, we use PROTECT if we don't want our Child (Album) deleted if Parent(Artist) deleted. PROTECT protects from deletion of objects.
And, RESTRICT is of three steps. Parent and Child and Grand Child. RESTRICT (a limiting condition or measure) only restricts from deletion of objects up to a certain limit.
You need to understand a real world scenario why we use RESTRICT.
Lets say there are multiple Artists. Each Artists have multiple Albums. Each Album has multiple songs. see the below code
>>> artist_one = Artist.objects.create(name='artist one')
>>> artist_two = Artist.objects.create(name='artist two')
>>> album_one = Album.objects.create(artist=artist_one)
>>> album_two = Album.objects.create(artist=artist_two)
>>> song_one = Song.objects.create(artist=artist_one, album=album_one)
>>> song_two = Song.objects.create(artist=artist_one, album=album_two)
>>> album_one.delete()
# Raises RestrictedError.
>>> artist_two.delete()
# Raises RestrictedError.
>>> artist_one.delete()
(4, {'Song': 2, 'Album': 1, 'Artist': 1})
Note that, from above code,
song_one and song_two are from same Artist and different Albums from different Artists.
One song can be sung/written/shared by one or more Artists as well.
One Song can be in many Albums sung/written by one or more Artists.
One Album contains many Songs sung/written by different Artists.
How RESTRICTS works:
Now, in real world, if we have to delete the Artist all his Albums and Songs in Albums should be deleted. But, only when all the songs in his Albums doesn't share relationship with other artists. In other words, when all songs referenced to the same Artist, then deletion of Artist, Albums and Songs will happen.
Note that we can't delete artist_two, because song_two shared his album_two along with artist_one.
In simple words, in Song object, if artist and artist from the album are same, RESTRICT allows to delete.

by taking #Roham example
class Artist(models.Model):
name = models.CharField(max_length=10)
class Album(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
class Song(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
album = models.ForeignKey(Album, on_delete=models.RESTRICT)
So here RESTRICT and PROTECT are suppose to stop the deletion of a Album instance that is referenced in a song instance. But in case of a special case, only RESTRICT will allow to delete the album instance such that instance of artist should also be deleted simultaneously (artist reference should be same for both album and song). If you will use PROTECT, it will protect deletion anyway. I hope this simple explanation helps you.

The short answer in simple words is:
CASCADE by deletion of parent, child also gets deleted.
SET_NULL lets parent be deleted but keeps the child.
PROTECT never lets the deletion of parent OF child.
while RESTRICT allows deletion of child only if all of its owners(parents) are deleted in past or currently are being deleted (makes sure other instances are not involved).

Related

Django - Models - Linking models to another and vice versa

I am trying to link venues to the products they supply. The products supplied are not unique to each venue.
As a result, Venue 1 and 2 could both provide Product A.
The outcome I am looking for is twofold:
when a Product is added to the database, there is an option to link it to an existing Venue
When looking at a venue in particular, I would like to have the list of all the product that can be supplied
Outcome 1. and current problem
I tried using Foreign Keys and ManyToManyFields but this only seems to add all the products available to the database to all the venues without leaving a choice.
This is what venue = models.ManyToManyField(Venue, blank=True, related_name="available_products") renders in the admin panel. In this example, by adding ManyToMany Field all Venues have been added to Product 1. Whereas I would like the possibility to add only specific venues (not all)
Outcome 2. and current problem
The second problem is obviously referring to Product from the Venue model. If I input a foreign key or any form of relation in it, Django gets upset and tells me Product is not defined.
I thought of creating a 3rd model, that could combine both Venue and Products, but it feels like there must be something more sophisticated that could done.
(edit: I replaced the FK by ManyToManyField as suggested by David Schultz)
class Venue(models.Model):
name = models.CharField(verbose_name="Name",max_length=100, null=True, blank=True)
class Product(models.Model):
name = models.CharField('Product Name', max_length=120, null=True)
venue = models.ManyToManyField(Venue, blank=True, related_name="available_products")
A ManyToManyField should in fact be perfect for what you want to do. It only associates those objects to one another for which relations have been explicitly created, e.g. in the admin or programmatically. The fact that your admin shows you all objects at once does not mean that they have been assigned, but just that they are available. In the list from your screenshot, selection can be done by Ctrl+Mouseklick, and when you then save the Product and reload the page, precisely the Venues you selected before should now again show up with a distinct background color – this means that they have indeed been saved.
Regarding your second problem: The argument related_name works differently than you apparently think: In your last line of code, you should rather write something like related_name="available_products", because related_name becomes the name of an attribute of your Venue instances, by which you can then access all Product objects that have been associated to that Venue object, e.g. like so: venue.available_products.all()
related_name works the same for ManyToManyField and ForeignKey.
You can define your ManyToManyField either on Product or on Venue; some more info is in the documentation page. So all in all, you should do something like:
class Venue(models.Model):
name = models.CharField(verbose_name="Name",max_length=100, blank=True)
class Product(models.Model):
name = models.CharField('Product Name', max_length=120, blank=True)
venues = models.ManyToManyField(Venue, blank=True, related_name="available_products")
(Side note: For CharFields, it is recommended not to set null=True and instead only use blank=True, because otherwise there would be two different options for "no data", namely Null and an empy string. Details in the docs.)

Deleting values connected to each other in the database

My database models.py looks like this:
class Item(models.Model):
title = models.CharField(max_length=100)
price = models.FloatField()
slug = models.SlugField(unique=True)
description = models.TextField()
quantity = models.IntegerField(default=1)
class Bike(models.Model):
item = models.OneToOneField(Item, on_delete=models.CASCADE)
image = models.ImageField(upload_to='bikes')
When I am deleting an entry in the table Item it also deletes everything that is associated in the table Bike. But when I am deleting an entry in the Bike table it doesn't delete any data in the Item table. Is there any possible way to delete all data in the Item table, when you delete an entry in the Bike table?
First, the behavior you describe (Bike is deleted when you delete Item) corresponds to the CASCADE argument you put in the field:
item = models.OneToOneField(Item, on_delete=models.CASCADE)
You should know, CASCADE is not the only behavior available, you can check them out there.
But I don't think any of them has the exact behavior you want.
To achieve this behavior you should use a pre_delete or post_delete signal. But in the end this is not a good solution in my opinion.
From what I read I understand that Bike and Item are not just related. They are the same thing. You shouldn't represent their relationship with a OneToOneField (from what I see). Bikeshould be a child of Item so that a Bike instance has every attribute of Itemand the attributes you want specifically for the a Bike. With this solution you'll find things more scalable and every use of your models will be more logical. You can learn more about model inheritance in the Django Documenation.
If I were wrong, please tell me.

Best way to get around the unique-constraint for many-to-many relationships in Django?

I have a situation where I need to be able to add multiple copies of the same object to a many-to-many relationship.
Let's say that the problem is recording the types of furniture someone has. Here are my base models:
class Person(models.Model):
name = models.CharField(max_length=100)
class Furniture(models.Model):
furniture_name = models.CharField(max_length=100) #e.g. Chair, Sofa.
Lets say I want to record that Sam owns 3 chairs and 2 sofas. But I don't want to make more than one sofa object in the DB. How can I do this with a many-to-many relationship? The traditional many-to-many has a unique-constraint that prevents this.
I'm thinking of using a through table, with another field in the unique-constraint (date-purchased, or just a random string). Will that work?
Does anyone have a better way of doing this?
Creating a through table is a good approach here, as everything will be DRY and pretty easy to use:
class Ownership:
owner = models.ForeignKey(Person, on_delete=models.CASCADE)
furniture = models.ForeignKey(Furniture, on_delete=models.CASCADE)
items_owned = models.IntegerField(default=0)
class Meta:
unique_together = [
'owner', 'furniture'
]
person = Person.objects.get(name='Bob')
furniture = Furniture.objects.get(furniture_name='Sofa')
# update the number of items owned
Ownership.objects.update_or_create(
owner=person, furniture=furniture, defaults=dict(items_owned=3))
# get the number of items owned
person.ownership_set.get(furniture=furniture).items_owned
# or for example
Ownership.objects.get(owner__name='Bob', furniture__name='Sofa').items_owned
You can then abstract away this complex querying and updating logic with custom managers https://docs.djangoproject.com/en/2.0/topics/db/managers/#custom-managers

Formset for MainClass<-ForeignKey<-OneToOneField?

I need to process applications to an amateur sports event. An event has several distances/subclasses, each of them has some restrictions (age, etc).
My models are
class Event(models.Model):
title = models.CharField(max_length=255)
# more fields
class Klass(models.Model):
title = models.CharField(max_length=255)
capacity = models.IntegerField()
event = models.ForeignKey('Event', related_name="klasses")
# more fields
class TeamRestrictions(models.Model):
age_min = models.IntegerField()
age_max = models.IntegerField()
klass = models.OneToOneField(TeamRestrictions, related_name='restrict')
# more fields
I want to have a single page where a user creates a new event: names it, adds several subclasses into it and restrictions for every subclass. Well, without this one-to-one relationship, for just Event with several Klasses, I could use FormSet.
Of course, I could move all TeamRestrictions fields to Klass, but that looks ugly for me.
What should I use for this more complex structure?
You should create for each model a form and do it separately or you can create really sofisticated form which will do it for you.
This form then would have fields as title (Event), title (Klass), capacity, event, age_min ... so for the relation fields as ForeignKey you will have to use the ChoiceField which will be populated with choices in the __init__ function and so on. Then it should have good cleaning function so that it would have olny valid data and at the end the save. You will have to look if user has selected a field or is creating a new one (such as Event for Klass) and then process them and link and create everything. But that's not the best solution (even it could be in one step) but it is a choice. It could look great even if you added some javascript.

Recursive delete foreign keys for Django object

Suppose I have an object called Person that has a foreign key that links to CLothes which links to
class Person(models.Model):
clothes = models.ForeignKey('Clothes', on_delete=models.PROTECT)
jokes = models.ManyToManyField(to='Jokes')
class Clothes(models.Model):
fabric = models.ForeignKey('Material', on_delete=models.PROTECT)
class Material(models.Model):
plant = models.ForeignKey('Plant', on_delete=models.PROTECT)
And if I wanted to delete person, I would have to delete Clothes, Jokes, Materials attached to it. Is there a way to recursively detect all the foreign keys so that I can delete them?
The django.db.models.deletion.Collector is suited for this task. It is what Django uses under the hood to cascade deletions.
You can use it this way:
from django.db.models.deletion import Collector
collector = Collector(using='default') # You may specify another database
collector.collect([some_instance])
for model, instance in collector.instances_with_model():
# Our instance has already been deleted, trying again would result in an error
if instance == some_instance:
continue
instance.delete()
For more information about the Collector class, you can refer to this question:
How to show related items using DeleteView in Django?
As mentioned in the comments, using on_delete=models.CASCADE would be the best solution but if you do not have control over that, this should work.

Categories

Resources