← Tüm yazılar

SqlBulkCopy ile CSV verisini toplu kaydetmek

Bir CSV dosyasını veritabanına taşırken mesele yalnız hızlı INSERT yapmak değil. Bir satır hatalıysa hangi kayıtların içeride kaldığını da bilmek gerekir. Bu örnekte doğrulama, kolon eşleme ve transaction sınırını aynı aktarımın parçaları olarak kuruyorum.

t.Turgay
SqlBulkCopy ile CSV verisini toplu kaydetmek

CSV içe aktarma ekranındaki “Kaydet” düğmesi küçük görünür; arkasındaki iş birkaç bin satır olunca kayıt sınırı önem kazanır. İlk satırlar yazılmışken son satırın hataya düşmesi halinde kullanıcının dosyayı yeniden göndermesi ne yapacak? Önce bu soruyu çözüp ardından aktarımı hızlandırırım. Arşivimdeki DataTable, SqlBulkCopy ve transaction yardımcılarını bu nedenle birlikte ele alıyorum.

CSV ayrıştırma ile veritabanına yazmayı ayırmak

Aktarımın girdisi ham dosya metni değil, kolonları belirlenmiş bir DataTable. CSV okuyucu; ayraçları, çift tırnak içindeki satır sonlarını ve dosya kodlamasını çözmüş olmalı. Satırı virgülden bölmek, “Ankara, merkez” gibi bir alanı iki kolona ayırır. Sonraki katmanın bu bozukluğu tahmin ederek onarmasını istemem; ayrıştırma hatasını dosya satırı ve kolonuyla geri veririm.

Örneği dar tutuyorum: kaynakta Name adlı metin kolonu, hedefte aynı adlı zorunlu alan var. Tabloyu ayrı bir SQL Server deneme veritabanında oluşturmak yeterli. Bu anlatımdaki kod, projede görülen aktarım tekniğinin bağımsız örneği; mevcut bir uygulamanın tüm içe aktarma süreci değil.

CREATE TABLE dbo.ImportDemo
(
    Name nvarchar(100) NOT NULL
);

Aynı bağlantı, aynı transaction

using System.Data;
using Microsoft.Data.SqlClient;

static async Task ImportAsync(string connectionString, DataTable rows)
{
    if (!rows.Columns.Contains("Name")) throw new ArgumentException("Name required");
    foreach (DataRow row in rows.Rows)
        if (string.IsNullOrWhiteSpace(row.Field<string>("Name")))
            throw new ArgumentException("Empty name");
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();
    using var transaction = connection.BeginTransaction();
    using var bulk = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction);
    bulk.DestinationTableName = "dbo.ImportDemo";
    bulk.ColumnMappings.Add("Name", "Name");
    try { await bulk.WriteToServerAsync(rows); transaction.Commit(); }
    catch { transaction.Rollback(); throw; }
}

İlk kontrol kolonun varlığını, ikinci kontrol zorunlu değerleri doğrular. Bunları bağlantıyı açmadan yapmak gereksiz veritabanı işi üretmez. Ancak Field<string> bir tür dönüştürücü değildir: Name kolonu int olarak oluşturulduysa bu kod cast hatasına düşer. Şemayı aktarımın başında belirlemek, her satırda farklı tip tahmin etmekten daha öngörülebilir.

Asıl bağ SqlBulkCopy kurucusundaki connection ve transaction parametreleridir. Aynı bağlantıda transaction açmış olmam, ayrı bağlantı dizesiyle oluşturulan başka bir bulk nesnesinin ona katılacağı anlamına gelmez. Burada sahiplik açık: transaction bağlantıdan alınır, bulk ona katılır, bütün yazma tamamlanınca Commit çağrılır. Hata halinde Rollback denenir ve hata üst katmana taşınır.

BatchSize, commit aralığı demek değildir

Büyük aktarımda BatchSize ayarlamak tek başına “her parti bağımsız kaydedilsin” kararı vermez. Buradaki dış transaction bütün yazmayı kapsar. UseInternalTransaction ile parti başına transaction seçmek başka bir sözleşmedir; önceki partiler kalırken sonraki parti geri alınabilir. İkisini gelişigüzel birleştirmem. Kullanıcıya tüm dosya mı, başarılı satırlar mı, yoksa numaralanmış partiler mi kaydedildiğini net söylemek gerekir.

Kolonları adlarıyla eşlemek sıralama bağımlılığını kaldırır. Yine de uzunluk, null ve hedef tablonun diğer alanları ayrı kontroldür. Özellikle CHECK ve trigger davranışları seçilen SqlBulkCopyOptions değerleriyle değerlendirilmelidir; uygulamadaki ön kontrolün tüm veritabanı kurallarını çalıştırdığını varsaymam.

Yeniden gönderilen dosyayı nasıl ele alırım?

Bu kısa örnek aynı dosyanın ikinci kez gönderilmesini engellemez. İkinci denemede Ada ve Ece yeniden eklenir. Gerçek aktarımda dosya kimliği, iş anahtarı veya staging tablosu üzerinden birleştirme politikası belirlerim. Transaction yarım kaydı önler; mükerrer kaydı kendiliğinden çözmez. “Hata gördüm, tekrar bastım” davranışını desteklemek için ikisine de ihtiyaç olabilir.

DataTable bütün veriyi bellekte tutar. Dosya büyüdüğünde IDataReader üzerinden akış değerlendirilebilir; bunun karşılığında bütün dosyayı önceden doğrulamak zorlaşır. Uzun transaction log büyümesi ve kilit süresi yaratabilir. Parti boyutuna ezbere bir rakam koymak yerine gerçek dosya boyutu, hedef indeksler ve kabul edilebilir hata sınırıyla ölçerim. İş uzun sürüyorsa HTTP isteği yerine takip edilebilir aktarım işi daha uygundur.

Başarıyı satır sayısıyla sınamak

Önce iki geçerli satırla farkın iki olduğunu doğrularım. Ardından boş Name için bağlantı açılmadan ret beklerim. Son olarak ikinci satırı hedefteki bir unique kısıtına takıp ilk satırın da geri alındığını kontrol ederim. Bağlantının kopması ve commit cevabının kaybolması gibi belirsiz sonuçlarda otomatik yeniden deneme eklemeden önce mükerrer kayıt politikasına dönerim. Ekrandaki yeşil bildirim, bu denemelerin yerine geçmez.

ADO.NET: bulk copy ve transaction seçenekleri; SqlBulkCopyOptions sözleşmesi.

✳ Bu yörüngenin sonuna geldin.
Diğer yazıları keşfet →
turgay.Biraz kod, biraz piksel, bolca merak.© 2026 · Dünya’dan sevgilerle ✳