0

我刚刚遇到了一个场景,我不知道如何用我的文档的现有结构来解决。如下所示,我显然可以通过一些重构来解决这个问题,但我很好奇如何以最有效的方式解决这个问题并尊重相同的结构

请注意,这个问题与如何在 MongoEngine 的 ListField 中的 EmbeddedDocument 上进行原子更新不同?

让我们假设以下模型:

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 的扫描字段更改为 ReferenceFields 的 ListField 以扫描文档。然后很容易,因为我只需要先使用“更新”和操作符“add_to_set”和选项“upsert=True”来保存扫描。然后,一旦操作被批准,保存 ScanSettings。我将需要扫描实例的数量来插入 + 1 个查询。

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

选项 C 忽略唯一性。考虑到我的模型的结构,我很确定不会有重复,或者如果有的话,它可以忽略不计。然后在阅读时处理重复项。对于像我这样来自关系数据库的人来说,这个解决方案感觉很麻烦。

我是 Mongo 的新手,所以我很感激任何评论。谢谢。

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

提前非常感谢。

4

1 回答 1

0

我终于选择了选项 A,所以我将我的模型重构为:

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

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

c) 现实情况是 Scans 和 ScanSettings 现在继承自 Mixin 类,因为这两个类都需要分别保证它们的属性“documents”和“scans”的唯一性。因此,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'}
于 2018-10-24T09:55:24.867 回答