Django: overriding insert, update, delete in a model - python

Sometimes you want to do something when there is an event for model (on create object, on update object, on delete object).
There is a method you can override in Model called save. And it even has a parameter forced_insert which I first thought would always be set to a proper value meaning whether an object will be created or updated. But the parameter is optional and you cannot expect it to be right.
Searching in source code of Model led me to methods _do_update and _do_insert, but the underscore at the beginning tells me that this method is not recommended for use. And it also have a lot of parameters which pollute code when you override it.
The only solution left that I could find is using django.db.models.signals. But I believe they are meant to be used for external purposes like when you want to create a UserProfile on every User create. But I have some internal purposes like updating fields on update. Also using signals makes code look spreaded and harder to understand.
What would be the right way deal with implementing functional on these Model events?

Look at this simplified condition from django base Model _save_table method - decision to update or insert depends on model pk and force_insert value:
def _save_table(self, raw=False, cls=None, force_insert=False,
force_update=False, using=None, update_fields=None):
updated = False
# ...
if pk_set and not force_insert:
updated = self._do_update()
if not updated:
result = self._do_insert()
# ...
return updated
And you can go that same way in your overrided save method if you want entirely custom update/insert operations:
def save(self, force_insert=False, **kwargs):
updated = False
if self.pk and not force_insert:
updated = self.custom_update()
if not updated:
self.custom_insert()
return updated

Related

How to insert an item into a Queryset which is sorted by a numeric field and increment the value of the field of all subsequent items

Let´s say, there is a Django model called TaskModel which has a field priority and we want to insert a new element and increment the existing element which has already the priority and increment also the priority of the following elements.
priority is just a numeric field without any special flags like unique or primary/foreign key
queryset = models.TaskModel.objects.filter().order_by('priority')
Can this be done in a smart way with some methods on the Queryset itself?
I believe you can do this by using Django's F expressions and overriding the model's save method. I guess you could instead override the model's __init__ method as in this answer, but I think using the save method is best.
class TaskModel(models.Model):
task = models.CharField(max_length=20)
priority = models.IntegerField()
# Override the save method so whenever a new TaskModel object is
# added, this will be run.
def save(self, *args, **kwargs):
# First get all TaskModels with priority greater than, or
# equal to the priority of the new task you are adding
queryset = TaskModel.objects.filter(priority__gte=self.priority)
# Use update with the F expression to increase the priorities
# of all the tasks above the one you're adding
queryset.update(priority=F('priority') + 1)
# Finally, call the super method to call the model's
# actual save() method
super(TaskModel, self).save(*args, **kwargs)
def __str__(self):
return self.task
Keep in mind that this can create gaps in the priorities. For example, what if you create a task with priority 5, then delete it, then add another task with priority 5? I think the only way to handle that would be to loop through the queryset, perhaps with a function like the one below, in your view, and call it whenever a new task is created, or it's priority modified:
# tasks would be the queryset of all tasks, i.e, TaskModels.objects.all()
def reorder_tasks(tasks):
for i, task in enumerate(tasks):
task.priority = i + 1
task.save()
This method is not nearly as efficient, but it will not create the gaps. For this method, you would not change the TaskModel at all.
Or perhaps you can also override the delete method of the TaskModel as well, as shown in this answer, but I haven't had a chance to test this yet.
EDIT
Short Version
I don't know how to delete objects using a similar method to saving while keeping preventing priorities from having gaps. I would just use a loop as I have shown above.
Long version
I knew there was something different about deleting objects like this:
def delete(self, *args, **kwargs):
queryset = TaskModel.objects.filter(priority__gt=self.priority)
queryset.update(priority=F('priority') - 1)
super(TaskModel, self).delete(*args, **kwargs)
This will work, in some situations.
According to the docs on delete():
Keep in mind that this [calling delete()] will, whenever possible, be executed purely in
SQL, and so the delete() methods of individual object instances will
not necessarily be called during the process. If you’ve provided a
custom delete() method on a model class and want to ensure that it is
called, you will need to “manually” delete instances of that model
(e.g., by iterating over a QuerySet and calling delete() on each
object individually) rather than using the bulk delete() method of a
QuerySet.
So if you delete() a TaskModel object using the admin panel, the custom delete written above will never even get called, and while it should work if deleting an instance, for example in your view, since it will try acting directly on the database, it will not show up in the python until you refresh the query:
tasks = TaskModel.objects.order_by('priority')
for t in tasks:
print(t.task, t.priority)
tr = TaskModel.objects.get(task='three')
tr.delete()
# Here I need to call this AGAIN
tasks = TaskModel.objects.order_by('priority')
# BEFORE calling this
for t in tasks:
print(t.task, t.priority)
# to see the effect
If you still want to do it, I again refer to this answer to see how to handle it.

How to add additional keyword argument to all Django fields?

The application I am working on requires merging of identical type Django models. These models hold state that can be altered by chronological events, so it is not as straightforward as deep copying one object to the other, as it is not always correct to take the latest value or always copy truthy values for example.
I have written a model merging class to handle this operation, however, I need to be able to describe on a field by field basis whether it should be included in that merge and if it is to be included, how to handle that merge.
I have already tried creating a dictionary to describe this behaviour and pass it into the merger. However, this becomes unwieldy at greater levels of nesting and is very brittle to codebase change.
I have also tried adding a merge method to each individual model, which solved the problem but is highly susceptible to failure if a foreign key relationship that lives on a different model is missed, or the codebase changes.
I have started writing a custom version of every field in Django, as the fields feel like the correct place for the logic to live, but it also feels unwieldy and brittle to have to maintain custom versions of every field.
Is there a way in Django to add an additional keyword argument to the base Field class or perhaps decorate each field without having to subclass them?
Thanks
Just in case this helps anybody else, I have ended up creating a mixin and subclassing each individual field. Below is a cut down example.
from django.db import models
class MappableFieldMixin():
def __init__(self, should_map=True, map_mode=None, *args, **kwargs):
self.should_map = should_map
if should_map and not map_mode:
raise TypeError('Mappable field requires map_mode if should_map set to True')
self.map_mode = map_mode
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs['should_map'] = self.should_map
kwargs['map_mode'] = self.map_mode
return name, path, args, kwargs
class MappableBooleanField(MappableFieldMixin, models.BooleanField):
pass
Usage:
class Membership(models.Model):
is_active = MappableBooleanField(map_mode=MapMode.MAP_ALWAYS, default=True)
You can find further information on creating custom fields in the Django documentation.

Django - NEVER update a column when saving

I am trying to use citus data (https://www.citusdata.com/) with Django.
Most everything is working so far, except trying to save a model that has already been saved:
NotSupportedError: modifying the partition value of rows is not allowed
This is because django always includes every single field in the update SQL, even if that field has not changed.
In Citus, you must pick a field to be your partitioning field, and then you cannot change it. So, when I'm saving an object, it doesn't like that the partition key is in the update statement, even if it didn't change.
I know that you can pass the update_fields keyword arg to the save method, but I'm wondering if I can somehow tell django to NEVER include a field when updating?
Django does not provide this functionality "out of the box". You could override the save method of your class to set all fields other than your partition field as the value for update_fields
def save(self, **kwargs):
kwargs.setdefault('update_fields', ['field1', 'field2'])
return super(Class, self).save(**kwargs)
A more dynamic option, if you do not want to update this method everytime you change the fields of your class, would be to use the Meta API to get all fields of the class and exclude your partition field
def save(self, **kwargs):
kwargs.setdefault(
'update_fields',
[f.name for f in self.__class__._meta.get_fields() if f.name != 'partition_field']
)
return super(Class, self).save(**kwargs)
There are several other methods by which Django will attempt to update your models. Maybe a base class that all your models inherit from that implement these methods would work

Django ManyToMany access in save() function

I want to create additional objects based on user input during the .save() method of my model. the info is stored in a ManyToMany-relation .available_specs.
My code works, if one presses save twice, but not at the first time. The problem is described in the comments here Django foreign key access in save() function, too.
Here is what I do:
def save(self, *args, **kwargs):
my_save_result = super().save(*args, **kwargs) #save first to create an id
self.refresh_from_db() #just as a test, did not help at all
#the following line is the problem:
# in the first save, available_specs.all() returns []
# when I re-open the model and save again, I get the correct list
for spec in self.available_specs.all():
VehicleSpec.objects.get_or_create(vehicle=self, spec=spec)
return my_save_result
I'm using Python 3.5 and Django 1.9
Edit:
I tried to use the post_save() signal using the following code (and removed the overridden save()-method):
#receiver(post_save, sender=VehicleModel)
def create_dependent_vehicle_specs(sender, **kwargs):
vehicle = kwargs['instance']
#in the first save-process I get an empty list here,
#after I hit save again, the code works.
print(vehicle, vehicle.available_specs.all())
for spec in vehicle.available_specs.all():
VehicleSpec.objects.get_or_create(vehicle=vehicle, spec=spec)
Edit2: m2m_changed did the trick:
#receiver(m2m_changed, sender=VehicleModel)
def create_dependent_vehicle_specs(sender, **kwargs):
vehicle = kwargs['instance']
for spec in vehicle.available_specs.all():
VehicleSpec.objects.get_or_create(vehicle=vehicle, spec=spec)
m2m_changed.connect(create_dependent_vehicle_specs, sender=VehicleModel.available_specs.through) # #UndefinedVariable
BTW: A related question is here: Django accessing ManyToMany fields from post_save signal
You need to use a m2m_changed signal. You should not be calling another models' get_or_create in another models' save method

Is there a better way to create this model? (Django)

I have a model that pings a REST service and saves the results.
class StoreStatus(models.Model):
store = models.OneToOneField(Store)
status = models.TextField()
def save(self, *args, **kwargs):
self.status = get_store_information(self.store.code)
self.pk = self.store.pk
super( StoreStatus, self ).save(*args, **kwargs)
I need to run it every repeatedly and figure I can just .save() it in a view, since the "Store" object is in the majority of my views.
Is there a better way to do this? I had to the set the pk manually because I was getting duplicate errors when I tried to save a second time.
Seems kind of dirty, and I'm trying to improve my coding.
Thanks
That looks quite bad.
First, associating the retrieval of the status information with the saving of the object is quite a bad idea. If 'updating' is the only action you'll ever perform on this model, then maybe a better idea would be to write a an "update()" method that automatically saves once the status is updated, instead of doing it the other way around.
def update(self):
self.status = get_store_information(self.store.code)
self.save()
Second: how are you creating the first instance of this model? You'll get duplication errors if you are trying to save a new instance every time the model gets updated. Say, if you do something like this:
# this will crap out
update = Update(mystore)
update.save()
What you should be doing is something like:
# this will work (provided we have 'update')
mystore.status.update()
Or:
# always retrieve the stored instance before saving
status, created = StoreStatus.objects.get_or_create(store=mystore)
status.update()
In case you are lazy enough, you can always add an "update_status" method to your Store model and perform the creation/update there. It's always better to be explicit about what you are doing. Remember: Django is based upon the principle of least surprise, and so should your code! :)
If I were you, I would have created a function that would:
1. Accept the Store object as a parameter,
2. Make the REST call, and
3. On receiving the response then update the status in the StoreStatus.
This would be desirable to enable loose coupling which is required for architectures involving web-based services.
Moreover, if you just want to avoid duplicate PK errors, you can check for id to safely loop the update and create conditions.
def save(self, *args, **kwargs):
if self.id:
# Update case
pass
else:
# New object
# Process for the new object
pass
# Save the changes
super(StoreStatus, self).save(*args, **kwargs)

Categories