[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