MongoDB C++ Sürücüsü ile CRUD İşlemleri

Selamlar, bu yazıda MongoDB'nin resmi C++ sürücüsü olan mongocxx ile CRUD işlemlerini nasıl temiz bir şekilde yazabileceğimize bakacağız. C++ tarafında MongoDB pek tartışılmıyor; oysa oyun sunucularından yüksek frekanslı veri servislerine kadar epey yerde işe yarıyor.

mongocxx kısaca nedir?

mongocxx, MongoDB'nin C sürücüsü (libmongoc) üzerine oturmuş bir C++ sarmalayıcısı. BSON tarafı ise bsoncxx kütüphanesinden geliyor. Yani projenize iki paket eklemeniz gerekiyor: biri belgeleri (BSON document) inşa eden bsoncxx, diğeri istemci ve koleksiyon nesnelerini sunan mongocxx. RAII prensibine sadık çalıştığı için açık kalan bir bağlantı ya da elle silinmesi gereken bir cursor derdi yaşamıyorsunuz.

Bence bu sürücünün en sevilesi tarafı stream tabanlı belge inşası. << operatörüyle alanları zincirleyip finalize ile kapatıyorsunuz; sonuç hem okunaklı hem de tip güvenli oluyor.

Bağlantı havuzu ve istemci

İlk kuralımız: uygulama ömrü boyunca tek bir mongocxx::instance olmalı. Bu nesne sürücünün global state'ini başlatıyor ve birden fazla kez yaratırsanız program çöküyor. Çoklu thread kullanan servisler için ise pool üzerinden istemci dağıtmanız lazım, her thread'e bir tane.

#include <mongocxx/instance.hpp>
#include <mongocxx/pool.hpp>
#include <mongocxx/uri.hpp>
#include <bsoncxx/builder/stream/document.hpp>

using bsoncxx::builder::stream::document;
using bsoncxx::builder::stream::finalize;
using bsoncxx::builder::stream::open_document;
using bsoncxx::builder::stream::close_document;

static mongocxx::instance kInstance{};
static mongocxx::pool     kPool{ mongocxx::uri{'mongodb://localhost:27017/?maxPoolSize=20'} };

auto handle      = kPool.acquire();
auto& client     = *handle;
auto  collection = client['shop']['products'];

acquire() çağrısı havuzdan bir istemci kiralıyor, scope dışına çıktığında iade ediyor. Production servislerinde her isteği kendi acquire çağrısıyla sarın; kısa, basit ve race condition'a kapalı bir model.

Belge oluşturma ve insert

Bir ürünü koleksiyona ekleyelim. document{} ile başlayıp << alan << değer zinciri kurup finalize ile bitiriyoruz.

auto doc = document{}
    << 'name'     << 'Laptop'
    << 'price'    << 999.99
    << 'category' << 'electronics'
    << 'inStock'  << true
    << finalize;

if (auto result = collection.insert_one(doc.view())) {
    auto oid = result->inserted_id().get_oid().value.to_string();
    std::cout << 'Eklendi: ' << oid << '\n';
}

insert_one size optional<result::insert_one> döndürüyor; başarısız olursa bool testi false çıkıyor. Toplu ekleme istiyorsanız bsoncxx::document::value vektörüyle insert_many çağırabilirsiniz, yine aynı stream builder ile belgeyi inşa edip vektöre push_back etmeniz yeter.

Sorgulama

Find tarafı klasik filtre + cursor mantığında çalışıyor. Cursor doğrudan range-based for ile dolaşılabiliyor, bu da kodu epey sadeleştiriyor.

auto filter = document{} << 'category' << 'electronics' << finalize;
auto cursor = collection.find(filter.view());

for (auto& view : cursor) {
    std::cout << bsoncxx::to_json(view) << '\n';
}

Sıralama ve sayfalama için mongocxx::options::find yapısını kullanıyoruz:

mongocxx::options::find opts{};
opts.sort(document{} << 'price' << 1 << finalize);
opts.limit(10);

auto sorted = collection.find(document{} << 'inStock' << true << finalize, opts);

Tek belge yetiyorsa find_one daha ekonomik; sonucu yine optional ile dönüyor, mutlaka kontrol edin.

Güncelleme ve silme

Güncellemelerde MongoDB operatörlerini ($set, $inc, $push vb.) BSON içinde alt belge olarak vermeniz gerekiyor. open_document ve close_document tam burada işe yarıyor:

auto f = document{} << 'name' << 'Laptop' << finalize;
auto u = document{}
    << '$set' << open_document
        << 'price' << 899.99
    << close_document
    << finalize;

collection.update_one(f.view(), u.view());

collection.delete_one(document{} << 'name' << 'Mouse' << finalize);

Birden fazla belgeyi etkileyecekseniz update_many ve delete_many aynı imzayla çalışıyor, dönüş nesneleri modified_count ve deleted_count veriyor.

Hata yönetimi

mongocxx hataları istisna fırlatarak bildiriyor. En çok karşılaşacağınız tipler mongocxx::exception (genel sürücü hataları), mongocxx::bulk_write_exception (yazma hataları) ve mongocxx::query_exception.

try {
    collection.insert_one(doc.view());
} catch (const mongocxx::bulk_write_exception& e) {
    std::cerr << 'Yazma hatasi: ' << e.what() << '\n';
}

Şahsi tercihim, repository katmanını try/catch ile sarıp domain'e özel hata tipleri fırlatmak. Böylece üst katman mongocxx'i tanımak zorunda kalmıyor.

CMake ile paketleme

Projeyi derlerken find_package ile iki kütüphaneyi de bağlamak gerekiyor:

find_package(mongocxx REQUIRED)
find_package(bsoncxx REQUIRED)

add_executable(shop main.cpp)
target_link_libraries(shop PRIVATE mongo::mongocxx_shared mongo::bsoncxx_shared)

Sonra cmake --build ile derlersiniz, gerisi standart C++ dünyasının akışı.

Sık karşılaşılan hatalar

  • Birden fazla mongocxx::instance yaratmak: Program açılır açılmaz çökme alırsınız. Tek bir global örnek yeter.
  • View'ı yaşatmadan kullanmak: doc.view() çağırdığınız belge scope dışına çıkarsa view geçersiz hale gelir; belgeyi en azından çağrı süresince saklayın.
  • optional kontrolünü atlamak: find_one ve insert_one sonuçları opsiyonel; doğrudan -> ile erişmek tanımsız davranışa kapı aralar.
  • Pool kullanmadan thread paylaşımı: mongocxx::client thread-safe değil. Çoklu thread kullanıyorsanız mutlaka pool::acquire.

Kapanış

mongocxx ilk bakışta C++'ın söz dizimi yüzünden kalabalık görünüyor olabilir, ama stream builder'a alıştığınızda kod aslında oldukça sade akıyor. Bence yüksek performans gereken servislerde Mongo + C++ ikilisi hâlâ az değerlendirilen bir kombinasyon. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.