MongoDB'de CRUD: find, Projection, Update Operator'leri, Upsert ve Array Sorguları
MongoDB'de güvenli CRUD'un özü, okuma, kontrol ve yazmayı tek bir atomik update'te birleştirmektir: koşullu $inc stoğu negatife düşürmez, oku-sonra-yaz yaklaşımı ise aynı stoğu birden çok kez satar. Gerçek çıktılarla $elemMatch olmadan yanlış eşleşen siparişler, ordered/unordered insertMany, projection, skip'in maliyeti, update operator'leri, positional ve filtered positional update, upsert, findOneAndUpdate, 50 eşzamanlı alıcıyla stok yarışı ve replaceOne tuzağı.
Sürüm notu: Bu yazıdaki örnekler MongoDB 9.0.8 Community Server, mongosh 2.6, .NET 10 ve MongoDB.Driver 3.14 ile çalıştırılmıştır. Script'lerin tamamı MongoDB 8.0.38'de de aynı çıktıyı verdi.
Önce Tahmin Et
Bir önceki yazıda kurduğumuz lab'da Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) siparişleri var; her siparişin kalemleri items array'inde. Kampanya ekibi Dune'dan (bookId 26) tek siparişte en az 2 adet alanları soruyor. İlk akla gelen sorgu iki koşulu dot notation ile yan yana yazmak:
db.orders.countDocuments({ "items.bookId": 26, "items.quantity": { $gte: 2 } })
Lab'da Dune'u içeren 27 sipariş var. Bu sorgu kaç tane döndürür ve hepsi gerçekten Dune'dan 2 adet içerir mi?
// 03 - Önce Tahmin Et: Dune'dan (bookId 26) en az 2 adet içeren siparişler
const dotted = db.orders.countDocuments({ "items.bookId": 26, "items.quantity": { $gte: 2 } });
const elemMatch = db.orders.countDocuments({ items: { $elemMatch: { bookId: 26, quantity: { $gte: 2 } } } });
print("dot notation:", dotted, " $elemMatch:", elemMatch);
// Fazladan gelenler: iki koşulu farklı element'ler sağlıyor
printjson(db.orders.find(
{
"items.bookId": 26,
"items.quantity": { $gte: 2 },
items: { $not: { $elemMatch: { bookId: 26, quantity: { $gte: 2 } } } }
},
{ _id: 1, "items.bookId": 1, "items.quantity": 1 }
).toArray());
dot notation: 7 $elemMatch: 5
[
{
_id: 10,
items: [
{
bookId: 22,
quantity: 2
},
{
bookId: 26,
quantity: 1
}
]
},
{
_id: 361,
items: [
{
bookId: 26,
quantity: 1
},
{
bookId: 8,
quantity: 1
},
{
bookId: 19,
quantity: 1
},
{
bookId: 53,
quantity: 2
}
]
}
]
Yedi sipariş döndü, ama ikisi yanlış. Dot notation ile yazılan her koşul array'in herhangi bir element'iyle sağlanabilir ve iki koşulu aynı element'in sağlaması gerekmez. Sipariş 10'da Dune'dan 1 adet var; "en az 2 adet" koşulunu kitap 22'nin kalemi sağlıyor. İki koşulun aynı element'te birlikte doğru olmasını istiyorsan $elemMatch kullanman gerekiyor. Bu yazıda bu türden ayrıntılarla CRUD'u (oluşturma, okuma, güncelleme ve silme işlemleri) Bookstore verisi üzerinde baştan sona geçiyoruz. Her script repodaki scripts klasöründe; örneğin docker compose exec mongo mongosh "mongodb://bookstore_app@localhost:27017/bookstore?authSource=bookstore" --quiet /lab/scripts/03-guess.js -p. Script'ler değiştirdikleri veriyi sonda geri alıyor; yine de lab'ı temizlemek istersen 2. yazıdaki 02-load.sh'yi yeniden çalıştırman yeterli.
insertOne ve insertMany: Ordered ve Unordered
Tek document için insertOne, birden fazlası için insertMany var. insertMany varsayılan olarak ordered çalışır: document'leri sırayla yazar ve ilk hatada durur. Ortadaki document'in _id'si zaten var olan Dune'la çakışan üç kitaplık bir batch'i iki modda deneyelim:
// 03 - insertMany: ordered ve unordered
db.books.deleteMany({ _id: { $in: [101, 102, 103] } });
const batch = [
{ _id: 101, isbn: "9798900001012", title: "Persuasion", listPrice: Decimal128("310.00") },
{ _id: 26, isbn: "9798900000268", title: "Dune", listPrice: Decimal128("390.50") },
{ _id: 102, isbn: "9798900001029", title: "Mansfield Park", listPrice: Decimal128("295.00") }
];
for (const ordered of [true, false]) {
db.books.deleteMany({ _id: { $in: [101, 102] } });
try {
db.books.insertMany(batch, { ordered });
} catch (e) {
print(`ordered=${ordered}: ${e.name}, inserted=${e.result.insertedCount}, ` +
`errors=${e.writeErrors.length} (index ${e.writeErrors[0].index}, code ${e.writeErrors[0].code})`);
}
print(" 101/102 in collection:", db.books.countDocuments({ _id: { $in: [101, 102] } }));
}
db.books.deleteMany({ _id: { $in: [101, 102] } });
ordered=true: MongoBulkWriteError, inserted=1, errors=1 (index 1, code 11000)
101/102 in collection: 1
ordered=false: MongoBulkWriteError, inserted=2, errors=1 (index 1, code 11000)
101/102 in collection: 2
İki modda da hata kodu 11000, yani duplicate key. Ordered modda Persuasion yazıldı, Dune'da durdu ve Mansfield Park hiç denenmedi. Unordered modda sunucu hatalı document'i atlayıp devam etti ve iki kitap yazıldı; ayrıca document'leri paralel ve kendi seçtiği sırayla yazabilir. Hangisini seçeceğin batch'in anlamına bağlı: sıranın önemli olduğu, yarıda kalırsa baştan alınacak bir yükleme için ordered; birbirinden bağımsız kayıtların toplu yüklenmesi için unordered daha hızlı ve hatalı kayıtları sonradan raporlamak daha kolay. İki modda da yarıda kalan batch geri alınmaz: yazılanlar yazılmış olarak kalır. Hepsi ya da hiçbiri gerekiyorsa transaction gerekir, 10. yazının konusu.
find: Filtre, Projection, Sort ve Limit
// 03 - find: filtre, projection, sort ve limit
printjson(db.books.find(
{ "category.name": "Science Fiction", listPrice: { $lt: Decimal128("500") } },
{ _id: 0, title: 1, listPrice: 1, "authors.name": 1 }
).sort({ listPrice: -1 }).limit(3).toArray());
print("$in :", db.books.countDocuments({ "category.name": { $in: ["Dystopian", "Science Fiction"] } }));
print("not ex. :", db.books.countDocuments({ publishedYear: { $exists: false } }));
[
{
title: 'Neuromancer',
authors: [
{
name: 'William Gibson'
}
],
listPrice: Decimal128('480.50')
},
{
title: 'The War of the Worlds',
authors: [
{
name: 'H. G. Wells'
}
],
listPrice: Decimal128('450.50')
},
{
title: 'Dune',
authors: [
{
name: 'Frank Herbert'
}
],
listPrice: Decimal128('390.50')
}
]
$in : 13
not ex. : 3
find'ın ilk argümanı filtre, ikincisi projection. Filtrede alanlar arasında virgül AND demek; alt document'lerdeki alanlara "category.name" gibi dot notation ile ulaşılıyor. Karşılaştırma operator'leri SQL'deki karşılıklarıyla aynı mantıkta: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin. Fiyat Decimal128 olduğu için sınırı da Decimal128("500") olarak verdik; MongoDB sayısal tipleri birbiriyle karşılaştırabiliyor, ama aynı tipte kalmak hem okunabilirliği hem de index kullanımını kolaylaştırır. $exists: false alanın hiç olmadığı document'leri buluyor; SQL'den taşırken NULL olan yayın yılını yazmadığımız üç kitap bunlar.
Projection'da 1 alanı dahil eder, 0 dışarıda bırakır; _id açıkça 0 denmedikçe her zaman gelir. Dahil etme ve dışarıda bırakma _id dışında aynı projection'da karışamaz. "authors.name": 1 array'in her element'inden yalnız name'i getirdi. Çıktıdaki alan sırası projection'daki sırayı değil, document'teki sırayı izliyor. Projection ağdan dönen veriyi küçültür ama sunucunun document'i okumasını engellemez; bunun model kararlarına etkisi 4. yazıda.
İki maliyeti şimdiden görmekte fayda var. Birincisi büyük/küçük harf duyarsız $regex:
// $regex: büyük/küçük harf duyarsız arama; index yokken her document taranır
const regexStats = db.books.find({ title: { $regex: "^the", $options: "i" } })
.explain("executionStats").executionStats;
print("regex : returned", regexStats.nReturned, "docsExamined", regexStats.totalDocsExamined,
"stage", regexStats.executionStages.stage);
regex : returned 22 docsExamined 77 stage COLLSCAN
22 kitap bulmak için 77 kitabın hepsi okundu (COLLSCAN, yani bütün collection'ın taranması). Burada title üzerinde index yok, ama olsaydı da değişmezdi: dokümana göre $regex collation'ı bilmediği için case-insensitive aramada index'ten verimli yararlanamaz. Büyük/küçük harf duyarlı ve ^ ile başlayan bir prefix araması ise index'te bir aralık taraması olarak çalışabilir. Türkçe harflerle case-insensitive arama ve sıralamanın doğru yolu collation'lı index, 6. yazının konusu.
İkincisi skip. Siparişleri en yeniden eskiye 20'şerli sayfalarla okuyalım ve farklı sayfalarda ne kadar iş yapıldığına bakalım:
// skip: atlanan kayıtlar da okunur (_id index'i üzerinden sıralı okuma)
for (const skip of [0, 60, 300]) {
const s = db.orders.find({}, { status: 1 }).sort({ _id: -1 }).skip(skip).limit(20)
.explain("executionStats").executionStats;
print(`skip ${String(skip).padStart(3)} : returned ${s.nReturned}, keysExamined ${s.totalKeysExamined}, docsExamined ${s.totalDocsExamined}`);
}
skip 0 : returned 20, keysExamined 20, docsExamined 20
skip 60 : returned 20, keysExamined 80, docsExamined 20
skip 300 : returned 20, keysExamined 320, docsExamined 20
Her sayfa 20 document döndürüyor, ama 16. sayfa için 320 index girdisinden geçildi. Sunucu atlanan document'leri getirmese de index'te onları tek tek saymak zorunda; sayfa numarası büyüdükçe iş doğrusal olarak artıyor. Sıralama index'le yapılamasaydı durum daha kötü olurdu: sunucu bütün siparişleri okuyup bellekte sıralar. Derin sayfalamada skip yerine son görülen değerden devam eden keyset pagination kullanılır; ayrıntısı Çözüm Desenleri serisinin "Pagination Stratejileri Derinlemesine: Offset, Keyset ve Opaque Cursor Token" yazısında.
Update Operator'leri: $set, $inc, $unset, $addToSet ve $pull
MongoDB'de update, document'in tamamını yeniden göndermek değil, sunucuya "şu alanı şöyle değiştir" demek. Dune'a geçici bir kampanya fiyatı ve etiketler ekleyelim:
// 03 - update operator'leri
db.books.updateOne({ _id: 26 }, { $unset: { tags: "", promo: "" }, $set: { listPrice: Decimal128("390.50") } });
let r = db.books.updateOne(
{ _id: 26 },
{
$set: { listPrice: Decimal128("351.45"), promo: { label: "SciFi Week", endsAt: ISODate("2026-07-07T00:00:00Z") } },
$addToSet: { tags: { $each: ["classic", "space-opera", "classic"] } }
}
);
print("matched", r.matchedCount, "modified", r.modifiedCount);
r = db.books.updateOne({ _id: 26 }, { $addToSet: { tags: "classic" } });
print("same $addToSet again -> modified", r.modifiedCount);
db.books.updateOne({ _id: 26 }, { $pull: { tags: "space-opera" }, $unset: { promo: "" } });
printjson(db.books.findOne({ _id: 26 }, { _id: 0, listPrice: 1, tags: 1, promo: 1 }));
matched 1 modified 1
same $addToSet again -> modified 0
{
listPrice: Decimal128('351.45'),
tags: [
'classic'
]
}
$set alanı yazar, yoksa oluşturur; $unset alanı siler. $addToSet değeri array'de yoksa ekler, $each ile birden çok değer alır ve listedeki ikinci classic'i eklemedi. Aynı $addToSet'i tekrar çalıştırınca matchedCount 1 ama modifiedCount 0: document bulundu, değişecek bir şey yoktu. Bu iki sayının farkı, "işlem yapıldı mı" ile "bir şey değişti mi" sorularını ayırmak için önemli. $pull koşula uyan bütün element'leri array'den çıkarır. Bir update içinde birden çok operator kullanılabilir ve hepsi tek document üzerinde atomik olarak uygulanır: bir okuyucu fiyatın değiştiğini ama etiketlerin henüz eklenmediğini görmez. Tek kısıt aynı alanı iki operator'le birden değiştirememek. Sayısal alanlarda $inc, $mul, $min, $max, tarihlerde $currentDate de var; aşağıda stokta kullanacağız.
Array Element'lerini Güncellemek: $, $[] ve $[<id>]
Siparişin kalemleri array'de durduğu için "şu kitabın adedini artır" demek, array'in içindeki doğru element'i bulmayı gerektiriyor. Bekleyen sipariş 379'da Dracula'dan (kitap 20) bir adet daha isteniyor:
// Positional $: eşleşen ilk array element'ini günceller (bekleyen sipariş 379, kitap 20)
db.orders.updateOne(
{ _id: 379, status: "Pending", "items.bookId": 20 },
{ $inc: { "items.$.quantity": 1, total: Decimal128("440.00") } }
);
printjson(db.orders.findOne({ _id: 379 }, { _id: 0, items: 1, total: 1 }));
{
items: [
{
bookId: 20,
title: 'Dracula',
quantity: 2,
unitPrice: Decimal128('440.00')
}
],
total: Decimal128('880.00')
}
items.$ filtrede eşleşen ilk array element'ini gösteriyor. Aynı update kalemin adedini ve siparişin toplamını birlikte değiştirdi; ikisi aynı document'te olduğu için arada tutarsız bir an yok. Filtredeki status: "Pending" da bilinçli: teslim edilmiş bir siparişin kalemi bu update'le değişmez. Koşula uyan bütün element'leri değiştirmek için filtered positional operator $[<id>] ve arrayFilters var. Teslim edilmiş sipariş 372'de müşteri iki kitap için iade talebi açıyor:
// Filtered positional $[it]: koşula uyan TÜM element'ler (iade talebi)
r = db.orders.updateOne(
{ _id: 372 },
{ $set: { "items.$[it].returnRequested": true } },
{ arrayFilters: [{ "it.bookId": { $in: [53, 43] } }] }
);
printjson(db.orders.findOne({ _id: 372 }, { _id: 0, items: { bookId: 1, quantity: 1, returnRequested: 1 } }));
{
items: [
{
bookId: 53,
quantity: 2,
returnRequested: true
},
{
bookId: 43,
quantity: 1,
returnRequested: true
},
{
bookId: 48,
quantity: 1
}
]
}
it bir isim; arrayFilters içinde o isimle hangi element'lerin seçileceğini söylüyoruz. Koşulsuz olarak bütün element'leri değiştirmek için $[] var; script'in sonunda iade işaretini $unset: { "items.$[].returnRequested": "" } ile hepsinden kaldırıyoruz. Array'e eklemenin de ince ayarları var: $push $each ile birden çok değer, $slice ile array'in en fazla kaç element tutacağını alıyor. Müşterinin son görüntülediği üç kitabı tutmak için:
db.customers.updateOne({ _id: 3 }, { $unset: { recentlyViewed: "" } });
for (const bookId of [26, 3, 42, 64]) {
db.customers.updateOne(
{ _id: 3 },
{ $push: { recentlyViewed: { $each: [bookId], $slice: -3 } } }
);
}
printjson(db.customers.findOne({ _id: 3 }, { _id: 0, firstName: 1, recentlyViewed: 1 }));
{
firstName: 'Grace',
recentlyViewed: [
3,
42,
64
]
}
Dört kitap eklendi, $slice: -3 sondaki üçünü tuttu ve ilk görüntülenen Dune düştü. Array hiçbir zaman sınırsız büyümüyor; 4. yazıda bunun neden önemli olduğunu ölçeceğiz.
upsert ve findOneAndUpdate
Sepet, ilk kitap eklendiğinde oluşan bir document. "Varsa güncelle, yoksa oluştur" mantığı için önce okuyup sonra insert ya da update seçmek yerine tek bir upsert kullanılır:
// 03 - upsert ile sepet; $push + $each + $slice ile son görüntülenenler
db.carts.deleteMany({ customerId: 3 });
for (const bookId of [26, 42]) {
const r = db.carts.updateOne(
{ customerId: 3 },
{
$setOnInsert: { createdAt: ISODate("2026-06-30T09:00:00Z") },
$push: { items: { bookId, quantity: 1 } }
},
{ upsert: true }
);
print(`book ${bookId}: matched ${r.matchedCount}, upserted ${r.upsertedCount}`);
}
printjson(db.carts.findOne({ customerId: 3 }, { _id: 0 }));
book 26: matched 0, upserted 1
book 42: matched 1, upserted 0
{
customerId: 3,
createdAt: ISODate('2026-06-30T09:00:00.000Z'),
items: [
{
bookId: 26,
quantity: 1
},
{
bookId: 42,
quantity: 1
}
]
}
İlk çağrıda eşleşen document yoktu; sunucu filtredeki eşitliklerden (customerId: 3) yeni bir document kurdu, update'i ona uyguladı ve _id olarak bir ObjectId verdi. $setOnInsert yalnız bu oluşturma anında çalışıyor, bu yüzden createdAt ikinci çağrıda değişmedi. Bir uyarı: aynı müşteri için iki upsert tam aynı anda gelirse ikisi de "document yok" görüp iki sepet oluşturabilir. Bunu önleyen şey customerId üzerindeki unique index. Dokümana göre unique index varken yalnız bir upsert insert eder; filtre bu index'in alanları üzerinde eşitlikten oluşuyorsa diğerleri yeni document'i günceller, değilse duplicate key hatası alır. Index'ler 6. yazıda.
updateOne değişen document'i döndürmez. Güncellenmiş değere ihtiyacın varsa ayrı bir find atmak yerine findOneAndUpdate kullanırsın; değişiklik ve okuma tek atomik işlem. Bir sonraki bölümde stokta göreceğiz.
Bookstore: Stoğu Negatife Düşürmeden Azaltmak
Kitabın stoğu stock.onHand alanında. Sipariş geldiğinde stoğu azaltmanın doğru yolu, kontrolü filtreye koymak: "stok en az istenen kadarsa azalt".
// 03 - koşullu $inc: stok negatife düşmez; findOneAndUpdate ile yeni değeri al
db.books.updateOne({ _id: 26 }, { $set: { "stock.onHand": 3 } });
for (const quantity of [2, 2]) {
const r = db.books.updateOne(
{ _id: 26, "stock.onHand": { $gte: quantity } },
{ $inc: { "stock.onHand": -quantity } }
);
print(`take ${quantity}: matched ${r.matchedCount}, modified ${r.modifiedCount}`);
}
const after = db.books.findOneAndUpdate(
{ _id: 26, "stock.onHand": { $gte: 1 } },
{ $inc: { "stock.onHand": -1 }, $currentDate: { "stock.updatedAt": true } },
{ returnDocument: "after", projection: { _id: 0, "stock.onHand": 1 } }
);
printjson(after);
print("next call:", db.books.findOneAndUpdate(
{ _id: 26, "stock.onHand": { $gte: 1 } },
{ $inc: { "stock.onHand": -1 } },
{ returnDocument: "after" }
));
take 2: matched 1, modified 1
take 2: matched 0, modified 0
{
stock: {
onHand: 0
}
}
next call: null
Stok 3 iken ilk istek 2 aldı, ikinci istek eşleşmedi çünkü kalan 1 adet 2'den az. Uygulama matchedCount 0'ı "stok yetersiz" olarak yorumlar. findOneAndUpdate son adedi aldı ve yeni değeri tek seferde döndürdü; stok bittikten sonraki çağrı null döndü. Bu yaklaşımın gücü eşzamanlılıkta ortaya çıkıyor. Lab projesindeki stock-race komutu Dune'un stoğunu 10'a çekip 50 alıcıyı aynı anda gönderiyor; önce oku-kontrol et-yaz yaklaşımıyla, sonra koşullu $inc ile:
// Yanlış: önce oku, uygulamada kontrol et, sonra yeni değeri yaz
private static async Task<bool> ReadThenWriteAsync(IMongoCollection<BsonDocument> books)
{
BsonDocument book = await books.Find(new BsonDocument("_id", BookId)).FirstAsync();
int onHand = book["stock"]["onHand"].AsInt32;
if (onHand < 1)
{
return false;
}
await Task.Delay(5); // fiyat ve kampanya kontrolü gibi uygulama işi
await books.UpdateOneAsync(new BsonDocument("_id", BookId),
new BsonDocument("$set", new BsonDocument("stock.onHand", onHand - 1)));
return true;
}
// Doğru: kontrol ve azaltma tek bir atomik update'te
private static async Task<bool> ConditionalIncAsync(IMongoCollection<BsonDocument> books)
{
UpdateResult result = await books.UpdateOneAsync(
new BsonDocument { { "_id", BookId }, { "stock.onHand", new BsonDocument("$gte", 1) } },
new BsonDocument("$inc", new BsonDocument("stock.onHand", -1)));
return result.ModifiedCount == 1;
}
dotnet run --project src/Bookstore.MongoLab -- stock-race
read-then-write : sold 50 of 10, stock.onHand = 4
conditional $inc : sold 10 of 10, stock.onHand = 0
Oku-sonra-yaz yaklaşımı 10 kitaplık stokla 50 satış yaptı ve stoğu 4 olarak bıraktı. Alıcıların çoğu aynı stok değerini okudu, her biri kendi hesapladığı değeri $set ile yazdı ve birbirinin yazdığını ezdi: klasik lost update. Üç koşuda bu sayı 4, 8 ve 7 çıktı, yani sonuç her seferinde başka. Koşullu $inc her koşuda tam 10 satış yaptı ve stok 0'da durdu, çünkü kontrol ile azaltma sunucuda, aynı document üzerinde tek bir atomik işlem. Bu kod driver'ı BsonDocument ile en ham hâliyle kullanıyor; typed collection'lar ve Builders 9. yazıda. Bir siparişte birden çok kitabın stoğunu birlikte düşürmek ise birden çok document demek; onun yolları (transaction ya da rezervasyon) 4. ve 10. yazılarda.
Aynı fikir yorumlara verilen "faydalı" oyunda da işe yarıyor. Bir müşteri aynı yoruma iki kez oy verememeli ve sayaç oy verenlerle tutarlı kalmalı:
// 03 - "faydalı" oyu: aynı müşteri ikinci kez sayılmaz
const review = db.reviews.findOne({ bookId: 26, body: { $exists: true } }, { _id: 1 });
db.reviews.updateOne({ _id: review._id }, { $unset: { helpfulVoters: "", helpfulCount: "" } });
for (const customerId of [7, 7, 12]) {
const r = db.reviews.updateOne(
{ _id: review._id, helpfulVoters: { $ne: customerId } },
{ $addToSet: { helpfulVoters: customerId }, $inc: { helpfulCount: 1 } }
);
print(`customer ${customerId}: modified ${r.modifiedCount}`);
}
printjson(db.reviews.findOne({ _id: review._id }, { _id: 0, helpfulVoters: 1, helpfulCount: 1 }));
customer 7: modified 1
customer 7: modified 0
customer 12: modified 1
{
helpfulCount: 2,
helpfulVoters: [
7,
12
]
}
Filtredeki helpfulVoters: { $ne: customerId } müşteri listede yoksa eşleşiyor; ikinci oyda document eşleşmedi ve ne liste ne sayaç değişti. Yalnız $addToSet kullansaydık liste doğru kalırdı ama $inc her seferinde sayacı artırırdı. Bu yöntem, bir yorumun oy veren sayısı makul kaldığı sürece iyi; sayının sınırsız büyüyebileceği durumlar için 4. yazıdaki unbounded array tartışmasına bak.
Array Sorguları: $elemMatch, $size ve $all
"Önce Tahmin Et"teki kural kısaca şu: array alanında tek bir koşul varsa ("items.bookId": 26) dot notation yeterli ve doğru. Aynı element'e ait birden çok koşul varsa $elemMatch şart. Birkaç array operator'ü daha:
// 03 - array sorguları: $size ve $all
printjson(db.books.find({ authors: { $size: 0 } }, { _id: 0, title: 1 }).toArray());
printjson(db.books.find(
{ "authors.name": { $all: ["Neil Gaiman", "Terry Pratchett"] } },
{ _id: 0, title: 1, "authors.name": 1 }
).toArray());
[
{
title: 'Beowulf'
},
{
title: 'One Thousand and One Nights'
}
]
[
{
title: 'Good Omens',
authors: [
{
name: 'Terry Pratchett'
},
{
name: 'Neil Gaiman'
}
]
}
]
$size array'in tam uzunluğunu eşler; yazarı bilinmeyen iki klasiği buldu. $size aralık almaz: "üçten fazla yazarı olan" sorusu için { "authors.3": { $exists: true } } gibi bir index konumu kontrolü ya da aggregation gerekir. $all verilen değerlerin hepsinin array'de olmasını ister ve sıraya bakmaz; sorguda Gaiman önce yazıldı, document'te Pratchett önce. Bir array'i doğrudan bir array'le karşılaştırmak ({ tags: ["a", "b"] }) ise tam eşitlik arar: aynı element'ler, aynı sırayla.
Üretimde Dikkat
Sonuç sayılarını kontrol et. Her update matchedCount ve modifiedCount döndürür; koşullu update'lerde bu sayılar iş kuralının sonucudur. Stok örneğindeki gibi 0 bir hata değil, bir cevap; uygulama onu kullanıcıya anlamlı bir mesajla çevirmeli.
Kuralı veritabanına yaklaştır. Okuyup uygulamada karar verip yazan her akış eşzamanlılıkta bozulmaya aday. Kontrolü filtreye, hesaplamayı update operator'üne ($inc, $min, $max) koy; tek document'e sığmayan kurallar için transaction ya da modelin kendisi 4. ve 10. yazılarda.
Toplu yüklemede batch'leri küçük tut ve unordered kullan. Tek bir insertMany'ye yüz binlerce document vermek yerine birkaç bin document'lik batch'ler hem bellek hem de hata yönetimi açısından daha rahat. Varsayılan write concern majority; yazmanın kalıcılığı ve hızı arasındaki ayar 12. yazıda.
Sık Düşülen Tuzaklar
Replacement ile document'i ezmek. Yalnız fiyatı değiştirmek isteyen biri yanlışlıkla replaceOne ya da bir driver'ın replace metodunu kullanırsa:
// Yalnız fiyatı değiştirmek isterken bütün document'i değiştirmek
db.books.replaceOne({ _id: 103 }, { listPrice: Decimal128("260.00") });
printjson(db.books.findOne({ _id: 103 }));
try {
db.books.updateOne({ _id: 103 }, { listPrice: Decimal128("250.00") });
} catch (e) {
print(e.name + ":", e.message);
}
{
_id: 103,
listPrice: Decimal128('260.00')
}
MongoInvalidArgumentError: Update document requires atomic operators
Kitabın başlığı, yazarı ve stoğu gitti; geriye yalnız _id ve fiyat kaldı. replaceOne tam olarak bunu yapar: _id dışında her şeyi verdiğin document'le değiştirir. updateOne ise operator'süz bir document'i reddediyor; eski mongo shell'deki update() bu durumda da document'i değiştirirdi, bu yüzden eski script'lerde ve örneklerde bu hataya sık rastlanır. Kural basit: kısmi değişiklik her zaman operator'lü bir update.
null ile eksik alanı karıştırmak. SQL'den taşıdığımız veride alt başlığı olmayan kitapların subtitle alanı yok:
// null hem null değeri hem de alanın hiç olmamasını eşler
print("subtitle: null ->", db.books.countDocuments({ subtitle: null }));
print("subtitle: $exists ->", db.books.countDocuments({ subtitle: { $exists: false } }));
print("subtitle: $type null->", db.books.countDocuments({ subtitle: { $type: "null" } }));
subtitle: null -> 71
subtitle: $exists -> 71
subtitle: $type null-> 0
{ subtitle: null } hem değeri null olan hem de alanı hiç olmayan document'leri eşler; burada hepsi ikinci türden. Ayrım önemliyse $exists ya da $type: "null" kullan. Bir collection'da aynı anlam için iki temsil (bazen null, bazen eksik) kullanmamak ise en temiz çözüm.
Tek document atomikliğini çoklu document'e genellemek. updateMany her document'i ayrı ayrı ve atomik olarak günceller, ama işlem bütün olarak atomik değil: güncelleme sürerken okuyan biri bazı document'lerin yeni, bazılarının eski hâlini görebilir ve işlem yarıda kesilirse güncellenmiş olanlar geri alınmaz. "Kampanyadaki 9 kitabın fiyatını aynı anda değiştir" gibi bir gereksinim ya transaction ister ya da kampanyayı tek bir document'te tutan farklı bir model.
Küçük Bir Deneme
★☆☆ Dune'u (bookId 26) en az 2 adet içeren 5 siparişi $elemMatch ile listele, yalnız _id, müşteri adı ve sipariş tarihini döndür, en yeniden eskiye sırala.
★★☆ Sepete aynı kitap ikinci kez eklendiğinde yeni bir kalem yerine mevcut kalemin adedini artıran bir upsert yaz. İpucu: iki adım gerekebilir; önce kalemi içeren sepette positional $ ile $inc dene, eşleşme yoksa $push'lı upsert yap. İki adım arasında neyin yarışabileceğini düşün.
★★★ stock-race komutuna, alıcı başına 1 ile 3 arasında adet isteyen bir varyant ekle. Koşullu $inc'i { "stock.onHand": { $gte: quantity } } ile kur ve toplam satılan adedin hiçbir koşuda 10'u geçmediğini göster.
Bu yazıda verinin şeklini veri olarak kabul ettik: kalemler siparişte, stok kitapta, yorumlar ayrı. Bir sonraki yazıda bu kararların kendisine dönüyoruz: neyi gömmeli, neye referans vermeli ve 16 MB sınırı, sınırsız büyüyen array'ler ve kopyalanan verinin güncelleme maliyeti bu kararı nasıl etkiliyor.