EF6 を利用して SQL Server のテーブルの CRUD 操作を行う ASP.NET MVC5 アプリで、新規レコードの Create の際に Primary / Unique キー制約違反例外をキャッチし、エラーメッセージを表示する方法を書きます。
ADO.NET + SqlClient を使って SqlCommand.ExecuteNonQuery メソッドで INSERT 操作を行う場合は、例外が発生すると SqlException がスローされますので、try - catch 句でそれを補足して SqlException.Number プロパティを調べれば例外の内容が分かります。ちなみに、2627 が Primary キー制約違反、2601 が Unique キー制約違反になります。
しかしながら、EF6 の DbContext.SaveChanges メソッドで INSERT 操作を行う場合、スローされる例外は DbUpdateException, DbUpdateConcurrencyException などで、SqlException を直接補足することができません。
そこをどうするかですが、ググって調べた記事「EF6とSQL ServerでUniqueKey違反の例外をキャッチするにはどうすればよいですか?」によると、try - catch 句で DbUpdateException を補足し、その InnerException の InnerException から SqlException を取得できるとのことです。
そういうことなので、上の記事の 2 つ目の回答を参考に、Primary / Unique キー制約違反例外をキャッチし、エラーメッセージを表示する機能を実装してみました。
それが以下のコードです。上の画像は SQL Server のテーブルの ProductName 列に付与した Unique キー制約違反を補足してエラーメッセージを表示したものです。
エンティティクラス (Model)
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Mvc5App2.Models
{
public class ProductModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ProductId { get; set; }
[Required]
[StringLength(128)]
[Index(IsUnique = true)]
public string ProductName { get; set; }
[Required]
public decimal UnitPrice { get; set; }
}
}
コンテキストクラス
using Mvc5App2.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace Mvc5App2.DAL
{
public class ProductContext : DbContext
{
public ProductContext() : base("name=DefaultConnection")
{
}
public DbSet<ProductModel> Products { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
}
上のエンティティクラスとコンテキストクラスから、EF6 Code First の機能を利用して生成された SQL Server のテーブルの構造は以下の通りです。
Controller / Action Method
スキャフォールディング機能を利用して生成した controller のコードの中の Create アクションメソッドに、上に紹介した記事の Primary / Unique キー制約違反例外を補足するコードを実装したものです。
using System.Data.Entity;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5App2.DAL;
using Mvc5App2.Models;
using System.Data.Entity.Infrastructure;
using System.Data.SqlClient;
namespace Mvc5App2.Controllers
{
public class ProductModelsController : Controller
{
private ProductContext db = new ProductContext();
// ・・・中略・・・
// GET: ProductModels/Create
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(
[Bind(Include = "ProductId,ProductName,UnitPrice")]
ProductModel productModel)
{
if (ModelState.IsValid)
{
db.Products.Add(productModel);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException e)
when (e.InnerException?.InnerException is SqlException sqlEx &&
(sqlEx.Number == 2601 || sqlEx.Number == 2627))
{
if (sqlEx.Number == 2627)
{
ModelState.AddModelError("ProductId", "PK 制約違反");
}
if (sqlEx.Number == 2601)
{
ModelState.AddModelError("ProductName", "Unique 制約違反");
}
return View(productModel);
}
return RedirectToAction("Index");
}
return View(productModel);
}
}
// ・・・中略・・・
}
View
スキャフォールディング機能を利用して生成した create.cshtml のコードのままで手は加えていません。
@model Mvc5App2.Models.ProductModel
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>ProductModel</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.ProductId,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ProductId,
new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.ProductId, "",
new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ProductName,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ProductName,
new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.ProductName, "",
new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.UnitPrice,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.UnitPrice,
new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.UnitPrice, "",
new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Primary / Unique キー両方に制約違反がある場合は Primary キーの制約違反だけしか補足できません。両方補足して両方のエラーメッセージを表示する方法は今のところ分かりません。今後の課題ということで。