29

Maybe I misunderstand the purpose of Django's update_or_create Model method.

Here is my Model:

from django.db import models
import datetime
from vc.models import Cluster

class Vmt(models.Model):
    added = models.DateField(default=datetime.date.today, blank=True, null=True)
    creation_time = models.TextField(blank=True, null=True)
    current_pm_active = models.TextField(blank=True, null=True)     
    current_pm_total = models.TextField(blank=True, null=True)
    ... more simple fields ...
    cluster = models.ForeignKey(Cluster, null=True)


    class Meta:
        unique_together = (("cluster", "added"),)

Here is my test:

from django.test import TestCase
from .models import *
from vc.models import Cluster
from django.db import transaction


# Create your tests here.
class VmtModelTests(TestCase):
    def test_insert_into_VmtModel(self):
        count = Vmt.objects.count()
        self.assertEqual(count, 0)

        # create a Cluster
        c = Cluster.objects.create(name='test-cluster')
        Vmt.objects.create(
            cluster=c,
            creation_time='test creaetion time',
            current_pm_active=5,
            current_pm_total=5,
            ... more simple fields ...
        )
        count = Vmt.objects.count()
        self.assertEqual(count, 1)
        self.assertEqual('5', c.vmt_set.all()[0].current_pm_active)

        # let's test that we cannot add that same record again
        try:
            with transaction.atomic():

                Vmt.objects.create(
                    cluster=c,
                    creation_time='test creaetion time',
                    current_pm_active=5,
                    current_pm_total=5,
                    ... more simple fields ...
                )
                self.fail(msg="Should violated integrity constraint!")
        except Exception as ex:
            template = "An exception of type {0} occurred. Arguments:\n{1!r}"
            message = template.format(type(ex).__name__, ex.args)
            self.assertEqual("An exception of type IntegrityError occurred.", message[:45])

        Vmt.objects.update_or_create(
            cluster=c,
            creation_time='test creaetion time',
            # notice we are updating current_pm_active to 6
            current_pm_active=6,
            current_pm_total=5,
            ... more simple fields ...
        )
        count = Vmt.objects.count()
        self.assertEqual(count, 1)

On the last update_or_create call I get this error:

IntegrityError: duplicate key value violates unique constraint "vmt_vmt_cluster_id_added_c2052322_uniq"
DETAIL:  Key (cluster_id, added)=(1, 2018-06-18) already exists.

Why didn't wasn't the model updated? Why did Django try to create a new record that violated the unique constraint?

2
  • 1
    Well update_or_create contains the filter criteria, and in defaults={..} you specify the fields you want to update. Commented Jun 18, 2018 at 19:02
  • So for each field that I need to update I need to specify that field in the defaults. Commented Jun 18, 2018 at 19:33

3 Answers 3

70

The update_or_create(defaults=None, **kwargs) has basically two parts:

  1. the **kwargs which specify the "filter" criteria to determine if such object is already present; and
  2. the defaults which is a dictionary that contains the fields mapped to values that should be used when we create a new row (in case the filtering fails to find a row), or which values should be updated (in case we find such row).

The problem here is that you make your filters too restrictive: you add several filters, and as a result the database does not find such row. So what happens? The database then aims to create the row with these filter values (and since defaults is missing, no extra values are added). But then it turns out that we create a row, and that the combination of the cluster and added already exists. Hence the database refuses to add this row.

So this line:

Model.objects.update_or_create(field1=val1,
                               field2=val2,
                               defaults={
                                   'field3': val3,
                                   'field4': val4
                               })

Is to semantically approximately equal to:

try:
    item = Model.objects.get(field1=val1, field2=val2)
except Model.DoesNotExist:
    Model.objects.create(field1=val1, field2=val2, field3=val3, field4=val4)
else:
    item = Model.objects.filter(
        field1=val1,
        field2=val2,
    ).update(
        field3 = val3
        field4 = val4
    )

(but the original call is typically done in a single query).

You probably thus should write:

Vmt.objects.update_or_create(
    cluster=c,
    creation_time='test creaetion time',
    defaults = {        
        'current_pm_active': 6,
        'current_pm_total': 5,
    }
)

(or something similar)

Sign up to request clarification or add additional context in comments.

9 Comments

Thank you for clear explanation. I am trying to reproduce in my unit test what will be happening in production. The data for the Vmt model comes from a CSV file I access via a URL. If there are new lines in this cvs file I want to create new Vmt record but if a line has changed I want to update the Vmt record for the day the cvs file was read.
@RedCricket: well, probably creation_time is a the "culprit" here. Personally I think it is a bit problematic to make these unique together, since typically time is something that constantly increases. So that means sometimes you will create a duplicate, and sometimes you won't. Which is rather "unstable".
create_time is just a string and not part of any constraint on the database table.
@RedCricket: ah ok, well then probably this should work. So in case there exists already a Vmt with the given cluster and creation_time, we update that row, otherwise we create one.
The unique_together in the model specifies cluster and added. I hope that will ensure I will only have one set of Vmt data for given cluster on a given day.
|
7

You should separate your field:

  1. Fields that should be searched for
  2. Fields that should be updated

for example: If I have the model:

class User(models.Model):
    username = models.CharField(max_length=200)
    nickname = models.CharField(max_length=200)

And I want to search for username = 'Nikolas' and update this instance nickname to 'Nik'(if no User with username 'Nikolas' I need to create it) I should write this code:

User.objects.update_or_create(
    username='Nikolas', 
    defaults={'nickname': 'Nik'},
)

see in https://docs.djangoproject.com/en/3.1/ref/models/querysets/

1 Comment

Shouldn't the defaults in your example be a dictionary?
3

This is already answered well in the above.

To be more clear the update_or_create() method should have **kwargs as those parameters on which you want to check if that data already exists in DB by filtering.

select some_column from table_name where column1='' and column2='';

Filtering by **kwargs will give you objects. Now if you wish to update any data/column of those filtered objects, you should pass them in defaults param in update_or_create() method.

so lets say you found an object based on a filter now the default param values are expected to be picked and updated.

and if there's no matching object found based on the filter then it goes ahead and creates an entry with filters and the default param passed.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.