考虑下这样的场景,咱们的订单数据是这样的java
商品:
{
"_id": productId,
"name": name,
"price": price,
}
订单:
{
"_id": orderId,
"user": userId,
"items": [
productId1,
productId2,
productId3
]
}
复制代码
当咱们查询订单内容的时候,先经过orderId查询订单,而后在经过订单信息中的productId查询到对应的商品信息。这种设计下一次查询没法获取完整的订单。mongodb
范式化结果就是读取速度比较忙,当全部订单的一致性会有保证。数组
在来看看反范式化设计bash
订单:
{
"_id": orderId,
"user": userId,
"items": [
{
"_id": productId1,
"name": name,
"price": price,
},
{
"_id": productId2,
"name": name,
"price": price,
},
]
}
复制代码
这里将商品信息做为内嵌文档存在订单数据中,这样当显示的时候就只须要一次查询就能够了。ide
反范式读取速度快,一致性稍弱,商品信息的变动不能原子性地更新到多个文档。post
那么咱们通常使用哪个呢?咱们在设计的时候要考虑如下问题优化
可能读取了商品信息一万次才修改一次它的详细信息,为了那一次写入快一点或者保证一致性,搭上一万次的读取消耗值得吗?还有你认为引用的数据多久会更新一次?更新越少,越适合反范式化。有些极少变化的数据几乎根本不值得引用。好比名字,性别,地址等。spa
若是是确定的,则应该范式化。设计
订单文档很是适合反范式化,由于其中的商品信息不常常变化。就算变了也没必要更新到全部订单。范式化再次就没有什么优点可言了。3d
因此本例中就是将订单反范式化。
当一个商品打折或者换了图片,并不须要更改原来的订单中的信息。相似这种特定于某一时刻的时间点数据,都应该作嵌入处理。
在咱们上面提到的订单文档中有一处也是这样,地址就属于时间点数据。若某人更新了我的信息,那么并不须要改变其以往的订单内容。
MongoDB存储数据的机制决定了对数组不断追加数据是很低效的。在正常使用中数组和对象大小应该相对固定。
嵌入20,100,或者100000个子文档都不是问题,关键是提早这么作,以后基本保持不变。不然听任文档增加会使得系统慢的你受不了。
对于那些不断增长的内容,必须评论这个时候应该将其做为单独的文档处理比较合适。
只要知道文档开始比较小,后来会变为肯定的大小就可使用这种优化方法,一开始插入文档的时候,就用和最终数据大小同样的垃圾数据填充,好比添加一个garbage字段(其中包含一个字符串,串大小与文档最终大小相同),而后立刻重置字段
db.collection.insert({"_id" : 1,/* other fields */, "garbase": longString});
db.collection.update({"_id" : 1, });
复制代码
这样,MongDB就会为文档从此的增加分配足够的空间
mongodb中存储文档是预留了空间的,容许文档扩容,可是当文档增大到必定地步的时候,就会超过本来分配的空间,此时文档就会进行移动
一个常见的问题就是内嵌的信息究竟是用数组仍是用子文档存。若是确切知道要查询的内容,就要用子文档。若是有时候不太清楚查询的具体内容,就要用数组。当知道一些条目的查询条件时,一般该使用数组。
假设我想记录下游戏中某些物品的属性。咱们能够这样建模
{
"_id": 1,
"items" : {
"slingshot": {
"type" : "weapon",
"damage" : 30,
"ranged" : true
},
"jar" : {
"type": "container",
"contains": "fairy"
}
}
}
复制代码
假设要找出全部damage大于20的武器,子文档不支持这种查找方式,你只能知晓具体某种物品的信息才能查找,好比{"items.jar.damage": {"$gt":20}}. 若是无需标识符,就要用数组
{
"_id": 1,
"items" : [
{
"id" : "slingshot"
"type" : "weapon",
"damage" : 30,
"ranged" : true
},
{
"id" : "jar",
"type": "container",
"contains": "fairy"
}
]
}
复制代码
好比{"items.damage":{"$gt":20}}
就好了。若是还须要多条件查询,可使用$elemMatch.
有时候在使用过程当中受限于业务或者其余状况,并不想使用ObjectId,而是想要使用自动Id来代替。可是MongoDB自己并无提供这个功能,那么如何实现呢?
能够新建一个collection来保存自增id
{
"_id" : ObjectId("59ed8d3df772d09a67eb25f6"),
"fieldName" : "user",
"seq" : NumberLong(100064)
}
复制代码
fieldName表示哪一个集合,那么下次要使用的时候只用取出这个值加1就能够了。代码以下
public Long getNextSequence(String fieldName, long gap) {
try {
Query query = new Query();
query.addCriteria(Criteria.where("fieldName").is(fieldName));
Update update = new Update();
update.inc("seq", gap);
FindAndModifyOptions options = FindAndModifyOptions.options();
options.upsert(true);
options.returnNew(true);
Counter counter = mongoTemplate.findAndModify(query, update, options, Counter.class);
if (counter != null) {
return counter.getSeq();
}
} catch (Throwable t) {
log.error("Exception when getNextSequence from mongodb", t);
}
return gap;
}
复制代码
索引是很强大,可是要提醒你的是,不是全部查询均可以用索引的。好比你要返回集合中90%的文档而非获取一些记录,就不该该使用索引。
若是对这种查询用了索引,结果就是几乎遍历整个索引树,把其中一部分,比方说40GB的索引都加载到内存。而后按照索引中的指针加载集合中200GB的文档数据,最终将加载 200GB + 40GB = 240GB的数据,比不用索引还多。
因此索引通常用在返回结果只是整体数据的小部分的时候。根据经验,一旦要大约返回集合通常的数据就不要使用索引了。
如果已经对某个字段创建了索引,又想在大规模查询时不使用它(由于使用索引可能会较低效),可使用天然排序来强制MongoDB禁用索引。天然排序就是“按照磁盘上的存储顺序返回数据",这样MongDB就不会使用索引了。
db.students.find().sort({"$natural" : 1});
复制代码
若是某个查询不用索引,MongoDB就会作全表扫描。
若是你想要返回某些字段且这些字段均可以放到索引中,那么MongoDB能够作索引覆盖查询,这种查询不会访问指针指向的文档,而是直接用索引的数据返回结果,好比有以下索引
db.students.ensureIndex(x : 1, y : 1, z : 1);
复制代码
如今查询被索引的字段,并要求只返回这些字段,MongoDB就没有必要加载整个文档
db.students.find({"x" : "xxx", "y" : "xxx"},{x : 1, y : 1, z : 1, "_id" : 0});
复制代码
注意因为_id是默认返回的,而它又不是索引的一部分,因此MongoDB就须要到文档中获取_id,去掉它,就能够仅根据索引返回结果了。
如果查询值返回几个字段,则考虑将其放到索引中,即便不对他们执行查询,也能作索引覆盖查询。好比上面的字段z。
假设要查询知足条件A、B、C的文档。知足A的文档有40000,知足B的有9000,知足C的有200,要是让MongoDB按照这个顺序查询,效率可不高。
若是把C放到最前,而后是B,而后是A,则针对B,C只须要查询最多200个文档。
这样工做量显著减小了。要是已知某个查询条件更加苛刻,则要将其放置到最前面。
OR与AND查询相反,匹配最多的查询语句应该放到最前面,由于MongDB每次都要匹配不在结果集中的文档。
开发中,对于简单的查询我通常使用MongoRepository来实现功能,若是有复杂的结合MongoTemplate,注意这二者是能够混合使用的。
开发中咱们要写对于一个collection,其中有些特殊的类型(好比枚举)须要咱们写converter,大多时候是双向的,好比db-->collection和collection-->db 若是只有一个类型须要转换,咱们能够针对这一个属性进行转换,好比下面的例子
@WritingConverter
@Component
public class UserStatusToIntConverter implements Converter<UserStatus, Integer> {
@Override
public Integer convert(UserStatus userStatus) {
return userStatus.getStatus();
}
}
@ReadingConverter
@Component
public class UserStatusFromIntConverter implements Converter<Integer, UserStatus> {
@Override
public UserStatus convert(Integer source) {
return UserStatus.findStatus(source);
}
}
复制代码
一个字段还好,若是一个类中有不少个字段都须要作转换的话,就会产生不少个converter,这个时候咱们能够写一个类级别的转换器
@ReadingConverter
@Component
public class OperateLogFromDbConverter extends AbstractReadingConverter<Document, OperateLog> {
@Override
public OperateLog convert(Document source) {
OperateLog opLog = convertBasicField(source);
if (source.containsKey("_id")) {
opLog.setId(source.getLong("_id"));
}
if (source.containsKey("module")) {
opLog.setModule(ModuleEnum.findModule(source.getInteger("module")));
}
if (source.containsKey("opType")) {
opLog.setOpType(OpTypeEnum.findOpType(source.getInteger("opType")));
}
if (source.containsKey("level")) {
opLog.setLevel(OpLevelEnum.findOpLevel(source.getInteger("level")));
}
return opLog;
}
private OperateLog convertBasicField(Document source) {
Gson gson = new Gson();
return gson.fromJson(source.toJson(), OperateLog.class);
}
}
复制代码
上面代码我用了GSON作common field的转换,若是你不这样写,就须要判断每一个字段,而后进行填充。
想关注文章动态的能够关注公众号哟: