JavaScript Veritabanı Yönetimi: Modern Best Practices Rehberi
Yazar: Burak Balkı | Kategori: Database | Okuma Süresi: 10 dk
JavaScript projelerinde veritabanı yönetimi için bağlantı havuzu, ORM kullanımı, güvenlik standartları ve performans optimizasyonu konularını kapsayan kapsam...
## JavaScript Veritabanı Yönetimi: Modern Uygulama Standartları
Modern web geliştirme ekosisteminde, **JavaScript** tabanlı uygulamaların başarısı büyük ölçüde veritabanı etkileşimlerinin verimliliğine bağlıdır. İster Node.js ile sunucu tarafında ister modern bir frontend framework'ü ile doğrudan etkileşimde olun, veritabanı katmanını doğru yönetmek; uygulamanın ölçeklenebilirliği, güvenliği ve hızı için kritiktir. Bu rehberde, JavaScript projelerinde veritabanı yönetimi için endüstri standardı olan en iyi uygulamaları derinlemesine inceleyeceğiz.
## JavaScript ve Veritabanı Entegrasyonunun Temelleri
JavaScript, asenkron yapısı sayesinde veritabanı işlemlerinde yüksek verimlilik sunar. Ancak, bu asenkron yapı yanlış yönetildiğinde **race conditions** (yarış durumları) veya bellek sızıntılarına yol açabilir. Veritabanı katmanında kullanılan temel araçlar üç ana kategoriye ayrılır:
1. **Native Drivers:** Veritabanı ile doğrudan iletişim kuran düşük seviyeli kütüphaneler (Örn: `pg`, `mysql2`, `mongodb`).
2. **Query Builders:** SQL sorgularını JavaScript fonksiyonları ile oluşturmanızı sağlar (Örn: `Knex.js`).
3. **ORM (Object-Relational Mapping):** Veritabanı tablolarını sınıflar veya nesnelerle eşleştirir (Örn: `Prisma`, `Sequelize`, `TypeORM`, `Mongoose`).
| Araç Tipi | Avantajları | Dezavantajları |
| :--- | :--- | :--- |
| **Native Driver** | Maksimum performans, tam kontrol | Karmaşık sorgularda yazım zorluğu, güvenlik riski |
| **Query Builder** | Esneklik, SQL bilgisi gerektirir | Soyutlama katmanı performansı biraz etkileyebilir |
| **ORM** | Hızlı geliştirme, tip güvenliği (TypeScript) | Karmaşık sorgularda performans kaybı, öğrenme eğrisi |
## Veritabanı Bağlantı Yönetimi ve Connection Pooling
Veritabanı bağlantıları maliyetli kaynaklardır. Her istek için yeni bir bağlantı açmak, sunucu kaynaklarını hızla tüketir. Bu nedenle **Connection Pooling** (Bağlantı Havuzu) kullanımı zorunludur.
### Bağlantı Havuzu Yapılandırması
Bağlantı havuzu, önceden oluşturulmuş bağlantıları hazırda tutar ve ihtiyaç duyulduğunda uygulamaya sunar. İşte `pg` (PostgreSQL) sürücüsü ile bir havuz örneği:
```javascript
const { Pool } = require('pg');
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: 5432,
max: 20, // Maksimum bağlantı sayısı
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
module.exports = {
query: (text, params) => pool.query(text, params),
};
```
> **Önemli Not:** `max` bağlantı sayısını belirlerken sunucunuzun RAM ve CPU kapasitesini, ayrıca veritabanı sunucusunun izin verdiği maksimum bağlantı sayısını dikkate almalısınız.
## Veri Modelleme ve Şema Doğrulama Stratejileri
Veritabanına giren verinin doğruluğu, uygulama bütünlüğü için esastır. JavaScript'in esnek yapısı bazen veri kirliliğine yol açabilir. Bu noktada **Zod** veya **Joi** gibi kütüphanelerle şema doğrulaması yapmak hayati önem taşır.
### Mongoose ile Şema Tanımlama ve Validasyon
MongoDB gibi NoSQL veritabanlarında bile katı bir şema yapısı kullanmak hataları minimize eder.
```javascript
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Kullanıcı adı zorunludur'],
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
match: [/^\S+@\S+\.\S+$/, 'Geçerli bir e-posta giriniz']
},
createdAt: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', userSchema);
```
## Güvenlik Standartları: SQL Injection ve Veri Sızıntısını Önleme
JavaScript uygulamalarında en sık karşılaşılan güvenlik açığı **SQL Injection**'dır. Kullanıcıdan alınan veriler asla doğrudan sorgu dizelerine eklenmemelidir.
### Parametreli Sorgu Kullanımı
Veritabanı sürücülerinin sunduğu parametreli sorgu (prepared statements) özelliğini kullanmak, saldırganların sorguyu manipüle etmesini engeller.
```javascript
// HATALI KULLANIM (Güvenli değil!)
const query = `SELECT * FROM users WHERE id = ${userId}`;
// DOĞRU KULLANIM (Güvenli)
const query = 'SELECT * FROM users WHERE id = $1';
const values = [userId];
const res = await pool.query(query, values);
```
Buna ek olarak, hassas verilerin (şifreler, API anahtarları) veritabanına kaydedilmeden önce mutlaka hashlenmesi gerekir. **bcrypt** bu işlem için endüstri standardıdır.
## Performans Optimizasyonu: İndeksleme ve Sorgu Analizi
Veritabanı performansını artırmanın en etkili yolu doğru **Indexing** (İndeksleme) stratejisidir. Ancak gereksiz her indeks, yazma (INSERT/UPDATE) işlemlerini yavaşlatır.
- Sık kullanılan `WHERE`, `JOIN` ve `ORDER BY` kolonlarına indeks ekleyin.
- Büyük tablolar için **Explain Plan** kullanarak sorgu maliyetini analiz edin.
- Gereksiz verileri çekmekten kaçının (`SELECT *` yerine sadece gerekli kolonları seçin).
```javascript
// Verimsiz
const users = await User.find({});
// Verimli (Projection kullanımı)
const users = await User.find({}).select('username email');
```
## Transaction Yönetimi ve Veri Tutarlılığı
Birden fazla tablonun veya koleksiyonun güncellendiği durumlarda, işlemlerin ya hep ya hiç mantığıyla çalışması gerekir. Buna **Atomicity** denir.
### Prisma ile Transaction Yönetimi
```javascript
const transferMoney = await prisma.$transaction(async (tx) => {
const sender = await tx.account.update({
data: { balance: { decrement: amount } },
where: { id: senderId },
});
if (sender.balance < 0) {
throw new Error('Yetersiz bakiye');
}
const receiver = await tx.account.update({
data: { balance: { increment: amount } },
where: { id: receiverId },
});
return { sender, receiver };
});
```
## Hata Yönetimi ve Loglama Mekanizmaları
Veritabanı hataları (bağlantı kopması, timeout, kısıtlama ihlalleri) uygulamanın çökmesine neden olmamalıdır. Global bir hata yakalama mekanizması ve detaylı loglama şarttır.
```javascript
try {
const result = await pool.query('SELECT * FROM non_existent_table');
} catch (err) {
console.error('Veritabanı Hatası:', {
message: err.message,
stack: err.stack,
query: err.query
});
// Uygun bir hata mesajı döndür veya retry mekanizması çalıştır
}
```
## Caching Katmanı: Redis ile Yanıt Sürelerini Kısaltma
Sık erişilen ve nadir değişen veriler için veritabanına gitmek yerine **Redis** gibi bir in-memory cache kullanmak performansı katlar.
```javascript
const redis = require('redis');
const client = redis.createClient();
async function getUserData(userId) {
const cacheKey = `user:${userId}`;
const cachedData = await client.get(cacheKey);
if (cachedData) {
return JSON.parse(cachedData);
}
const user = await db.users.findById(userId);
await client.setEx(cacheKey, 3600, JSON.stringify(user));
return user;
}
```
## Veritabanı Migrasyon Süreçleri ve Versiyon Kontrolü
Veritabanı şemasındaki değişiklikler manuel olarak değil, **Migration** araçları ile yönetilmelidir. Bu, ekip çalışmasında şema uyumsuzluklarını önler.
- **Prisma Migrate** veya **Sequelize CLI** gibi araçlar kullanarak şema değişikliklerini kod olarak kaydedin.
- Migration dosyalarını mutlaka Git versiyon kontrol sistemine dahil edin.
- Production ortamında migration'ları otomatize edin (CI/CD hatlarında).
## Sık Yapılan Hatalar ve Kaçınılması Gerekenler
1. **N+1 Sorgu Problemi:** Bir liste çekerken her eleman için ayrı bir veritabanı sorgusu atmak. Çözüm: `JOIN` veya `Eager Loading`.
2. **Bağlantıları Kapatmamak:** Singleton pattern kullanmamak ve her fonksiyonda yeni bağlantı oluşturmak.
3. **Hassas Verileri Loglamak:** Hata loglarında kullanıcı şifrelerini veya kredi kartı bilgilerini açık metin olarak bırakmak.
4. **Index Eksikliği:** Milyonlarca satırlık tabloda indeksiz arama yapmak.
5. **Environment Variables Kullanmamak:** DB kimlik bilgilerini kodun içine hard-coded yazmak.
## Sık Sorulan Sorular
**1. Hangi ORM'i seçmeliyim?**
TypeScript kullanıyorsanız **Prisma** modern özellikleri ve tip güvenliği ile öne çıkar. Daha geleneksel ve geniş topluluk desteği isterseniz **Sequelize** iyi bir tercihtir.
**2. NoSQL mi yoksa SQL mi kullanmalıyım?**
Veri yapınız ilişkisel ve katı kurallara bağlıysa (finansal uygulamalar gibi) **PostgreSQL** gibi bir SQL veritabanı; esnek şema ve yatay ölçeklenebilirlik gerekiyorsa **MongoDB** tercih edilmelidir.
**3. Connection Pool boyutu ne olmalıdır?**
Genellikle çekirdek sayısı x 2 formülü başlangıç için iyidir, ancak uygulamanızın trafik yüküne göre yük testleri ile optimize edilmelidir.
**4. Veritabanı güvenliğini nasıl sağlarım?**
Parametreli sorgular kullanın, veritabanı kullanıcısının yetkilerini kısıtlayın (Principle of Least Privilege) ve ağ seviyesinde sadece uygulama sunucusunun erişimine izin verin.
**5. Büyük verilerde sayfalama (pagination) nasıl yapılmalı?**
`OFFSET` ve `LIMIT` yerine, büyük verilerde performans için **Cursor-based pagination** (imleç tabanlı sayfalama) kullanılmalıdır.
## Özet ve Sonuç
JavaScript projelerinde veritabanı yönetimi, sadece veri yazıp okumaktan çok daha fazlasıdır. **Bağlantı havuzu yönetimi**, **şema doğrulaması**, **güvenlik önlemleri** ve **performans optimizasyonu** bir bütün olarak ele alınmalıdır. Bu rehberde belirtilen best practices prensiplerini uygulayarak, hem geliştirme sürecinizi hızlandırabilir hem de son kullanıcıya sorunsuz bir deneyim sunabilirsiniz. Unutmayın, sağlam bir veritabanı katmanı, sürdürülebilir bir yazılımın temel taşıdır.