← Back to list

Virgüllü Verilere Veda: Veritabanında Many-to-Many (Çoka-Çok) İlişki Refactoring’i

Selamlar! Geliştiriciler olarak hepimizin kariyerinin bir noktasında düştüğü o meşhur tuzağı konuşalım: Bir veritabanı hücresine birden…

Hilmi Cihan Yıldırım · 2026-04-10 07:36 · 0 claps · 3.6 min read
#database-normalization #database-design #entityframework-core
Open on Medium ↗
Wiki topics: 💻 · Programming 🐾 · Pets & Animals

Virgüllü Verilere Veda: Veritabanında Many-to-Many (Çoka-Çok) İlişki Refactoring’i

1NF Normalization

1NF Normalization

Selamlar! Geliştiriciler olarak hepimizin kariyerinin bir noktasında düştüğü o meşhur tuzağı konuşalım: Bir veritabanı hücresine birden fazla veriyi virgülle ayırarak kaydetmek.

Literatürde buna “Jaywalking” (Yaya geçidi olmadan karşıya geçmek) Anti-Pattern’ı deniyor. İlk başta “Hızlıca çözelim, ne olacak ki?” diyerek body_part_id kolonuna bağladığımız "Hips, Waist" gibi veriler, proje büyüdükçe bir kabusa dönüşür.

Peki bu teknik borçtan nasıl kurtuluruz? Entity Framework Core ve PostgreSQL gücünü birleştirerek, veri kaybı yaşamadan bu yapıyı sağlıklı bir Many-to-Many (Çoka-Çok) ilişkiye nasıl taşıyacağımızı adım adım inceleyelim.

Problem: Neden Virgüllü Veriler Baş Belasıdır?

Diyelim ki bir fitness uygulaması geliştiriyorsunuz. Egzersizlerimiz ve bu egzersizlerin çalıştırdığı vücut bölümleri var. İlk tasarımımızda tablolarımız şöyle görünüyordu:

**exercises Tablosu:**

Exercises Table

Exercises Table

**body_parts Tablosu:**

body_parts table

body_parts table

Sorunlar nerede başlıyor?

  1. “Sadece ‘Waist’ çalıştıran egzersizleri getir” demek istediğinizde mecburen LIKE '%Waist%' kullanmak zorunda kalırsınız. Bu hem performansı öldürür hem de indeksleri (index) çöpe atar.
  2. Veritabanı tasarımının altın kuralı olan First Normal Form (1NF) ihlal edilmiştir. Bir hücredeki veri atomik (tekil) olmalıdır.

💡 Çözüm: Junction (Ara) Tablo ile Many-to-Many İlişki

Çözüm basit: Ortaya exercise_body_parts adında bir köprü (junction) tablo kuracağız. Ancak asıl zorluk tabloyu kurmak değil, eski virgüllü verileri kaybetmeden yeni tabloya parçalayarak taşımaktır. İşte EF Core ile yazdığımız, hem şemayı değiştiren (Schema Migration) hem de veriyi taşıyan (Data Migration) o harika çözüm:

C#

using Microsoft.EntityFrameworkCore.Migrations;
namespace FitnessApp.Infrastructure.Migrations
{
    public partial class NormalizeBodyParts : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            // 1️⃣ Adım: Köprü tablomuzu inşa ediyoruz.
            migrationBuilder.Sql(@"
                CREATE TABLE exercise_body_parts (
                    exercise_id VARCHAR(20) NOT NULL,
                    body_part_id INTEGER NOT NULL,
                    PRIMARY KEY (exercise_id, body_part_id),
                    CONSTRAINT fk_ebp_exercise FOREIGN KEY (exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
                    CONSTRAINT fk_ebp_bodypart FOREIGN KEY (body_part_id) REFERENCES body_parts(id) ON DELETE CASCADE
                );
            ");
// 2️⃣ Adım: Zaten temiz olan (virgül içermeyen) verileri yeni evlerine taşıyoruz.
            migrationBuilder.Sql(@"
                INSERT INTO exercise_body_parts (exercise_id, body_part_id)
                SELECT e.id, bp.id
                FROM exercises e
                JOIN body_parts bp ON e.body_part_id = bp.id
                WHERE bp.name NOT LIKE '%, %'
                  AND e.body_part_id IS NOT NULL;
            ");
// 3️⃣ Adım: İŞİN SİHRİ BURADA! Virgüllü metinleri parçalayıp satırlara dönüştürüyoruz.
            migrationBuilder.Sql(@"
                INSERT INTO exercise_body_parts (exercise_id, body_part_id)
                SELECT DISTINCT e.id, atomic_bp.id
                FROM exercises e
                JOIN body_parts compound_bp ON e.body_part_id = compound_bp.id
                CROSS JOIN LATERAL string_to_table(compound_bp.name, ', ') AS part_name
                JOIN body_parts atomic_bp ON atomic_bp.name = part_name
                WHERE compound_bp.name LIKE '%, %'
                  AND e.body_part_id IS NOT NULL
                ON CONFLICT DO NOTHING;
            ");
// 4️⃣ Adım: Artık eski sisteme ihtiyacımız kalmadı. Kolonu uçuruyoruz!
            migrationBuilder.DropColumn(table: "exercises", name: "body_part_id");
// 5️⃣ Adım: "Hips, Waist" gibi gereksiz kayıtları sözlükten siliyoruz.
            migrationBuilder.Sql(@"
                DELETE FROM body_parts WHERE name LIKE '%, %';
            ");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            // Geri alma işlemleri (Veri kaybı riski barındırır)
            migrationBuilder.AddColumn<int>(name: "body_part_id", table: "exercises", type: "integer", nullable: true);
            migrationBuilder.DropTable("exercise_body_parts");
        }
    }
}

🪄 3. Adımda Neler Oluyor? (PostgreSQL Sihri)

Eminim kodda en çok dikkatinizi çeken yer 3. adımdaki string_to_table fonksiyonu olmuştur. PostgreSQL'in bu harika fonksiyonu, verdiğiniz metni belirttiğiniz ayırıcıdan (', ') böler ve sanal bir tablo gibi size satır satır geri döndürür.

Gelin bu karmaşık SQL sorgusunun veritabanımızdaki 101 ID'li Squat egzersizi üzerinde nasıl çalıştığını adım adım görselleştirelim.

🔍 Somut Bir Örnek: “101 — Squat” Nasıl Dönüştü?

Mevcut (Kirli) Durum:

exercises tablosunda Squat egzersizi, body_part_id = 15 olan bir kayda bakıyor.

body_parts tablosunda ise 15 ID'li kayıt: "Hips, Waist" şeklinde birleşik.

Ayrıca body_parts tablosunda temiz (atomik) kayıtlarımız da var:

  • ID: 3 -> Hips
  • ID: 7 -> Waist

İşte migration çalıştığında gerçekleşen sihirli dönüşüm:

Adım 1: Eşleştirme ve Parçalama (string_to_table)

Sistem, 15 ID’li "Hips, Waist" metnini bulur ve virgülden ikiye böler.

  • 101 (Squat) -> "Hips"
  • 101 (Squat) -> "Waist"

Adım 2: Gerçek ID’leri Bulma (JOIN)

Sistem bu metinleri body_parts tablosunda arar ve asıl tekil ID'lerini bulur.

  • "Hips" -> Gerçek ID'si: 3
  • "Waist" -> Gerçek ID'si: 7

Adım 3: Yeni Ara Tabloya Yazma ve Temizlik

Sonuç olarak yepyeni exercise_body_parts tablomuzda şu temiz kayıtlar oluşur:

Yeni exercise_body_parts Tablosu (Sonuç):

1NF table

1NF table

Ve final dokunuşu: Migration’ın 4. ve 5. adımları sayesinde, eski body_part_id kolonu ve içi "Hips, Waist" yazan o çirkin 15 ID'li satır veritabanından tamamen silinir.

Sonuç

Tadaa. Artık veritabanımız 1NF kurallarına tam uyumlu, indekslenebilir, performansı yüksek ve tertemiz bir yapıya kavuştu. Üstelik Entity Framework Core’un migration yapısını sadece tablo oluşturmak için değil, güvenli bir veri taşıma aracı olarak da kullanmış olduk.

Artık “Bana ‘Waist’ çalıştıran egzersizleri getir” demek istediğinizde, hiçbir metin aramasına (LIKE) girmeden, sadece ara tablo üzerinden ışık hızında sorgu atabilirsiniz!

Kodunuza her zaman gözünüz gibi bakın, veritabanınızı virgüllerden uzak tutun! SQL Antipatterns (Bill Karwin): “Jaywalking” teriminin literatüre girdiği o meşhur başucu kitabı.

Veritabanı Normalizasyonu (1NF): Microsoft’un veritabanı normalizasyonunun temellerini anlattığı resmi dokümanı. Bir hücrenin neden “atomik” olması gerektiğini çok iyi açıklar.


메타데이터
post_id
51c4fe762b95
slug
virgüllü-verilere-veda-veritabanında-many-to-many-çoka-çok-i̇lişki-refactoringi-51c4fe762b95
url
https://medium.com/@se.hilmiyildirim/virg%C3%BCll%C3%BC-verilere-veda-veritaban%C4%B1nda-many-to-many-%C3%A7oka-%C3%A7ok-i%CC%87li%C5%9Fki-refactoringi-51c4fe762b95
canonical_url
https://medium.com/@se.hilmiyildirim/virg%C3%BCll%C3%BC-verilere-veda-veritaban%C4%B1nda-many-to-many-%C3%A7oka-%C3%A7ok-i%CC%87li%C5%9Fki-refactoringi-51c4fe762b95
author_url
https://medium.com/@se.hilmiyildirim
status
ok
fetched_at
2026-08-08 17:17:50