One-to-One munosabat — HasOne, WithOne, FK joylashuvi

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

Muammodan boshlaylik

Foydalanuvchi jadvalida 40 ta ustun: login ma'lumotlari, profil (bio, rasm, ijtimoiy tarmoqlar), sozlamalar (til, mavzu, bildirishnomalar), yetkazib berish manzili...

  • Har bir login so'rovida bio va rasm URL lari ham yuklanadi
  • Ko'p ustunlar ko'pchilik foydalanuvchilarda null
  • Profil va xavfsizlik ma'lumotlari aralash — tasodifan profil API sida parol xeshi qaytib qolishi oson

Bir ob'ektga bitta qo'shimcha ob'ekt to'g'ri keladi — lekin ularni alohida saqlash kerak.


One-to-one nima?

Bitta yozuvga ko'pi bilan bitta bog'liq yozuv:

  • Foydalanuvchi ↔ Profil
  • Buyurtma ↔ Yetkazib berish ma'lumoti
  • Xodim ↔ Ish o'rni (stol)

Hayotiy o'xshatma

Fuqaro va uning pasporti. Har bir fuqaroga bitta amaldagi pasport, har bir pasport — bitta fuqaroniki. Pasport alohida hujjat sifatida saqlanadi (alohida jadval), va unda egasining raqami yozilgan (foreign key) — fuqaroning tug'ilganlik guvohnomasida pasport raqami yozilmaydi.


Qaysi tomonda foreign key?

One-to-one da bir tomon — asosiy (principal), ikkinchisi — bog'liq (dependent). Foreign key bog'liq tomonda bo'ladi:

  • Profil foydalanuvchisiz mavjud bo'la olmaydi → profil — bog'liq, FK profilda
  • Foydalanuvchi profilsiz mavjud bo'la oladi → foydalanuvchi — asosiy
public class Foydalanuvchi
{
    public int Id { get; set; }
    public string Email { get; set; } = "";
    public string ParolXesh { get; set; } = "";

    public Profil? Profil { get; set; }              // ixtiyoriy — bo'lmasligi mumkin
}

public class Profil
{
    public int Id { get; set; }
    public string? Bio { get; set; }
    public string? RasmUrl { get; set; }
    public string Til { get; set; } = "uz";

    public int FoydalanuvchiId { get; set; }          // FK — bog'liq tomonda
    public Foydalanuvchi Foydalanuvchi { get; set; } = null!;
}

Fluent API

mb.Entity<Foydalanuvchi>()
    .HasOne(f => f.Profil)
    .WithOne(p => p.Foydalanuvchi)
    .HasForeignKey<Profil>(p => p.FoydalanuvchiId)   // <Profil> — FK qaysi entity da
    .OnDelete(DeleteBehavior.Cascade);

One-to-one da EF Core FK qaysi tomonda ekanini o'zi har doim aniqlay olmaydi — HasForeignKey<T> bilan aniq ko'rsating. EF Core FoydalanuvchiId ga unique index qo'yadi — bitta foydalanuvchiga ikkinchi profil yozib bo'lmaydi.


Shared primary key

Bog'liq jadvalning primary key i — ayni paytda foreign key:

public class Profil
{
    public int FoydalanuvchiId { get; set; }          // PK va FK
    public Foydalanuvchi Foydalanuvchi { get; set; } = null!;
    public string? Bio { get; set; }
}

mb.Entity<Profil>().HasKey(p => p.FoydalanuvchiId);
mb.Entity<Foydalanuvchi>()
    .HasOne(f => f.Profil)
    .WithOne(p => p.Foydalanuvchi)
    .HasForeignKey<Profil>(p => p.FoydalanuvchiId);

Alohida Id ustuni yo'q; profil ID si = foydalanuvchi ID si.


Ishlatish

// birga yaratish
var f = new Foydalanuvchi
{
    Email = "lola@mail.uz",
    ParolXesh = xesh,
    Profil = new Profil { Bio = "Backend dasturchi", Til = "uz" }
};
db.Foydalanuvchilar.Add(f);
await db.SaveChangesAsync(ct);

// profil bilan o'qish
var u = await db.Foydalanuvchilar
    .Include(x => x.Profil)
    .FirstOrDefaultAsync(x => x.Id == id, ct);
Console.WriteLine(u?.Profil?.Bio ?? "Bio yo'q");

// login — profilsiz, faqat kerakli ustunlar
var login = await db.Foydalanuvchilar
    .Where(x => x.Email == email)
    .Select(x => new { x.Id, x.ParolXesh })
    .FirstOrDefaultAsync(ct);

// profilni alohida yangilash
var profil = await db.Set<Profil>().FirstOrDefaultAsync(p => p.FoydalanuvchiId == userId, ct);
profil ??= db.Set<Profil>().Add(new Profil { FoydalanuvchiId = userId }).Entity;
profil.Bio = dto.Bio;
await db.SaveChangesAsync(ct);

Owned types — alohida jadvalsiz

Ma'lumot har doim egasi bilan birga bo'lsa va alohida identifikatsiyaga ega bo'lmasa (manzil, pul summasi) — owned type:

public class Manzil
{
    public string Shahar { get; set; } = "";
    public string Kocha { get; set; } = "";
    public string? Uy { get; set; }
}

public class Buyurtma
{
    public int Id { get; set; }
    public Manzil YetkazishManzili { get; set; } = new();
}

mb.Entity<Buyurtma>().OwnsOne(b => b.YetkazishManzili);

Bazada — o'sha jadvalning ustunlari: YetkazishManzili_Shahar, YetkazishManzili_Kocha. Kodda — tartibli alohida klass. Include kerak emas — doim birga yuklanadi.

[!INFO] EF Core 8+ da complex types (ComplexProperty) ham bor — owned type ga o'xshash, lekin qiymat semantikasi bilan (identifikatsiyasiz). Manzil, koordinata, pul kabi qiymat ob'ektlari uchun yangi va tavsiya etilgan usul.


Qachon alohida jadval

Holat Tanlov
Ma'lumot kamdan-kam kerak (bio, katta matn) One-to-one — alohida jadval
Ko'pchilik yozuvlarda yo'q (ixtiyoriy) One-to-one
Xavfsizlik bo'yicha ajratish (maxfiy va ochiq) One-to-one
Har doim birga, identifikatsiyasiz (manzil) Owned / complex type
Doim birga va kam ustun Bitta jadvaldagi oddiy ustunlar

[!TIP] One-to-one ni haddan tashqari ko'p ishlatmang: har bir alohida jadval — qo'shimcha JOIN. Ajratish — haqiqiy sabab bo'lganda: performance, ixtiyoriylik yoki xavfsizlik.


Tez-tez uchraydigan xatolar

FK tomoni noto'g'ri:

The dependent side could not be determined for the one-to-one relationship between 'Foydalanuvchi.Profil' and 'Profil.Foydalanuvchi'.

HasForeignKey<Profil>(...) bilan aniq ko'rsating.

Ikkinchi profil qo'shish — unique index buzilishi; mavjudini yangilang.

Include siz — Profil doim null ko'rinadi.


Xulosa

One-to-one — bitta yozuvga ko'pi bilan bitta bog'liq yozuv. FK — bog'liq (dependent) tomonda; Fluent API: HasOne().WithOne().HasForeignKey<Bog'liq>(), EF Core unique index qo'yadi. Shared primary key — bog'liq jadval PK si = FK.

Qo'shish — navigation orqali birga; o'qish — Include yoki Select (login da profil kerak emas). Har doim birga, identifikatsiyasiz ma'lumot — owned (OwnsOne) yoki complex type. Alohida jadval — performance, ixtiyoriylik yoki xavfsizlik uchun.


Keyingi dars

57-dars: Repository Pattern — IRepository<T>, GenericRepository, abstraktsiya