EF6 Löschen über Navigationspunkte

Entity Framework und Löschen aus einer 1:n Collection

Versionsgeschichte
Version Aktualisierungsdatum
0.1 (Erste Fassung) 26.05.2014

Beim direkten Löschen eines Kindselementes aus der Elternelement heraus mit parent.Childs.Remove(child); löscht zwar die Beziehung zwischen den Beiden (Child.Parent = null), aber nicht das Kindselement selbst. Das führt beim Speichern unweigerlich zu einem Fehler, wenn in der Datenbankdefinition das Foreign Key nicht NULL sein darf. In diesem Tutorial zeige ich zwei gangbare Lösungen, wie man dieses Problem umschiffen kann. Hoffentlich wird es bald in EF möglich sein, dieses Verhalten auch als Standard zu konfigurieren.

Problembeschreibung

Nehmen wir ein einfaches Modell für Entity Framework (Code First), dass aus eine Tabelle für die Kategorie (mit ID, Name, Beschreibung und Aktive-Flag besteht) und einer zweiten Tabelle für die Unterkategorien (mit ID, Kategorie ID, Name, Beschreibung und Aktive-Flag besteht). Die Kategorie ID (Foreign Key) in der Unterkategorie Tabelle darf dabei nicht NULL sein. Eine klassische 1 zu n Beziehung.

Wenn man das Modell erstellt, erhält die Kategorie Klasse die Auflistung aus Unterkategorien als Navigationspunkt und die Unterkategorie Klasse die Kategorie.

Objekt-Klassen

using System.Collections.Generic;

namespace Tutorials.EfRemove {
   public class Category {
      public Category() {
         this.SubCategories = new List<SubCategory>();
      }

      public long Id { get; set; }
      public string Name { get; set; }
      public string Description { get; set; }
      public bool IsActive { get; set; }
      public ICollection<SubCategory> SubCategories { get; set; }
   }

   public class SubCategory {
      public long Id { get; set; }
      public long CategoryId { get; set; }
      public Category Category { get; set; }
      public string Name { get; set; }
      public string Description { get; set; }
      public bool IsActive { get; set; }
   }
}

Lösungsansätze

Nun gibt es zwei Lösungsansätze, wie man das Löschen direkt über die Navigationspunkte anstoßen kann, die ich im Internet bei den Recherchen gefunden habe.

  1. Identifying Relationship (Identifizierende Beziehung)
  2. Überschreibung der SaveChages-Methode

Identifying Relationship

Bei dieser Methode geht es darum, dem Entity Framework vorzugeben, dass das Kindselement ohne das Elternelement nicht existieren kann. Dazu reicht es leider nicht, die Spalte mit dem ForeignKey als Required (notwendig) zu deklarieren. Man muss dafür sowohl das eigentliche Primary Key (in unserem Fall Id) als auch das Foreign Key (in unserem Fall CategoryId) zu einen zusammengesetzten Primary Key definieren. Dann funktioniert das Löschen über den Navigationspunkt des Elternelementes wie erwartet.

Dieses zusammengesetztes Primary Key muss natürlich auch in der Datenbank sich wiederspiegeln.

In meinem Fall nutze ich sehr oft SQLite als Datenbank mit EF. Die oben beschriebene Lösung konnte ich mit SQLite leider nicht einsetzten, da SQLite bei zusammengesetzten Primary Keys kein Autoincrement auf die ID-Spalte zulässt. Ob und wie weit es von den anderen Datenbanken unterstützt wird, muss jeder selbst nachforschen. Wenn man die IDs selbst vergibt (z.B. ID-Spalte statt long mit Guid nutzen), ist dies die einfachste Lösung für das Problem.

using System.Data.Entity.ModelConfiguration;

namespace Tutorials.EfRemove {
   public class CategoryMap : EntityTypeConfiguration<Category> {
      public CategoryMap() {
         #region Primary Key
         this.HasKey(p => p.Id);
         #endregion

         #region Required Fields
         this.Property(p => p.Name)
            .IsRequired();
         #endregion

         #region Database mapping
         this.ToTable("CATEGORY");
         this.Property(p => p.Id).HasColumnName("ID");
         this.Property(p => p.Name).HasColumnName("NAME");
         this.Property(p => p.Description).HasColumnName("DESCRIPTION");
         this.Property(p => p.IsActive).HasColumnName("ACTIVE");
         #endregion
      }
   }

   public class SubCategoryMap :EntityTypeConfiguration<SubCategory> {
      //#region Funktioniert nicht fürs Löschen aus dem Elternelement heraus
      //public SubCategoryMap() {
      //   #region Primary Key
      //   this.HasKey(p => p.Id);
      //   #endregion

      //   #region Required Fields
      //   this.Property(p => p.CategoryId).IsRequired();
      //   this.Property(p => p.Name).IsRequired();
      //   #endregion

      //   #region Database Mapping
      //   this.ToTable("SUB_CATEGORY");
      //   this.Property(p => p.Id).HasColumnName("ID");
      //   this.Property(p => p.CategoryId).HasColumnName("CATEGORY_ID");
      //   this.Property(p => p.Name).HasColumnName("NAME");
      //   this.Property(p => p.Description).HasColumnName("DESCRIPTION");
      //   this.Property(p => p.IsActive).HasColumnName("ACTIVE");
      //   #endregion

      //   #region Relations
      //   this.HasRequired(p => p.Category)
      //      .WithMany(p => p.SubCategories)
      //      .HasForeignKey(p => p.CategoryId);
      //   #endregion
      //}
      //#endregion

      #region Funktioniert, wenn die Datenbank mitspielt
      public SubCategoryMap() {
         #region Primary Key
         this.HasKey(p => new {
            p.Id,
            p.CategoryId
         });
         #endregion

         #region Required Fields
         this.Property(p => p.CategoryId).IsRequired();
         this.Property(p => p.Name).IsRequired();
         #endregion

         #region Database Mapping
         this.ToTable("SUB_CATEGORY");
         this.Property(p => p.Id).HasColumnName("ID");
         this.Property(p => p.CategoryId).HasColumnName("CATEGORY_ID");
         this.Property(p => p.Name).HasColumnName("NAME");
         this.Property(p => p.Description).HasColumnName("DESCRIPTION");
         this.Property(p => p.IsActive).HasColumnName("ACTIVE");
         #endregion

         #region Relations
         this.HasRequired(p => p.Category)
            .WithMany(p => p.SubCategories)
            .HasForeignKey(p => p.CategoryId);
         #endregion
      }
      #endregion
   }
}

Überschreibung der SaveChanges-Methode

Bei diesem Lösungsansatz machen wir praktisch das, was meiner Meinung nach das Entity Framework von sich aus machen sollte, wenn beim Kindselement das Elternelement als notwendig (required) eingetragen ist. Wir schauen uns die lokalen Änderungen des Contexts für die Kindselemente an und löschen explizit solche, bei denen das Elternelement null ist. Anschließend wird die normale SaveChanges-Methode aus der Basisklasse aufgerufen.

Man muss praktisch für jede 1:n Beziehung solchen Einzeiler schreiben, um aus den Elternelementen über die Navigationspunkte die Kindselemente löschen zu können. Das ist zwar ein wenig umständlich, aber deutlich transparenter, als wenn jeder Entwickler beim Löschen der Kindselemente immer das Context und nicht den Navigatioinspunkt nutzen müsste.

using System.Data.Entity;
using System.Linq;

namespace Tutorials.EfRemove {
   public class MyContext : DbContext {
      public IDbSet<Category> Categories { get; set; }
      public IDbSet<SubCategory> SubCategories { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder) {
         modelBuilder.Configurations.Add<Category>(new CategoryMap());
         modelBuilder.Configurations.Add<SubCategory>(new SubCategoryMap());
      }

      public override int SaveChanges() {
         #region Bereinigung der gelöschten Einträge aus Elternelement heraus
         //// Ausgeschriebene Fassung
         //var localChilds = this.SubCategories.Local.ToList();
         //var deletedChilds = localChilds.Where(w => w.Category == null).ToList();
         //foreach(var child in deletedChilds) {
         //   this.SubCategories.Remove(child);
         //}

         // Kurzfassung mit LINQ
         this.SubCategories.Local
            .Where(w => w.Category == null).ToList()
            .ForEach(fe => this.SubCategories.Remove(fe));
         #endregion

         return base.SaveChanges();
      }
   }
}

Fazit

Am Ende bleibt zu hoffen, dass bald bei Entity Framework bald solche Workarounds mehr notwendig sein werden. Denn die notwendigen Informationen liegen dem Entity Framework vor um könnten fürs saubere Löschen genutzt werden.