One-to-Many munosabat — navigation property, foreign key, Include
Muammodan boshlaylik
Har bir mahsulot bitta kategoriyaga tegishli, bitta kategoriyada esa ko'p mahsulot. Har bir buyurtmada ko'p qator, har bir mijozda ko'p buyurtma. Ma'lumotlar bazasida bu — jadvallar orasidagi munosabat.
Faqat ID bilan ishlasangiz:
var m = await db.Mahsulotlar.FindAsync(id);
var k = await db.Kategoriyalar.FindAsync(m!.KategoriyaId); // qo'shimcha so'rov
Console.WriteLine(quot;{m.Nomi} — {k!.Nomi}");
Har safar qo'lda bog'lash, va kategoriyani o'chirsangiz — mahsulotlar "yetim" qoladi. Kerak: C# da m.Kategoriya.Nomi va k.Mahsulotlar deb yozish, va bazada butunlikni saqlash.
Hayotiy o'xshatma
Maktab va sinflar. Har bir o'quvchining kartochkasida sinf raqami yozilgan (foreign key). Sinf jurnalida esa barcha o'quvchilar ro'yxati (kolleksiya). O'quvchidan "qaysi sinfdasan?" (navigation) va sinfdan "kimlar o'qiydi?" (teskari navigation) — ikkala tomondan ham javob topish mumkin.
Entity lar
public class Kategoriya
{
public int Id { get; set; }
public string Nomi { get; set; } = "";
public List<Mahsulot> Mahsulotlar { get; set; } = new(); // "ko'p" tomoni — kolleksiya
}
public class Mahsulot
{
public int Id { get; set; }
public string Nomi { get; set; } = "";
public decimal Narx { get; set; }
public int KategoriyaId { get; set; } // foreign key
public Kategoriya Kategoriya { get; set; } = null!; // navigation property
}
| A'zo | Ma'nosi |
|---|---|
KategoriyaId |
Foreign key — bazadagi ustun |
Kategoriya |
Reference navigation — ota ob'ektga havola |
Mahsulotlar |
Collection navigation — bola ob'ektlar ro'yxati |
EF Core konvensiya bo'yicha KategoriyaId + Kategoriya juftligidan munosabatni o'zi tushunadi. Migratsiya:
CREATE TABLE "Mahsulotlar" (
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
"KategoriyaId" integer NOT NULL,
...
CONSTRAINT "FK_Mahsulotlar_Kategoriyalar_KategoriyaId"
FOREIGN KEY ("KategoriyaId") REFERENCES "Kategoriyalar" ("Id") ON DELETE CASCADE
);
CREATE INDEX "IX_Mahsulotlar_KategoriyaId" ON "Mahsulotlar" ("KategoriyaId");
[!INFO]
= null!— "bu yerda null bo'lmaydi, EF Core to'ldiradi" degan belgi (nullable ogohlantirishini o'chirish). Foreign keyint(nullable emas) — munosabat majburiy: mahsulot kategoriyasiz bo'lmaydi. Ixtiyoriy bo'lsa —int? KategoriyaIdvaKategoriya? Kategoriya.
Fluent API bilan aniq sozlash
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Mahsulot>()
.HasOne(m => m.Kategoriya)
.WithMany(k => k.Mahsulotlar)
.HasForeignKey(m => m.KategoriyaId)
.OnDelete(DeleteBehavior.Restrict);
}
O'chirish xatti-harakati
DeleteBehavior |
Kategoriya o'chirilsa |
|---|---|
Cascade |
Uning mahsulotlari ham o'chadi (majburiy munosabatda standart) |
Restrict |
Mahsulotlari bor kategoriyani o'chirib bo'lmaydi — xato |
SetNull |
Mahsulotlarda KategoriyaId = null (FK nullable bo'lishi kerak) |
[!WARNING]
Cascade— qulay, lekin xavfli: bitta kategoriyani o'chirish yuzlab mahsulotni va (zanjir bo'ylab) ularga bog'liq buyurtma qatorlarini ham o'chirib yuborishi mumkin. Biznes ma'lumotlari uchun ko'pinchaRestrict+ soft delete (52-dars) xavfsizroq.
Qo'shish
// FK orqali — eng oddiy
db.Mahsulotlar.Add(new Mahsulot { Nomi = "Choynak", Narx = 120_000m, KategoriyaId = 2 });
// navigation orqali — yangi kategoriya bilan birga
var k = new Kategoriya { Nomi = "Idishlar" };
k.Mahsulotlar.Add(new Mahsulot { Nomi = "Piyola", Narx = 25_000m });
k.Mahsulotlar.Add(new Mahsulot { Nomi = "Kosa", Narx = 30_000m });
db.Kategoriyalar.Add(k);
await db.SaveChangesAsync(ct); // avval kategoriya, keyin mahsulotlar — to'g'ri FK bilan
O'qish
Include
var mahsulot = await db.Mahsulotlar
.Include(m => m.Kategoriya)
.FirstOrDefaultAsync(m => m.Id == id, ct);
Console.WriteLine(mahsulot!.Kategoriya.Nomi);
var kategoriya = await db.Kategoriyalar
.Include(k => k.Mahsulotlar.Where(m => m.Faol).OrderBy(m => m.Narx)) // filtrlangan include
.FirstOrDefaultAsync(k => k.Id == id, ct);
Select — afzal
var royxat = await db.Kategoriyalar
.Select(k => new
{
k.Id,
k.Nomi,
MahsulotlarSoni = k.Mahsulotlar.Count,
EngArzon = k.Mahsulotlar.Min(m => (decimal?)m.Narx),
})
.ToListAsync(ct);
Bitta SQL so'rovda — COUNT va MIN bazada hisoblanadi, mahsulotlarning o'zi yuklanmaydi.
Filtr — munosabat bo'yicha
var telefonlar = await db.Mahsulotlar
.Where(m => m.Kategoriya.Nomi == "Telefonlar")
.ToListAsync(ct);
var boshKategoriyalar = await db.Kategoriyalar
.Where(k => !k.Mahsulotlar.Any())
.ToListAsync(ct);
JSON va siklik havolalar
[HttpGet("{id}")]
public async Task<Kategoriya?> Ol(int id) =>
await db.Kategoriyalar.Include(k => k.Mahsulotlar).FirstOrDefaultAsync(k => k.Id == id);
// ❌ JsonException: A possible object cycle was detected
// Kategoriya → Mahsulotlar → Kategoriya → Mahsulotlar → ...
Yechim — DTO (45-dars):
public record KategoriyaDto(int Id, string Nomi, List<MahsulotQisqaDto> Mahsulotlar);
[!TIP] Har bir munosabatda ikkala tomonda ham navigation property bo'lishi shart emas. Agar "kategoriyaning barcha mahsulotlari" hech qachon kerak bo'lmasa —
Kategoriya.Mahsulotlarni yozmang. Kamroq navigation — kamroq siklik havola va chalkashlik.
Tez-tez uchraydigan xatolar
Include siz navigation:
var m = await db.Mahsulotlar.FindAsync(id);
Console.WriteLine(m!.Kategoriya.Nomi); // ❌ NullReferenceException — Kategoriya yuklanmagan
Mavjud bo'lmagan FK:
DbUpdateException: insert or update on table "Mahsulotlar" violates foreign key constraint
KategoriyaId = 999 — bunday kategoriya yo'q. Avval tekshiring yoki 404/400 qaytaring.
Entity ni JSON ga — siklik havola; DTO.
Xulosa
One-to-many — bitta ota (kategoriya), ko'p bola (mahsulotlar). Bola tomonida: foreign key (KategoriyaId) + reference navigation (Kategoriya); ota tomonida: collection navigation (Mahsulotlar). EF Core konvensiya bo'yicha tushunadi; Fluent API — HasOne().WithMany().HasForeignKey().OnDelete().
O'chirish: Cascade, Restrict, SetNull — biznes ma'lumotlari uchun ehtiyot bo'ling. Qo'shish — FK yoki navigation orqali. O'qish — Include (filtrlangan ham) yoki yaxshisi Select bilan agregatlar. Navigation lar — Include siz yuklanmaydi; JSON ga — DTO.
Keyingi dars
55-dars: Many-to-Many munosabat — join table, pivot, EF Core 5+