İçeriğe geç

C# .NET ile PostgreSQL Bulk Insert: Dapper Kullanarak Hızlı Veri Ekleme Deneyimlerim

Geçenlerde bir projede tonla veri eklemem gerekti, biliyorsunuz ya o tür işler hep son dakikaya kalır. Oturmuşum bilgisayar başına, C# ile .NET projesinde PostgreSQL’e veri pompalayacağım ama her seferinde tek tek insert atınca saatler sürüyor. Neticede Dapper’ı kullanıyorum genelde, pratik diye ama bulk insert için ne yapayım diye düşündüm. Aslında yıllardır kod yazıyorum, elektronik devre tasarımı falan da yapıyorum ama bu veri yükleme işi her seferinde başımı ağrıtıyor. Neyse efendim, bu sefer biraz araştırdım ve güzel bir yol buldum, paylaşayım dedim.

Evet, bulk insert dediğimiz şey, birden fazla kaydı aynı anda veritabanına atmak. Neden önemli derseniz, özellikle büyük veri setlerinde zaman kazandırıyor, sunucuyu yormuyor. Mesela bir API geliştirirken, kullanıcı verilerini toplu yüklemek zorunda kalırsanız, yavaşlık olmaz. Bana göre PostgreSQL bu konuda çok iyi, COPY komutu falan var ama C#’tan nasıl entegre ederiz? Dapper ile direkt destek yok ama ufak bir hileyle hallediyoruz. Tabi, önce bağlantıyı doğru kurmak lazım, yoksa her şey boşa gidiyor.

Bu arada aklıma geldi, geçen sefer bir devre tasarımı yaparken benzer bir sorun yaşadım. Kartı test ederken veriler akmıyordu, meğer bağlantı pooling’i unutmuşum, tıpkı veritabanında gibi. Neyse, konuya dönelim. PostgreSQL’de bulk insert için en pratik yol, Npgsql kütüphanesini kullanmak Dapper ile birlikte. Öncelikle NuGet’ten Dapper ve Npgsql’i yüklemeniz gerekiyor, eğer yoksa. Ben hep bunları kullanıyorum, mysql de php ile falan karışık işlerde de pratik oluyor.

Sanırım en temel yaklaşım, listeyi string’e çevirip COPY komutunu çalıştırmak. Evet, şöyle bir şey: Verilerinizi bir CSV formatına dönüştürün, sonra PostgreSQL’in COPY fonksiyonuyla yükleyin. Ama C#’ta bunu Dapper ile nasıl yaparız? Aslında Dapper’ın ExecuteAsync metoduyla SQL komutunu çalıştırıyoruz. Mesela bir liste veriListesi’niz var, Join ile birleştiriyorsunuz “\n” ile, sonra connection.CreateCommand() falan. Detaylarını tam hatırlamıyorum ama temel mantık bu.

Pratik Bir Örnekle Anlatalım

Şimdi oturun bir proje açın, .NET Core console app diyelim. using’leri ekleyin: using Dapper; using Npgsql; using System.Data; Sonra bir connection string tanımlayın, PostgreSQL’inize bağlanın. Benim connection string’im şöyle bir şey: “Host=localhost;Database=mydb;Username=user;Password=pass”. Tabi sizinkini değiştirin. Şimdi bir liste oluşturalım, diyelim List<User> users = new List<User> { new User { Name = “Ali”, Age = 30 }, new User { Name = “Veli”, Age = 25 } }; Ama bulk için CSV’ye çevirmek lazım.

Fakat burada bir hata yaptım ilk seferde, CSV’yi doğru formatlamamıştım, PostgreSQL hata verdi. Neyse efendim, şöyle yapıyorsun: StringBuilder ile satırları oluştur, her user için Name,Age gibi “Ali,30\nVeli,25” diye. Sonra string csvData = sb.ToString(); Ardından using var conn = new NpgsqlConnection(connectionString); conn.Open(); using var cmd = new NpgsqlCommand(“COPY users (name, age) FROM STDIN WITH (FORMAT CSV);”, conn); Ama Dapper ile karıştırmayın, direkt Npgsql kullanın bu kısımda. Sonra cmd.ExecuteNonQuery(); değil, COPY için binary mod falan var ama basit tutarsak, NpgsqlBinaryImporter kullanmak en iyisi.

Evet gayet güzel çalışıyordu derken, ilk testte 1000 kayıt attım, 2 saniyede bitti. Normal insert’le 10 saniye sürüyordu, farkı görünce şaşırdım. Bu arada, eğer Dapper’ı ısrarla kullanmak isterseniz, extension method yazabilirsiniz ama bence Npgsql direkt daha hızlı. Sanırım %50 zaman tasarrufu sağlıyor, galiba öyleydi bir sitede okumuştum.

Konudan biraz sapayım, geçen gün kamp yapmaya gittik Bursa civarına, dağcılık yapmayı seviyorum ya, orada bile aklıma kodlama geldi. Eşim ve çocuğumla vakit geçirirken fikir üretiyorum bazen, mesela bu bulk insert’i nasıl optimize ederim diye düşündüm. Ama tabi aile zamanı kod yok, sadece sohbet. Neyse, geri dönelim.

Açıkçası ben Dapper’ı seviyorum çünkü hafif, ORM’lere göre daha az overhead. PostgreSQL ile birlikte REST API geliştirirken mükemmel. Mesela bir endpoint’te toplu veri alıyorsun, bulk insert’le kaydediyorsun. Fakat dikkat etmeniz gereken, transaction kullanmak. Yoksa yarıda kalırsa veri tutarsız olur. Yani şöyle: using var transaction = conn.BeginTransaction(); Sonra importer’ı transaction ile kullan, commit et en son. Bu şekilde güvenli oluyor.

Dapper ile Entegrasyon Detayları

Şimdi Dapper’ı bulk için nasıl uyarlarız? Aslında Dapper’ın BulkInsert extension’ı yok ama community’de var, GitHub’da falan. Ama ben basit tuttum, custom method yazdım. Mesela public static void BulkInsert<T>(this IDbConnection connection, string tableName, IEnumerable<T> entities) { // içini doldur } Ama detayını vermeyeyim, yoksa uzun sürer. Öncelikle properties’leri reflection ile al, CSV oluştur. Evet, reflection biraz yavaş ama bulk için kabul edilebilir. Alternatif olarak, SqlBulkCopy gibi ama PostgreSQL’de NpgsqlCopyIn kullanmak daha iyi.

Bu arada PostgreSQL resmi sitesinde COPY komutunun docs’u var, oraya bakın derim. Detaylı anlatıyor, ben de ordan öğrendim. Neyse efendim, bir de performans testi yaptım, 10 bin kayıt için normal loop insert 45 saniye, bulk ile 5 saniye. Ne güzel değil mi? Tabi sunucu config’ine göre değişir, benimki localhost’ta.

Sanırım en önemli kısım error handling. Eğer veri formatı yanlışsa PostgreSQL patlıyor, try-catch koyun mutlaka. Mesela bir seferinde yaş field’ını string yaptım, hata verdi, düzeltmek 10 dakika aldı. Kendi hatam :). Gerçi Dapper ile query’lerde de benzer sorunlar oluyor ama alıştık artık.

Fakat bulk insert her zaman en iyisi mi? Küçük verilerde overkill, 100 kayıt için normal insert yeterli. Bana göre 1000+ için kullanın. Bu arada, eğer Vue.js tarafında frontend varsa, API’den veri gönderirken array olarak post edin, backend’de bulk’la kaydedin. jQuery ile AJAX çağrısı yapıyorsun, basit.

Geçen gün şöyle bir fail yaşadım, bulk insert sırasında connection timeout oldu. Meğer pool size’ı düşük ayarlamışım, PostgreSQL config’inde max_connections artırdım, sorun çözüldü. Kısa bir hikaye ama faydalı olsun diye paylaştım, 1-2 paragraf yeter. Neyse, devam edelim.

Evet, şimdi bir kod örneği vereyim, çalışır halde. Önce model: public class User { public string Name { get; set; } public int Age { get; set; } } Sonra method: using Npgsql; using System.IO; public async Task BulkInsertUsersAsync(string connStr, List<User> users) { await using var conn = new NpgsqlConnection(connStr); await conn.OpenAsync(); using var importer = conn.BeginBinaryImport(“COPY users (name, age) FROM STDIN (FORMAT BINARY)”); foreach (var user in users) { importer.StartRow(); importer.Write(user.Name, NpgsqlDbType.Text); importer.Write(user.Age, NpgsqlDbType.Integer); } await importer.CompleteAsync(); } Bu kadar, async yaptım ki API’de bloklamasın. Benim projemde böyle kullandım, sorunsuz.

Tabi, binary format daha hızlı ama text de olur, COPY … FROM STDIN (FORMAT TEXT). Seçiminize göre. Aslında binary öneririm, %20 daha hızlı galiba. Bir teknoloji sitesinde okudum, tam linkini unuttum ama Google’da ‘dapper bulk insert postgresql’ ara, bol örnek çıkar.

Şimdi şüpheci olayım, bu yöntem her zaman güvenli mi? Büyük verilerde memory usage artıyor, liste oluştururken. Eğer milyonlarca kayıt varsa, streaming yapın, batch’lere bölün. Mesela 1000’er 1000’er insert atın. Benim deneyimime göre bu şekilde sorun çıkmıyor. Sen ne dersin, denedin mi böyle bir şey?

Optimizasyon İpuçları

Optimizasyon için index’leri kontrol edin, bulk öncesi drop edip sonra create edin ama dikkatli olun, production’da riskli. Neticede veri kaybı olmasın. Bu arada Microsoft docs’ta Dapper için genel ipuçları var, oraya göz atın. Neyse efendim, ben hep transaction içinden yapıyorum, rollback kolay olsun diye.

Açıkçası PostgreSQL’i mysql’e tercih ediyorum, bulk’ta daha esnek. php ile de benzer işler yaptım eskiden ama C# daha temiz. Garip değil mi, yıllar geçtikçe araçlar değişiyor ama mantık aynı kalıyor. Hani ya, veri ekle, oku, sil.

Sonuç olarak, bu bulk insert yöntemiyle projelerim hızlandı, REST API’lerim daha responsive oldu. Tavsiyem, küçük testlerle başlayın, sonra scale edin. Eğer sorun yaşarsanız, Google’da ‘npgsql bulk insert example’ diye aratın, forumlar dolu. Benim gibi yazmaya devam edeceğim, bir sonraki yazıda belki Vue ile entegrasyonunu anlatırım. Değil mi?

Bir de Reddit’te r/dotnet subreddit’inde benzer tartışmalar var, community’den yardım alın. Evet, öyleki bu işler paylaşınca kolaylaşıyor.