paridin
paridin

Reputation: 439

django - compare two objects [using fields dynamically]

I need compare two objects, to determine if a field has changed or not

class Country(models.Model):  # country code 'MX' -> Mexico
    code = models.CharField(max_length=2)
    name = models.CharField(max_length=15)


class Client(models.Model): # id=1, name=pedro, country.code=MX, rfc=12345
    name = models.CharField(max_length=100)
    country = models.ForeignKey(Country)
    rfc = models.CharField(max_length=13)



> obj_db = Client.object.get(id=1)

> country = Country.objects.get(code='MX')
obj_no_db = Client(**{'id':1, 'name':'pedro', 'country': country, 'rfc':12345})

> obj_db == obj_no_db # True

> obj_no_db = Client(**{'id':1, 'name':'pedro', 'country': country, 'rfc':1})

> obj_db == obj_no_db # True # but isn't True because the rfc has change, how can compare field by field

> obj_db.rfc == obj_no_db.rfc # False I expected this result

I need to build a function to do it generic, the problem i don't found information about it, i think i can use the ._meta options, but i'm not sure. I developed this function but i can't discover the way to compare field by field.

def get_insert_update(obj, key, obj_list, fields=None, exclude_fields=None):
    """

    :param obj: The object for compare
    :param key: a the key for compare to determine if we need to update or insert
    :param obj_list: list objects to compare
    :return: to_insert, _update
    """
    db = {}
    to_insert = []
    to_update = []
    if key == 'pk':  # the field pk doesn't exists so we change to id, because its the same
        key = 'id'
    exclude_fields = exclude_fields or []
    fields = fields or []
    if 'pk' in fields:
        fields[fields.index('pk')] = 'id'  # we change the field pk, because it doesn't exists
    if 'pk' in exclude_fields:
        exclude_fields[exclude_fields.index('pk')] = 'id'  # we change the field pk, because it doesn't exists

    meta = obj._meta  # we define meta object

    if fields is None:
        fields = meta.get_all_field_names()

    fields = [f for f in meta.fields if f.attname in fields]

    # dumping db into memory
    for _obj in obj.objects.all():
        if isinstance(key, list):  # first check if is a list to create a custom key
            _key = _get_key(_obj, key)
        else:
            _key = _obj.__dict__[key]
        # if exclude fields exists
        if exclude_fields:
            d = {f.attname: _obj.__dict__[f.attname] for f in fields if f.attname not in exclude_fields}
            db[_key] = obj(**d)
        else:  # we save the full object
            db[_key] = _obj
    # read local objects to determine if the record will be insert or update
    for _obj in obj_list:
        if isinstance(key, list):  # first check if is a list to create a custom key
            _key = _get_key(_obj, key)
        else:
            _key = _obj.__dict__[key]
        if _key in db:  # if the key is in db so we check if it equal
            # if _obj.pk == 6: # debug
            #    print(_obj.__dict__, db[_key].__dict__, _obj.__dict__ == db[_key].__dict__)
            if _obj != db[_key]: # HERE i need the determine if the fields are equal or not.
                to_update.append(_obj)  # if the object has changed, we update it
            else:
                pass  # if the object is equal, we didn't do it anything
        else:
            to_insert.append(_obj)  # because we didn't found into the database, we create it
    return to_insert, to_update

def _get_key(obj, lst):
    """
    create a string key using multiples keys 
    Example: obj.id -> 1, obj.name -> 'foo' 
    lst['id', 'name']
    :param lst: list of keys
    :return: 1_foo
    """
    k = []
    for t in lst:
        k.append(str(obj.__dict__[t]))
    return "_".split(k)

Upvotes: 5

Views: 4731

Answers (1)

Daniel Roseman
Daniel Roseman

Reputation: 599460

Django's Model class defines the __eq__ method to compare based on the value of the pk attribute, which is why your models compare equal.

One simple way to do this would be to override that method on your own model to compare the value of the __dict__, which contains all the instance's values.

There's a slight gotcha with this, in that __dict__ also contains a hidden _state object that will be compared by ID, so you'd need to filter that out of the comparison.

def __eq__(self, other):
     values = [(k,v) for k,v in self.__dict__.items() if k != '_state']
     other_values = [(k,v) for k,v in other.__dict__.items() if k != '_state']
     return values == other_values

Upvotes: 11

Related Questions