当且仅当最内层列表的唯一性得到尊重时,才能有效地更新嵌入文档列表中的文档的ListField

时间:2018-10-23 15:36:05

标签: python mongoengine listfield embedded-documents

我刚遇到一个我不知道如何解决文档现有结构的情况。如下所示,我显然可以通过一些重构来解决此问题,但我很好奇如何尽可能有效地解决此问题并尊重相同的结构

请注意,该问题与How to Do An Atomic Update on an EmbeddedDocument in a ListField in MongoEngine?

不同

让我们假设以下模型:

class Scans(mongoengine.EmbeddedDocument):
    peer = mongoengine.ReferenceField(Peers, required=True)
    site = mongoengine.ReferenceField(Sites, required=True)
    process_name = mongoengine.StringField(default=None)
    documents = mongoengine.ListField(mongoengine.ReferenceField('Documents'))
    is_complete = mongoengine.BooleanField(default=False)  
    to_start_at = mongoengine.DateTimeField()  
    started = mongoengine.DateTimeField()  
    finished = mongoengine.DateTimeField()


class ScanSettings(mongoengine.Document):
    site = mongoengine.ReferenceField(Sites, required=True)
    max_links = mongoengine.IntField(default=100)  
    max_size = mongoengine.IntField(default=1024)  
    mime_types = mongoengine.ListField(default=['text/html'])
    is_active = mongoengine.BooleanField(default=True)  
    created = mongoengine.DateTimeField(default=datetime.datetime.now)
    repeat = mongoengine.StringField(choices=REPEAT_PATTERN)
    scans = mongoengine.EmbeddedDocumentListField(Scans)

我想做的是,当且仅当扫描字段的所有元素(“扫描”嵌入式文档的列表)的文档列表又具有唯一性时,才插入ScanSettings对象?唯一的意思是列表中数据库级别的所有元素,而不是整个列表,这很容易。

用简单的英语来说,如果在插入ScanSetting时,扫描列表的任何元素都具有一个扫描实例,而该扫描实例具有重复的文档列表,则不应进行这种插入。我指的是数据库级别的唯一性,考虑到现有记录(如果有)。

鉴于Mongo在同一文档中不支持列表中所有元素的唯一性,我发现了两种解决方案:

选项A

我重构了“模式”,并使Scans集合继承自Document而不是Embedded文档,并将ScanSettings的scans字段更改为ReferenceFields的ListField来扫描文档。这样就很容易了,因为我只需要首先使用带有操作符“ add_to_set”和选项“ upsert = True”的“更新”来保存扫描。然后,一旦操作获得批准,请保存ScanSettings。我将需要插入1个查询的扫描实例数。

选项B 我保持相同的“模式”,但是以某种方式为Scans嵌入式文档生成了唯一的ID。然后,在使用非空扫描字段插入“扫描设置”之前,我将获取已经存在的记录,以查看刚检索的记录和要插入的记录之间是否存在重复的文档的ObjectId。 换句话说,我将通过Python而不是MogoneEngine / Mongodb来检查唯一性。我将需要插入2 x个扫描实例(读取并使用add_set_operator更新)+ 1个ScanSettings保存

选项C 忽略唯一性。鉴于我的模型将如何构造,我非常确定不会有重复项,或者如果有重复项,则可以忽略不计。然后在阅读时处理重复项。对于像我这样的来自关系数据库的人来说,这种解决方案很麻烦。

我是Mongo的新手,因此,我感谢您提出任何意见。谢谢。

PS:我正在使用最新的MongoEngine和免费的Mongodb。

非常感谢。

1 个答案:

答案 0 :(得分:0)

我最终选择了选项A,因此我将模型重构为:

a)创建一个继承自Document类的Mixin类,以添加两种方法:覆盖“保存”,以便仅在唯一文档列表为空时允许保存;以及“ save_with_uniqueness”,允许在保存唯一文档时进行保存和/或更新。文档列表为空。这个想法是要强制唯一性。

b)重构扫描和ScanSettings,以便前者将“ scans”字段重新定义为对Scans的引用的ListField,后者则重新定义为继承自Document而不是Embedded Document。

c)现实是Scans和ScanSettings现在是从Mixin类继承的,因为这两个类都需要分别保证其属性“文档”和“扫描”的唯一性。因此就是Mixin类。

使用a)和b),我可以保证唯一性,并首先为它保存每个扫描实例,以后再以通常的方式添加到ScanSettings.scans中。

针对像我这样的新手的几点建议:

  1. 看到我正在使用继承。为了使其正常工作,您还需要在元字典中添加一个属性以允许继承,如下面的模型所示。
  2. 就我而言,我想将Scans和ScanSettings放在不同的集合中,所以我也必须使它们“抽象”,如Mixin类的元字典中所示。
  3. 对于save_with_uniqueness,我使用upsert = True以便可以创建一条不存在的记录。想法是使用“ save_with_uniqueness”与“保存,创建或更新文档(如果存在或不存在)”相同。
  4. 我还使用了'full_result'标志,因为我需要返回插入的最新记录的ObjectId。
  5. Document._fields是一个字典,其中包含组成该文档的字段。实际上,我实际上想创建一个通用的save_with_uniqueness方法,所以我不想手动输入Document的字段,也不必重复不必要的代码-因此就是Mixin。

最后是代码。它尚未经过全面测试,但足以使主要思想适合我的需求。

class UniquenessMixin(mongoengine.Document):


def save(self, *args, **kwargs):
    try:
        many_unique = kwargs['many_unique']
    except KeyError:
        pass
    else:
        attribute = getattr(self, many_unique)
        self_name = self.__class__.__name__
        if len(attribute):
            raise errors.DbModelOperationError(f"It looks like you are trying to save a {self.__class__.__name__} "
                                               f"object with a non-empty list of {many_unique}. "
                                               f"Please use '{self_name.lower()}.save_with_uniqueness()' instead")
    return super().save(*args, **kwargs)

def save_with_uniqueness(self, many_unique):
    attribute = getattr(self, many_unique)
    self_name = self.__class__.__name__
    if not len(attribute):
        raise errors.DbModelOperationError(f"It looks like you are trying to save a {self_name} object with an "
                                           f"empty list {many_unique}. Please use '{self_name.lower()}.save()' "
                                           f"instead")

    updates, removals = self._delta()
    if not updates:
        raise errors.DbModelOperationError(f"It looks like you are trying to update '{self.__class__.__name__}' "
                                           f"but no fields were modified since this object was created")

    kwargs = {(key if key != many_unique else 'add_to_set__' + key): value for key, value in updates.items()}
    pk = bson.ObjectId() if not self.id else self.id
    result = self.__class__.objects(id=pk).update(upsert=True, full_result=True, **kwargs)

    try:
        self.id = result['upserted']
    except KeyError:
        pass
    finally:
        return self.id

meta = {'allow_inheritance': True, 'abstract': True}

class Scans(UniquenessMixin):

    peer = mongoengine.ReferenceField(Peers, required=True)
    site = mongoengine.ReferenceField(Sites, required=True)
    process_name = mongoengine.StringField(default=None)
    documents = mongoengine.ListField(mongoengine.ReferenceField('Documents'))
    is_complete = mongoengine.BooleanField(default=False)  
    to_start_at = mongoengine.DateTimeField()  
    started = mongoengine.DateTimeField()  
    finished = mongoengine.DateTimeField()

    meta = {'collection': 'Scans'}


class ScanSettings(UniquenessMixin):

       site = mongoengine.ReferenceField(Sites, required=True)
    max_links = mongoengine.IntField(default=100)  
    max_size = mongoengine.IntField(default=1024)  
    mime_types = mongoengine.ListField(default=['text/html'])
    is_active = mongoengine.BooleanField(default=True)  
    created = mongoengine.DateTimeField(default=datetime.datetime.now)
    repeat = mongoengine.StringField(choices=REPEAT_PATTERN)
    scans = mongoengine.ListField(mongoengine.ReferenceField(Scans))

    meta = {'collection': 'ScanSettings'}