LINQ to SQL - DuplicateKeyException during update

2020-02-14 06:34发布

问题:

The code listed below attempts to update a row in the database, but throws an exception instead:

System.Data.Linq.DuplicateKeyException: Cannot add an entity with a key that is already in use

Most examples I've seen query the database to obtain an instance of an entity, modify some of the instance's properties, and then update it. Here I'm getting the object from a different source entirely (it's being parsed from an XML file) and querying to see if there's already a row for this data. If there is, I'm setting the primary key and attempting to run an update. What's the correct way to do this?

Here's the trimmed down version of the code:

Customer customer = new Customer(); // Customer has a database generated
                                    // identity column called CustomerId

// populate customer object
customer.Name = "Mr. X";
customer.Email = "x@company.com";
// etc.

// is customer already in database?
// identify customer by email
var results = ctx.Where(c => c.Email == customer.Email); // ctx is a DataContext

if (results.Any())
{
   Customer existing = results.Single();

   // set primary key to match existing one
   customer.CustomerId = existing.CustomerId;

   // update database
   customerTable.Attach(customer);  // customerTable is a Table<Customer>
   ctx.SubmitChanges();
}

// otherwise do insert
// ...   

回答1:

I am new to LINQ to SQL, so if someone smarter than me sees this is wrong, please correct me. But, I believe your problem is that when you get into the if statement, you are getting the entity from the results (via results.Single()) and you are setting the value to the NEW customer object. When you attempt to submit the customer object to the database, the primary key already exists, so you receive the error.

Instead, you want to update the existing customer and submit that back to the database.



回答2:

Make this change:

customerTable.Attach(customer, existing);

^ I am not sure why the above wouldn't work. The second argument is the "original state" of the entity, perhaps because it's a different reference to a different instance, L2S thinks it needs to insert a whole new object.

I think it would be better to do something like:

var customer = ctx.Where(...).SingleOrDefault();
if (customer == null)
{
  customer = new Customer()
  {
    Name = name,
    Email = email
  };
  customerTable.InsertOnSubmit(customer);
}
else
{
  customer.Name = name;
  customer.Email = email;
}

ctx.SubmitChanges();


回答3:

Somewhere in the web I found this solution:

static void CopyProperties<T>(ref T Target, T Source)
        {
            foreach (PropertyInfo PI in Target.GetType().GetProperties())
            {
                if (PI.CanWrite && PI.CanRead)
                {
                    PI.SetValue(Target, PI.GetValue(Source, null), null);
                }
            }
        }

....

static void Save(Test_TableData ChangedData)
        {
            using (DataClasses1DataContext D = new DataClasses1DataContext())
            {
                Test_TableData UpdateTarget = D.Test_TableDatas.SingleOrDefault(i => i.ID == ChangedData.ID);
                if (UpdateTarget != null)
                {
                    CopyProperties<Test_TableData>(ref UpdateTarget, ChangedData);
                }
                else
                {
                    D.Test_TableDatas.InsertOnSubmit(ChangedData);
                }
                D.SubmitChanges();
            }
    }


回答4:

The way that I do this is the following

I would pull the customer that already exists like you do then update the item with the matching id that you are using, with the values you've pulled from xml. Then when you call you datacontext.SubmitChanges() method it will do the update for you.

Or you could use the Attach Method, in your case your code would look something like

customerTable.Attach(customer, existing);

Attach was created exactly for this case.

Edit

Why don't you change the order you are doing things in, instead of making a new customer, populating that customer, Do something like this

var results = ctx.Where(c => c.Email == customer.Email);

Customer customer = (results.Any ? results.Single : new Customer)

Then query your xml to fill the customer then do your insert/update.



回答5:

Apparently this is not a new problem. Here's a sampling of some of the posts that discuss this issue:

http://www.west-wind.com/weblog/posts/134095.aspx

http://www.codeproject.com/KB/linq/linq-to-sql-detach.aspx

http://social.msdn.microsoft.com/forums/en-US/linqprojectgeneral/thread/3848c02c-464e-40ff-87b6-813bff7b1263/

I got it working by creating a new DataContext and Table before doing the update. My modified code looks like this:

Customer customer = new Customer(); // Customer has a database generated
                                    // identity column called CustomerId

// populate customer object
customer.Name = "Mr. X";
customer.Email = "x@company.com";
// etc.

// is customer already in database?
// identify customer by email
var results = ctx.Where(c => c.Email == customer.Email); // ctx is a DataContext

if (results.Any())
{
   Customer existing = results.Single();

   // set primary key to match existing one
   customer.CustomerId = existing.CustomerId;

   // **** CODE CHANGES HERE ****
   // create new DataContext and table to avoid DuplicateKeyException errors
   var ctx = new DataContext(customerTable.Context.Connection.ConnectionString);
   customerTable = ctx.GetTable<Customer>();

   // update database
   customerTable.Attach(customer);  // customerTable is a Table<Customer>

   // **** ANOTHER CODE CHANGE ****
   // without this line the data won't be updated with the new values
   ctx.Refresh(RefreshMode.KeepCurrentValues, customer);

   ctx.SubmitChanges();
}

// otherwise do insert
// ...  

The way I understand this is that the DataContext can only contain one instance of each unique entity. Attempting to attach a new entity with the same primary key causes the error, as there will now be two instances of the same entity. The new DataContext does not know about the existing entity and so has no problem attaching the new one.


UPDATE: It looks like this question has already been answered.


UPDATE: Don't use my sample code as-is. It caused me other problems.