考慮下這樣的場景,咱們的訂單數據是這樣的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的轉換,若是你不這樣寫,就須要判斷每一個字段,而後進行填充。
想關注文章動態的能夠關注公衆號喲: