WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

EF Core で PK / Unique 制約違反例外をキャッチ

by WebSurfer 2021年3月25日 10:26

EF Core 5.0 を利用して SQL Server のテーブルの CRUD 操作を行う ASP.NET Core 5.0 MVC アプリで、新規レコードの Create の際に PK / Unique 制約違反例外をキャッチし、エラーメッセージを表示する方法を書きます。

PK / Unique 制約の検証

先の記事「EF6 で PK / Unique 制約違反例外をキャッチ」の Core 版です。ほとんど EF6 の記事と同じですが、いくつか違う点があるので以下まとめて書いておきます。

  1. EF Code First でのデータ注釈を使用したインデックスとユニーク制約の付与は EF Core 5.0 で導入されたそうです。詳しくは Microsoft のドキュメント Indexes を見てください。(この記事でアプリのプラットフォームを .NET 5.0 としたのはそれが理由です。ちなみに、EF Core 3.x 以前は NuGet パッケージ Toolbelt.EntityFrameworkCore.IndexAttribute を利用できるらしいです)
  2. DbUpdateException から SqlException を取得する方法が EF6 とは異なります。EF6 では DbUpdateException の InnerException のさらに下の InnerException から SqlException を取得していましたが、EF Core では DbUpdateException 直下の InnerException で取得することができます。(EF6 も EF Core もコードを書いて試した結果の話です。必ずそうなると明記した Microsoft のドキュメントは見つからないので将来変わるかもしれないという不安要素はあります)
  3. DbUpdateException の InnerException から取得できる SqlException は System.Data.SqlClient 名前空間でなく Microsoft.Data.SqlClient 名前空間に属するものになります。(EF6 の場合は System.Data.SqlClient 名前空間)

先の EF6 の記事と同様に PK / Unique 制約違反例外をキャッチし、エラーメッセージを表示する機能を実装してみました。それが以下のコードです。上の画像が実行結果で SQL Server のテーブルの ProductId 列に付与した PK 制約違反を補足してエラーメッセージを表示しています。

エンティティクラス (Model)

上にも書きましたが、IndexAttribute が使用できるのは EF Core 5.0 以降です。プロパティには付与できなくてクラスに付与する必要がある点が EF6 とは異なります。

Migration の際 decimal 型のプロパティには "This will cause values to be silently truncated ..." という警告が出るので No Type Was Specified for the Decimal Column を参考に ColumnAttribute を付与してみました。その必要はないと思いますが。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

// IndexAttribute の属する名前空間
using Microsoft.EntityFrameworkCore;

namespace MvcCore5App2.Models
{
    [Index(nameof(ProductName), IsUnique = true)]
    public class PkUnique
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(128)]
        public string ProductName { get; set; }

        [Required]
        [Column(TypeName = "decimal(18,4)")]
        public decimal UnitPrice { get; set; }
    }
}

コンテキストクラス

using Microsoft.EntityFrameworkCore;
using MvcCore5App.Models;

namespace MvcCore5App.DAL
{
    public class PkUniqueContext : DbContext
    {
        public PkUniqueContext(DbContextOptions<PkUniqueContext> options)
            : base(options)
        {

        }

        public DbSet<PkUnique> PkUnique { set; get; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // DB に生成されるテーブルの名前を Product にした
            modelBuilder.Entity<PkUnique>().ToTable("Product");
        }
    }
}

Startup.cs に追加

上のコンテキストクラス PkUniqueContext のインスタンスを Controller に DI できるよう Startup.cs の ConfigureServices メソッドに以下のコードを追加します。

コード内の "DefaultConnection" はテンプレートで生成されたプロジェクトの appsettings.json に含まれる ASP.NET Identity 用の接続文字列です。なので、ASP.NET Identity 用のデータベースの中に Product というテーブルが追加で生成されます。

services.AddDbContext<PkUniqueContext>(options =>
  options.UseSqlServer(
    Configuration.GetConnectionString("DefaultConnection")));

上のエンティティクラスとコンテキストクラスから、EF Code First の機能を利用して生成された SQL Server のテーブルの構造は以下の通りです。上のエンティティクラスの UnitPrice プロパティに [Column(TypeName = "decimal(18,4)")] 属性を付与したのでデータ型が decimal(18,4) になっています。ちなみに、属性を設定しないと decimal(18,2) になります。

生成されたテーブルの構造

Controller / Action Method

スキャフォールディング機能を利用して Controller と View を生成します。以下のコードは、その Controller の Create アクションメソッドに、上に紹介した記事の PK / Unique 制約違反例外を補足するコードを実装したものです。

上に書きましたが、DbUpdateException から SqlException を取得する方法、取得した SqlException は Microsoft.Data.SqlClient 名前空間に属することに注意してください。

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcCore5App2.Data;
using MvcCore5App2.Models;

// SqlException が属する名前空間
using Microsoft.Data.SqlClient;

namespace MvcCore5App2.Controllers
{
    public class PkUniqueController : Controller
    {
        private readonly PkUniqueContext _context;

        public PkUniqueController(PkUniqueContext context)
        {
            _context = context;
        }

        // ・・・中略・・・

        // GET: PkUnique/Create
        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
            [Bind("ProductId,ProductName,UnitPrice")] PkUnique pkUnique)
        {
            if (ModelState.IsValid)
            {
                _context.Add(pkUnique);

                try
                {
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateException e)
                when (e.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(pkUnique);
                }
                return RedirectToAction(nameof(Index));
            }
            return View(pkUnique);
        }

        // ・・・中略・・・
    }
}

View

スキャフォールディング機能を利用して生成した create.cshtml のコードそのままです。

@model MvcCore5App2.Models.PkUnique

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>PkUnique</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="ProductId" class="control-label"></label>
                <input asp-for="ProductId" class="form-control" />
                <span asp-validation-for="ProductId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ProductName" class="control-label"></label>
                <input asp-for="ProductName" class="form-control" />
                <span asp-validation-for="ProductName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="UnitPrice" class="control-label"></label>
                <input asp-for="UnitPrice" class="form-control" />
                <span asp-validation-for="UnitPrice" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

EF6 と同様に PK / Unique 両方に制約違反がある場合は PK 制約違反だけしか補足できません。両方補足して両方のエラーメッセージを表示する方法は分かりません。分かったらこの記事に追記します。

Tags: , , ,

CORE

EF6 で PK / Unique 制約違反例外をキャッチ

by WebSurfer 2021年3月8日 18:08

EF6 を利用して SQL Server のテーブルの CRUD 操作を行う ASP.NET MVC5 アプリで、新規レコードの Create の際に Primary / Unique キー制約違反例外をキャッチし、エラーメッセージを表示する方法を書きます。

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 キーの制約違反だけしか補足できません。両方補足して両方のエラーメッセージを表示する方法は今のところ分かりません。今後の課題ということで。

Tags: , , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar