WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

by WebSurfer 25. March 2021 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

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  May 2021  >>
MoTuWeThFrSaSu
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar