如何使用 EF6 和 SQL Server 捕获 UniqueKey Violation 异常?

Posted

技术标签:

【中文标题】如何使用 EF6 和 SQL Server 捕获 UniqueKey Violation 异常?【英文标题】:How can I catch UniqueKey Violation exceptions with EF6 and SQL Server? 【发布时间】:2015-10-09 12:47:45 【问题描述】:

我的一个表有一个唯一键,当我尝试插入重复记录时,它会按预期抛出异常。但是我需要将唯一键异常与其他异常区分开来,这样我就可以自定义违反唯一键约束的错误消息。

我在网上找到的所有解决方案都建议将ex.InnerException 转换为System.Data.SqlClient.SqlException 并检查Number 属性是否等于2601 或2627,如下所示:

try

    _context.SaveChanges();

catch (Exception ex)

    var sqlException = ex.InnerException as System.Data.SqlClient.SqlException;

    if (sqlException.Number == 2601 || sqlException.Number == 2627)
    
        ErrorMessage = "Cannot insert duplicate values.";
    
    else
    
        ErrorMessage = "Error while saving data.";
    

但问题是,将ex.InnerException 转换为System.Data.SqlClient.SqlException 会导致无效转换错误,因为ex.InnerException 实际上是System.Data.Entity.Core.UpdateException 的类型,而不是System.Data.SqlClient.SqlException

上面的代码有什么问题?如何捕获违反唯一键约束的行为?

【问题讨论】:

【参考方案1】:

使用 EF6 和 DbContext API(用于 SQL Server),我目前正在使用这段代码:

try

  // Some DB access

catch (Exception ex)

  HandleException(ex);


public virtual void HandleException(Exception exception)

  if (exception is DbUpdateConcurrencyException concurrencyEx)
  
    // A custom exception of yours for concurrency issues
    throw new ConcurrencyException();
  
  else if (exception is DbUpdateException dbUpdateEx)
  
    if (dbUpdateEx.InnerException != null
            && dbUpdateEx.InnerException.InnerException != null)
    
      if (dbUpdateEx.InnerException.InnerException is SqlException sqlException)
      
        switch (sqlException.Number)
        
          case 2627:  // Unique constraint error
          case 547:   // Constraint check violation
          case 2601:  // Duplicated key row error
                      // Constraint violation exception
            // A custom exception of yours for concurrency issues
            throw new ConcurrencyException();
          default:
            // A custom exception of yours for other DB issues
            throw new DatabaseAccessException(
              dbUpdateEx.Message, dbUpdateEx.InnerException);
        
      

      throw new DatabaseAccessException(dbUpdateEx.Message, dbUpdateEx.InnerException);
    
  

  // If we're here then no exception has been thrown
  // So add another piece of code below for other exceptions not yet handled...

正如你提到的UpdateException,我假设你正在使用ObjectContext API,但它应该是相似的。

【讨论】:

查看了您分享的代码后,我现在可以看到我的代码的问题非常明显。我应该写“ex.InnerException.InnerException as SqlException”而不是“ex.InnerException as SqlException”。 有没有办法同时检测出哪一列违规?一张表中可能有多个唯一键... 这不是打破了 ORM 的模式,创建了对数据库的直接依赖吗?这是否意味着每次使用其他数据库时,我都必须重新编程异常处理以识别特定代码? @ken2k 此代码依赖于 SQL Server 实现,不适用于其他数据库。正如 Daniel Lobo 所说的那样,它打破了 ORM 的想法。 我只是想知道有多少次有人将他们的应用程序移动到完全不同的数据库系统?我已经编程 30 年了。我们从未将应用程序迁移到不同的数据库系统【参考方案2】:

在我的例子中,我使用的是 EF 6,并用以下方式装饰了我的模型中的一个属性:

[Index(IsUnique = true)]

为了发现违规行为,我使用 C# 7 执行以下操作,这变得更加容易:

protected async Task<IActionResult> PostItem(Item item)

  _DbContext.Items.Add(item);
  try
  
    await _DbContext.SaveChangesAsync();
  
  catch (DbUpdateException e)
  when (e.InnerException?.InnerException is SqlException sqlEx && 
    (sqlEx.Number == 2601 || sqlEx.Number == 2627))
  
    return StatusCode(StatusCodes.Status409Conflict);
  

  return Ok();

注意,这只会捕获唯一索引约束违规。

【讨论】:

【参考方案3】:
// put this block in your loop
try

   // do your insert

catch(SqlException ex)

   // the exception alone won't tell you why it failed...
   if(ex.Number == 2627) // <-- but this will
   
      //Violation of primary key. Handle Exception
   

编辑:

您也可以只检查异常的消息组件。像这样的:

if (ex.Message.Contains("UniqueConstraint")) // do stuff

【讨论】:

不幸的是,catch(SqlException ex) 没有捕获唯一键违规异常并抛出此错误:EntityFramework.dll 中发生“System.Data.Entity.Infrastructure.DbUpdateException”类型的异常,但是未在用户代码中处理 检查错误消息中的“UniqueConstraint”应该可以,但这似乎不是最好的方法。【参考方案4】:
try

   // do your insert

catch(Exception ex)

   if (ex.GetBaseException().GetType() == typeof(SqlException))
   
       Int32 ErrorCode = ((SqlException)ex.InnerException).Number;
       switch(ErrorCode)
       
          case 2627:  // Unique constraint error
              break;
          case 547:   // Constraint check violation
              break;
          case 2601:  // Duplicated key row error
              break;
          default:
              break;
        
    
    else
    
       // handle normal exception
    

【讨论】:

我喜欢这样一个事实,即这增加了额外的错误代码,并且开关使代码看起来更干净,但我认为将InnerException 转换为ErrorCode 存在一个小错误。我想你想打电话给GetBaseException() 而不是使用InnerException 此外,这会吞噬所有 SqlException 类型,而不仅仅是唯一键违规。我认为您的default 案子可能应该重新提出。【参考方案5】:

我认为展示一些代码可能很有用,不仅可以处理重复行异常,还可以提取一些可用于编程目的的有用信息。例如。编写自定义消息。

这个Exception 子类使用正则表达式来提取数据库表名、索引名和键值。

public class DuplicateKeyRowException : Exception

    public string TableName  get; 
    public string IndexName  get; 
    public string KeyValues  get; 

    public DuplicateKeyRowException(SqlException e) : base(e.Message, e)
    
        if (e.Number != 2601) 
            throw new ArgumentException("SqlException is not a duplicate key row exception", e);

        var regex = @"\ACannot insert duplicate key row in object \'(?<TableName>.+?)\' with unique index \'(?<IndexName>.+?)\'\. The duplicate key value is \((?<KeyValues>.+?)\)";
        var match = new System.Text.RegularExpressions.Regex(regex, System.Text.RegularExpressions.RegexOptions.Compiled).Match(e.Message);

        Data["TableName"] = TableName = match?.Groups["TableName"].Value;
        Data["IndexName"] = IndexName = match?.Groups["IndexName"].Value;
        Data["KeyValues"] = KeyValues = match?.Groups["KeyValues"].Value;
    

DuplicateKeyRowException 类很容易使用...只需像之前的答案一样创建一些错误处理代码...

public void SomeDbWork() 
    // ... code to create/edit/update/delete entities goes here ...
    try  Context.SaveChanges(); 
    catch (DbUpdateException e)  throw HandleDbUpdateException(e); 


public Exception HandleDbUpdateException(DbUpdateException e)

    // handle specific inner exceptions...
    if (e.InnerException is System.Data.SqlClient.SqlException ie)
        return HandleSqlException(ie);

    return e; // or, return the generic error


public Exception HandleSqlException(System.Data.SqlClient.SqlException e)

    // handle specific error codes...
    if (e.Number == 2601) return new DuplicateKeyRowException(e);

    return e; // or, return the generic error

【讨论】:

【参考方案6】:

如果你想捕捉唯一约束

try  
   // code here 
 
catch(Exception ex)  
   //check for Exception type as sql Exception 
   if(ex.GetBaseException().GetType() == typeof(SqlException))  
     //Violation of primary key/Unique constraint can be handled here. Also you may //check if Exception Message contains the constraint Name 
    

【讨论】:

【参考方案7】:

您在编写代码时必须非常具体。

     try
     
         // do your stuff here.
     
     catch (Exception ex)
     
         if (ex.Message.Contains("UNIQUE KEY"))
          
            Master.ShowMessage("Cannot insert duplicate Name.", MasterSite.MessageType.Error);
         
         else  Master.ShowMessage(ex.Message, MasterSite.MessageType.Error); 
     

我刚刚更新了上面的代码,它对我有用。

【讨论】:

【参考方案8】:

SQL Server 的错误信息可以用这个语句捕获。

try
 
     //trying to insert unique key data      
 
 catch (Exception ex)
 
    var exp = ((SqlException)ex.InnerException.InnerException).Message;
    // exp hold error message generated by sql
 

【讨论】:

好吧,ex.InnerException.InnerException 可以是任何东西,或者抛出一个 NRE。这不安全。

以上是关于如何使用 EF6 和 SQL Server 捕获 UniqueKey Violation 异常?的主要内容,如果未能解决你的问题,请参考以下文章

使用 EF6 SQL Server CE Code First 创建数据库崩溃

带有 EF6 的 WPF 应用程序不会在 SQL Server Enterprise 中创建新数据库

SQL Server to .Net Decimals with EF6 database first issue

(4.7)怎么捕获和记录SQL Server中发生的死锁?

ASP.NET MVC 4 EF6 无法连接到 SQL Server Express 数据库

如何使用实体框架打开受密码保护的 SQL Server CE 数据库