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:
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.