61

I understand that, with a singleton situation, you can perform such an operation as:

spam == eggs

and if spam and eggs are instances of the same class with all the same attribute values, it will return True. In a Django model, this is natural because two separate instances of a model won't ever be the same unless they have the same .pk value.

The problem with this is that if a reference to an instance has attributes that have been updated by middleware somewhere along the way and it hasn't been saved, and you're trying to it to another variable holding a reference to an instance of the same model, it will return False of course because they have different values for some of the attributes. Obviously, I don't need something like a singleton, but I'm wondering if there some official Djangonic (ha, a new word) method for checking this, or if I should simply check that the .pk value is the same, by running:

spam.pk == eggs.pk

I'm sorry if this was a huge waste of time, but it just seems like there might be a method for doing this and something I'm missing that I'll regret down the road if I don't find it.

UPDATE (02-27-2015)

You should disregard the first part of this question since you shouldn't compare singletons with ==, but rather with is. Singletons really have nothing to do with this question.

Jeffrey Benjamin Brown
  • 3,427
  • 2
  • 28
  • 40
orokusaki
  • 55,146
  • 59
  • 179
  • 257

8 Answers8

103

From Django documentation:

To compare two model instances, just use the standard Python comparison operator, the double equals sign: ==. Behind the scenes, that compares the primary key values of two models.

phoenix
  • 7,988
  • 6
  • 39
  • 45
Gunnar
  • 1,062
  • 1
  • 8
  • 2
  • 10
    What if you query for the same model twice and then update one but not the other? Comparing primary keys would result in equality while that shouldn't be the case. – KeatsKelleher Sep 03 '15 at 12:52
  • 2
    I'd like to hear a response to this - my instinct is to say that the primary key should never, ever be updated under any circumstances, so even if one of the instances changes the primary key will not, and the `==` check will work as expected. – Gershom Maes Oct 19 '15 at 21:16
  • Updating the primary key (to None) and then saving is a common way of copying a model instance in Django. But @KeatsKelleher is right that you can get "incorrect" equality by querying the same instance twice and then changing a non-pk value on one of them. If that's a concern, you should implement a deep comparison. I've never personally had it come up. – Jonathan Richards Jan 18 '20 at 10:24
  • 2
    This answer is simply wrong. Equality by comparing PKs is too naive. The simplest and more correct way to compare is by exporting/serializing both instances and comparing results. i.e. obj1.to_dict() == obj2.to_dict() – Eugene K Jul 13 '21 at 09:31
21

spam.pk == eggs.pk is a good way to do that.

You may add __eq__ to your model but I will avoid that, because it is confusing as == can mean different things in different contexts, e.g. I may want == to mean content is same, id may differ, so again best way is

spam.pk == eggs.pk

Edit: btw in django 1.0.2 Model class has defined __eq__ as

def __eq__(self, other):
    return isinstance(other, self.__class__) and self._get_pk_val() == other._get_pk_val() 

which seems to be same as spam.pk == eggs.pk as pk is property which uses _get_pk_val so I don't see why spam == eggs is not working ?

Anurag Uniyal
  • 85,954
  • 40
  • 175
  • 219
  • It seems like that functionality should safely be built-in to the already overloaded `__eq__` method on `Model`, but currently if you use `a == b` and both objects don't have a `pk`, it returns `True` which is odd. Thanks, I'll stick to that method. – orokusaki Mar 25 '10 at 06:29
  • 4
    Interesting. The problem is that if neither instance has a primary key, it will return true always. So if you have 5 model instances that you want to test against each other and none have been saved, they could all be different, but they will test `__eq__` as `True`. – orokusaki Mar 26 '10 at 16:29
  • This has been fixed. There's a check if `pk is None` https://code.djangoproject.com/ticket/18250 and https://github.com/django/django/blob/6af05e7a0f0e4604d6a67899acaa99d73ec0dfaa/django/db/models/base.py#L467 – user Jul 03 '14 at 21:18
21

The source code for model instance equality is this (from Django 4.0.5):

def __eq__(self, other):
    if not isinstance(other, Model):
        return NotImplemented
    if self._meta.concrete_model != other._meta.concrete_model:
        return False
    my_pk = self.pk
    if my_pk is None:
        return self is other
    return my_pk == other.pk

That is, two model instances are equal if they come from the same database table and have the same primary key. If either primary key is None they're only equal if they're the same object.

(So getting back to the OP's question, simply comparing the instances would be fine.)

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
  • Thanks for this. I found out that if you compare two related objects (obj.related_obj == obj2.related_obj, it will always do a database SELECT query). – gabn88 Oct 27 '21 at 09:58
  • One little detail to add... Django has "Model inheritance" where the top level parent uses a single table. It's possible for `obj1 == obj2` to be `False` even if they "come from the same database table and have the same primary key" if obj1 and obj2 are different model classes (ex. one is a sub-class of the other). It does work if one is a proxy-model of the other, though (that's why it compares `concrete_model`). – Tim Tisdall Jun 03 '22 at 14:30
  • @TimTisdall: Are you talking about inheritance using [abstract base classes](https://docs.djangoproject.com/en/dev/topics/db/models/#abstract-base-classes)? It's not the case that the child and parent use a single table; rather the parent doesn't have a table at all. – Kevin Christopher Henry Jun 03 '22 at 15:30
  • @KevinChristopherHenry - No. It's hard to explain in a comment, but you can have a `Car(models.Model)` and then have a `Suv(Car)` model and all the `Car` attributes are in a single table (whether you pull the info as a `Car` or `Suv`). The docs explain [multi-table inheritance](https://docs.djangoproject.com/en/4.0/topics/db/models/#multi-table-inheritance) better. – Tim Tisdall Jun 03 '22 at 17:50
  • @TimTisdall: The docs you linked to say the following: "Each model corresponds to its *own database table*." What you're describing is [Single Table Inheritance](https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html), an approach that some frameworks, like Ruby On Rails, use. But that's not how inheritance works in Django. – Kevin Christopher Henry Jun 03 '22 at 21:55
  • @KevinChristopherHenry - Yes, that part of the docs is confusingly worded. I actually tested it and all the attributes in the parent class are in a single table (for both classes) and the child attributes are in their own table with a `OneToOneField` automagically linking them. So, `Restaurant(id=1)` and `Place(id=1)` are not equal but are using a single shared table with a single primary key column. The `Restaurant` one will additionally pull other attributes from a 2nd table using a foreign key into the `Place` table. – Tim Tisdall Jun 06 '22 at 12:42
  • @TimTisdall: *So, `Restaurant(id=1)` and `Place(id=1)` are not equal but are using a single shared table with a single primary key column.* No. They are represented by two different tables, each with its own primary key column. If you query `Restaurant` for `pk=1` it will search the restaurant table, not the place table. Take a step back here: the *reason* the two instances don't evaluate as equal is that they have different `concrete_models`, which *means* they come from different database tables. – Kevin Christopher Henry Jun 06 '22 at 17:31
  • @KevinChristopherHenry - You can empirically test it yourself (as I did). If you create `Restaurant(id=1)` it will create 2 rows in 2 tables. One in the "place" table with the source primary key and one in the "restaurant" table with the additional attributes with a `OneToOneField` pointing to the primary key in the "place" table. Then you can pull `Place(id=1)` from the "place" table. Both models share a **single** table with a single primary key sequence. – Tim Tisdall Jun 07 '22 at 12:39
  • @TimTisdall: I understand the database structure, I just don't agree that it's useful or accurate to say that the models are "sharing a single table". That is not standard database terminology, and is not how the Django documentation describes the situation (which is why you find it "confusingly worded"; I find it quite clear). The `Restaurant` table does have a primary key, it's the foreign key to `Place`. That is the [standard Django idiom](https://docs.djangoproject.com/en/dev/ref/models/fields/#onetoonefield) for `OneToOneFields`, it's not something unique to multi-table inheritance. – Kevin Christopher Henry Jun 09 '22 at 01:18
  • @TimTisdall: In any case, if you want to think of this as "sharing the same table", that's fine, we will just have to agree to disagree. – Kevin Christopher Henry Jun 09 '22 at 01:19
  • @KevinChristopherHenry - It's not a case of "want to think", it literally uses a single table for both models. The underlying database uses a single sequence to generate primary keys for both objects and it's only on the shared `Place` table. You can not insert anything in the `Restaurant` table without first inserting in the `Place` table as you need to generate a `Place` primary key to point to in the `OneToOneField`. Any way, this isn't important except that your statement wasn't strictly true, so I left a "one little detail to add" comment to clarify how so. – Tim Tisdall Jun 10 '22 at 13:18
7

You can define the Class' __eq__ method to chage that behaviour:

http://docs.python.org/reference/datamodel.html

piotr
  • 5,657
  • 1
  • 35
  • 60
4

Just for the record, comparing:

    spam == eggs

is dangerous if there is any chance that either of them could be a deferred model instance created by Model.objects.raw() query or by .defer() applied to a 'normal' QuerySet.

I put more details here: Django QuerySet .defer() problem - bug or feature?

Community
  • 1
  • 1
Tomasz Zieliński
  • 16,136
  • 7
  • 59
  • 83
2

from https://djangosnippets.org/snippets/2281/

i changed to compare two instance and return Boolean value

def is_same(self, obj):
    excluded_keys = 'timestamp', 'creator_id' # creater_id is one foreign key ins table
    return _is_same(self, obj, excluded_keys)


def _is_same(obj1, obj2, excluded_keys):
    d1, d2 = obj1.__dict__, obj2.__dict__
    for k, v in d1.items():
         # print('check key: ' + k)
        if k in excluded_keys or k in ['_state', '_django_cleanup_original_cache']: # _state make difference so automatically exclude it
            # print(k + ' is in excluded keys')
            continue

        if v != d2[k]:
            # print('value in not equal in second object')
            return False
        else:
            # print('it is same')
            continue

    # print('all keys checked, so both object is same')
    return True
Kosar
  • 170
  • 2
  • 13
1

As orokusaki comments, "if neither instance has a primary key, it will return true always". If you want this to work, you could extend your model like so:

def __eq__(self, other):
    eq = isinstance(other, self.__class__) and self._get_pk_val() == other._get_pk_val()

    if eq and self._get_pk_val() is None:
        return id(self) == id(other)
    return eq
Aidan Fitzpatrick
  • 1,950
  • 1
  • 21
  • 26
0

It would be strange if two model instances compared as equal if they had different attributes. Most of the time that would be undesirable.

What you want is a special case. Comparing spam.pk == eggs.pk is a good idea. If there's no pk yet, because they haven't been saved, then it's harder to define which instances are "really" the same if some attributes are different.

How about adding a custom attribute to your instances when creating them, eg: spam.myid=1, eggs.myid=2

That way at some point in your code when spamcopy1.seasoning=ketchup and spamcopy2.seasoning=blackpepper you can compare their myid attribute to see if they're really the "same" spam.

Anentropic
  • 32,188
  • 12
  • 99
  • 147