2016-04-20 2 views
2

Я использую Entity Framework v6, и я пытаюсь убедиться, что могу выполнять атомарную Вставку или Выбрать запись, если она еще не существует, чтобы в ферме серверов (или несколько потоков) Я могу гарантировать, что я не получаю уникальное нарушение ограничения ключа.Как вставитьOrSelect с платформой Entity

У меня есть простой пример с таблицей, подобной этой, и соответствующей простой моделью с двумя свойствами.

CREATE TABLE [dbo].[NewItem] 
(
    [ID] [int] IDENTITY(1,1) NOT NULL, 
    [Name] [varchar](40) NOT NULL, 

    CONSTRAINT [PK_NewItem] PRIMARY KEY CLUSTERED ([ID] ASC), 
    CONSTRAINT [IX_NewItem] UNIQUE NONCLUSTERED ([Name] ASC) 
) 

Когда я пишу код Entity Framework, я не могу гарантировать, что объект не вставляются после .Any возвращает ложь.

using (var myContext = new MyContext()) 
{ 
    using (var transaction = myContext.Database.BeginTransaction()) 
    { 
     if (!myContext.NewItems.Any(item => item.Name == identifier)) 
     { 
      var newItem = new NewItem { Name = identifier }; 
      myContext.NewItems.Add(newItem); 

      try 
      { 
       var result = myContext.SaveChanges(); 
      } 
      catch (Exception ex) 
      { 
       Debug.Print(ex.ToString()); 
       throw; 
      } 
     } 

     transaction.Commit(); 
    } 
} 

Если бы я должен был выполнить такой же код, используя прямое заявление SQL через SqlCommand объект, это то, что я хотел бы использовать, и, насколько я могу сказать, что это всегда работает.

using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["Test"].ConnectionString)) 
{ 
    using (var cmd = connection.CreateCommand()) 
    { 
     cmd.CommandText = "IF (NOT EXISTS(SELECT ID FROM NewItem WHERE Name = @name)) " + 
      " BEGIN " + 
      " INSERT INTO NewItem VALUES (@name) " + 
      " SELECT SCOPE_IDENTITY() AS ID " + 
      " END " + 
      " ELSE " + 
      " BEGIN " + 
      " SELECT ID FROM NewItem WHERE Name = @name " + 
      " END"; 
     var nameParam = new SqlParameter("@name", System.Data.SqlDbType.VarChar, 40); 
     nameParam.Value = Name; 
     cmd.Parameters.Add(nameParam); 

     connection.Open(); 
     var result = cmd.ExecuteScalar(); 
     connection.Close(); 

     Id = Convert.ToInt32(result); 
    } 
} 

Есть ли какой-то способ с Entity Framework, которые я могу выполнять ту же операцию, как мой сырой SqlCommand так, что если запись не существует, то он получает вставлено и если он уже существует, я просто получить тот, который там в атомной операции?

Чтобы продемонстрировать это некоторые из кода потоков, которые я использовал для моделирования нескольких серверов.

private static ExecutionMode mode; 
private static ManualResetEventSlim wait; 
private static string identifier; 

private enum ExecutionMode 
{ 
    None = 0, 
    Entityframework, 
    RawSql 
} 

static void Main(string[] args) 
{ 
    //Comment out the relevant item to test 
    //mode = ExecutionMode.RawSql; 
    mode = ExecutionMode.Entityframework; 

    wait = new ManualResetEventSlim(false); 
    identifier = Guid.NewGuid().ToString(); 

    var threads = new List<Thread>(); 
    for (var i = 0; i < 20; i++) 
    { 
     var t = new Thread(RunCreate); 
     threads.Add(t); 
     t.Start(); 
    } 

    wait.Set(); 

    for (var i = 0; i < 20; i++) 
    { 
     var t = threads[i]; 
     t.Join(); 
    } 
} 

private static void RunCreate() 
{ 
    wait.Wait(); 
    switch (mode) 
    { 
     case ExecutionMode.Entityframework: 
      { 
       //perform the Entityframework code above 
      } 
      break; 
     case ExecutionMode.RawSql: 
      { 
       var i = new NewItem { Name = identifier }; 
       i.InsertOrSelect(); //This is just using the code for the raw SQL Statements 
      } 
      break; 
    } 
} 

Исключения опыт работы с Entityframework, показывая ex.Message зацикливание через exception-> InnerException до InnerException не является нулевым

С По умолчанию Уровень изоляции транзакций

BeginTransaction() 
A first chance exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' 
    occurred in EntityFramework.dll 
An error occurred while updating the entries. 
    See the inner exception for details. 
An error occurred while updating the entries. 
    See the inner exception for details. 
Violation of UNIQUE KEY constraint 'IX_NewItem'. Cannot insert duplicate key in object 'dbo.NewItem'. 
    The duplicate key value is (f32c6a59-1462-49c3-85e2-5126c96ad484). 

С Сериализуемый tranaction Изоляция

BeginTransaction(System.Data.IsolationLevel.Serializable) 
A first chance exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' 
    occurred in EntityFramework.dll 
Transaction (Process ID 59) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction. 

ответ

1

Ни один из них гарантированно работают. Кусок, который вам не хватает, - Transaction Isolation Level.

Уровень изоляции по умолчанию SQL Server: READ COMMITTED. Это имеет минимальную защиту от запирания. Это означает, что другая команда может вставить новый элемент после того, как будет выполнено условие IF, но до того, как будет вставлена ​​новая строка. Необработанная команда SQL-сервера много с меньшей вероятностью столкнется с этим сценарием, потому что обрабатывается без взаимного перехода между командами: меньше времени для вмешательства другого сервера.

Если вы установили уровень изоляции на Serializable, вам гарантированно либо вставить элемент, либо выбрать существующий элемент. В Entity Framework код будет выглядеть следующим образом:

using (var myContext = new MyContext()) 
{ 
    using (var transaction = myContext.Database.BeginTransaction(System.Data.IsolationLevel.Serializable)) 
    { 
     if (!myContext.NewItems.Any(item => item.Name == identifier)) 
     { 
      var newItem = new NewItem { Name = identifier }; 
      myContext.NewItems.Add(newItem); 
      try 
      { 
       var result = myContext.SaveChanges(); 
      } 

      catch (Exception ex) 
      { 
       Debug.Print(ex.ToString()); 
       throw; 
      } 
     } 
     transaction.Commit(); 
    } 
} 
+0

Я просто попытался добавить 'System.Data.IsolationLevel.Serializable' к BeginTransaction и я получаю тот же результат с нарушением. –

+0

Имеет ли это нарушение, когда код работает только на одном сервере, в одном потоке? Является ли код, в котором вы разместили точный код, который вы используете? Я предполагаю, что он модифицирован, чтобы отображать только необходимые данные. –

+0

Нарушение * не * происходит в одном сценарии потока/сервера –

Смежные вопросы