Türkiye'de ilk Mobil & PC Aynı Anda Metin2 Oyna. Triarchonline kalıcı ve uzun ömürlü yapısı ile 24 Temmuz'da açılıyor. | 1-99 Mobil Metin2 Triarch HEMEN TIKLA!
[NODE JS] Sequelize Associations (İlişkiler)
Veritabanı tasarımında tablolar arası ilişkiler, veri modellemesinin belki de en kritik parçasıdır. Bir kullanıcının birden fazla gönderisi olabilir, bir gönderi birden fazla etikete sahip olabilir, bir sipariş birden fazla ürün içerebilir — bu ilişkileri doğru kurmak uygulamanızın başarısını doğrudan etkiler. Sequelize, bu ilişkileri tanımlamak için dört ana metod sunar: hasOne, belongsTo, hasMany ve belongsToMany.
hasOne ve belongsTo, bire bir (one-to-one) ilişkiyi tanımlar. Örneğin her kullanıcının tek bir profili vardır. hasOne ilişkisi kaynak modelden hedef modele doğru tanımlanır, belongsTo ise tersi yöndedir. Her iki tarafı da tanımlamak, çift yönlü sorgulama yapabilmek için önemlidir.
hasMany ve belongsTo birlikte bire çok (one-to-many) ilişkiyi oluşturur. Bir kategorinin birden fazla ürünü olabilir ama her ürün tek bir kategoriye ait olabilir. Bu en yaygın ilişki türüdür ve çoğu uygulamada sıkça karşılaşırız. Yabancı anahtar (foreign key), "çok" tarafındaki tabloda yer alır.
belongsToMany, çoka çok (many-to-many) ilişkiyi tanımlar. Bu ilişki türü, ara bir tablo (junction table) gerektirir. Örneğin bir öğrenci birden fazla ders alabilir ve bir dersi birden fazla öğrenci alabilir. Ara tablo, iki tablonun birincil anahtarlarını yabancı anahtar olarak içerir ve isteğe bağlı ek alanlar barındırabilir.
Eager loading (istekli yükleme) ve lazy loading (tembel yükleme) kavramları ilişkili verilerle çalışırken çok önemli. Eager loading, include seçeneğiyle ilişkili verileri ana sorguyla birlikte getirir. Lazy loading ise ilişkili verileri sadece ihtiyaç duyulduğunda ayrı bir sorguyla getirir. Eager loading genellikle N+1 sorgu problemini çözmek için tercih edilir.
İlişkilerde cascade (basamaklı) davranışları da dikkatli yapılandırılmalıdır. onDelete ve onUpdate seçenekleriyle, bir kayıt silindiğinde veya güncellendiğinde ilişkili kayıtların ne olacağını belirleyebiliriz. CASCADE, SET NULL, RESTRICT ve NO ACTION seçenekleri mevcuttur. Bu ayarlar veri bütünlüğü açısından kritik.
JavaScript:
// Sequelize Associations - Kapsamlı İlişki Örnekleri
const { Sequelize, DataTypes, Model, Op } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:', {
logging: false
});
// =============================================
// Model Tanımları
// =============================================
// Kullanıcı modeli
class Kullanici extends Model {
tamIsim() {
return `${this.ad} ${this.soyad}`;
}
}
Kullanici.init({
ad: { type: DataTypes.STRING, allowNull: false },
soyad: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, unique: true, allowNull: false }
}, { sequelize, modelName: 'Kullanici', tableName: 'kullanicilar' });
// Profil modeli (bire bir ilişki için)
class Profil extends Model {}
Profil.init({
biyografi: DataTypes.TEXT,
avatarUrl: DataTypes.STRING,
websitesi: DataTypes.STRING,
dogumTarihi: DataTypes.DATEONLY,
sehir: DataTypes.STRING,
meslek: DataTypes.STRING
}, { sequelize, modelName: 'Profil', tableName: 'profiller' });
// Kategori modeli
class Kategori extends Model {}
Kategori.init({
ad: { type: DataTypes.STRING, allowNull: false },
slug: { type: DataTypes.STRING, unique: true },
aciklama: DataTypes.TEXT
}, { sequelize, modelName: 'Kategori', tableName: 'kategoriler' });
// Gönderi modeli
class Gonderi extends Model {}
Gonderi.init({
baslik: { type: DataTypes.STRING, allowNull: false },
icerik: { type: DataTypes.TEXT, allowNull: false },
durum: {
type: DataTypes.ENUM('taslak', 'yayinda', 'arsiv'),
defaultValue: 'taslak'
},
goruntulemeSayisi: { type: DataTypes.INTEGER, defaultValue: 0 }
}, { sequelize, modelName: 'Gonderi', tableName: 'gonderiler' });
// Yorum modeli
class Yorum extends Model {}
Yorum.init({
icerik: { type: DataTypes.TEXT, allowNull: false },
puan: { type: DataTypes.INTEGER, validate: { min: 1, max: 5 } }
}, { sequelize, modelName: 'Yorum', tableName: 'yorumlar' });
// Etiket modeli (çoka çok ilişki için)
class Etiket extends Model {}
Etiket.init({
ad: { type: DataTypes.STRING, allowNull: false, unique: true },
slug: DataTypes.STRING
}, { sequelize, modelName: 'Etiket', tableName: 'etiketler' });
// Gönderi-Etiket ara tablosu
class GonderiEtiket extends Model {}
GonderiEtiket.init({
onem: {
type: DataTypes.INTEGER,
defaultValue: 0
}
}, { sequelize, modelName: 'GonderiEtiket', tableName: 'gonderi_etiketler' });
// =============================================
// İlişki Tanımları
// =============================================
// 1. Bire Bir (One-to-One): Kullanıcı <-> Profil
Kullanici.hasOne(Profil, {
foreignKey: 'kullaniciId',
as: 'profil',
onDelete: 'CASCADE'
});
Profil.belongsTo(Kullanici, {
foreignKey: 'kullaniciId',
as: 'kullanici'
});
// 2. Bire Çok (One-to-Many): Kullanıcı -> Gönderiler
Kullanici.hasMany(Gonderi, {
foreignKey: 'yazarId',
as: 'gonderiler',
onDelete: 'CASCADE'
});
Gonderi.belongsTo(Kullanici, {
foreignKey: 'yazarId',
as: 'yazar'
});
// 3. Bire Çok: Kategori -> Gönderiler
Kategori.hasMany(Gonderi, {
foreignKey: 'kategoriId',
as: 'gonderiler'
});
Gonderi.belongsTo(Kategori, {
foreignKey: 'kategoriId',
as: 'kategori'
});
// 4. Bire Çok: Gönderi -> Yorumlar
Gonderi.hasMany(Yorum, {
foreignKey: 'gonderiId',
as: 'yorumlar',
onDelete: 'CASCADE'
});
Yorum.belongsTo(Gonderi, {
foreignKey: 'gonderiId',
as: 'gonderi'
});
// 5. Bire Çok: Kullanıcı -> Yorumlar
Kullanici.hasMany(Yorum, {
foreignKey: 'yazarId',
as: 'yorumlarim'
});
Yorum.belongsTo(Kullanici, {
foreignKey: 'yazarId',
as: 'yorumSahibi'
});
// 6. Çoka Çok (Many-to-Many): Gönderi <-> Etiket
Gonderi.belongsToMany(Etiket, {
through: GonderiEtiket,
foreignKey: 'gonderiId',
otherKey: 'etiketId',
as: 'etiketler'
});
Etiket.belongsToMany(Gonderi, {
through: GonderiEtiket,
foreignKey: 'etiketId',
otherKey: 'gonderiId',
as: 'gonderiler'
});
// =============================================
// Kullanım Örnekleri
// =============================================
async function main() {
await sequelize.sync({ force: true });
// Kullanıcı ve profil oluşturma
const kullanici = await Kullanici.create({
ad: 'Burak',
soyad: 'Yılmaz',
email: 'burak@ornek.com'
});
// Bire bir: Profil oluşturma
await kullanici.createProfil({
biyografi: 'Full-stack geliştirici',
sehir: 'İstanbul',
meslek: 'Yazılım Mühendisi',
websitesi: 'https://burak.dev'
});
// Kategori oluşturma
const teknoloji = await Kategori.create({
ad: 'Teknoloji', slug: 'teknoloji', aciklama: 'Teknoloji yazıları'
});
const yasam = await Kategori.create({
ad: 'Yaşam', slug: 'yasam', aciklama: 'Yaşam tarzı'
});
// Gönderi oluşturma (bire çok)
const gonderi1 = await Gonderi.create({
baslik: 'Node.js ile API Geliştirme',
icerik: 'Node.js kullanarak RESTful API geliştirme rehberi...',
durum: 'yayinda',
yazarId: kullanici.id,
kategoriId: teknoloji.id
});
const gonderi2 = await Gonderi.create({
baslik: 'Veritabanı Tasarım İpuçları',
icerik: 'İyi bir veritabanı tasarımının temel ilkeleri...',
durum: 'yayinda',
yazarId: kullanici.id,
kategoriId: teknoloji.id
});
// Etiket oluşturma ve ilişkilendirme (çoka çok)
const etiketler = await Etiket.bulkCreate([
{ ad: 'JavaScript', slug: 'javascript' },
{ ad: 'Node.js', slug: 'nodejs' },
{ ad: 'Veritabanı', slug: 'veritabani' },
{ ad: 'API', slug: 'api' }
]);
// Gönderiye etiket ekleme
await gonderi1.addEtiketler([etiketler[0], etiketler[1], etiketler[3]], {
through: { onem: 1 }
});
await gonderi2.addEtiketler([etiketler[1], etiketler[2]]);
// Yorum ekleme
await Yorum.bulkCreate([
{ icerik: 'Harika bir yazı, çok faydalı!', puan: 5, gonderiId: gonderi1.id, yazarId: kullanici.id },
{ icerik: 'Örnekler çok açıklayıcı', puan: 4, gonderiId: gonderi1.id, yazarId: kullanici.id },
{ icerik: 'Veritabanı konusunu çok iyi öğrendim', puan: 5, gonderiId: gonderi2.id, yazarId: kullanici.id }
]);
// =============================================
// Sorgulama Örnekleri
// =============================================
// Eager Loading - Tüm ilişkili verilerle kullanıcı
const kullaniciDetay = await Kullanici.findOne({
where: { id: kullanici.id },
include: [
{ model: Profil, as: 'profil' },
{
model: Gonderi, as: 'gonderiler',
include: [
{ model: Kategori, as: 'kategori' },
{ model: Etiket, as: 'etiketler' },
{ model: Yorum, as: 'yorumlar' }
]
}
]
});
console.log('Kullanıcı:', kullaniciDetay.tamIsim());
console.log('Şehir:', kullaniciDetay.profil?.sehir);
console.log('Gönderi sayısı:', kullaniciDetay.gonderiler.length);
kullaniciDetay.gonderiler.forEach(g => {
console.log(` - ${g.baslik} [${g.etiketler.map(e => e.ad).join(', ')}]`);
console.log(` Yorum sayısı: ${g.yorumlar.length}`);
});
// Etiket ile gönderi arama
const jsGonderileri = await Etiket.findOne({
where: { slug: 'nodejs' },
include: [{
model: Gonderi,
as: 'gonderiler',
include: [{ model: Kullanici, as: 'yazar', attributes: ['ad', 'soyad'] }]
}]
});
console.log(`
"${jsGonderileri.ad}" etiketi ile gönderiler:`);
jsGonderileri.gonderiler.forEach(g => {
console.log(` - ${g.baslik} (Yazar: ${g.yazar.ad} ${g.yazar.soyad})`);
});
// Kategori bazlı gönderi sayısı
const kategoriler = await Kategori.findAll({
include: [{
model: Gonderi,
as: 'gonderiler',
attributes: []
}],
attributes: {
include: [
[sequelize.fn('COUNT', sequelize.col('gonderiler.id')), 'gonderiSayisi']
]
},
group: ['Kategori.id']
});
console.log('
Kategori istatistikleri:');
kategoriler.forEach(k => {
console.log(` ${k.ad}: ${k.dataValues.gonderiSayisi} gönderi`);
});
await sequelize.close();
}
main().catch(console.error);
Sequelize'da ilişki tanımları, uygulamanızın veri modelinin temelini oluşturur. Doğru tanımlanmış ilişkiler, karmaşık sorguları basitleştirir ve veri bütünlüğünü korur. Eager loading ile N+1 sorgu problemini çözmek, cascade davranışlarını doğru yapılandırmak ve ara tabloları etkili kullanmak, performanslı ve güvenilir uygulamalar geliştirmenin anahtarıdır.
Emeğiniz ve katkınız için teşekkür ederiz!
Konunuz gerekli incelemelerden geçerek onaylandı. Topluluğumuza katkılarınızın devamını bekliyoruz.
İyi forumlar!
Konunuz gerekli incelemelerden geçerek onaylandı. Topluluğumuza katkılarınızın devamını bekliyoruz.
İyi forumlar!

