[autotest] Sync based on key instead of id for attribute-like models

HostAttribute and JobKeyval are updated on both shard and
master. Both table should be synced based on "key" instead
auto-incremental id.

BUG=chromium:453122
DEPLOY=shard_cient, apache
TEST=python
>>import common
>>from autotest_lib.frontend import setup_django_environment
>>from autotest_lib.frontend.afe import models
>>m = models.Host.objects.get(id=20)
>>s = m.serialize()
>>s['hostattribute_set'] = A LIST OF NEW ATTRIBUTE RECORDS
>>m = models.Host.deserialize(s)
>>m.hostattribute_set.all()[0].__dict__

>>j = models.Job.objects.get(id=35)
>>s = j.serialize()
>>s['jobkeyval_set'] = A LIST OF NEW JOBKEYVAL RECORDS
>>m = models.Job.deserialize(s)
>>m.jobkeyval_set.all()[0].__dict__

TEST=Run a shard, schedule a dummy suite, ensure everything still
works.

Change-Id: I9c6450f93f06510477312bd02b119fd96b70857c
Reviewed-on: https://chromium-review.googlesource.com/245560
Reviewed-by: Fang Deng <fdeng@chromium.org>
Tested-by: Fang Deng <fdeng@chromium.org>
Commit-Queue: Fang Deng <fdeng@chromium.org>
diff --git a/frontend/afe/model_logic.py b/frontend/afe/model_logic.py
index c48fcf8..5d98e09 100644
--- a/frontend/afe/model_logic.py
+++ b/frontend/afe/model_logic.py
@@ -1214,12 +1214,38 @@
         @param link: Name of the relation.
         @param data: Serialized representation of the related objects.
                      This is a list with of dictionaries.
+        @param related_class: A class representing a django model, with which
+                              this class has a one-to-many relationship.
         """
         relation_set = getattr(self, link)
+        if related_class == self.get_attribute_model():
+            # When deserializing a model together with
+            # its attributes, clear all the exising attributes to ensure
+            # db consistency. Note 'update' won't be sufficient, as we also
+            # want to remove any attributes that no longer exist in |data|.
+            #
+            # core_filters is a dictionary of filters, defines how
+            # RelatedMangager would query for the 1-to-many relationship. E.g.
+            # Host.objects.get(
+            #     id=20).hostattribute_set.core_filters = {host_id:20}
+            # We use it to delete objects related to the current object.
+            related_class.objects.filter(**relation_set.core_filters).delete()
         for serialized in data:
             relation_set.add(related_class.deserialize(serialized))
 
 
+    @classmethod
+    def get_attribute_model(cls):
+        """Return the attribute model.
+
+        Subclass with attribute-like model should override this to
+        return the attribute model class. This method will be
+        called by _deserialize_2m_relation to determine whether
+        to clear the one-to-many relations first on deserialization of object.
+        """
+        return None
+
+
 class ModelWithInvalid(ModelExtensions):
     """
     Overrides model methods save() and delete() to support invalidation in