2

I have very limited experience using EF4. I am successfully deserializing entities from a webservice in a detached state, and I now would like to save to the database. When SaveChanges is used I get the following exception:

System.Data.UpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: Violation of PRIMARY KEY constraint '[Primary key constraint name]'. Cannot insert duplicate key in object '[Related Table Name]'. The duplicate key value is (1). The statement has been terminated.

the entity I am trying to save has related entities as properties and properties that are a collection of entities.

The IDs from the web service are used as the primary key for the tables, so no automatically generated ID's are used.

The following test illustrates the issue that i'm trying to resolve:

    [TestMethod]
    public void SaveRelatedDetachedEntitiesWithoutDuplicatesTest(){
        using (var db = ProductEntities()){ 
            //testing pre-saved existing category
            if (!db.CategoryGroups.Any(e => e.Id == 3)){
                db.CategoryGroups.AddObject(new Database.CategoryGroupEntity(){
                                                                                Id = 3,
                                                                                Name = "test group 3"
                                                                            });
                db.SaveChanges();
            }

            var categoryList = new List<CategoryEntity>(){
               new CategoryEntity(){
                    Id = 1,
                    Name = "test category 1",
                    Groups =  new List<CategoryGroupEntity> (){new CategoryGroupEntity(){
                                                                                    Id = 1,
                                                                                    Name = "test group 1"
                                                                                },//duplicate
                                                                                new CategoryGroupEntity(){
                                                                                    Id = 2,
                                                                                    Name = "test group 2"
                                                                                }
                                                                            }
                },      
                new CategoryEntity(){
                    Id = 2,
                    Name = "test category 2",
                    Groups =  new  List<CategoryGroupEntity>{
                                                                            new CategoryGroupEntity(){
                                                                                Id = 1,
                                                                                Name = "test group 1"
                                                                            },//duplicate
                                                                            new CategoryGroupEntity(){
                                                                                Id = 3,
                                                                                Name = "test group 3"
                                                                            }//already in db
                                                                        }
                }
            };

            var product = new ProductEntity(){          
                Categories = categoryList,          
                Id = 1,
                Name = "test product 1",            
                Type = new TypeEntity { Id = 1, Name = "test type" }
            };
//all above code cannot be altered as it reflects what results from the deserialization.
            db.Products.AddObject(product); 

//need code here to handle the duplicates
            db.SaveChanges();

            var test = db.Products.Where(e => e.Id == 1).FirstOrDefault();
            Assert.IsNotNull(test);
            Assert.IsTrue(test.Categories.Count() == 2, "missing categories from product");
            Assert.IsTrue(test.Categories.ElementAt(0).Groups.Any(e => e.Id == 1), "missing group from category 1");
            Assert.IsTrue(test.Categories.ElementAt(1).Groups.Any(e => e.Id == 1), "missing group from category 2");
        }
    }

Your help is appreciated.

Edit: I can get a list of groups that are duplicates using the following code

                var added = db.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added)
                    .Where(e => !e.IsRelationship).Select(e => e.Entity)
                    .OfType<CategoryGroupEntity>();
                var duplicates = added.GroupBy(e => e.Id)
                    .Where(g => g.Count() > 1)
                    .SelectMany(g => g.Where(e => e != g.First())

Things I have tried but did not work:

-attaching entities that are duplicates to the Data Context with an unchanged state. as attaching a CategoryGroupEntity causes all related entities to be attached the duplicate key issue remains

-removing entity instances from the Categories collection and replacing them with the CategoryGroupEntity instance that was first created results in the same issue

-detaching the duplicate entity instances results in the second category losing group id 1

As a side note, I also need to avoid the duplicate key issue when a particular CategoryGroupEntity already existing in the database and an attempt is made to save an entity with the same id.

So, I need to avoid the duplicate key issue both when an entity with that ID exists both in the database or in an added state in the ObjectStateManager. the test I included above incorporates both cases.

X-Dev
  • 465
  • 1
  • 6
  • 22
  • Why does Entity Framework Reinsert Existing Objects into My Database? msdn.microsoft.com/en-us/magazine/dn166926.aspx – Colin May 12 '14 at 08:52
  • possible duplicate of [How to avoid duplicate insert in Entity Framework 4.3.1](http://stackoverflow.com/questions/13285485/how-to-avoid-duplicate-insert-in-entity-framework-4-3-1) – Colin May 12 '14 at 08:57

2 Answers2

0

In Entity Framework if that entity you wanted to add is already exist then just giving its primary key wont work , you need to load that entity and assign it

var product = new ProductEntity(){          
                Categories = categoryList,          
                Id = 1,
                Name = "test product 1",            
                Type = new TypeEntity { Id = 1, Name = "test type" }
         };

So in your code for example is TypeEntity with Id=1 is already present then you need to change above code to something like

var product = new ProductEntity(){          
                Categories = categoryList,          
                Id = 1,
                Name = "test product 1",            
                Type = db.TypeEntity.Find(1);
         };

EF works on object so in order say that you are not modifying that object but is just using it as a relation ship you need to find that object and assign it where you want to use.

Update Something similar e.g.

 public class Princess 
 { 
     public int Id { get; set; } 
     public string Name { get; set; } 
     public virtual ICollection<Unicorn> Unicorns { get; set; } 
 }

var princess = context.Princesses.Find(#id);

 // Load the unicorns related to a given princess using a string to 
// specify the relationship 
context.Entry(princess).Collection("Unicorns").Load();

Source: http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

Anshul Nigam
  • 1,608
  • 1
  • 12
  • 26
  • Hi, thanks for your prompt response. As I said, the entities are deserialized in a detached state and all of the properties/collections are pre-populated. How do I check to see if the entity exists and use it if it does and save it if it doesn't. the Groups collection of the categories is the main concern here. I can load the Type by id and update the product but the category groups are a lot more difficult. Any ideas? – X-Dev May 12 '14 at 05:05
  • I could really use some sample code for how you'd handle the groups. I'll update the test code to reflect what I need. – X-Dev May 12 '14 at 05:12
  • I was trying to find you on Stack Overflow chat but not able to find you. Though you had not shared your models but i am assuming you will be having two relationship mentioned in your models.Might be Categories model having ProductEntity in it. If you you can use something like db.Entry(product ) .Collection(c => c.Categories ) .Query() .Where(#some condition) .Load(); – Anshul Nigam May 12 '14 at 05:41
  • This may be a Newbie issue but db.Entry() and DbCollectionEntry.Load() I thought was EF5 and above. I'm restricted to EF4 here. the Model can be inferred from the test code provided. Product-> has many Categories -> has many groups. – X-Dev May 12 '14 at 06:14
  • Forgot to note that the Categories and group entities have no navigation property to a parent entity. i.e. you cannot get a reference to the product entity from the category or group entities. – X-Dev May 12 '14 at 06:28
  • I had been doing mentioned scenario but in EF5 & EF6 with 2 way navigation property set , it is always good to mention relationship in both tables, probably you can give it a try. – Anshul Nigam May 12 '14 at 06:34
  • when deserialized the navigation property would not be populated anyway. please correct me if I'm wrong. – X-Dev May 12 '14 at 06:37
  • Dont mix serializing with EF, serialization can be controlled via DataContract in models and you can mark which member to get serialize and which not – Anshul Nigam May 12 '14 at 06:43
  • the entities have been deserialized fine. hence my test does not involve any deserialization. the issue is the detached state of the entities causing db inserts to be performed when not required because the entity already exists. – X-Dev May 12 '14 at 07:05
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/52494/discussion-between-x-dev-and-anshul-nigam) – X-Dev May 12 '14 at 07:10
0

I got it working but it's definitely not the best way to do it.

save method I use has been included below:

  public static void SaveProduct(ProductEntity product) {
        using (var db = ProductEntities()) {


            //stored references to duplicate entities added to the objectContext
            var duplicateGroupsAdded = new List<Tuple<CategoryEntity, GroupEntity>>();
            var duplicateCategoriesAdded = new List<Tuple<ProductEntity, CategoryEntity>>();

            //using existing instace of entities includes associated parent into db update causing duplicate product insert attempt.
            //entities saved must be newly instantiated with no existing relationships.
            var categories = product.Categories.ToList();
            var type = new TypeEntity() {
                Id = product.Type.Id,
                Name = product.Type.Name
            };

            //empty the  collection 
            product.Categories.ToList().ForEach(category => {
                product.Categories.Remove(category);
            });
            //start off with clean product that we can populate with related entities
            product.Type = null;
            product.Group = null;

            //add to db

            db.Products.AddObject(product);

            categories.ForEach(oldCategory => {
                //new cloned category free of relationships
                var category = new CategoryEntity() {
                    Id = oldCategory.Id,
                    Name = oldCategory.Name

                };
                //copy accross Groups as clean entities free of relationships
                foreach (var group in oldCategory.Groups) {
                    category.Groups.Add(new GroupEntity() {
                        Id = group.Id,
                        Name = group.Name
                    });
                }
                //if the cat is alreay in the db use reference to tracked entity pulled from db
                var preexistingCategory = db.Categories.SingleOrDefault(e => e.Id == category.Id);
                if (preexistingCategory != null)
                    product.Categories.Add(preexistingCategory);
                else {
                    //category not in database, create new
                    var Groups = category.Groups.ToList();
                    category.Groups.ToList().ForEach(group => category.Groups.Remove(group));
                    Groups.ForEach(Group => {
                        //if the group is alreay in the db use reference to tracked entity pulled from db
                        var preexistingGroup = db.Groups.SingleOrDefault(e => e.Id == Group.Id);
                        if (preexistingGroup != null)
                            category.Groups.Add(preexistingGroup);
                        else
                            category.Groups.Add(Group);
                    });
                    product.Categories.Add(category);
                }
            });
            //if the type is alreay in the db use reference to tracked entity pulled from db
            var preexistingType = db.Types.SingleOrDefault(e => e.Id == type.Id);
            if (preexistingType != null)
                product.Type = preexistingType;
            else
                product.Type = type;

            //get lists of entities that are to be added to the database, and have been included in the update more than once (causes duplicate key error when attempting to insert).
            var EntitiesToBeInserted = db.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added)
                                 .Where(e => !e.IsRelationship).Select(e => e.Entity).ToList();
            var duplicateGroupInsertions = EntitiesToBeInserted
                                   .OfType<GroupEntity>()
                                   .GroupBy(e => e.Id)
                                   .Where(g => g.Count() > 1)
                                   .SelectMany(g => g.Where(e => e != g.First()));

            var duplicateCategoryInsertions = EntitiesToBeInserted
                               .OfType<CategoryEntity>()
                               .GroupBy(e => e.Id)
                               .Where(g => g.Count() > 1)
                               .SelectMany(g => g.Where(e => e != g.First()));

            foreach (var category in product.Categories) {
                //remove duplicate insertions and store references to add back in later
                var joinedGroups = duplicateGroupInsertions.Join(category.Groups, duplicateGroupInsertion => duplicateGroupInsertion, linkedGroup => linkedGroup, (duplicateGroupInsertion, linkedGroup) => duplicateGroupInsertion);
                foreach (var duplicateGroupInsertion in joinedGroups) {
                    if (category.Groups.Contains(duplicateGroupInsertion)) {
                        category.Groups.Remove(duplicateGroupInsertion);
                        db.Groups.Detach(duplicateGroupInsertion);
                        duplicateGroupsAdded.Add(new Tuple<CategoryEntity, GroupEntity>(category, duplicateGroupInsertion));
                    }
                }
            }
            //remove duplicate insertions and store references to add back in later
            var joinedCategories = duplicateCategoryInsertions.Join(product.Categories, duplicateCategoryInsertion => duplicateCategoryInsertion, linkedCategory => linkedCategory, (duplicateCategoryInsertion, linkedCategory) => duplicateCategoryInsertion);
            foreach (var duplicateCategoryInsertion in joinedCategories) {
                if (product.Categories.Contains(duplicateCategoryInsertion)) {
                    product.Categories.Remove(duplicateCategoryInsertion);
                    db.Categories.Detach(duplicateCategoryInsertion);
                    duplicateCategoriesAdded.Add(new Tuple<ProductEntity, CategoryEntity>(product, duplicateCategoryInsertion));
                }
            }
            db.SaveChanges();

            //entities not linked to product can now be added using references to the entities stored earlier
            foreach (var duplicateGroup in duplicateGroupsAdded) {
                var existingCategory = db.Categories.SingleOrDefault(e => e.Id == duplicateGroup.Item1.Id);
                var existingGroup = db.Groups.SingleOrDefault(e => e.Id == duplicateGroup.Item2.Id);
                existingCategory.Groups.Add(existingGroup);
            }
            foreach (var duplicateCategory in duplicateCategoriesAdded) {
                product = db.Products.SingleOrDefault(e => e.Id == duplicateCategory.Item1.Id);
                var existingCategory = db.Categories.SingleOrDefault(e => e.Id == duplicateCategory.Item2.Id);
                product.Categories.Add(existingCategory);
            }

            db.SaveChanges();

        }
    }

Any further suggestions are welcome

X-Dev
  • 465
  • 1
  • 6
  • 22
  • A further suggestion would be what is described in [this article](http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and) – Sjoerd222888 Aug 20 '15 at 09:22
  • Thanks for the suggestion @Sjoerd2228888, not sure if it completely fits the bill as it relies on checking for empty ids to identify new entries, the entries in my case entries have ids provided when deserialized and may not exist in the db. This means that an attached entity must be loaded from the db to check if the data already exists in the db. This causes further problems as the entity loaded from the database along with the entity being saved and other potential duplicates also deserialized. e.g. CategoryGroupEntity was deserialized more than once and could already exist in the db. – X-Dev Aug 20 '15 at 23:16