Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client cascade delete for owned entities #10179

Open
silarmani opened this issue Oct 28, 2017 · 13 comments
Open

Client cascade delete for owned entities #10179

silarmani opened this issue Oct 28, 2017 · 13 comments

Comments

@silarmani
Copy link

I am trying to delete entities just by giving the PK however it fails if the entity contains Complex Type fields

Example

    public class BowtieCauseDetails
    {
        [Key]
        public int NodeId { get; set; }

        public BowtieCauseTimeFields Current { get; set; }

        public BowtieCauseTimeFields Proposed { get; set; }
    }

    public class BowtieCauseTimeFields
    {
        public BowtieNodeInOutFrequencyFields Outgoing { get; set; }
    }

    public class BowtieNodeInOutFrequencyFields
    {
        public decimal Frequency { get; set; }
    }

Example

            var ctx = new MyDbContext();

            var toBeDeleted = ctx
                .BowtieCauseDetails
                .AsNoTracking()
                .Select(e => new BowtieCauseDetails
                {
                    NodeId = e.NodeId // PK only
                })
                .ToList();

            ctx.BowtieCauseDetails.RemoveRange(toBeDeleted);

            ctx.SaveChanges();

It throws the following exception

Unhandled Exception: System.InvalidOperationException: The entity of 'BowtieCauseDetails' is sharing the table 'BowtieCauseDetails' with 'BowtieCauseDetails.Current#BowtieCauseTimeFields', but there is no entity of this type with the same key value that has been marked as 'Deleted'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.

If I provide empty place holder for those fields, it still fails but with a different message

Example

            var ctx = new MyDbContext();

            var toBeDeleted = ctx
                .BowtieCauseDetails
                .AsNoTracking()
                .Select(e => new BowtieCauseDetails
                {
                    NodeId = e.NodeId,
                    Current = new BowtieCauseTimeFields
                    {
                        Outgoing = new BowtieNodeInOutFrequencyFields
                        {
                        }
                    },
                    Proposed = new BowtieCauseTimeFields
                    {
                        Outgoing = new BowtieNodeInOutFrequencyFields
                        {
                        }
                    }
                })
                .ToList();

            ctx.BowtieCauseDetails.RemoveRange(toBeDeleted);

            ctx.SaveChanges();

Unhandled Exception: System.InvalidOperationException: The property 'BowtieCauseTimeFieldsBowtieCauseDetailsNodeId' on entity type 'BowtieCauseDetails.Current#BowtieCauseTimeFields.Outgoing#BowtieNodeInOutFrequencyFields' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.

However if I get at least one field from the DB then it works

Example

            var ctx = new MyDbContext();

            var toBeDeleted = ctx
                .BowtieCauseDetails
                .AsNoTracking()
                .Select(e => new BowtieCauseDetails
                {
                    NodeId = e.NodeId,
                    Current = new BowtieCauseTimeFields
                    {
                        Outgoing = new BowtieNodeInOutFrequencyFields
                        {
                            Frequency = e.Current.Outgoing.Frequency
                        }
                    },
                    Proposed = new BowtieCauseTimeFields
                    {
                        Outgoing = new BowtieNodeInOutFrequencyFields
                        {
                            Frequency = e.Current.Outgoing.Frequency
                        }
                    }
                })
                .ToList();

            ctx.BowtieCauseDetails.RemoveRange(toBeDeleted);

            ctx.SaveChanges();

I think it should work by giving the PK only.

Here is the full sample for easier replication

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace EfTests
{
    class Program
    {
        static int Main(string[] args)
        {
            var ctx = new MyDbContext();

            var toBeDeleted = ctx
                .BowtieCauseDetails
                .AsNoTracking()
                .Select(e => new BowtieCauseDetails
                {
                    NodeId = e.NodeId,
                    //Current = new BowtieCauseTimeFields
                    //{
                    //    Outgoing = new BowtieNodeInOutFrequencyFields
                    //    {
                    //        Frequency = e.Current.Outgoing.Frequency
                    //    }
                    //},
                    //Proposed = new BowtieCauseTimeFields
                    //{
                    //    Outgoing = new BowtieNodeInOutFrequencyFields
                    //    {
                    //        Frequency = e.Current.Outgoing.Frequency
                    //    }
                    //}
                })
                .ToList();

            ctx.BowtieCauseDetails.RemoveRange(toBeDeleted);

            ctx.SaveChanges();

            return 0;
        }
    }

    public class BowtieCauseDetails
    {
        [Key]
        public int NodeId { get; set; }

        public BowtieCauseTimeFields Current { get; set; }

        public BowtieCauseTimeFields Proposed { get; set; }
    }

    public class BowtieCauseTimeFields
    {
        public BowtieNodeInOutFrequencyFields Outgoing { get; set; }
    }

    public class BowtieNodeInOutFrequencyFields
    {
        public decimal Frequency { get; set; }
    }

    public class MyDbContext : DbContext
    {
        public DbSet<BowtieCauseDetails> BowtieCauseDetails { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=localhost,1433;Database=ipl_tux;Integrated Security=True;");            
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BowtieCauseDetails>()
                .OwnsOne(c => c.Current)
                .OwnsOne(c => c.Outgoing);

            modelBuilder.Entity<BowtieCauseDetails>()
                .OwnsOne(c => c.Proposed)
                .OwnsOne(c => c.Outgoing);

            base.OnModelCreating(modelBuilder);
        }
    }
}
@smitpatel
Copy link
Contributor

@ralmsdeveloper - This is different from #10180 because here there is no need of unique indexes.

The issue here is, when you are trying to delete an entity it would remove the row from database. But since the table is being shared, there would be other entities using the same row so unless all of the entities sharing the row are deleted, deleting the row could be incorrect and cause data corruption. Hence before sending command EF Core tried to verify if all the entities in that row is marked as deleted and in the absence threw above exception.

Though in this particular case, owned entities are sharing the table. If owner is deleted then owned entities should also lose its existence since it is tied with owner. If the entities are loaded in different state then its an error. But if it not then owner is deleted, owned children could be deleted too.

Another use of table sharing is identifying 1-to-1 relationship. In that case, we need to think about the effects. For the case of principal getting deleted with cascade delete is enabled, we can delete but not for all cases.

@ralmsdeveloper
Copy link
Contributor

@smitpatel, thanks for the explanation, my intention is to help. so I'm trying to get more involved. I will try to analyze the cases more preventively.

@smitpatel
Copy link
Contributor

@ralmsdeveloper - Thanks for all the efforts. We really appreciate it. My intention was not to stop you from doing the good work. I wrote explanation on both just in case people may wonder in terms of why they are same or different. Further it will be helpful during triage meeting.

@ralmsdeveloper
Copy link
Contributor

I understand @smitpatel, Thank you very much.

That makes it evolve.

@ajcvickers
Copy link
Contributor

@silarmani Thanks for reporting this and also issues #10180 and #10182. There are some situations where EF currently uses more than just the key information for deletes. How much we want to allow stub entities like this to be used in these situations is something we will discuss as a team. Long term, these kinds of operations are probably best done with set-based operations, as tracked by #795. For now, one direction you could go is to use the FromSql method to write a set-based deletes manually.

@ajcvickers
Copy link
Contributor

Notes from triage: we should delete owned types automatically in this case, but not types that are sharing the same table but without ownership. The reason for this is that for owned types, the delete is correct and safe by definition. For other types, it may not be, therefore for other types deleting a row requires that both entities are tracked and set as deleted.

@ajcvickers ajcvickers added this to the 2.1.0 milestone Nov 2, 2017
@ajcvickers ajcvickers modified the milestones: 2.1.0-preview1, 2.1.0 Jan 17, 2018
@ajcvickers ajcvickers modified the milestones: 2.1.0, Backlog Jan 26, 2018
@irmtim
Copy link

irmtim commented May 7, 2018

So now there's still no possibility to delete entity with owned type?

@AndriySvyryd
Copy link
Member

@irmtim Currently you would need to delete the owned type explicitly. If you are getting errors open a separate issue.

@BennieCopeland
Copy link

@AndriySvyryd How can deleting the owned type be achieved?

@AndriySvyryd
Copy link
Member

@BennieCopeland usually it should be deleted when you delete the principal, but if not you can do something like this:

 context.Entry(entity).Reference(e => e.Owned).TargetEntry.State = EntityState.Deleted;

@BennieCopeland
Copy link

I was having trouble with it earlier. I realized the issue I was having was because I gave the property a default value (Owned { get; set; } = new Owned();) since having a non-null value is an EF requirement. Apparently EF doesn't allow updating the property either, and having a default value was triggering that.

@diegoernestofarias
Copy link

You can fix it with a simple model configuration.
Add this to your DbContext class:

protected override void OnModelCreating(ModelBuilder modelBuilder) {
        foreach (var relationship in modelBuilder.Model.GetEntityTypes().Where(e => e.IsOwned()).SelectMany(e => e.GetForeignKeys())) {
                relationship.DeleteBehavior = DeleteBehavior.Cascade;
        }
}

Now, all the owned types (complex objects) will be deleted automatically when you delete the owner entity.

@AndriySvyryd AndriySvyryd changed the title Unable to delete entity by id only if it contains complex types Client cascade delete for owned entities Feb 13, 2020
@AndriySvyryd
Copy link
Member

AndriySvyryd commented Feb 13, 2020

Depends on #18960

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants