Data Model(数据模型)——数据模型示例和模式(三)——为特定的应用程序上下文建模
-
1) 原子性操作的数据模型
在MongoDB中,写操作,例如 db.collection.update(),db.collection.findAndModify(),db.collection.remove(),在单个文档的级别上是原子性的。 对于必须一起更新的字段,将字段嵌入同一文档中可确保字段可以进行原子性地更新。
例如,假设您需要维护有关图书的信息,包括可借阅的副本数量以及当前的借阅信息。
书籍的可借阅副本数量和借阅信息应同步。 因此,在同一文档中嵌入可借阅副本数量字段和借阅信息字段,可确保您可以原子性地更新这两个字段。{ _id: 123456789, title: "MongoDB: The Definitive Guide", author: [ "Kristina Chodorow", "Mike Dirolf" ], published_date: ISODate("2010-09-24"), pages: 216, language: "English", publisher_id: "oreilly", available: 3, checkout: [ { by: "joe", date: ISODate("2012-10-15") } ] }
要使用新的签出信息进行更新,可以使用db.collection.update()方法来原子性地更新可借阅副本数量字段和借阅信息字段:
db.books.update ( { _id: 123456789, available: { $gt: 0 } }, { $inc: { available: -1 }, $push: { checkout: { by: "abc", date: new Date() } } } )
操作返回WriteResult()对象,其中包含有关操作状态的信息:
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
nMatched字段显示1个文档与更新条件匹配,并且nModified显示操作更新了1个文档。
如果没有文档与更新条件匹配,则nMatched和nModified将为0,并表示您无法借出该书。2)支持关键字搜索的数据模型
注意
关键字搜索与文本搜索或全文搜索不同,并且不提供词干或其他文本处理功能。 有关详细信息,请参阅关键字索引的限制部分。
在2.4版中,MongoDB提供了一个文本搜索功能。 有关详细信息,请参阅文本索引。
如果应用程序需要对包含文本的字段的内容执行查询,则可以对文本执行完全匹配,或使用$regex使用正则表达式模式匹配。 然而,对于许多对文本的操作,这些方法并不能满足应用程序的要求。
此模式描述了一种使用MongoDB来提供关键字搜索的方法,来为应用程序提供搜索功能,该方法使用与文本字段位于同一文档中的数组中存储的关键字。 结合多键索引,此模式可以支持应用程序的关键字搜索操作。
要向文档添加结构以支持基于关键字的查询,请在文档中创建一个数组字段,并将关键字作为字符串添加到数组中。 然后,您可以在数组上创建多键索引,并创建从数组中选择值的查询。示例:
假设有一个图书馆藏书的集合,你想要提供基于主题的搜索。 对于每种藏书,添加主题数组,并为特定的藏书添加所需的关键字。
对于藏书Moby-Dick,您可能有以下文档:{ title : "Moby-Dick" , author : "Herman Melville" , published : 1851 , ISBN : 0451526996 , topics : [ "whaling" , "allegory" , "revenge" , "American" , "novel" , "nautical" , "voyage" , "Cape Cod" ] }
然后,在主题数组上创建多键索引:
db.volumes.createIndex( { topics: 1 } )
多键索引为主题数组中的每个关键字创建单独的索引条目。 例如,索引包含一个捕鲸的条目和一个寓言的条目。
然后根据关键字进行查询, 例如:db.volumes.findOne( { topics : "voyage" }, { title: 1 } )
注意
具有大量元素的数组(例如具有数百或数千个关键字的数组)将在插入时产生较大的索引成本。关键字索引的限制
MongoDB可以支持使用特定数据模型和多键索引的关键字搜索; 然而,这些关键字索引存在以下方面不足或不可与全文产品相比:
•词干。 MongoDB中的关键字查询无法解析词根或相关字词的关键字。
•同义词。 基于关键字的搜索功能必须支持应用程序层中的同义词或关联查询。
•排行。 本文档中描述的关键字查找不提供衡量结果的方法。
•异步索引。 MongoDB同步构建索引,这意味着用于关键字索引的索引始终是最新的,并且可以实时操作。 但是,异步批量索引对于某些种类的内容和工作负载可能更有效。3)貨幣型数据模型
处理货币数据的应用程序通常需要捕获货币小数部分的能力,并且需要在运算时以精确的精度模拟小数的四舍五入。 许多现代系统使用的基于二进制的浮点算法(即float,double)不能表示精确的小数部分,并且需要一定程度的近似,使得它不适合于货币算术。 这个约束是货币数据建模时的一个重要考虑因素。
有几种使用数值和非数值模型的方法在MongoDB中建模货币数据。
a)数值模型
如果您需要查询数据库以获取准确的,数学上有效的匹配或需要执行服务器端运算(例如$inc,$mul和聚合框架运算),那么数值模型可能是适当的。
以下方法遵循数值模型:
•使用十进制BSON类型,这是一种基于十进制的浮点格式,能够提供精确的精度。 可用于MongoDB 3.4及更高版本。
•使用比例因子,通过乘以10的乘方的比例因子将货币值转换为64位整数(长BSON类型)。
b)非数值模型
如果不需要对货币型数据执行服务器端运算,或者服务器端的近似值足够精确,则使用非数值模型来建模货币型数据可能是合适的。
以下方法遵循非数值模型:
•使用两个字段表示货币值:一个字段将精确的货币值存储为非数值字符串,另一个字段存储基于二进制的货币值的浮点近似值(双精度BSON类型)。
注意,本页提到的运算是指由mongod或mongos执行的服务器端运算,而不是客户端运算。数值模型
使用十进制BSON类型,这是3.4版的新特性。
十进制BSON类型使用IEEE 754 decimal128十进制浮点数值格式。 与二进制浮点格式(即双精度BSON类型)不同,decimal128四舍五入小数部分,能够提供处理货币数据所需的精确精度。
在mongo shell中,使用NumberDecimal()构造函数来分配和查询十进制值。 以下示例将包含气体价格的文档添加到gasprices集合:db.gasprices.insert{ "_id" : 1, "date" : ISODate(), "price" : NumberDecimal("2.099"), "station" : "Quikstop", "grade" : "regular" }
以下查询匹配上述文档:
db.gasprices.find( { price: NumberDecimal("2.099") } )
有关小数类型的更多信息,请参阅NumberDecimal。
将值转换为小数
通过执行一次性转换或通过修改应用程序逻辑以在访问记录时执行转换,集合值可以转换为小数类型。
一次性集合转换:
可以通过遍历集合中的所有文档,将货币值转换为小数类型,再将文档写回到集合,来转换集合。
注意,强烈建议将小数值作为新字段添加到文档中,并在新字段的值经过验证后删除旧字段。
警告,请确保在隔离的测试环境中测试小数转换。 一旦使用3.4版本的MongoDB创建或修改数据文件,它们将不再与以前的版本兼容,并且不支持降低包含小数的数据文件的版本。
比例因子转换:
假设以下集合,它使用比例因子方法并将货币值保存为表示美分数的64位整数:{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong("1999") }, { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong("3999") }, { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong("2999") }, { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong("2495") }, { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong("8000") }
通过使用$multiply运算符乘以price和NumberDecimal("0.01"),可以将long型值转换为格式正确的小数值。 以下聚合管道在$addFields阶段将转换后的值分配给新的priceDec字段:
db.clothes.aggregate( [ { $match: { price: { $type: "long" }, priceDec: { $exists: 0 } } }, { $addFields: { priceDec: { $multiply: [ "$price", NumberDecimal( "0.01" ) ] } } } ] ).forEach( ( function( doc ) { db.clothes.save( doc ); } ) )
可以使用db.clothes.find()查询来验证上述聚集管道的结果:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong(1999), "priceDec" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong(3999), "priceDec" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong(2999), "priceDec" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong(2495), "priceDec" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong(8000), "priceDec" : NumberDecimal("80.00") }
如果不想添加带有小数值的新字段,则可以覆盖原始字段。 以下update()方法首先检查price是否存在,并且它是一个long型值,然后将long型值转换为小数并将其存储在price字段中:
db.clothes.update( { price: { $type: "long" } }, { $mul: { price: NumberDecimal( "0.01" ) } }, { multi: 1 } )
可以使用db.clothes.find()查询来验证结果:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberDecimal("80.00") }
非数值转换:
假设以下集合,它使用非数值模型并将货币值作为精确表示值的字符串保存:{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99" } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99" } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99" } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95" } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00" }
以下函数首先检查price是否存在,并且它是一个字符串,然后将字符串值转换为小数值,并将其存储在priceDec字段中:
db.clothes.find( { $and : [ { price: { $exists: true } }, { price: { $type: "string" } } ] } ).forEach( function( doc ) { doc.priceDec = NumberDecimal( doc.price ); db.clothes.save( doc ); } );
该函数不向命令行输出任何内容。 可以使用db.clothes.find()查询来验证结果:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99", "priceDec" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99", "priceDec" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99", "priceDec" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95", "priceDec" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00", "priceDec" : NumberDecimal("80.00") }
应用逻辑转换:
可以在应用程序逻辑内执行到小数类型的转换。 在这种情况下,应用程序修改为在访问记录时执行转换。
典型的应用逻辑如下:
1. 测试新字段是否存在,并且它是小数类型。
2. 如果新的小数型字段不存在:
a. 通过适当转换旧字段的值来创建新字段;
b. 删除旧字段
c. 保存转换后的记录使用比例因子
注意,如果您使用的是MongoDB 3.4或更高版本来建模货币数据,使用小数类型比缩放因子方法更可取。
使用比例因子方法对货币数据进行建模:
1. 确定货币值所需的最大精度。 例如,您的应用程序可能需要对美元货币的值精确到十分之一美分。
2. 通过将值乘以10的乘方,将货币值转换为整数,确保所需的最大精度成为整数的最低有效位。 例如,如果所需的最大精度为十分之一美分,则将货币值乘以1000。
3. 存储转换后的货币值。
例如,以下将9.99 美元放大1000倍,以保持高达0.1美分的精度。{ price: 9990, currency: "USD" }
对于给定的货币值,该模型假定:
•比例因子对于一种货币是一致的,即对于给定货币,比例因子相同。
•比例因子是货币的常数和已知属性, 即应用程序可以从货币确定比例因子。
使用此模型时,应用程序必须始终准确地缩放货币值。
有关此模型的用例,请参阅Numeric Model.非数值模型
要使用非数值模型来建模货币数据,请将值存储在两个字段中:
1.在一个字段中,将确切的货币值编码为非数值数据类型,例如BinData或字符串。
2. 在第二个字段中,存储精确值的双精度浮点近似值。
以下示例使用非数值模型存储9.99 USD的价格和0.25 USD的费用:{ price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" }, fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" } }
如果足够谨慎,应用程序可以对数值近似值字段执行范围和排序查询。
然而,使用对近似值字段的查询和排序操作,要求应用程序执行客户端的后处理来解码精确值的非数值表示,然后过滤出根据精确的货币值返回的文档。
关于此模型的用例,请参阅Non-Numeric Model。3)时间型数据模型
默认情况下,MongoDB将时间存储为UTC形式,并将任何本地时间表示转换为此形式。 必须操作或报告某些未修改的本地时间值的应用程序,可以将UTC时间戳和时区存储在一起,并在其应用程序逻辑中计算原始本地时间。
示例
在MongoDB shell中,您可以存储当前日期和当前客户端与UTC的偏移量。var now = new Date();
db.data.save( { date: now,
offset: now.getTimezoneOffset() } );您可以通过利用保存的偏移重建原始的本地时间:
var record = db.data.findOne();
var localNow = new Date( record.date.getTime() - ( record.offset * 60000 ) );