One-to-Many munosabat — navigation property, foreign key, Include

~25 daq · .NET Backend · Entity Framework Core — Ma'lumotlar Bazasi

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 key int (nullable emas) — munosabat majburiy: mahsulot kategoriyasiz bo'lmaydi. Ixtiyoriy bo'lsa — int? KategoriyaId va Kategoriya? 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'pincha Restrict + 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.Mahsulotlar ni 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+