From 80b23f8fa713f62bcb8a2f3a90ed4de11072bcbe Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 3 Feb 2015 20:58:05 +0100 Subject: [PATCH] fix PetaPocoExtensions merge --- .../Persistence/PetaPocoExtensions.cs | 230 +++++++----------- 1 file changed, 88 insertions(+), 142 deletions(-) diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 265948a073..6b8e7b46ff 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -13,27 +13,59 @@ namespace Umbraco.Core.Persistence { public static class PetaPocoExtensions { - + // NOTE + // + // proper way to do it with TSQL and SQLCE + // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) + // BEGIN + // UPDATE table SET ... WHERE ... + // END + // ELSE + // BEGIN + // INSERT INTO table (...) VALUES (...) + // END + // + // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK + // + // proper way to do it with MySQL + // IF EXISTS (SELECT ... FROM table WHERE ... FOR UPDATE) + // BEGIN + // UPDATE table SET ... WHERE ... + // END + // ELSE + // BEGIN + // INSERT INTO table (...) VALUES (...) + // END + // + // MySQL locks the constraint ONLY if it exists, so INSERT may fail... + // in theory, happens in READ COMMITTED but not REPEATABLE READ + // http://www.percona.com/blog/2012/08/28/differences-between-read-committed-and-repeatable-read-transaction-isolation-levels/ + // but according to + // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + // it won't work for exact index value (only ranges) so really... + // + // MySQL should do + // INSERT INTO table (...) VALUES (...) ON DUPLICATE KEY UPDATE ... + // + // also the lock is released when the transaction is committed + // not sure if that can have unexpected consequences on our code? + // + // so... for the time being, let's do with that somewhat crazy solution below... /// - /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which - /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. + /// Safely inserts a record, or updates if it exists, based on a unique constraint. /// /// /// - /// - /// Returns the action that executed, either an insert or an update - /// - /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. - /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object + /// passed in will contain the updated value. /// - /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are - /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to - /// try to be as proficient as possible when we know this can occur and manually handle the issue. - /// - /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then - /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, - /// that's due to a race condition, in which case we need to retry and update. + /// We cannot rely on database-specific options such as MySql ON DUPLICATE KEY UPDATE or MSSQL MERGE WHEN MATCHED because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. /// internal static RecordPersistenceType InsertOrUpdate(this Database db, T poco) where T : class @@ -42,155 +74,69 @@ namespace Umbraco.Core.Persistence } /// - /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which - /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. + /// Safely inserts a record, or updates if it exists, based on a unique constraint. /// /// /// /// /// If the entity has a composite key they you need to specify the update command explicitly - /// - /// Returns the action that executed, either an insert or an update - /// - /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. - /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object + /// passed in will contain the updated value. /// - /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are - /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to - /// try to be as proficient as possible when we know this can occur and manually handle the issue. - /// - /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then - /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, - /// that's due to a race condition, in which case we need to retry and update. + /// We cannot rely on database-specific options such as MySql ON DUPLICATE KEY UPDATE or MSSQL MERGE WHEN MATCHED because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. /// internal static RecordPersistenceType InsertOrUpdate(this Database db, - T poco, - string updateCommand, + T poco, + string updateCommand, object updateArgs) where T : class { - if (poco == null) throw new ArgumentNullException("poco"); + if (poco == null) + throw new ArgumentNullException("poco"); + // try to update var rowCount = updateCommand.IsNullOrWhiteSpace() ? db.Update(poco) - : db.Update(updateCommand, updateArgs); - - if (rowCount > 0) return RecordPersistenceType.Update; - - try - { - db.Insert(poco); - return RecordPersistenceType.Insert; - } - //TODO: Need to find out if this is the same exception that will occur for all databases... pretty sure it will be - catch (SqlException ex) - { - //This will occur if the constraint was violated and this record was already inserted by another thread, - //at this exact same time, in this case we need to do an update - - rowCount = updateCommand.IsNullOrWhiteSpace() - ? db.Update(poco) - : db.Update(updateCommand, updateArgs); - - if (rowCount == 0) - { - //this would be strange! in this case the only circumstance would be that at the exact same time, 3 threads executed, one - // did the insert and the other somehow managed to do a delete precisely before this update was executed... now that would - // be real crazy. In that case we need to throw an exception. - throw new DataException("Record could not be inserted or updated"); - } - + : db.Update(updateCommand, updateArgs); + if (rowCount > 0) return RecordPersistenceType.Update; - } - } - /// - /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which - /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. - /// - /// - /// - /// - /// Returns the action that executed, either an insert or an update - /// - /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. - /// - /// - /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are - /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to - /// try to be as proficient as possible when we know this can occur and manually handle the issue. - /// - /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then - /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, - /// that's due to a race condition, in which case we need to retry and update. - /// - internal static RecordPersistenceType InsertOrUpdate(this Database db, T poco) - where T : class - { - return db.InsertOrUpdate(poco, null, null); - } + // failed: does not exist, need to insert + // RC1 race cond here: another thread may insert a record with the same constraint - /// - /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which - /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. - /// - /// - /// - /// - /// If the entity has a composite key they you need to specify the update command explicitly - /// - /// Returns the action that executed, either an insert or an update - /// - /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. - /// - /// - /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are - /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to - /// try to be as proficient as possible when we know this can occur and manually handle the issue. - /// - /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then - /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, - /// that's due to a race condition, in which case we need to retry and update. - /// - internal static RecordPersistenceType InsertOrUpdate(this Database db, - T poco, - string updateCommand, - object updateArgs) - where T : class - { - if (poco == null) throw new ArgumentNullException("poco"); - - var rowCount = updateCommand.IsNullOrWhiteSpace() - ? db.Update(poco) - : db.Update(updateCommand, updateArgs); - - if (rowCount > 0) return RecordPersistenceType.Update; - - try + var i = 0; + while (i++ < 4) { - db.Insert(poco); - return RecordPersistenceType.Insert; - } - //TODO: Need to find out if this is the same exception that will occur for all databases... pretty sure it will be - catch (SqlException ex) - { - //This will occur if the constraint was violated and this record was already inserted by another thread, - //at this exact same time, in this case we need to do an update - - rowCount = updateCommand.IsNullOrWhiteSpace() - ? db.Update(poco) - : db.Update(updateCommand, updateArgs); - - if (rowCount == 0) + try { - //this would be strange! in this case the only circumstance would be that at the exact same time, 3 threads executed, one - // did the insert and the other somehow managed to do a delete precisely before this update was executed... now that would - // be real crazy. In that case we need to throw an exception. - throw new DataException("Record could not be inserted or updated"); + // try to insert + db.Insert(poco); + return RecordPersistenceType.Insert; } + catch (SqlException) // TODO: need to find out if all db will throw that exception - probably OK + { + // failed: exists (due to race cond RC1) + // RC2 race cond here: another thread may remove the record - return RecordPersistenceType.Update; + // try to update + rowCount = updateCommand.IsNullOrWhiteSpace() + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); + if (rowCount > 0) + return RecordPersistenceType.Update; + + // failed: does not exist (due to race cond RC2), need to insert + // loop + } } + + // this can go on forever... have to break at some point and report an error. + throw new DataException("Record could not be inserted or updated."); } ///