简体   繁体   中英

Error deleting a table row splitted on multiple entities

I want to delete a table row that is split on two entities.

If I try to delete the main entity, I get an error if before I don't load the related other entity using context.Entry(...).Reference ..

Is a bit silly to retrieve the related entities before, when I am going to delete the full row?

I got the following error if I keep commented the context.Entry(...) line

Invalid data encountered. A required relationship is missing. Examine State Entries to determine the source of the constraint violation.

I add the code below. Could please someone help me in deleting split entities without having to "load" the related ones before?

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

 namespace Split
 {
   class Program
   {
     static void Main(string[] args)
     {
        using (var context = new DataContext())
        {
            var product = new Product()
            {
                Name = "my Article",
                Photo = new ProductPhoto() { PhotoUrl = "http://myfoto.jpg" }
            };

            context.Products.Add(product);
            context.SaveChanges();
        }

        using (var context = new DataContext())
        {
            var product = context.Products.First();
            //context.Entry(product).Reference(e => e.Photo).Load();
            context.Products.Remove(product);
            context.SaveChanges();
        }
     }
   }

  class Product
  {
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual ProductPhoto Photo { get; set; }
  }

  class ProductPhoto
  {
    public virtual int ProductId { get; set; }
    public virtual string PhotoUrl { get; set; }
    public virtual Product Product { get; set; }
  }

  class DataContext : DbContext
  {
    public DataContext()
        : base("name=DefaultConnection")
    {
        Configuration.ProxyCreationEnabled = false;
        Configuration.LazyLoadingEnabled = false;
    }

    public DbSet<Product> Products { get; set; }
    public DbSet<ProductPhoto> ProductPhotos { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .ToTable("Products")
            .HasKey(e => e.Id)
            .HasRequired(e => e.Photo)
            .WithRequiredPrincipal(e => e.Product);

        modelBuilder.Entity<ProductPhoto>()
            .ToTable("Products")
            .HasKey(e => e.ProductId);

        base.OnModelCreating(modelBuilder);
     }
   }
}

The best way to do this is by using a stub entity : an entity object that's only got an Id value:

var product = context.Products.First();
var photo = new ProductPhoto { ProductId = product.ProductId }; // Stub
context.Entry(photo).State = System.Data.Entity.EntityState.Deleted;
context.Products.Remove(product);
context.SaveChanges();

If you know a Product 's Id you can even delete both the Product and its ProductPhoto by only creating two stubs.

Possibly load the product like so:

var product = context.Products.Include(x => x.Photo).First();

Saves a line but will still load the photo from the db.

Try to add Cascade Delete rule to the model. You MUST have a corresponding DELETE rule in the database to avoid loading of the dependents into memory as noted here.

Hi the path to solve as Gert Arnold suggest is to have in memory stubs one for the entity and another for related sub entities I had pasted a new code with that works (see comments)

I posted the solution with the idea suggested by Gert Arnold, perhaps the code could be optimized, I tried to make it as much generic that I can.

If your entity contains any concurrency tokens, these properties are also used to construct the DELETE statement. You can still use the stub entity approach, but you will need to set values for the concurrency token properties as well.

Quoted from: “Programming Entity Framework: DbContext by Julia Lerman and Rowan Miller (O'Reilly). Copyright 2012 Julia Lerman and Rowan Miller, 978-1-449-31296-1.”

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

namespace Split
{
    class Program
    {
        static void Main()
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<DataContext>());

            const int id = 1;
            const string split = "Info"; // contract: if the entity being delete has an Info property then the row has been splitted

            using (var context = new DataContext()) // Add
            {
                var product = new Product 
                {
                    Name = "my Article 1",
                    Info = new ProductInfo { PhotoUrl = "http://myphoto.jpg" } // when adding an entity the subEntity MUST BE included on the graph
                };

                context.Products.Add(product);
                context.SaveChanges();
            }

            using (var context = new DataContext())
            {
                var product = context.Products.Find(id);
                context.Entry(product).Reference(e => e.Info).Load(); // when adding an entity the subEntity COULD BE OR NOT included on the graph, no need to include it if we are not going to modify it

                product.Name = "MY ARTICULE 1";
                product.Info.PhotoUrl = "HTTP://MYPHOTO.JPG";
                context.Entry(product).State = EntityState.Modified;
                context.SaveChanges();
            }

            using (var context = new DataContext())
            {
                PropertyInfo propertyInfo;

                context.Products.Find(id); // uncoment bring it to memory and test with entity in memory

                var entity = context.Products.Local.FirstOrDefault(e => e.Id == id);

                context.Entry(entity).Reference(e => e.Info).Load();

                if (entity != null)                                      // there is a entity already yet in memory
                {
                    propertyInfo = entity.GetType().GetProperty(split);  // contract

                    if (propertyInfo != null)
                    {
                        var subEntity = propertyInfo.GetValue(entity);         // get subEntity from entity Info property
                        context.Entry(subEntity).State = EntityState.Detached; // remove sub entity from ChangeTracker API
                        propertyInfo.SetValue(entity, null);                   // remove subEntity and relationship
                    }

                    context.Entry(entity).State = EntityState.Detached;  // remove entity from ChangeTracker API
                }

                entity = new Product { Id = id };                        // new entity stub
                propertyInfo = entity.GetType().GetProperty(split);      // contract:
                if (propertyInfo != null)
                {
                    propertyInfo.SetValue(entity, null);                 // remove subEntity and and relationship

                    var subEntity = Activator.CreateInstance(propertyInfo.PropertyType); // create a new subEntity stub
                    subEntity.GetType().GetProperty("Id").SetValue(subEntity, id);       // set the foreinkey relation
                    context.Entry(subEntity).State = EntityState.Deleted;                // mark as deleted on context
                }

                context.Entry(entity).State = EntityState.Deleted;       // delete the entity
                context.SaveChanges();
            }
        }
    }

    class Product
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }

        public virtual ProductInfo Info { get; set; }
    }

    class ProductInfo
    {
        public virtual int Id { get; set; }
        public virtual string PhotoUrl { get; set; }
        public virtual Product Product { get; set; }
    }

    class DataContext : DbContext
    {
        public DataContext()
            : base("name=DefaultConnection")
        {
            Configuration.ProxyCreationEnabled = false;
            Configuration.LazyLoadingEnabled = false;


        }
        public DbSet<Product> Products { get; set; }
        public DbSet<ProductInfo> ProductInfos { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>() // one-to-one
                .ToTable("Products")
                .HasKey(e => e.Id)
                .HasRequired(e => e.Info)
                .WithRequiredDependent(e => e.Product);

            modelBuilder.Entity<ProductInfo>() // map to the same table Products
                .ToTable("Products")
                .HasKey(e => e.Id);

            base.OnModelCreating(modelBuilder);
        }

    }
}

Replace your override method with this code and check

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
       modelBuilder.Entity<Product>() // one-to-one
           .ToTable("Products")
           .HasKey(e => e.Id)
           .HasRequired(e => e.Info)
           .WithRequiredDependent(e => e.Product);

       modelBuilder.Entity<ProductInfo>() // map to the same table Products
           .ToTable("Products")
           .HasKey(e => e.Id);

       //add this code
       modelBuilder.Entity<Product>()
            .HasRequired(p => p.Photo) // "Photo"  defined in Product class for ProductPhoto class's object name
            .WithRequiredPrincipal(c => c.Product);// "Product"  defined in ProductPhoto class for Product's class object name


            base.OnModelCreating(modelBuilder);
}

I know this isn't the "correct" answer, but with all the hassle of getting table-splitting working and getting errors on deleting, I just went with a good old SQL command:

DbContext.Database.ExecuteSqlCommand($"DELETE FROM Products WHERE Id = {id}");

Always an option when you're spending hours trying to get a small feature working!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM