Oracle Veritabanı Performansı: .NET'te Bağlantı Havuzu ve Çok Kullanıcılı Stratejiler
.NET yığınında (stack) Oracle Veritabanı, 2025'te en gözde kombinasyon değil; ancak kurumsal bankacılıkta gerçeklik bu. Sistemimiz, her biri farklı veri erişim kalıbı ve güvenlik gereksinimine sahip birden fazla rolde — müşteriler, gişe görevlileri, şube müdürleri, yöneticiler — binlerce eşzamanlı kullanıcıya hizmet veriyor. İşte rol tabanlı bağlantı dizgeleri, kritik yollarda Dapper ve gerçekten işe yarayan bağlantı havuzu stratejileri kullanarak veritabanı katmanımızı performans ve güvenlik için nasıl tasarladığımı anlatıyorum.
Çok Kullanıcılı Bağlantı Stratejisi
Çoğu eğitim, appsettings.json'da size tek bir bağlantı dizgesi gösterir. Bizde dört tane var — her rol katmanı için bir tane:
json{
"ConnectionStrings": {
"OracleCustomer": "User Id=APP_CUSTOMER;Password=***;Data Source=BANKDB;Min Pool Size=20;Max Pool Size=100;Connection Timeout=15;",
"OracleTeller": "User Id=APP_TELLER;Password=***;Data Source=BANKDB;Min Pool Size=10;Max Pool Size=50;Connection Timeout=15;",
"OracleManager": "User Id=APP_MANAGER;Password=***;Data Source=BANKDB;Min Pool Size=5;Max Pool Size=20;Connection Timeout=15;",
"OracleAdmin": "User Id=APP_ADMIN;Password=***;Data Source=BANKDB;Min Pool Size=2;Max Pool Size=10;Connection Timeout=15;"
}
}
Her Oracle kullanıcısının farklı yetkileri (grant) vardır. APP_CUSTOMER yalnızca view'lerden SELECT yapabilir ve belirli paketleri EXECUTE edebilir. APP_TELLER ise işlem (transaction) tablolarında INSERT/UPDATE yetkisi alır. Bu, derinlemesine savunmadır (defense in depth) — uygulama katmanımız ele geçirilse bile, veritabanı kullanıcısının ayrıcalıkları hasar yarıçapını (blast radius) sınırlar.
Bağlantı Fabrikası (Connection Factory)
csharppublic interface IDbConnectionFactory
{
IDbConnection CreateConnection(UserRole role);
}
public class OracleConnectionFactory : IDbConnectionFactory
{
private readonly IConfiguration _configuration;
private readonly ILogger<OracleConnectionFactory> _logger;
public OracleConnectionFactory(
IConfiguration configuration,
ILogger<OracleConnectionFactory> logger)
{
_configuration = configuration;
_logger = logger;
}
public IDbConnection CreateConnection(UserRole role)
{
var connectionString = role switch
{
UserRole.Customer => _configuration.GetConnectionString("OracleCustomer"),
UserRole.Teller => _configuration.GetConnectionString("OracleTeller"),
UserRole.Manager => _configuration.GetConnectionString("OracleManager"),
UserRole.Admin => _configuration.GetConnectionString("OracleAdmin"),
_ => throw new ArgumentOutOfRangeException(nameof(role))
};
_logger.LogDebug("Creating connection for role {Role}", role);
var connection = new OracleConnection(connectionString);
return connection;
}
}
Durumsuz (stateless) olduğu için bunu DI'da singleton olarak kaydediyoruz — asıl bağlantılar, bağlantı dizgesi başına Oracle'ın yerleşik havuzundan gelir.
Performans Sorguları İçin Neden Entity Framework Yerine Dapper
Entity Framework, CRUD için gayet iyidir. Ancak performans açısından kritik sorgularımızda — hesap bakiyesi sorgulamaları, karmaşık filtrelemeli işlem geçmişi, gerçek zamanlı pano (dashboard) toplamaları — Dapper açık ara kazanır.
csharppublic class TransactionRepository : ITransactionRepository
{
private readonly IDbConnectionFactory _connectionFactory;
public TransactionRepository(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<PagedResult<TransactionDto>> GetTransactionsAsync(
TransactionFilter filter,
UserRole callerRole)
{
using var connection = _connectionFactory.CreateConnection(callerRole);
var sql = @"
SELECT t.TRANSACTION_ID as TransactionId,
t.AMOUNT,
t.TRANSACTION_DATE as TransactionDate,
t.DESCRIPTION,
a.ACCOUNT_NUMBER as AccountNumber,
t.STATUS
FROM TRANSACTIONS t
INNER JOIN ACCOUNTS a ON t.ACCOUNT_ID = a.ACCOUNT_ID
WHERE t.ACCOUNT_ID = :AccountId
AND t.TRANSACTION_DATE BETWEEN :StartDate AND :EndDate
AND (:Status IS NULL OR t.STATUS = :Status)
ORDER BY t.TRANSACTION_DATE DESC
OFFSET :Offset ROWS FETCH NEXT :PageSize ROWS ONLY";
var countSql = @"
SELECT COUNT(*)
FROM TRANSACTIONS t
WHERE t.ACCOUNT_ID = :AccountId
AND t.TRANSACTION_DATE BETWEEN :StartDate AND :EndDate
AND (:Status IS NULL OR t.STATUS = :Status)";
var parameters = new
{
filter.AccountId,
filter.StartDate,
filter.EndDate,
filter.Status,
Offset = (filter.Page - 1) * filter.PageSize,
filter.PageSize
};
// Execute both queries on the same connection
var transactions = await connection.QueryAsync<TransactionDto>(sql, parameters);
var totalCount = await connection.ExecuteScalarAsync<int>(countSql, parameters);
return new PagedResult<TransactionDto>
{
Items = transactions.ToList(),
TotalCount = totalCount,
Page = filter.Page,
PageSize = filter.PageSize
};
}
}
Fark ölçülebilir düzeyde. Üç tabloyu join eden işlem geçmişi sorgumuzda, EF Core'un ürettiği SQL ~180ms sürdü. Elle ince ayar yapılmış Dapper sorgusu ise: ~45ms. Binlerce eşzamanlı kullanıcıya hizmet verirken bu 4 kat iyileşme önemlidir.
Bağlantı Havuzu İnce Ayarı
Oracle'ın bağlantı havuzu, bağlantı dizgesi başınadır. İnce ayar yaparken öğrendiklerim şunlar:
Min Pool Size=20;Max Pool Size=100;
Connection Lifetime=300;
Connection Timeout=15;
Incr Pool Size=5;
Decr Pool Size=2;
Validate Connection=true;
Min Pool Size: Bunu temel eşzamanlı bağlantı sayınıza ayarlayın. Müşteri katmanı için analizler, mesai saatleri boyunca neredeyse her zaman 20 bağlantının kullanımda olduğunu gösterdi. Bunu ayarlamak, talep üzerine bağlantı oluşturmanın soğuk başlangıç (cold-start) cezasından kaçınır.
Max Pool Size: Bu sizin tavanınızdır. Müşteri katmanı için 100 bağlantı yüksek gelebilir, ancak unutmayın — her API isteği, sorgu süresi boyunca bir bağlantıyı tutar. 200ms ortalama sorgu süresi ve saniyede 500 istekle, yetişebilmek için tam 100 bağlantıya ihtiyacınız var.
Connection Lifetime: 300 saniyeye (5 dakika) ayarlayın. Bu, bağlantıların geri dönüştürülmesini zorlar ki bu da Oracle RAC düğüm yeniden dengelenmesini ele alır ve ağ kesintilerinden sonra bayatlamış bağlantıları önler.
Validate Connection: Bu, bağlantıyı kodunuza teslim etmeden önce onu ping'ler. ~1ms ek yük getirir ama korkulan "ORA-03113: end-of-file on communication channel" hatalarını önler.
Array Binding ile Toplu İşlemler
Toplu işleme için (gün sonu mutabakatı, toplu bildirim eklemeleri), Oracle'ın array binding'i bireysel eklemelerden çarpıcı biçimde daha hızlıdır:
csharppublic async Task BulkInsertNotificationsAsync(
List<NotificationEntity> notifications,
UserRole callerRole)
{
using var connection = (OracleConnection)_connectionFactory
.CreateConnection(callerRole);
await connection.OpenAsync();
using var command = connection.CreateCommand();
command.CommandText = @"
INSERT INTO NOTIFICATIONS (NOTIFICATION_ID, USER_ID, MESSAGE, CREATED_AT, STATUS)
VALUES (:Id, :UserId, :Message, :CreatedAt, :Status)";
command.ArrayBindCount = notifications.Count;
command.Parameters.Add(new OracleParameter("Id",
OracleDbType.Varchar2, notifications.Select(n => n.Id).ToArray(),
ParameterDirection.Input));
command.Parameters.Add(new OracleParameter("UserId",
OracleDbType.Varchar2, notifications.Select(n => n.UserId).ToArray(),
ParameterDirection.Input));
command.Parameters.Add(new OracleParameter("Message",
OracleDbType.Varchar2, notifications.Select(n => n.Message).ToArray(),
ParameterDirection.Input));
command.Parameters.Add(new OracleParameter("CreatedAt",
OracleDbType.Date, notifications.Select(n => n.CreatedAt).ToArray(),
ParameterDirection.Input));
command.Parameters.Add(new OracleParameter("Status",
OracleDbType.Varchar2, notifications.Select(n => n.Status).ToArray(),
ParameterDirection.Input));
await command.ExecuteNonQueryAsync();
}
10.000 bildirimi tek tek eklemek: ~45 saniye. Array binding ile: ~1,2 saniye. Bu, 37 katlık bir iyileşme.
Sağlık Kontrolleri ve İzleme
Bağlantı havuzu sağlığını ASP.NET sağlık kontrolleri (health checks) aracılığıyla açığa çıkarıyoruz:
csharppublic class OracleHealthCheck : IHealthCheck
{
private readonly IDbConnectionFactory _factory;
public OracleHealthCheck(IDbConnectionFactory factory)
{
_factory = factory;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken)
{
var roles = new[] { UserRole.Customer, UserRole.Teller };
var results = new Dictionary<string, object>();
foreach (var role in roles)
{
try
{
using var connection = _factory.CreateConnection(role);
await ((OracleConnection)connection).OpenAsync(cancellationToken);
var poolStats = new
{
IsOpen = connection.State == ConnectionState.Open,
};
results[role.ToString()] = poolStats;
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
$"Oracle connection failed for role {role}",
ex);
}
}
return HealthCheckResult.Healthy("All Oracle connections healthy",
results);
}
}
Çıkarılan Dersler
Bu mimariyi üretimde iki yıl çalıştırdıktan sonra temel çıkarımlar şunlar: rol tabanlı bağlantı dizgeleri, gerçek güvenlik sınırları sağladıkları için eklenen karmaşıklığa değer; Dapper, EF Core ile ya o ya bu seçeneği değildir ve her birinin parladığı yerde ikisini birden kullanırız; bağlantı havuzu ince ayarı tahminle değil üretim metrikleriyle yapılır; ve Oracle array binding, 100 satırın üzerindeki herhangi bir toplu işlem için varsayılanınız olmalıdır.
Oracle + .NET kombinasyonu popülerlik yarışmalarını kazanmayabilir, ancak doğru mimariyle kurumsal ölçekteki iş yüklerini güvenilir biçimde kaldırır. Ve bankacılıkta güvenilirlik, her zaman yenilikten ağır basar.