MongoDB'de var olan belgeleri değiştirmek için üç yöntem kullanılır: updateOne() filtreye uyan ilk belgeyi, updateMany() uyan tüm belgeleri günceller; replaceOne() ise ilk eşleşen belgenin _id dışındaki bütün içeriğini yeni bir belgeyle değiştirir. Güncelleme belgesinde $set, $inc, $unset, $rename gibi güncelleme operatörleri kullanılır; upsert: true seçeneği ise eşleşme yoksa yeni belge ekler.

Bu yazıda yöntemlerin sözdizimini, döndürdükleri sonuç belgesini, temel alan operatörlerinin davranışını, upsert kurallarını ve atomiklik gibi üretimde önemli olan noktaları MongoDB 9.0 belgelerine dayanarak anlatıyoruz. Dizi elemanlarını değiştiren $push, $pull ve konumsal operatörler dizi güncelleme yazısında ayrıca ele alınıyor.

updateOne, updateMany ve replaceOne söz dizimi

updateOne, updateMany ve replaceOne söz dizimi: updateOne(), updateMany(), replaceOne()

Üç yöntem de ilk parametre olarak find() ile aynı sorgu operatörlerini kabul eden bir filtre alır. İkinci parametre updateOne() ve updateMany() için yalnızca güncelleme operatörü ifadeleri içeren bir belge ya da bir aggregation boru hattıdır; replaceOne() için ise yalnızca alan-değer çiftlerinden oluşan bir yedek belgedir. Üçüncü parametre seçenekler belgesidir: upsert, writeConcern, collation, hint, sort ve yönteme göre arrayFilters, let, maxTimeMS.

javascript
db.collection.updateOne(
   <filter>,
   <update>,
   { upsert: <boolean>, writeConcern: <document>, collation: <document>,
     arrayFilters: [ ... ], hint: <document|string>, let: <document>,
     sort: <document>, maxTimeMS: <int> }
)
YöntemKaç belgeİkinci parametre
updateOne()İlk eşleşenGüncelleme operatörleri veya pipeline
updateMany()Tüm eşleşenlerGüncelleme operatörleri veya pipeline
replaceOne()İlk eşleşenYalnızca alan-değer çiftleri (operatör yok)

updateOne() filtre için boş belge ({}) alırsa koleksiyonda dönen ilk belgeyi günceller. Birden çok belge eşleştiğinde hangisinin güncelleneceğini belirlemek için sort seçeneği vardır; belge bu seçeneğin 8.0 sürümünde eklendiğini belirtir. Bu seçenek verildiğinde updateOne() sıralamaya göre ilk gelen belgeyi günceller.

Dönen sonuç belgesi

updateOne() ve updateMany() bir sonuç belgesi döndürür: matchedCount filtreye uyan belge sayısı, modifiedCount gerçekten değişen belge sayısı, upsertedId upsert ile eklenen belgenin _id değeri, upsertedCount eklenen belge sayısıdır. acknowledged ise işlem bir write concern ile çalıştıysa true, write concern kapatıldıysa false olur.

javascript
db.restoran.updateOne(
   { ad: "Central Perk Cafe" },
   { $set: { ihlal: 3 } }
)
// çıktı: { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

// Eşleşme yoksa
// çıktı: { "acknowledged" : true, "matchedCount" : 0, "modifiedCount" : 0 }

matchedCount ile modifiedCount arasındaki fark önemlidir. Bir alanı zaten sahip olduğu değere ayarlarsanız belge eşleşir ama değişmez; bu durumda modifiedCount, matchedCount'tan küçük olur. Uygulama kodunda "güncelleme başarılı mı?" sorusunu yanıtlarken hangi sayıya baktığınızı bilinçli seçin: kaydın var olup olmadığını matchedCount, gerçekten değişiklik olup olmadığını modifiedCount söyler.

$set: alan atamak ve oluşturmak

$set bir alanın değerini verilen değerle değiştirir. Alan yoksa, tür kısıtını ihlal etmediği sürece yeni alan ekler. Nokta gösterimiyle var olmayan iç içe bir yol verirseniz, yolu tamamlamak için gereken gömülü belgeleri de oluşturur. Belge, $set'in tüm belgeyi yeniden yazmaya göre avantajlarını da sıralar: yalnızca belirtilen alanları değiştirir, oplog'a yalnızca değişen alanları yazdığı için replikasyonu hafifletir ve uygulamanın değişen alanları kendisinin hesaplamasına gerek bırakmaz.

javascript
// Birden çok alan ve iç içe yol
db.urunler.updateOne(
   { sku: "KTP-001" },
   { $set: { fiyat: 45, "boyut.birim": "cm", "etiket.renk": "mavi" } }
)
// etiket alanı yoksa { renk: "mavi" } gömülü belgesiyle birlikte oluşturulur

Güncelleme belgesinde birden çok operatörü bir arada kullanabilirsiniz: { $set: { durum: "D" }, $inc: { adet: 2 } }. Belgeye göre MongoDB 5.0'dan itibaren güncelleme operatörleri adı metin olan alanları sözlük sırasıyla, adı sayı olan alanları ise sayısal sırayla işler. Ayrıca boş bir operand ile (örneğin { $set: {} }) gönderilen güncelleme artık hata vermez; hiçbir değişiklik yapmaz ve oplog kaydı oluşturmaz.

$inc: sayaçları atomik artırmak

$inc bir alanı verilen miktar kadar artırır; negatif değer azaltır. Alan yoksa oluşturur ve değerini verilen miktara ayarlar. Değeri null olan bir alanda $inc kullanmak hata üretir. $inc tek belge içinde atomik bir işlemdir; bu yüzden stok azaltma, görüntülenme sayacı gibi işlemlerde önce okuyup sonra yazmak yerine doğrudan $inc kullanmak eşzamanlı isteklerde kayıp güncellemeleri önler.

javascript
db.urunler.insertOne( { _id: 1, sku: "abc123", quantity: 10, metrics: { orders: 2, ratings: 3.5 } } )

db.urunler.updateOne(
   { sku: "abc123" },
   { $inc: { quantity: -2, "metrics.orders": 1 } }
)
// Belgeye göre güncellenmiş belge:
// { _id: 1, sku: 'abc123', quantity: 8, metrics: { orders: 3, ratings: 3.5 } }

Stoğun eksiye düşmesini engellemek istiyorsanız koşulu filtreye koyun: { sku: "abc123", quantity: { $gte: 2 } }. Filtre ve güncelleme aynı belge üzerinde atomik çalıştığı için, stok yetersizse matchedCount 0 döner ve uygulama bunu "stok yok" olarak yorumlayabilir. Benzer amaçlarla $min, $max, $mul ve $currentDate operatörleri de vardır.

Alan güncelleme operatörleri özeti

Resmî güncelleme operatörleri sayfası alan düzeyindeki operatörleri aşağıdaki gibi listeler. $min ve $max özellikle "en düşük fiyatı sakla" ya da "en yüksek skoru tut" gibi karşılaştırmalı güncellemelerde işe yarar, çünkü değer yalnızca koşul sağlanırsa yazılır ve ayrı bir okuma gerekmez.

OperatörNe yapar
$setAlanın değerini atar, yoksa oluşturur
$incAlanı verilen miktar kadar artırır veya azaltır
$mulAlanı verilen miktarla çarpar
$minVerilen değer mevcut değerden küçükse günceller
$maxVerilen değer mevcut değerden büyükse günceller
$currentDateAlanı geçerli tarihe (Date veya Timestamp) ayarlar
$renameAlanın adını değiştirir
$unsetAlanı belgeden siler
$setOnInsertYalnızca upsert ekleme yaptığında değer atar

$unset ve $rename

$unset ve $rename

$unset bir alanı belgeden siler. Sözdiziminde verilen değer (genellikle "") işlemi etkilemez; alan yoksa işlem hiçbir şey yapmaz. Dizi elemanında konumsal $ ile kullanıldığında ise elemanı silmez, null yapar; dizi boyu ve eleman konumları korunur.

javascript
db.musteriler.updateMany( { }, { $unset: { eskiAlan: "", "adres.faks": "" } } )

$rename bir alanın adını değiştirir. Belgeye göre işlem içeride önce eski alanı ve yeni addaki alanı $unset ile siler, sonra eski alanın değeriyle yeni adı $set eder. Bunun sonuçları şunlardır: belgede yeni adla zaten bir alan varsa o alan silinir; eski alan yoksa işlem hiçbir şey yapmaz; alan sırası korunmayabilir. $rename gömülü belgelerin içine ve dışına alan taşıyabilir ama dizilerin içindeki gömülü belgelerde çalışmaz. Yeni ad eski adla aynı olamaz.

javascript
// nickname alanını alias, cell alanını mobile olarak yeniden adlandır
db.ogrenciler.updateOne( { _id: 1 }, { $rename: { nickname: "alias", cell: "mobile" } } )

// Yazım hatalı alan adını tüm koleksiyonda düzelt
db.ogrenciler.updateMany( { nmae: { $exists: true } }, { $rename: { nmae: "name" } } )

replaceOne: belgeyi bütünüyle değiştirmek

replaceOne(), ilk eşleşen belgenin _id dışındaki tüm içeriğini ikinci parametredeki belgeyle değiştirir. Yedek belge yalnızca alan-değer çiftlerinden oluşmalıdır; güncelleme operatörü içeremez. Yedek belgedeki alanlar eskisinden farklı olabilir. _id değiştirilemez olduğu için yedek belgede bu alanı yazmanıza gerek yoktur; yazarsanız değeri mevcut değerle aynı olmalıdır.

javascript
db.restoran.replaceOne(
   { ad: "Central Perk Cafe" },
   { ad: "Central Pork Cafe", semt: "Manhattan" }
)
// çıktı: { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

upsert: yoksa ekle

upsert: true verildiğinde, filtreye uyan belge varsa güncellenir; yoksa yeni bir belge eklenir. Güncelleme operatörleriyle yapılan upsert'te yeni belge, filtredeki eşitlik koşullarından bir taban belge oluşturularak ve ardından güncelleme ifadeleri uygulanarak kurulur. Filtredeki karşılaştırma koşulları ($gt gibi) yeni belgeye girmez. Belgede _id yoksa MongoDB bir ObjectId ekler.

javascript
db.restoran.updateOne(
   { ad: "Pizza Rat's Pizzaria" },
   { $set: { _id: 4, ihlal: 7, semt: "Manhattan" } },
   { upsert: true }
)
// çıktı: {
//    "acknowledged" : true,
//    "matchedCount" : 0,
//    "modifiedCount" : 0,
//    "upsertedId" : 4,
//    "upsertedCount": 1
// }

Bu örnekte yeni belgenin ad alanı filtreden, diğer alanları $set'ten gelir. Yalnızca ekleme sırasında yazılması gereken alanlar (oluşturulma tarihi, varsayılan değerler) için $setOnInsert kullanılır: işlem ekleme ile sonuçlanırsa bu alanlar atanır, var olan bir belge güncellenirse $setOnInsert hiçbir şey yapmaz.

javascript
db.urunler.updateOne(
   { _id: 1 },
   { $set: { item: "apple" }, $setOnInsert: { defaultQty: 100 } },
   { upsert: true }
)
// Koleksiyon boşsa eklenen belge: { "_id" : 1, "item" : "apple", "defaultQty" : 100 }
// Belge zaten varsa yalnızca $set uygulanır

Eşzamanlı upsert ve yinelenen belgeler

Belge önemli bir uyarı yapar: aynı filtreyle neredeyse aynı anda birden çok upsert gönderilirse ve hepsi sorgu aşamasını herhangi biri ekleme yapmadan önce tamamlarsa, her biri ayrı bir belge ekleyebilir. Bunu önlemenin yolu filtre alanlarında unique indeks bulunmasıdır. Unique indeks varken yalnızca bir işlem ekleme yapar; diğerleri ya yeni eklenen belgeyi günceller ya da benzersiz anahtar çakışmasıyla başarısız olur. Bu çakışma hatasının ayrıntıları E11000 duplicate key hatası yazısında, unique indeks oluşturma unique ve partial indeks yazısında anlatılıyor.

Atomiklik, updateMany ve idempotent güncellemeler

MongoDB'de tüm yazma işlemleri belge düzeyinde atomiktir. updateMany() her belgeyi ayrı ayrı günceller; her belge yazımı atomiktir ama işlemin bütünü atomik değildir. Bir belgenin güncellenmesi başarısız olursa, ondan önce yazılmış güncellemeler kalır, kalan belgeler güncellenmez. Birden çok belge üzerinde ya hep ya hiç davranışı gerekiyorsa transaction kullanın.

Bu nedenle belge, updateMany()'nin yalnızca idempotent işlemler için kullanılmasını önerir: aynı komutu iki kez çalıştırmak sonucu değiştirmemelidir. Örneğin "puanı 3'ün altında olanlara 0.5 ekle ve puanArtti: true yap" komutu, filtreye puanArtti alanının olmadığı koşulunu eklerseniz güvenle yeniden çalıştırılabilir; her belge yalnızca bir kez artırılır.

javascript
db.filmler.updateMany(
   { puan: { $lt: 3.0 }, puanArtti: { $exists: false } },
   { $inc: { puan: 0.5 }, $set: { puanArtti: true } }
)

Diğer davranış notları: _id alanı bir kez atandıktan sonra güncellenemez. Yazma işlemleri alan sırasını korur; istisnalar _id'nin her zaman ilk alan olması ve $rename içeren güncellemelerin sırayı değiştirebilmesidir. updateOne() başarılı olursa oplog'a bir kayıt yazılır; başarısız olursa ya da güncellenecek belge bulunamazsa yazılmaz. Onay düzeyini write concern belirler. Çok sayıda farklı güncellemeyi tek istekte göndermek için bulkWrite kullanabilirsiniz.

Aggregation boru hattıyla güncelleme

Bir alanı başka bir alanın değerine göre hesaplamak ya da koşullu güncelleme yapmak gerektiğinde, ikinci parametre olarak bir dizi içinde aggregation aşamaları verilebilir. Belgeye göre bu boru hattında yalnızca $addFields ve takma adı $set, $project ve takma adı $unset, $replaceRoot ve takma adı $replaceWith aşamaları kullanılabilir. Buradaki $set güncelleme operatörü değil aggregation aşamasıdır.

javascript
db.ogrenciler.updateOne(
   { _id: 1 },
   [
      { $set: { yorumlar: [ "$yorum1", "$yorum2" ], sonGuncelleme: "$$NOW" } },
      { $unset: [ "yorum1", "yorum2" ] }
   ]
)

Aşamaların ayrıntıları için $addFields, $set ve $project, boru hattı kavramı için aggregation pipeline yazılarına bakın.

Uygulamalı mini örnek: stok ve sayaç

Bir e-ticaret sepetinde ödeme anında stok düşmek, satış sayacını artırmak ve son satış tarihini yazmak istiyoruz. Tek bir updateOne() çağrısı bu üç değişikliği aynı belgede atomik olarak yapar. Filtredeki stok: { $gte: 2 } koşulu, stok yetersizse hiçbir şeyin değişmemesini sağlar.

javascript
const sonuc = db.urunler.updateOne(
   { sku: "KTP-001", stok: { $gte: 2 } },
   { $inc: { stok: -2, satis: 2 }, $currentDate: { sonSatis: true } }
)
if ( sonuc.matchedCount === 0 ) {
   print( "Stok yetersiz veya ürün yok" )
}

Ürün kataloğunu dış bir kaynaktan eşitlerken ise upsert kullanırız: ürün varsa fiyatı güncellenir, yoksa oluşturulma tarihiyle birlikte eklenir. sku alanında unique indeks bulunması, eşzamanlı eşitleme işlerinde yinelenen ürün oluşmasını engeller.

javascript
db.urunler.createIndex( { sku: 1 }, { unique: true } )

db.urunler.updateOne(
   { sku: "KTP-002" },
   { $set: { fiyat: 120 }, $setOnInsert: { olusturma: new Date(), stok: 0 } },
   { upsert: true }
)

Sık yapılan hatalar

Sık yapılan hatalar: Güncelleme operatörü olmadan alan vermek, Kısmi değişiklik için replaceOne() kullanmak, Oku-değiştir-yaz yapmak, Unique indeks olmadan upsert
  • Güncelleme operatörü olmadan alan vermek: updateOne() ikinci parametrede yalnızca operatör ifadeleri (veya pipeline) kabul eder; tam belge değiştirmek için replaceOne() vardır.
  • Kısmi değişiklik için replaceOne() kullanmak: yazılmayan alanlar silinir.
  • Oku-değiştir-yaz yapmak: eşzamanlı isteklerde güncelleme kaybolur; $inc ve filtre koşulu kullanın.
  • Unique indeks olmadan upsert: eşzamanlı isteklerde yinelenen belgeler oluşabilir.
  • null alanda $inc: hata üretir; alanı önce sayı yapın ya da $setOnInsert ile başlangıç değeri verin.
  • updateMany()'yi idempotent olmayan biçimde yazmak: yarıda kalan bir işlemi tekrar çalıştırmak bazı belgeleri iki kez değiştirir.
  • _id'yi değiştirmeye çalışmak: _id değiştirilemez; yeni belge ekleyip eskisini silmeniz gerekir.

Sık Sorulan Sorular

Güncellenen belgeyi sonuç olarak nasıl alırım?

updateOne() yalnızca sayıları döndürür. Güncellenmiş belgenin kendisine ihtiyacınız varsa findOneAndUpdate() yöntemi kullanılır; bu yöntem de upsert seçeneğini destekler.

modifiedCount neden 0 geliyor?

Filtre bir belgeyi eşleştirmiş ama belgede değişiklik olmamış olabilir; örneğin alan zaten aynı değere sahiptir. matchedCount değerine bakarak belgenin bulunup bulunmadığını ayırt edebilirsiniz.

Sharded koleksiyonda upsert için shard key gerekir mi?

Belgeye göre MongoDB 8.0'dan itibaren updateOne() ve replaceOne() ile upsert: true kullanırken filtrede tam shard key bulunması gerekmez. updateMany() sayfası ise sharded koleksiyonda upsert için filtrede tam shard key gerektiğini belirtir. Sharding temelleri sharding ve shard key yazısında.

Dizi elemanlarını nasıl güncellerim?

Dizilere eleman eklemek, çıkarmak veya belirli elemanları değiştirmek için $push, $pull, $, $[] ve arrayFilters kullanılır; hepsi dizi güncelleme yazısında.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: MongoDB öğrenme rehberi · Dizi güncelleme · find sorguları · E11000 duplicate key hatası · bulkWrite toplu işlemler · Transaction kullanımı