Many-to-Many munosabat — join table, pivot, EF Core 5+

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

Muammodan boshlaylik

Blog: har bir maqolada bir nechta teg (csharp, dotnet, efcore), har bir tegda esa ko'p maqola. Do'kon: bir mahsulot bir nechta aksiyada, bir aksiyada ko'p mahsulot. Talabalar va kurslar.

Foreign key bitta ustun — u faqat bitta qiymatni saqlaydi. Maqolada TegId — faqat bitta teg. TegIdlar = "1,5,9" satri — qidirish, index va butunlik nazorati imkonsiz.


Join table

Relyatsion bazalarda many-to-many — oraliq (join, pivot) jadval orqali:

Maqolalar              MaqolaTeg              Teglar
┌────┬─────────┐      ┌──────────┬───────┐     ┌────┬────────┐
│ Id │ Sarlavha│      │ MaqolaId │ TegId │     │ Id │ Nomi   │
├────┼─────────┤      ├──────────┼───────┤     ├────┼────────┤
│ 1  │ EF Core │──┐   │    1     │   1   │  ┌──│ 1  │ csharp │
│ 2  │ LINQ    │  ├──▶│    1     │   3   │──┤  │ 2  │ api    │
└────┴─────────┘  │   │    2     │   1   │  └──│ 3  │ efcore │
                  └──▶└──────────┴───────┘     └────┴────────┘

Har bir qator — bitta bog'lanish. Ikki one-to-many munosabat (54-dars) birgalikda.


Hayotiy o'xshatma

Universitet: talabalar va kurslar. Talaba bir nechta kursga yoziladi, kursda ko'p talaba. Dekanatda alohida "yozilishlar" jurnali bor: har bir qatorda — talaba va kurs. Kursdan kimlar o'qishini yoki talaba qaysi kurslarga yozilganini — shu jurnaldan topasiz.


EF Core 5+ — skip navigation (oddiy holat)

public class Maqola
{
    public int Id { get; set; }
    public string Sarlavha { get; set; } = "";
    public List<Teg> Teglar { get; set; } = new();
}

public class Teg
{
    public int Id { get; set; }
    public string Nomi { get; set; } = "";
    public List<Maqola> Maqolalar { get; set; } = new();
}

Ikkala tomonda kolleksiya — EF Core join jadvalini (MaqolaTeg) o'zi yaratadi, uni klass sifatida yozish shart emas.

var csharp = new Teg { Nomi = "csharp" };
var efcore = new Teg { Nomi = "efcore" };

db.Maqolalar.Add(new Maqola { Sarlavha = "EF Core asoslari", Teglar = { csharp, efcore } });
await db.SaveChangesAsync(ct);
// INSERT Maqolalar, INSERT Teglar (x2), INSERT MaqolaTeg (x2)

Mavjud teglarni bog'lash

var maqola = await db.Maqolalar.Include(m => m.Teglar).FirstAsync(m => m.Id == id, ct);
var teglar = await db.Teglar.Where(t => dto.TegIdlar.Contains(t.Id)).ToListAsync(ct);

maqola.Teglar.Clear();
maqola.Teglar.AddRange(teglar);
await db.SaveChangesAsync(ct);       // MaqolaTeg da kerakli qo'shish va o'chirishlar

So'rovlar

// teg bo'yicha maqolalar
var maqolalar = await db.Maqolalar
    .Where(m => m.Teglar.Any(t => t.Nomi == "efcore"))
    .Select(m => new { m.Id, m.Sarlavha, Teglar = m.Teglar.Select(t => t.Nomi).ToList() })
    .ToListAsync(ct);

// eng mashhur teglar
var mashhur = await db.Teglar
    .OrderByDescending(t => t.Maqolalar.Count)
    .Take(10)
    .Select(t => new { t.Nomi, Soni = t.Maqolalar.Count })
    .ToListAsync(ct);

Join entity — qo'shimcha ma'lumot bilan

Bog'lanishning o'zida ma'lumot bo'lsa (talaba qachon yozildi, bahosi; savatdagi mahsulot soni) — join jadvalini aniq klass sifatida yozing:

public class Talaba
{
    public int Id { get; set; }
    public string Ism { get; set; } = "";
    public List<Yozilish> Yozilishlar { get; set; } = new();
}

public class Kurs
{
    public int Id { get; set; }
    public string Nomi { get; set; } = "";
    public List<Yozilish> Yozilishlar { get; set; } = new();
}

public class Yozilish
{
    public int TalabaId { get; set; }
    public Talaba Talaba { get; set; } = null!;

    public int KursId { get; set; }
    public Kurs Kurs { get; set; } = null!;

    public DateTime YozilganSana { get; set; } = DateTime.UtcNow;
    public int? Baho { get; set; }
}
protected override void OnModelCreating(ModelBuilder mb)
{
    mb.Entity<Yozilish>(e =>
    {
        e.HasKey(y => new { y.TalabaId, y.KursId });     // composite primary key
        e.HasOne(y => y.Talaba).WithMany(t => t.Yozilishlar).HasForeignKey(y => y.TalabaId);
        e.HasOne(y => y.Kurs).WithMany(k => k.Yozilishlar).HasForeignKey(y => y.KursId);
    });
}
db.Add(new Yozilish { TalabaId = 1, KursId = 5 });

var alochilar = await db.Yozilishlar
    .Where(y => y.KursId == 5 && y.Baho >= 5)
    .Select(y => y.Talaba.Ism)
    .ToListAsync(ct);

Composite key { TalabaId, KursId } — bir talaba bir kursga ikki marta yozila olmaydi.

[!TIP] Hozir qo'shimcha ma'lumot bo'lmasa ham, bog'lanish biznes ahamiyatiga ega bo'lsa (yozilish, a'zolik, savat qatori), boshidanoq join entity yarating. Keyinchalik "qachon qo'shilgan?" degan savol albatta tug'iladi, va skip navigation dan join entity ga o'tish — migratsiya va kodni qayta yozishni talab qiladi.


Ikkala usulni birga

mb.Entity<Maqola>()
    .HasMany(m => m.Teglar)
    .WithMany(t => t.Maqolalar)
    .UsingEntity<MaqolaTeg>(
        r => r.HasOne(mt => mt.Teg).WithMany().HasForeignKey(mt => mt.TegId),
        l => l.HasOne(mt => mt.Maqola).WithMany().HasForeignKey(mt => mt.MaqolaId),
        j => j.Property(mt => mt.QoshilganVaqt).HasDefaultValueSql("now()"));

maqola.Teglar orqali qulay navigatsiya va MaqolaTeg da qo'shimcha ustunlar.


Qaysi birini tanlash

Skip navigation Join entity
Kod Minimal — faqat ikki kolleksiya Uchinchi klass va konfiguratsiya
Bog'lanishda qo'shimcha ma'lumot ❌ ✅
Join jadvalga to'g'ridan-to'g'ri so'rov Cheklangan ✅
Misol Teglar, oddiy kategoriyalar Yozilishlar, savat, buyurtma qatorlari, a'zolik

[!INFO] Buyurtma qatorlari (BuyurtmaQatori: buyurtma + mahsulot + soni + narx) — klassik join entity. Narx buyurtma paytidagi qiymat sifatida qatorda nusxalanadi — keyin mahsulot narxi o'zgarsa ham, eski buyurtma o'zgarmaydi.


Tez-tez uchraydigan xatolar

Include siz kolleksiyani o'zgartirish:

var m = await db.Maqolalar.FindAsync(id);
m!.Teglar.Clear();          // kolleksiya yuklanmagan — bo'sh; eski bog'lanishlar o'chmaydi

Bir xil juftlikni ikki marta qo'shish — primary key buzilishi (DbUpdateException).

JSON ga entity — Maqola → Teglar → Maqolalar → ... — siklik havola; DTO.


Xulosa

Many-to-many — join jadval orqali (ikki one-to-many). EF Core 5+ da skip navigation: ikkala tomonda kolleksiya, join jadvali avtomatik; qo'shish — kolleksiyaga element qo'shib SaveChanges. Bog'lanishda qo'shimcha ma'lumot bo'lsa — join entity: aniq klass, composite key (HasKey(x => new { A, B })), ikki HasOne().WithMany().

UsingEntity — ikkalasini birlashtirish. Biznes ahamiyatli bog'lanishlar uchun boshidanoq join entity. Kolleksiyani o'zgartirishdan oldin Include; JSON uchun DTO.


Keyingi dars

56-dars: One-to-One munosabat — HasOne, WithOne, FK joylashuvi