在存储较大的文档时,遇到了一个异常,提示文档大小超过了MongoDB的最大文档大小限制。MongoDB的最大文档大小为16MB,这是一个合理的设计决策。首先,将解释为什么会遇到这个问题,然后介绍是如何解决的。
最初,对这个异常感到惊讶,因为同样的数据集在CSV格式下只有6MB。但是,重新考虑数据后,意识到这个数据集主要是一个稀疏矩阵,因为很多属性都是null。在CSV格式中,每个null属性的成本非常低,只需要一个分号。但是,以面向对象的方式表示,如.NET对象或BSON文档,情况就不同了。对于每个null属性,成本要高得多,因为仍然需要存储属性名和“null”符号。当有几十个属性时(是的,有充分的理由在单个对象中有那么多属性),开销可能会非常大,占据了总大小的大部分。
因此,文档看起来可能像这样:
{
a: "Some data",
b: null,
c: null,
d: "Some other data",
e: null,
f: null,
g: null,
...
z: "Last data"
}
文档的大部分被无用的标记填充,增加了它的大小,却没有提供额外的信息。对于BSON来说,BSON文档比CSV文档大6倍!
幸运的是,MongoDB .NET驱动程序的开发者意识到了这类问题,并在设计驱动程序时考虑了这一点,允许自定义生成BSON文档的方式。至少有两种解决方案:标记应该忽略的属性为null,或者为整个应用程序域注册一个全局策略。
如果想单独标记属性,可以使用BsonIgnoreIfNull属性:
class Data {
[BsonIgnoreIfNull]
public string A { get; set; }
[BsonIgnoreIfNull]
public string B { get; set; }
[BsonIgnoreIfNull]
public string C { get; set; }
}
这样做的好处是它非常明确。但是,如果像一样有几十种属性需要标记,这会增加很多代码。此外,它可能会侵入性地污染业务实体,尽管如果没有更简单的解决方案,会这样做:再次强调,实用主义应该始终优于教条主义,尽管一些教条主义者更喜欢复制代码并添加映射以清楚地隔离业务实体。(是一个正在康复的教条主义者。)
对于当前的问题,选择了另一种方式,通过注册一个全局策略:
ConventionPack pack = new ConventionPack();
pack.Add(new IgnoreIfNullConvention(true));
ConventionRegistry.Register("Ignore null properties of data", pack, type => type == typeof(Data));
最后一个谓词确保策略只适用于“Data”类。将这段代码放在MongoDB数据库的入口类型的静态构造函数中。所以,如果不需要MongoDB,这个类型就不会被CLR加载,这段代码也不会被执行。也可以将这段代码放在应用程序的Main中,但如果有多个应用程序使用MongoDB层,可能需要复制代码,所以更喜欢静态构造函数或任何其他“Init”方法。