Entity Framework a vazba N:N

Tomáš Holan       02.12.2013       Entity Framework       14998 zobrazení

Pokud pro přístup k datům používáme Entity Framework, přesněji jeho variantu code first, mohli jsme se setkat s následujícím problémem s vazbou N:N.

Mějme například následující datový model a jemu odpovídající code first třídy pro jednotlivé entity:

Bonusy

internal sealed class OsobaHodnoceni
{
    #region OsobaHodnoceniConfiguration
    internal sealed class OsobaHodnoceniConfiguration : EntityTypeConfiguration<OsobaHodnoceni>
    {
        #region constructors and destructors
        public OsobaHodnoceniConfiguration()
        {
            //Primary Key
            this.HasKey(t => t.oh_IDOsobaHodnoceni);

            //Table & Column Mappings
            this.ToTable("OsobaHodnoceni");
            this.Property(t => t.oh_IDOsobaHodnoceni).HasColumnName("oh_IDOsobaHodnoceni");
            this.Property(t => t.oh_Rok).HasColumnName("oh_Rok");
            this.Property(t => t.oh_Mesic).HasColumnName("oh_Mesic");
            this.Property(t => t.oh_IDOsoby).HasColumnName("oh_IDOsoby");

            //Relationships
            this.HasMany(t => t.BonusSubjektivni)
                .WithMany()
                .Map(m =>
                {
                    m.ToTable("OsobaHodnoceni_BonusSubjektivni");
                    m.MapLeftKey("ohuIDOsobaHodnoceni");
                    m.MapRightKey("ohuIDBonusSubjektivni");
                });
        }
        #endregion
    }
    #endregion

    #region constructors and destructors
    public OsobaHodnoceniEntity()
    {
        this.BonusSubjektivni = new List<BonusSubjektivniEntity>();
    }
    #endregion

    #region member varible and default property initialization
    public int oh_IDOsobaHodnoceni { get; set; }
    public int oh_Rok { get; set; }
    public int oh_Mesic { get; set; }
    public int oh_IDOsoby { get; set; }

    public ICollection<BonusSubjektivniEntity> BonusSubjektivni { get; set; }
    #endregion
}

internal sealed class BonusSubjektivni
{
    #region BonusSubjektivniConfiguration
    internal sealed class BonusSubjektivniConfiguration : EntityTypeConfiguration<BonusSubjektivni>
    {
        #region constructors and destructors
        public BonusSubjektivniConfiguration()
        {
            //Primary Key
            this.HasKey(t => t.bsuIDBonusSubjektivni);

            //Properties
            this.Property(t => t.bsuOznaceni)
                .IsRequired()
                .HasMaxLength(70);

            //Table & Column Mappings
            this.ToTable("BonusSubjektivni");
            this.Property(t => t.bsuIDBonusSubjektivni).HasColumnName("bsuIDBonusSubjektivni");
            this.Property(t => t.bsuOznaceni).HasColumnName("bsuOznaceni");
        }
        #endregion
    }
    #endregion

    #region member varible and default property initialization
    public int bsuIDBonusSubjektivni { get; set; }
    public string bsuOznaceni { get; set; }
    #endregion
}

A třída pro datový kontext bude:

internal class OsobniHodnoceniContext : DbContext
{
    #region member varible and default property initialization
    public DbSet<OsobaHodnoceni> OsobaHodnoceni { get; set; }
    public DbSet<BonusSubjektivni> BonusSubjektivni { get; set; }
    #endregion

    #region private member functions
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new OsobaHodnoceni.OsobaHodnoceniConfiguration());
        modelBuilder.Configurations.Add(new BonusSubjektivni.BonusSubjektivniConfiguration());
    }
    #endregion
}

Datový model obsahuje dvě tabulky OsobaHodnoceni a BonusSubjektivní, mezi kterými je vazba N:N realizována třetí tabulkou OsobaHodnoceni_BonusSubjektivní. Pro účely našeho příkladu je datový model maximálně zjednodušený, ale dá se říct, že realizuje aplikaci pro měsíční osobní hodnocení zaměstnanců. Můžeme si to představit tak, že tabulka BonusSubjektivní je číselník nějakých položek, které můžeme zaměstnancům přidávat, a tabulka OsobaHodnoceni nese vlastní hodnocení pro konkrétního zaměstnance a měsíc. Vazba na zaměstnance je realizovaná pomoci IDOsoby, reálná aplikace by tedy měla ještě i tabulku Osoba apod.

Odpovídající třídy OsobaHodnoceni a BonusSubjektivní jsou pouze trochu “učesané” výchozí třídy, které lze automaticky vygenerovat (*). Vazební tabulce OsobaHodnoceni_BonusSubjektivní žádná třída neodpovídá, místo toho je ve třídě OsobaHodnoceniConfiguration vazba popsána pomoci fluent syntaxe metod HasMany, WithMany a Map.

S takto vytvořenými objekty se dá vcelku rozumně pracovat, bohužel až na jednu věc.

Předpokládejme, že při hodnocení nějakého zaměstnance nabídneme uživateli naší aplikace seznam všech položek z tabulky BonusSubjektivni a on některé z nich vybere. Pro jejich přidání k hodnocení pak budeme mít nějakou metodu. Co když ale tato metoda bude dostávat pouze seznam IDček vybraných položek, nikoliv seznam celých objektů – dříve načtených entit:

public static void AddBonusSubjektivni(int IDOsobaHodnoceni, IEnumerable<int> IDBonusSubjektivniList)
{
    using (var context = new OsobniHodnoceniContext())
    {
        var osobaHodnoceni = context.OsobaHodnoceni.First(o => o.oh_IDOsobaHodnoceni == IDOsobaHodnoceni);

        foreach (int IDBonusSubjektivni in IDBonusSubjektivniList)
        {
            var bonusSubjektivni = context.BonusSubjektivni.First(o => o.bsuIDBonusSubjektivni == IDBonusSubjektivni);
            osobaHodnoceni.BonusSubjektivni.Add(bonusSubjektivni);  //<--Metoda potřebuje načíst celý objekt entity nikoliv pouze ID
        }

        context.SaveChanges();
    }
}

To je problém, protože EF pro přidání položky potřebuje celou položku, samotné ID mu nestačí. Jak kód ukazuje, v důsledku to znamená spouštět opakovaně nad databází samostatný dotaz pro načtení entity odpovídající jednotlivému ID, což je samozřejmě velmi neefektivní. Přitom k tomu reálně není žádný důvod, pro zápis do vazební tabulky by nám přece ID mělo úplně stačit.

Pro řešení resp. obejití tohoto problému je potřeba na úrovni EF zavést i vazební tabulku OsobaHodnoceni_BonusSubjektivní jako entitu.

Update: Jak se ukázalo toto řešení není jediné, problém lze řešit i pro původní entity třídy pomoci metody Attach (viz. komentáře k tomuto článku).

internal sealed class OsobaHodnoceni
{
    #region OsobaHodnoceniEntityConfiguration
    internal sealed class OsobaHodnoceniConfiguration : EntityTypeConfiguration<OsobaHodnoceni>
    {
        #region constructors and destructors
        public OsobaHodnoceniConfiguration()
        {
            //Primary Key
            this.HasKey(t => t.oh_IDOsobaHodnoceni);

            //Table & Column Mappings
            this.ToTable("OsobaHodnoceni");
            this.Property(t => t.oh_IDOsobaHodnoceni).HasColumnName("oh_IDOsobaHodnoceni");
            this.Property(t => t.oh_Rok).HasColumnName("oh_Rok");
            this.Property(t => t.oh_Mesic).HasColumnName("oh_Mesic");
            this.Property(t => t.oh_IDOsoby).HasColumnName("oh_IDOsoby");
        }
        #endregion
    }
    #endregion

    #region constructors and destructors
    public OsobaHodnoceni()
    {
        this.BonusSubjektivni = new List<OsobaHodnoceni_BonusSubjektivni>();
    }
    #endregion

    #region member varible and default property initialization
    public int oh_IDOsobaHodnoceni { get; set; }
    public int oh_Rok { get; set; }
    public int oh_Mesic { get; set; }
    public int oh_IDOsoby { get; set; }

    public ICollection<OsobaHodnoceni_BonusSubjektivni> BonusSubjektivni { get; set; }
    #endregion
}

internal sealed class OsobaHodnoceni_BonusSubjektivni
{
    #region OsobaHodnoceni_BonusSpecifickyEntityConfiguration
    internal sealed class OsobaHodnoceni_BonusSubjektivniConfiguration : EntityTypeConfiguration<OsobaHodnoceni_BonusSubjektivni>
    {
        #region constructors and destructors
        public OsobaHodnoceni_BonusSubjektivniConfiguration()
        {
            //Primary Key
            this.HasKey(t => new { t.ohuIDOsobaHodnoceni, t.ohuIDBonusSubjektivni });

            //Table & Column Mappings
            this.ToTable("OsobaHodnoceni_BonusSpecificky");
            this.Property(t => t.ohuIDOsobaHodnoceni).HasColumnName("ohuIDOsobaHodnoceni");
            this.Property(t => t.ohuIDBonusSubjektivni).HasColumnName("ohuIDBonusSubjektivni");

            //Relationships
            this.HasRequired(t => t.BonusSubjektivni)
                .WithMany()
                .HasForeignKey(d => d.ohuIDBonusSubjektivni);
            this.HasRequired(t => t.OsobaHodnoceni)
                .WithMany(t => t.BonusSubjektivni)
                .HasForeignKey(d => d.ohuIDOsobaHodnoceni);
        }
        #endregion
    }
    #endregion

    #region member varible and default property initialization
    public int ohuIDOsobaHodnoceni { get; set; }
    public OsobaHodnoceni OsobaHodnoceni { get; set; }
    public int ohuIDBonusSubjektivni { get; set; }
    public BonusSubjektivni BonusSubjektivni { get; set; }
    #endregion
}

internal class OsobniHodnoceniContext : DbContext
{
    #region member varible and default property initialization
    public DbSet<OsobaHodnoceni> OsobaHodnoceni { get; set; }
    public DbSet<BonusSubjektivni> BonusSubjektivni { get; set; }
    #endregion

    #region private member functions
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new OsobaHodnoceni.OsobaHodnoceniConfiguration());
        modelBuilder.Configurations.Add(new BonusSubjektivni.BonusSubjektivniConfiguration());
        modelBuilder.Configurations.Add(new OsobaHodnoceni_BonusSubjektivni.OsobaHodnoceni_BonusSubjektivniConfiguration());
    }
    #endregion
}

S takto změněnou třídou OsobaHodnoceni, doplněnou novou třídou OsobaHodnoceni_BonusSubjektivni a lehce rozšířeným db kontextem bude již situace jiná.

Původní metodu AddBonusSubjektivni můžeme nyní upravit následovně:

public static void AddBonusSubjektivni(int IDOsobaHodnoceni, IEnumerable<int> IDBonusSubjektivniList)
{
    using (var context = new OsobniHodnoceniContext())
    {
        var osobaHodnoceni = context.OsobaHodnoceni.First(o => o.oh_IDOsobaHodnoceni == IDOsobaHodnoceni);

        foreach (int IDBonusSubjektivni in IDBonusSubjektivniList)
        {
            osobaHodnoceni.BonusSubjektivni.Add(new OsobaHodnoceni_BonusSubjektivni() { ohuIDBonusSubjektivni = IDBonusSubjektivni });
        }

        context.SaveChanges();
    }
}

Nyní je všechno v pořádku, protože přidávanou “položku”, kterou je nyní instance třídy OsobaHodnoceni_BonusSubjektivni, můžeme snadno vytvořit a nastavit jí jedinou požadovanou vlastnost ohuIDBonusSubjektivni.

Z tohoto důvodu vřele doporučuji pro každou N:N vazební tabulku zavést vždy i entity třídu. Jednak se zbavíme problémů v výchozí realizací vazby N:N v EF a navíc si do budoucna ušetříme přepisování kódu, pokud budeme potřebovat do vazební tabulky někdy později přidat nějaký další údaj.


(*) Pokud chcete v EF code first použít již existující databázi, obecně je možné si entity třídy vygenerovat pomoci funkce "Reverse Engineer Code First" z EF Power Tools.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Použití metody Attach

Nedávno jsem řešil podobný problém a vyřešil ho pomocí metody DbSet<T>.Attach(T entity).

Jake jsou nevýhody v tomto přistupu oproti Vašemu, kromě nemožnosti si rozšířit vazební tabulku.

public static void AddBonusSubjektivni(int IDOsobaHodnoceni, IEnumerable<int> IDBonusSubjektivniList)
{
    using (var context = new OsobniHodnoceniContext())
    {
        var osobaHodnoceni = context.OsobaHodnoceni.First(o => o.oh_IDOsobaHodnoceni == IDOsobaHodnoceni);
 
        foreach (int IDBonusSubjektivni in IDBonusSubjektivniList)
        {
            var bonusSubjektivni = new BonusSubjektivni(){Id=IDBonusSubjektivni};
            context.BonusSubjektivni.Attach(bonusSubjektivni);
            osobaHodnoceni.BonusSubjektivni.Add(bonusSubjektivni);  //<--Metoda potřebuje načíst celý objekt entity nikoliv pouze ID
        }
 
        context.SaveChanges();
    }
}
nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Tak super, to mi nějak uniklo, že by to takhle přes Attach mohlo jít. Když jsem to tenkrát hledat, toto řešení jsem nenašel. Díky.

Tedy pak zůstává jen ten druhý důvod, který je samozřejmě na posouzení každého, jak moc jsou takové změny v konkrétním projektu pravděpodobné.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback