ER Diagram: Chen ve Crow's Foot Notasyonu, Cardinality, Optionality ve Class Diagram'dan Tabloya
ER diagram, verinin hangi tablolarda durduğunu ve her ilişkinin iki ucunda en az ve en fazla kaç kayıt olabileceğini gösterir: Chen kavram konuşmasına, Crow's Foot tablo ve FK konuşmasına uygundur. Bu yazıda İzin modülünü iki notasyonla çizip composition, association class ve inheritance'ın tablolara dönüşünü EF Core çıktısıyla gösteriyor, Mermaid erDiagram'ı doğrudan EF Core modelinden üretiyoruz.
Sürüm notu: Bu yazıdaki diyagramlar UML (Unified Modeling Language) 2.5.1, Chen ve Crow's Foot notasyonlarıyla çizilmiştir; C# örnekleri .NET 10 ve Entity Framework Core (EF Core) 10.0.14 ile, Mermaid diyagramları Mermaid 12.3 ile denenmiştir.
Önce Tahmin Et: İlk Departmanı Kim Ekleyecek?
İK (insan kaynakları) Portalı'nın domain modelinde iki masum görünen ilişki var. Her çalışan bir departmanda çalışıyor (Employee.DepartmentId, zorunlu) ve her departmanın bir direktörü var (Department.DirectorId, zorunlu). Class diagram'da ikisi de 1 multiplicity'siyle çizildi, review'da kimse itiraz etmedi. Kod da diyagramın birebir karşılığı:
public class Department
{
public int Id { get; set; }
public required string Name { get; set; }
public int DirectorId { get; set; }
public Employee Director { get; set; } = null!;
}
public class Employee
{
public int Id { get; set; }
public required string EmployeeNumber { get; set; }
// ...
public int DepartmentId { get; set; }
public Department Department { get; set; } = null!;
}
Boş bir veritabanına ilk kaydı, yani Engineering departmanını ve direktörü Grace Hopper'ı eklemek istiyorsun. İkisini aynı SaveChanges içinde eklersen EF Core ne yapar? Önce departmanı, sonra çalışanı ekleyen elle yazılmış bir SQL (Structured Query Language) script'i çalışır mı? Cevabı yazının sonundaki tuzaklar bölümünde gerçek hata mesajıyla göreceğiz; o zamana kadar bir fikrin olsun, çünkü bu yazının konusu tam olarak bu: ilişkinin kaç ucu var, uçlar boş kalabilir mi ve bu bilgi tabloya nasıl yansır.
ER Diagram Neyi Cevaplar?
ER (Entity-Relationship) diagram, verinin nasıl saklandığını anlatan diyagramdır. Class diagram "hangi kavramlar var ve hangi davranışı taşıyorlar?" sorusunu cevaplıyordu; ER diagram ise "hangi kayıtlar var, birbirine nasıl bağlanıyor ve bağlantının iki ucunda kaç kayıt olabilir?" sorusunu cevaplar. Operation yoktur, visibility yoktur, inheritance doğrudan çizilmez. Karşılığında iki şeyi class diagram'dan çok daha net söyler: cardinality (bir uçta en fazla kaç kayıt) ve optionality (bir uçta en az kaç kayıt, yani boş kalabilir mi).
ER diagram UML'in 14 diyagramından biri değildir. Kökeni Peter Chen'in 1976'da ACM (Association for Computing Machinery) TODS (Transactions on Database Systems) dergisinin ilk sayısında yayımlanan "The Entity-Relationship Model—Toward a Unified View of Data" makalesidir. Chen o yıllarda birbiriyle yarışan network, relational ve entity set modellerini ortak bir dile oturtmak istiyordu; önerdiği diyagram, dikdörtgen entity'ler ve eşkenar dörtgen relationship'lerden oluşuyordu. Bugün sektörde daha çok gördüğün Crow's Foot notasyonu ise aynı yıl Gordon Everest'in "inverted arrow" (ters ok) dediği çatal sembolüyle başladı ve 1980'lerde Clive Finkelstein ile James Martin'in IE (Information Engineering) metodolojisiyle yaygınlaştı. MySQL Workbench'ten Mermaid'in erDiagram'ına kadar bugünkü araçların çoğu Crow's Foot konuşur.
Kime çiziyorsun sorusu burada da geçerli. Chen notasyonu kavramsal modelde, iş birimiyle "bir çalışanın kaç talebi olabilir?" konuşmasında iyidir; tablo, kolon ve tip bilmez. Crow's Foot ise mantıksal ve fiziksel modelde, DBA (database administrator) ve backend ekibiyle tablo, FK (Foreign Key) ve index konuşmasında iyidir. Aynı modelin iki notasyondaki hali:

Chen Notasyonu: Entity, Relationship, Attribute
Chen'de her kavramın kendi şekli var. Dikdörtgen bir entity set'tir (Employee, LeaveRequest). Eşkenar dörtgen bir relationship'tir ve fiille adlandırılır (submits, decides). Oval bir attribute'tur; altı çizili oval key attribute'tur. Kenarlardaki 1, N, M etiketleri cardinality'dir: bir çalışan N talep gönderir, her talebi tek bir çalışan gönderir. Bugün ders kitaplarında yerleşmiş ek semboller de var: çift çizgi total participation demektir (her talep mutlaka bir çalışana bağlı), çift dikdörtgen weak entity'dir (kendi başına kimliği yok), çift eşkenar dörtgen de onu sahibine bağlayan identifying relationship'tir.
Chen'in en güçlü olduğu yer relationship'in kendi attribute'larını taşıyabilmesidir. decides ilişkisi bir yönetici ile bir talep arasında kurulur ve karar ile tarih o ilişkinin özelliğidir, ne yöneticinin ne de talebin. Seride bunu UML'de association class (Approval) olarak çizmiştik; Chen aynı şeyi attribute'lu bir eşkenar dörtgenle söyler. Diyagramı elle çizmek zorunda değilsin, PlantUML Chen notasyonunu @startchen ile destekliyor:
@startchen
entity Employee {
EmployeeNumber <<key>>
FullName
}
entity LeaveRequest {
Id <<key>>
WorkingDays
}
entity LeaveAttachment <<weak>> {
FileName
}
relationship Submits {
}
relationship Decides {
Decision
DecidedAt
}
relationship Has <<identifying>> {
}
Employee -1- Submits
Submits =N= LeaveRequest
Employee -M- Decides
Decides -N- LeaveRequest
LeaveRequest -1- Has
Has =1= LeaveAttachment
@endchen
Bu kaynak PlantUML 1.2026.9 ile --check-syntax'tan geçip render ediliyor; =N= total participation'ı kalın çizgiyle, <<weak>> ve <<identifying>> ise çift kenarlı şekillerle çiziliyor. Chen'in zayıf yanı da hemen görünüyor: on tabloluk bir modelde her attribute için bir oval çizmek diyagramı okunmaz hale getirir. Bu yüzden Chen'i kavram konuşmasında, birkaç entity'lik bir parçada kullan; tablo seviyesine inince Crow's Foot'a geç.
Crow's Foot: Cardinality ve Optionality'yi Bir Sembolden Okumak
Crow's Foot'ta entity bir tablo kutusudur ve attribute'lar kutunun içinde satır satır yazılır. İlişki tek bir çizgidir; anlam çizginin iki ucundaki işaretlerdedir. Her uçta iki işaret var: varlığa bitişik olan maksimumu, ondan uzakta olan minimumu verir. Çatal "çok", dik çizgi "bir", halka "sıfır" demektir.

Okuma kuralı UML multiplicity'siyle aynı yöndedir: bir varlığın yanındaki işaret, karşı taraftaki tek bir kaydın bu varlıktan kaç tane gördüğünü söyler. Employees ||..o{ LeaveRequests satırında LeaveRequests tarafındaki o{ "bir çalışanın sıfır ya da çok talebi var", Employees tarafındaki || "her talebin tam bir çalışanı var" demektir. İkinci cümle SQL'de doğrudan görünür: EmployeeId int NOT NULL. Optionality'nin tablodaki karşılığı FK kolonunun nullable olup olmamasıdır; ManagerId int NULL olduğu için çalışan ile yöneticisi arasındaki ilişki |o ile çizilir.
Mermaid'in erDiagram söz dizimi bu işaretleri neredeyse birebir ASCII (American Standard Code for Information Interchange) karakterlerine döker. Seride kullandığımız modelin bir kesiti:
erDiagram
Employees ||..o{ LeaveRequests : "EmployeeId"
LeaveTypes ||..o{ LeaveRequests : "LeaveTypeId"
LeaveRequests ||..o{ Approvals : "LeaveRequestId (cascade)"
Employees ||..o{ Approvals : "ApproverId"
LeaveRequests ||--o| LeaveAttachments : "LeaveRequestId (cascade)"
LeaveRequests {
int Id PK
int EmployeeId FK
int LeaveTypeId FK
date Period_Start
date Period_End
int WorkingDays
nvarchar(30) Status
}
LeaveAttachments {
int LeaveRequestId PK, FK
nvarchar(255) FileName
}
PK, FK ve UK sırasıyla Primary Key, Foreign Key ve Unique Key işaretleridir; bir kolon PK, FK gibi ikisini birden taşıyabilir. Mermaid 11.16'dan beri tipin sonuna ? koyarak nullable kolonu da işaretleyebiliyorsun (int? ManagerId). Bu blok Mermaid CLI (Command-Line Interface) 12.2.0 (Mermaid 12.3.0) ile hatasız render ediliyor.
Identifying ve Non-Identifying İlişki, Weak ve Associative Entity
Yukarıdaki Mermaid kaynağında iki farklı çizgi var: .. ve --. Mermaid dokümantasyonu bunları non-identifying ve identifying ilişki olarak tanımlar ve kesikli ve düz çizgiyle çizer. Ayrım şu: çocuk tablonun FK'si kendi PK'sinin (Primary Key) parçasıysa ilişki identifying'dir, çocuk kimliğini ebeveyninden alır. FK sıradan bir kolonsa ilişki non-identifying'dir.
LeaveAttachments tablosunun PK'si LeaveRequestId'dir ve aynı kolon LeaveRequests'e FK'dir. Ek dosyanın kendine ait bir kimliği yok; "42 numaralı talebin eki" diye tanımlanıyor. Chen'deki weak entity tam olarak budur. Approvals tablosunun ise kendi Id'si var, LeaveRequestId sıradan bir FK; bu yüzden ilişki non-identifying. Bu bir tasarım kararı: Approvals'ın PK'si (LeaveRequestId, Step) olsaydı ilişki identifying olurdu. Surrogate key (yapay Id) kullanan .NET projelerinde ilişkilerin neredeyse hepsi non-identifying çıkar; identifying ilişkiyi genelde owned type'larda, junction tablolarda ve TPT (Table-per-Type) inheritance'ta görürsün.
Associative entity, M:N (many-to-many) bir ilişkinin kendi attribute'larıyla birlikte tabloya dönüşmüş halidir. Chen'deki decides eşkenar dörtgeni ve UML'deki Approval association class'ı, Crow's Foot'ta iki FK'li Approvals tablosu olur. Payload'sız bir many-to-many'de (sadece iki FK) EF Core junction tabloyu kendisi üretir; payload varsa entity'yi açıkça yazarsın. Bunun EF tarafını EF Core serisinin "İlişkiler 2: One-to-One, Many-to-Many ve UsingEntity" yazısında görmüştük.
Class Diagram'dan Tabloya: Dört Dönüşüm
Class diagram'daki her ilişki tipinin tabloda bir karşılığı var ve EF Core bu karşılığı configuration'dan okur. İK Portalı'nın HrPortalDbContext'inden ilgili kısım:
b.Entity<LeaveRequest>(e =>
{
e.ComplexProperty(x => x.Period);
e.Property(x => x.Status).HasConversion<string>().HasMaxLength(30);
// Association: talep sahibi silinirse talep silinmesin
e.HasOne(x => x.Employee).WithMany().HasForeignKey(x => x.EmployeeId)
.OnDelete(DeleteBehavior.Restrict);
// Composition: ek dosya talebin parçası, kendi tablosunda ama kendi kimliği yok
e.OwnsOne(x => x.Attachment, a =>
{
a.ToTable("LeaveAttachments");
a.Property(p => p.FileName).HasMaxLength(255);
});
// Composition: onaylar talep olmadan var olamaz
e.HasMany(x => x.Approvals).WithOne().HasForeignKey(x => x.LeaveRequestId)
.OnDelete(DeleteBehavior.Cascade);
});
b.Entity<Approval>(e =>
{
e.HasIndex(x => new { x.LeaveRequestId, x.Step }).IsUnique();
e.HasOne(x => x.Approver).WithMany().HasForeignKey(x => x.ApproverId)
.OnDelete(DeleteBehavior.Restrict);
});
b.Entity<LeaveBalance>(e =>
{
// Derived attribute (/RemainingDays) -> computed column
e.Property(x => x.RemainingDays).HasComputedColumnSql("[EntitledDays] - [UsedDays]");
e.HasIndex(x => new { x.EmployeeId, x.LeaveTypeId, x.Year }).IsUnique();
});
db.Database.GenerateCreateScript() bu modelin SQL Server DDL'ini (Data Definition Language) veritabanına bağlanmadan üretir. Çıktıdan ilgili satırlar:
CREATE TABLE [LeaveAttachments] (
[LeaveRequestId] int NOT NULL,
[FileName] nvarchar(255) NOT NULL,
[ContentType] nvarchar(100) NOT NULL,
[StoragePath] nvarchar(400) NOT NULL,
CONSTRAINT [PK_LeaveAttachments] PRIMARY KEY ([LeaveRequestId]),
CONSTRAINT [FK_LeaveAttachments_LeaveRequests_LeaveRequestId] FOREIGN KEY ([LeaveRequestId]) REFERENCES [LeaveRequests] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [LeaveBalances] (
/* ... */
[RemainingDays] AS [EntitledDays] - [UsedDays],
/* ... */
);
CREATE UNIQUE INDEX [IX_Approvals_LeaveRequestId_Step] ON [Approvals] ([LeaveRequestId], [Step]);
ALTER TABLE [Approvals] ADD CONSTRAINT [FK_Approvals_Employees_ApproverId] FOREIGN KEY ([ApproverId]) REFERENCES [Employees] ([Id]) ON DELETE NO ACTION;
ALTER TABLE [Approvals] ADD CONSTRAINT [FK_Approvals_LeaveRequests_LeaveRequestId] FOREIGN KEY ([LeaveRequestId]) REFERENCES [LeaveRequests] ([Id]) ON DELETE CASCADE;
Dört dönüşümü yan yana koyunca kural ortaya çıkıyor:
| Class diagram | ER diagram | Tablo (SQL Server) |
|---|---|---|
Association 1 ↔ 0..* | ||..o{, non-identifying | Çok tarafında NOT NULL FK, ON DELETE NO ACTION |
Association 0..1 ↔ 0..* | |o..o{ | Nullable FK (ManagerId int NULL) |
| Composition (dolu elmas) | Cascade'li FK; owned type ayrı tabloya giderse identifying ||--o| | ON DELETE CASCADE, owned type'ta PK = FK |
| Association class | Associative entity | İki FK'li tablo, payload kolonları, gerekirse unique index |
| Generalization | Çizilmez; mapping stratejisine göre tablo(lar) | TPH (Table-per-Hierarchy): tek tablo + discriminator; TPT: PK = FK alt tablolar |
Derived attribute (/RemainingDays) | Computed kolon ya da hiç kolon | AS [EntitledDays] - [UsedDays] |
EF Core'da Restrict'in SQL Server'da ON DELETE NO ACTION olarak üretildiğine dikkat et; SQL Server'da RESTRICT diye bir referential action yok, NO ACTION aynı işi constraint kontrolüyle görüyor. Composition'ın cascade'e dönüşmesi ise UML ile veritabanının en net buluştuğu yer: parça bütünle birlikte yaşar, bütünle birlikte silinir.
Inheritance ER'de doğrudan çizilmez, çünkü ilişkisel modelde inheritance yok; seçtiğin mapping stratejisi tablo yapısını belirler. Modelde bilinçli olarak LeaveType'ı bir lookup entity olarak tuttuk, ama izin türlerini class hiyerarşisi olarak modelleseydik ne olurdu? Küçük bir deneme modeli:
public abstract class LeaveRequestBase
{
public int Id { get; set; }
public int WorkingDays { get; set; }
}
public class AnnualLeaveRequest : LeaveRequestBase
{
public int BalanceYear { get; set; }
}
public class SickLeaveRequest : LeaveRequestBase
{
public string? MedicalReportPath { get; set; }
}
// Varsayılan strateji: TPH (tek tablo + Discriminator)
public class TphContext : HierarchyContext;
public class TptContext : HierarchyContext
{
protected override void OnModelCreating(ModelBuilder b)
{
base.OnModelCreating(b);
b.Entity<LeaveRequestBase>().UseTptMappingStrategy();
}
}
İki context'in modelinden üretilen ER (aşağıda yazacağımız generator'la):
-- TPH
erDiagram
LeaveRequests {
int Id PK
int? BalanceYear
nvarchar(21) Discriminator
nvarchar(max)? MedicalReportPath
int WorkingDays
}
-- TPT
erDiagram
AnnualLeaveRequest {
int Id PK, FK
int BalanceYear
}
LeaveRequests {
int Id PK
int WorkingDays
}
SickLeaveRequest {
int Id PK, FK
nvarchar(max)? MedicalReportPath
}
LeaveRequests ||--o| AnnualLeaveRequest : "Id (cascade)"
LeaveRequests ||--o| SickLeaveRequest : "Id (cascade)"
TPH (Table-per-Hierarchy) tek tablo üretir ve bedelini optionality'de öder: BalanceYear C#'ta int olduğu halde kolonda int? olur, çünkü hastalık izni satırlarında boş kalmak zorunda. TPT (Table-per-Type) her alt tip için ayrı tablo üretir ve alt tablolarla taban tablo arasındaki ilişki identifying 1:0..1'dir: SickLeaveRequest.Id hem PK hem FK. Yani TPT, ER diyagramında weak entity'lerle çizilir. Hangisini ne zaman seçeceğini EF Core serisinin "Inheritance Mapping: TPH, TPT ve Hangisini Ne Zaman Seçmeli" yazısında ölçerek karşılaştırmıştık.
Mermaid erDiagram'ı EF Core Modelinden Üretmek
ER diyagramının en büyük düşmanı bayatlamaktır. Kolonu migration'la eklersin, diyagramı unutursun. Tablo seviyesindeki ER zaten kodda yaşıyor: migration'ları üreten EF Core modeli. O halde onu elle çizmek yerine modelden üretmek mantıklı. EF Core'un relational model API'si (IModel.GetRelationalModel()) migration'ların da kullandığı tablo, kolon ve FK bilgisini verir:
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace HrPortal.Tools;
public static class MermaidErWriter
{
public static string Write(IModel model)
{
var sb = new StringBuilder("erDiagram\n");
var tables = model.GetRelationalModel().Tables.OrderBy(t => t.Name).ToList();
foreach (var table in tables)
{
var pk = table.PrimaryKey!.Columns;
sb.AppendLine($" {table.Name} {{");
foreach (var column in table.Columns)
{
List<string> keys = [];
if (pk.Contains(column)) keys.Add("PK");
if (table.ForeignKeyConstraints.Any(fk => fk.Columns.Contains(column))) keys.Add("FK");
if (table.Indexes.Any(i => i.IsUnique && i.Columns.Contains(column))) keys.Add("UK");
var type = column.StoreType.Replace(" ", "") + (column.IsNullable ? "?" : "");
List<string> parts = [type, column.Name];
if (keys.Count > 0) parts.Add(string.Join(", ", keys));
if (column.ComputedColumnSql is { } sql) parts.Add($"\"computed: {sql}\"");
sb.AppendLine(" " + string.Join(" ", parts));
}
sb.AppendLine(" }");
}
foreach (var table in tables)
{
var pk = table.PrimaryKey!.Columns;
foreach (var fk in table.ForeignKeyConstraints.OrderBy(f => f.Name))
{
// Principal tarafı: FK nullable ise 0..1, değilse tam 1
var left = fk.Columns.Any(c => c.IsNullable) ? "|o" : "||";
// Dependent tarafı: FK unique ise en fazla 1, değilse çok
var unique = fk.Columns.SequenceEqual(pk)
|| table.Indexes.Any(i => i.IsUnique && i.Columns.SequenceEqual(fk.Columns));
var right = unique ? "o|" : "o{";
// FK, PK'nin parçasıysa identifying (düz çizgi), değilse non-identifying (kesikli)
var line = fk.Columns.All(pk.Contains) ? "--" : "..";
var rule = fk.OnDeleteAction == ReferentialAction.Cascade ? "cascade" : "restrict";
var by = string.Join(",", fk.Columns.Select(c => c.Name));
sb.AppendLine($" {fk.PrincipalTable.Name} {left}{line}{right} {table.Name} : \"{by} ({rule})\"");
}
}
return sb.ToString();
}
}
Kod, yazının başından beri anlattığımız kuralları birebir uyguluyor: nullable FK principal tarafta |o, unique FK dependent tarafta o|, PK'nin parçası olan FK düz çizgi. Bağlantı cümlesi gerekmiyor, UseSqlServer ile kurulan options yeterli; model veritabanına gitmeden oluşuyor. Gerçek koşunun ilişki kısmı:
Employees ||..o{ Approvals : "ApproverId (restrict)"
LeaveRequests ||..o{ Approvals : "LeaveRequestId (cascade)"
Employees ||..o{ BalanceAdjustments : "AdjustedById (restrict)"
LeaveBalances ||..o{ BalanceAdjustments : "LeaveBalanceId (cascade)"
Employees ||..o{ Departments : "DirectorId (restrict)"
Departments ||..o{ Employees : "DepartmentId (restrict)"
Employees |o..o{ Employees : "ManagerId (restrict)"
LeaveRequests ||--o| LeaveAttachments : "LeaveRequestId (cascade)"
Employees ||..o{ LeaveBalances : "EmployeeId (restrict)"
LeaveTypes ||..o{ LeaveBalances : "LeaveTypeId (restrict)"
Employees ||..o{ LeaveRequests : "EmployeeId (restrict)"
LeaveTypes ||..o{ LeaveRequests : "LeaveTypeId (restrict)"
ve LeaveBalances kutusu:
LeaveBalances {
int Id PK
int EmployeeId FK, UK
int EntitledDays
int LeaveTypeId FK, UK
int RemainingDays "computed: [EntitledDays] - [UsedDays]"
int UsedDays
int Year UK
}
Üretilen dosya mmdc ile dokuz tabloluk bir diyagram olarak render ediliyor. İki sınırını bil: relational model kolonları ada göre sıralı verir, tablodaki sırayla değil; ve Mermaid'de bileşik unique key diye bir kavram yok, (EmployeeId, LeaveTypeId, Year) üçlüsü üç ayrı UK olarak görünüyor. Üretilen diyagram fiziksel modeldir; kavramsal modeli, yani Chen'i ya da domain class diagram'ını yerine koymaz. Bu dosyayı repo'da tutup CI (Continuous Integration) içinde "güncel mi?" diye kontrol etmeyi serinin finalinde kuracağız.
Sık Düşülen Tuzaklar
Zorunlu ilişkilerle döngü kurmak. Açılıştaki soruya dönelim. Departman ile direktörünü aynı SaveChanges'ta eklemenin gerçek sonucu:
Unable to save changes because a circular dependency was detected in the data to be saved: 'Department [Added] <-
ForeignKeyConstraint { 'DepartmentId' } Employee [Added] <-
ForeignKeyConstraint { 'DirectorId' } Department [Added]To show additional information call 'DbContextOptionsBuilder.EnableSensitiveDataLogging'.'.
Elle SQL yazmak da kurtarmaz: departmanı eklemek için var olan bir çalışanın Id'si, çalışanı eklemek için var olan bir departmanın Id'si lazım ve iki kolon da NOT NULL. SQL Server'da deferred constraint olmadığı için ilk satır hiç eklenemez. ER diyagramında bu, iki ucu da || olan bir döngüdür ve hata diyagramdaydı, kodda değil. Çözüm döngüdeki bir ucu optional yapmak: Department.DirectorId int? olur, ER'de Employees |o..o{ Departments diye çizilir ve kayıt iki adımda girilir:
await using var tx = await db.Database.BeginTransactionAsync();
// 1) Departman direktörsüz (DirectorId NULL) eklenir
var engineering = new Department { Name = "Engineering" };
db.Add(engineering);
await db.SaveChangesAsync();
// 2) Çalışan eklenir ve departmanın direktörü olarak atanır
var grace = new Employee
{
EmployeeNumber = "E-0007", FullName = "Grace Hopper", Email = "grace@example.com",
Department = engineering
};
engineering.Director = grace;
await db.SaveChangesAsync();
await tx.CommitAsync();
Console.WriteLine($"Department {engineering.Id}: director {engineering.DirectorId}; employee {grace.Id}: department {grace.DepartmentId}");
Department 1: director 1; employee 1: department 1
Bu, "direktörsüz departman" diye bir iş durumunu modele kabul etmek demek; bunu iş birimiyle konuşmak gerekir. Modeli değiştirmeden diyagramdan okunabilen bir döngüyü koda bırakmak ise bir sonraki sprintin bug'ıdır.
Fiziksel modeli kavramsal model sanmak. Approvals tablosu, RemainingDays computed kolonu, Period_Start gibi complex type kolonları fiziksel kararlardır. İş birimine üretilen ER'yi gösterirsen "Period_End nedir?" sorusuyla toplantı biter. Kavram konuşmasında Chen ya da domain model, tablo konuşmasında Crow's Foot.
Optionality'yi FK'ye bakmadan çizmek. Diyagramda || çizip kolonu nullable bırakmak (ya da tersi) en sık rastlanan tutarsızlık. Diyagramı modelden üretmek bu sınıf hatayı ortadan kaldırır, çünkü işaret doğrudan IsNullable'dan gelir.
Cascade'i her yere koymak. Composition olmayan bir association'da cascade, bir çalışanı silmenin onun onayladığı bütün kayıtları silmesi demektir. Ayrıca SQL Server aynı tabloya giden birden fazla cascade yolunu (multiple cascade paths) reddeder. Cascade'i yalnız dolu elmasın olduğu yerde kullan.
ER'yi normalizasyon aracı sanmak. ER diagram mevcut yapıyı gösterir; bir tablonun neden ikiye bölünmesi gerektiğini söylemez. Normal formlar ve fiziksel veri modeli kararları ilerideki Data Modeling ve Normalizasyon serisinin konusu.
Küçük Bir Deneme
★☆☆ BalanceAdjustment'ı Chen notasyonunda çiz. Weak entity mi, yoksa kendi kimliği olan bir entity mi? Kararını BalanceAdjustments tablosunun PK'sine bakarak gerekçelendir ve PlantUML'in @startchen söz dizimiyle render et.
★★☆ Yazıdaki generator'a bileşik unique index desteği ekle: (EmployeeId, LeaveTypeId, Year) gibi index'leri kolon başına UK yerine kutunun altına bir Mermaid yorum satırı ya da attribute comment'i olarak yaz. Çıktıyı mmdc ile render edip hâlâ geçerli olduğunu doğrula.
★★★ Approvals'ın PK'sini (LeaveRequestId, Step) yap ve Id'yi kaldır. Generator'ın çizdiği çizgi nasıl değişti? Migration'ın ürettiği SQL'i incele ve bu değişikliğin API'deki GET /leave-requests/{id} cevabını, EF Core'daki change tracking'i ve gelecekte "aynı adımda ikinci onay" requirement'ını nasıl etkileyeceğini yaz.
Bu yazıda bir ER diyagramını ilk kez elle değil koddan ürettik. Bir sonraki yazıda aynı fikri bütün diyagramlara genişletiyoruz: PlantUML ile diagrams-as-code, serideki use case, class, sequence, state ve C4 (Context, Containers, Components, Code) diyagramlarının kaynak dosyaları, ortak stil için !include, C4-PlantUML ve diyagramların bir PR (pull request) içinde GitHub Actions ile kontrol edilip render edilmesi.