LoginSignup
1
0

ASP.NET MVCでデータベースを操作する

Posted at

前回の記事

ASP.NET MVCにデータベースを接続する

本記事のゴール

  • データベースをアプリケーションから操作する
    • 取得 SELECT
    • 追加 Insert
    • 更新 Update
    • 削除 Delete

本記事の全体像は以下画像のようになります。

image.png

はじめに

本記事ではDBコンテキストクラスの依存性の注入(DI)までの実装が完了している状態を想定して進めます。
依存性の注入処理が未実装の場合は前回記事「ASP.NET MVCにデータベースを接続する」の内容を先に行ってください。

また、本記事では過去記事「【ViewModel】ASP.NET MVCでサーバー側と画面側でデータを受け渡す」で用いた画面表示系統のコードをベースに新規作成した各ファイルで実装していきます。画面表示やViewModelについての解説は過去記事をご覧ください。

Contextクラスを呼び出す

コンストラクタにContextクラスを指定することで呼び出しを行います。
DBに対する操作はここで呼び出したContextクラスに対して行っていきます。

コードは簡略化のためControllerに記載していますが、Service等においても同様のコードで呼び出すことができます。

GourmetController.cs
+ using Database;
  using Microsoft.AspNetCore.Mvc;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
+         private readonly GourmetDbContext _context;
+
+         public GourmetController(GourmetDbContext context)
+         {
+             _context = context;
+         }
+
          public IActionResult Index()
          {
              return View();
          }
      }
  }

データの取得(SELECT)

単一テーブルから全件取得

テーブル情報は _context.{エンティティ名} を参照することで取得できます。
この情報を ToList() でList型に変換してViewModelに渡します。

ToList() の代わりに ToArray() を使うと配列に変換することもできます。

GourmetController.cs
  using Database;
  using Microsoft.AspNetCore.Mvc;
+ using Web.Models;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

          public IActionResult Index()
          {
+             // 全件取得
+             var gourmetList = _context.Gourmet.ToList();
+
+             var viewModel = new GourmetViewModel()
+             {
+                 GourmetList = gourmetList
+             };
              return View(viewModel);
          }
      }
  }
実行結果

DBの値
image1.png

画面表示
image2.png

その他のソース
GourmetViewModel.cs
using Database.Entities;

namespace Web.Models
{
    public class GourmetViewModel
    {
        #region 画面表示

        /// <summary>
        /// グルメ情報
        /// </summary>
        public List<Gourmet> GourmetList { get; set; } = new List<Gourmet>();

        #endregion
    }
}
Index.cshtml
@* ViewModelを指定する *@
@model GourmetViewModel
@{
    ViewData["Title"] = "グルメ";
}

<div>各地のグルメリストを表示する画面です。</div>

<table class="table">
    <thead>
        <tr>
            <th>グルメID</th>
            <th>都道府県番号</th>
            <th>グルメ名</th>
            <th>評価</th>
        </tr>
    </thead>
    <tbody>
        @* テーブルのデータ行をグルメ情報の個数分繰り返し出力 *@
        @foreach(var gourmet in Model.GourmetList)
        {
            <tr>
                <td>@gourmet.GourmetId</td>
                <td>@gourmet.PrefectureCode</td>
                <td>@gourmet.GourmetName</td>
                <td>@gourmet.Rate</td>
            </tr>
        }
    </tbody>
</table>

単一テーブルから条件指定取得

取得したテーブル情報に対して条件をLINQ形式で指定していきます。

主に使用されるもの

  • Where 絞り込み
  • OrderBy 並び替え(第二ソート以降はThenByを使用、Descendingをつけると降順)
  • Select 選択(射影)

※条件指定の詳細は以下Microsoft公式ドキュメントを参照してください。
https://learn.microsoft.com/ja-jp/dotnet/csharp/linq/standard-query-operators/

GourmetController.cs
  using Database;
+ using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

          public IActionResult Index()
          {
-             // 全件取得
-             var gourmetList = _context.Gourmet.ToList();
+             // 全件
+             var entities = _context.Gourmet;
+             
+             // 条件指定で取得
+             var gourmetList = entities
+                 // 絞り込み
+                .Where(g => g.PrefectureCode < 10 || g.GourmetName == "辛子明太子")
+                // 並び替え(第一ソート、昇順)
+                .OrderBy(g => g.PrefectureCode)
+                // 並び替え(第二ソート、降順)
+                .ThenByDescending(g => g.Rate)
+                // 必要なデータを選択・加工
+                .Select(g => new Gourmet
+                {
+                    GourmetId = g.GourmetId,
+                    GourmetName = g.GourmetName == "辛子明太子"
+                        ? $"{g.GourmetName}!!!!!"
+                        : g.GourmetName,
+                    PrefectureCode = g.PrefectureCode,
+                    Rate = g.Rate * 10,
+                }).ToList();

              var viewModel = new GourmetViewModel()
              {
                  GourmetList = gourmetList
              };
              return View(viewModel);
          }
      }
  }
実行結果

DBの値
image1.png

画面表示
image3.png

なお、上記は「LINQのメソッド構文」での記法であり、同じ内容を「LINQのクエリ式構文」で書くこともできます。
メソッド構文ではC#の記述に似た形式(メソッドチェーン)で書くことができる一方、クエリ式構文ではSQLの記述に似た形式で書くことができます。

※クエリ式構文の詳細は以下Microsoft公式ドキュメントを参照してください。
https://learn.microsoft.com/ja-jp/dotnet/csharp/linq/get-started/query-expression-basics

クエリ式構文で書いた場合
var gourmetList = (from g in entities
                   where g.PrefectureCode < 10 || g.GourmetName == "辛子明太子"
                   orderby g.PrefectureCode, g.Rate descending
                   select new Gourmet {
                       GourmetId = g.GourmetId,
                       GourmetName = g.GourmetName == "辛子明太子"
                        ? $"{g.GourmetName}!!!!!"
                        : g.GourmetName,
                       PrefectureCode = g.PrefectureCode,
                       Rate = g.Rate * 10,
                   }).ToList();

単一テーブルから単一行取得

単一行(1件のみ)は主に以下の方法で取得します。

  • First 先頭1件取得(取得結果が0件の場合は例外発生)
  • FirstOrDefault 先頭1件取得(取得結果が0件の場合は既定値(null,0等)が返却される)
GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

          public IActionResult Index()
          {
              // 全件
              var entities = _context.Gourmet;
              
-             // 条件指定で取得
-             var gourmetList = entities
-                (中略)
-                }).ToList();

+             // 評価が高い順から先頭1件取得
+             var gourmet = entities
+                 .OrderByDescending(g => g.Rate)
+                 .FirstOrDefault();

              var viewModel = new GourmetViewModel()
              {
-                 GourmetList = gourmetList
+                 // ※表示側がリスト型のためリストに無理やり変換
+                 GourmetList = gourmet != null
+                     ? new List<Gourmet> { gourmet }
+                     : new List<Gourmet>()
              };
              return View(viewModel);
          }
      }
  }
実行結果

DBの値
image1.png

画面表示
image4.png

複数テーブルから取得

複数テーブルの情報を取得するにはテーブルの結合を行います。

LINQでの結合については以下ブログが参考になります。

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
+ using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

          public IActionResult Index()
          {
-             // 全件
-             var entities = _context.Gourmet;
-             
-             // 評価が高い順から先頭1件取得
-             var gourmet = entities
-                 .OrderByDescending(g => g.Rate)
-                 .FirstOrDefault();

+             // グルメテーブルと都道府県テーブル結合
+             var gourmetList = _context.Gourmet// 結合されるテーブル
+                 .Join(
+                     _context.Prefecture,// 結合するテーブル
+                     g => g.PrefectureCode,// 結合される側結合条件
+                     p => p.PrefectureCode,// 結合する側結合条件
+                     (g, p) => new
+                     {
+                         p.PrefectureCode,// 後続で必要なカラムを指定
+                         p.PrefectureName,
+                         g.GourmetName,
+                         g.Rate
+                     }
+                 ).OrderBy(x => x.PrefectureCode)
+                 .ThenBy(x => x.Rate)
+                 .Select(x => new GourmetData
+                 {
+                     Prefecture = x.PrefectureName,
+                     GourmetName = x.GourmetName,
+                     Rate = x.Rate != null
+                         ? ((int)x.Rate).ToString()
+                         : "-"
+                 }).ToList();

              var viewModel = new GourmetViewModel()
              {
                  GourmetList = gourmetList
              };
              return View(viewModel);
          }
      }
  }
実行結果

DBの値
image1.png
image5.png

画面表示
image6.png

その他のソース
GourmetViewModel.cs
- using Database.Entities;
+ using Web.Models.Data;

  namespace Web.Models
  {
      public class GourmetViewModel
      {
          #region 画面表示

          /// <summary>
          /// グルメ情報
          /// </summary>
-         public List<Gourmet> GourmetList { get; set; } = new List<Gourmet>();
+         public List<GourmetData> GourmetList { get; set; } = new List<GourmetData>();

          #endregion
      }
  }
GourmetData.cs
namespace Web.Models.Data
{
    public class GourmetData
    {
        /// <summary>
        /// 都道府県
        /// </summary>
        public required string PrefectureName { get; set; }

        /// <summary>
        /// グルメ名
        /// </summary>
        public required string GourmetName { get; set; }

        /// <summary>
        /// 評価
        /// </summary>
        public required string Rate { get; set; }
    }
}

Index.cshtml
  @* ViewModelを指定する *@
  @model GourmetViewModel
  @{
      ViewData["Title"] = "グルメ";
  }

  <div>各地のグルメリストを表示する画面です。</div>

  <table class="table">
      <thead>
          <tr>
-             <th>グルメID</th>
-             <th>都道府県番号</th>
+             <th>都道府県</th>
              <th>グルメ名</th>
              <th>評価</th>
          </tr>
      </thead>
      <tbody>
          @* テーブルのデータ行をグルメ情報の個数分繰り返し出力 *@
          @foreach(var gourmet in Model.GourmetList)
          {
              <tr>
-                 <td>@gourmet.GourmetId</td>
-                 <td>@gourmet.PrefectureCode</td>
+                 <td>@gourmet.PrefectureName</td>
                  <td>@gourmet.GourmetName</td>
                  <td>@gourmet.Rate</td>
              </tr>
          }
      </tbody>
  </table>
クエリ式構文で書いた場合
var gourmetList = (from g in _context.Gourmet // 結合されるテーブル
                   join p in _context.Prefecture // 結合するテーブル
                   on g.PrefectureCode equals p.PrefectureCode // 結合条件
                   orderby g.PrefectureCode, g.Rate
                   select new GourmetData
                   {
                       PrefectureName = p.PrefectureName,
                       GourmetName = g.GourmetName,
                       Rate = g.Rate != null
                            ? ((int)g.Rate).ToString()
                            : "-"
                   }).ToList();

注意点1:遅延評価について(IQueryable型)

LINQの特徴の一つに遅延評価があります。遅延評価ではIQueryable型の間はクエリが実行されずToList() などの処理を行いIQueryable型以外へ変換された時点でクエリが実行されます。
このため、よりパフォーマンスを求めるためにはクエリが実行されるタイミングを意識する必要があります。

例として、以下コードでは最終的に「グルメ名」が「たこ焼き」である1行をDBから取得しようとしています。このとき gourmet1 では一度DBから全件取得してからデータを1行取り出しており無駄なデータをDBから取得してしまっています。
改善例として、IQueryable型に対して直接 FirstOrDefault() を行い、DBから1件のみを取得するなどが挙げられます。

ただし、1件の取得を何度も行う場合はDBへのクエリ実行回数が増えてしまうため、1回だけDBから全件取得してデータを確保した中から1件を取り出したほうが結果的にパフォーマンスが良い場合もあります。このため要件に応じて使い分けをする必要があります。

遅延評価
// まだクエリは実行されない
IQueryable<Gourmet> query1 = _context.Gourmet.Where(g => g.Rate != null);
// まだクエリは実行されない
IQueryable<Gourmet> query2 = query1.OrderBy(g => g.Rate);

// ここでクエリが実行される(IQueryable型以外になるとき)
List<Gourmet> list = query2.ToList(); // DBから全件取得
Gourmet? gourmet1 = list.FirstOrDefault(g => g.GourmetName == "たこ焼き"); // 1件取り出し

/* 改善例 */
Gourmet? gourmet2 = query2.FirstOrDefault(g => g.GourmetName == "たこ焼き"); // DBから1件取得

注意点2:読み取り専用について(AsNoTracking)

IQueryable型に対して AsNoTracking() を指定すると読み取り専用のデータとすることができます。読み取り専用とした場合、Attach() などにより再度追跡を開始しない限りインスタンス内のデータを変更して更新処理を行ってもDBへの反映はされません。

読み取り専用とした場合、使用するメモリが少なくなりパフォーマンスが向上します。
※詳細は以下Microsoft公式ドキュメントを参照してください。
https://learn.microsoft.com/ja-jp/ef/core/querying/tracking

読み取り専用
// 読み取り&書き取りどちらも可
var readAndWrite = _context.Gourmet
    .Where(g => g.GourmetName == "牛タン")
    .First();

// 読み取り専用, 書き込み不可
var readOnly = _context.Gourmet
    .Where(g => g.GourmetName == "たこ焼き")
    .AsNoTracking()
    .First();

// 反映される
readAndWrite.GourmetName = "牛タン名前変更(読み取り&書き込みどちらも可)";
// 読み取り専用に対しての変更は反映されない
readOnly.GourmetName = "たこ焼き名前変更(読み取り専用)";

// DBに反映
_context.SaveChanges();

データの追加(INSERT)

単一データの追加

テーブルに対してデータを追加する場合は _context に対してエンティティのインスタンスを Add() し、SaveChanges()を実行します。

Add() メソッド及び後続の同様のメソッドについて、以下2種類の呼び方がありますが、内部的には同じ処理が実行されます。実際に使用する際にはコーディング規約等に従ってください。

  • Contextクラス(DbContext)から呼び出すメソッド _context.Add()
  • エンティティクラス(DbSet)から呼び出すメソッド _context.Gourmet.Add()

※詳細は以下Microsoft公式ドキュメントを参照してください。
https://learn.microsoft.com/ja-jp/ef/core/change-tracking/miscellaneous#dbcontext-versus-dbset-methods

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
  using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

+         public IActionResult Add()
+         {
+             // 追加するグルメ情報
+             var addGourmet = new Gourmet()
+             {
+                 GourmetName = "あんこう鍋",
+                 PrefectureCode = 8,
+                 Rate = 3
+             };
+
+             _context.Add(addGourmet);
+             // DBに反映
+             _context.SaveChanges();
+
+             return RedirectToAction(nameof(Index));
+         }
      }
  }
実行結果

更新前 DBの値
image1.png
更新前 画面表示(「グルメ追加」ボタン押下)
image7.png

更新後 DBの値
image8.png
更新後 画面表示
image9.png

その他のソース
Index.cshtml
  @* ViewModelを指定する *@
  @model GourmetViewModel
  @{
      ViewData["Title"] = "グルメ";
  }

  <div>各地のグルメリストを表示する画面です。</div>

+ <a class="btn btn-primary" asp-controller="Gourmet" asp-action="Add">グルメ追加</a>
+
  <table class="table">
      (中略)
  </table>

追加したデータを再利用する

追加データを処理内の別の箇所で利用する場合は対象データのインスタンスから直接値を取得します。
ただし、DBへ反映したタイミングでセットされる自動採番の主キーは一度DBへの反映を行った後で再度インスタンスを参照します。

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
  using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

          public IActionResult Add()
          {
              // 追加するグルメ情報
-             var addGourmet = new Gourmet()
-             {
-                 GourmetName = "あんこう鍋",
-                 PrefectureCode = 8,
-                 Rate = 3
-             };
-             
-             _context.Add(addGourmet);

+             var addGourmet1 = new Gourmet()
+             {
+                 GourmetName = "盛岡冷麺1",
+                 PrefectureCode = 3,
+                 Rate = 5
+             };
+             var incorrectGourmet1Id = addGourmet1.GourmetId;// DB反映前だと正しい値はとれない
+
+             _context.Add(addGourmet1);
+             // GourmetIdを確定させるために一度反映
+             _context.SaveChanges();
+
+             var correctGourmet1Id = addGourmet1.GourmetId;// DB反映後は正しい値
+
+             var addGourmet2 = new Gourmet()
+             {
+                 GourmetName = $"盛岡冷麺2 1のID:{addGourmet1.GourmetId}",
+                 PrefectureCode = addGourmet1.PrefectureCode,
+                 Rate = addGourmet1.Rate
+             };
+
+             _context.Add(addGourmet2);
              _context.SaveChanges();

              return RedirectToAction(nameof(Index));
          }
      }
  }
実行結果

更新前 DBの値
image8.png
更新前 画面表示(「グルメ追加」ボタン押下)
image9.png

更新後 DBの値
image10.png
更新後 画面表示
image11.png

複数データの追加

テーブルに対してデータを複数追加する場合は、リストや配列でまとめたものを AddRange() に渡して追加します。

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
  using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

+         public IActionResult AddRange()
+         {
+             // 追加するグルメ情報リスト
+             var addGourmetList = new List<Gourmet>()
+             {
+                 new Gourmet()
+                 {
+                     GourmetName = "和歌山ラーメン",
+                     PrefectureCode = 30,
+                     Rate = 2
+                 },
+                 new Gourmet()
+                 {
+                     GourmetName = "徳島ラーメン",
+                     PrefectureCode = 36,
+                     Rate = 2
+                 }
+             };
+
+             _context.AddRange(addGourmetList);
+             // DBに反映
+             _context.SaveChanges();
+
+             return RedirectToAction(nameof(Index));
+         }
      }
  }
実行結果

更新前 DBの値
image10.png
更新前 画面表示(「グルメ複数追加」ボタン押下)
image12.png

更新後 DBの値
image13.png
更新後 画面表示
image14.png

その他のソース
Index.cshtml
  @* ViewModelを指定する *@
  @model GourmetViewModel
  @{
      ViewData["Title"] = "グルメ";
  }

  <div>各地のグルメリストを表示する画面です。</div>

  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="Add">グルメ追加</a>
+ <a class="btn btn-primary" asp-controller="Gourmet" asp-action="AddRange">グルメ複数追加</a>

  <table class="table">
      (中略)
  </table>

データの更新(UPDATE)

テーブルのデータを更新する場合は、取得したデータを変更して SaveChanges() を実行します。

Update() の指定は基本的には不要です。詳細は以下記事を参照してください。

EF Coreで正しくUPDATEする方法 (jun1s様)
https://qiita.com/jun1s/items/3e2b3702a965bb5e2705

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
  using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

+         public IActionResult Update()
+         {
+             // 対象データ取得
+             var updateGourmet = _context.Gourmet
+                 .Where(g => g.GourmetName == "盛岡冷麺1")
+                 .FirstOrDefault();
+             if (updateGourmet == null) return RedirectToAction(nameof(Index));
+
+             // 取得したデータを変更
+             updateGourmet.GourmetName = "盛岡冷麵";
+             // DBに反映
+             _context.SaveChanges();
+             
+             return RedirectToAction(nameof(Index));
+         }
      }
  }
実行結果

更新前 DBの値
image13.png
更新前 画面表示(「グルメ更新」ボタン押下)
image15.png

更新後 DBの値
image16.png
更新後 画面表示
image17.png

その他のソース
Index.cshtml
  @* ViewModelを指定する *@
  @model GourmetViewModel
  @{
      ViewData["Title"] = "グルメ";
  }

  <div>各地のグルメリストを表示する画面です。</div>

  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="Add">グルメ追加</a>
  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="AddRange">グルメ複数追加</a>
+ <a class="btn btn-warning" asp-controller="Gourmet" asp-action="Update">グルメ更新</a>

  <table class="table">
      (中略)
  </table>

データの削除(DELETE)

単一データの削除

テーブルのデータを削除する場合は、_context に対して削除対象となるエンティティのインスタンスを Remove() で指定した後、 SaveChanges()を実行します。

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
  using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

+         public IActionResult Delete()
+         {
+             // 対象データ取得
+             var deleteGourmet = _context.Gourmet
+                 .Where(g => g.GourmetName == "盛岡冷麺2 1のID:6")
+                 .FirstOrDefault();
+             if (deleteGourmet == null) return RedirectToAction(nameof(Index));
+
+             _context.Remove(deleteGourmet);
+             // DBに反映
+             _context.SaveChanges();
+             
+             return RedirectToAction(nameof(Index));
+         }
      }
  }
実行結果

更新前 DBの値
image16.png
更新前 画面表示(「グルメ削除」ボタン押下)
image18.png

更新後 DBの値
image19.png
更新後 画面表示
image20.png

その他のソース
Index.cshtml
  @* ViewModelを指定する *@
  @model GourmetViewModel
  @{
      ViewData["Title"] = "グルメ";
  }

  <div>各地のグルメリストを表示する画面です。</div>

  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="Add">グルメ追加</a>
  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="AddRange">グルメ複数追加</a>
  <a class="btn btn-warning" asp-controller="Gourmet" asp-action="Update">グルメ更新</a>
+ <a class="btn btn-danger" asp-controller="Gourmet" asp-action="Delete">グルメ削除</a>

  <table class="table">
      (中略)
  </table>

複数データの削除

テーブルに対してデータを複数削除する場合は、リストや配列でまとめたものを RemoveRange() に渡して削除します。

GourmetController.cs
  using Database;
  using Database.Entities;
  using Microsoft.AspNetCore.Mvc;
  using Web.Models;
  using Web.Models.Data;

  namespace Web.Controllers
  {
      public class GourmetController : Controller
      {
          (中略)

+         public IActionResult DeleteRange()
+         {
+             // 対象データ取得
+             var deleteGourmetList = _context.Gourmet
+                 .Where(g => g.GourmetName.Contains("ラーメン"))
+                 .ToList();
+
+             _context.RemoveRange(deleteGourmetList);
+             // DBに反映
+             _context.SaveChanges();
+             
+             return RedirectToAction(nameof(Index));
+         }
      }
  }
実行結果

更新前 DBの値
image19.png
更新前 画面表示(「グルメ複数削除」ボタン押下)
image21.png

更新後 DBの値
image22.png
更新後 画面表示
image23.png

その他のソース
Index.cshtml
  @* ViewModelを指定する *@
  @model GourmetViewModel
  @{
      ViewData["Title"] = "グルメ";
  }

  <div>各地のグルメリストを表示する画面です。</div>

  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="Add">グルメ追加</a>
  <a class="btn btn-primary" asp-controller="Gourmet" asp-action="AddRange">グルメ複数追加</a>
  <a class="btn btn-warning" asp-controller="Gourmet" asp-action="Update">グルメ更新</a>
  <a class="btn btn-danger" asp-controller="Gourmet" asp-action="Delete">グルメ削除</a>
+ <a class="btn btn-danger" asp-controller="Gourmet" asp-action="DeleteRange">グルメ複数削除</a>

  <table class="table">
      (中略)
  </table>

Execute系メソッド(ExecuteUpdate / ExecuteDelete)

EFCore7以降では、ここまで解説してきた「データトラッキング(追跡)したデータを操作し、SaveChangesでDBに反映」という動作を省略し、直接DBへ対してデータ操作を投げるExecute系のメソッドが実装されています。
これにより、メモリ内で各データをトラッキングする必要がないためパフォーマンスの向上が期待できます。

ただし、SaveChangesでは追跡した結果の複数変更を同時に行うのに対して、Execute系ではコード実行時に都度実行されるため排他制御や実行順、効率的な記述などを考慮する必要があります。

ExecuteUpdate と ExecuteDelete - Microsoft Learn

動作サンプルや注意点については以下ブログが参考になります。

大量のデータを操作する(BulkExtension)

数千件やそれ以上の大量のデータを操作する際、本記事でのデータ操作では処理に時間を要する場合があります。この場合、BulkExtensionを使うなどの対処方法が挙げられます。

BulkExtensionでは数千件以上の大量のデータを効率的に操作できる一方、少量のデータではかえって非効率的な操作となる場合がありますので要件に応じて使用を検討してください。

EF Core のツールと拡張機能 / EFCore.BulkExtensions - Microsoft Learn

EFCore.BulkExtensions - nuget

おまけ:データの検索と新規追加・更新ができる画面を実装する

ここまでの内容を利用して作成した、グルメ情報の一覧画面のサンプルコードを以下に示します。なお、簡略化のためコントローラーに処理を記載しています。
また、競合やエラーメッセージの日本語化など、考慮されていない点もあるため、実装の参考とする際はご注意ください。

image24.png

ViewModel GourmetViewModel.cs

主に検索系のプロパティを追加しています。
また、都道府県をドロップダウンで選択できるようにするためSelectListを渡しています。

/Web/Models/GourmetViewModel.cs(修正)
using Microsoft.AspNetCore.Mvc.Rendering;
using Web.Models.Data;

namespace Web.Models
{
    /// <summary>
    /// グルメ画面用ViewModel
    /// </summary>
    public class GourmetViewModel
    {
        #region 画面表示

        /// <summary>
        /// 都道府県選択用セレクトリスト
        /// </summary>
        public List<SelectListItem>? PrefectureSelectList;

        /// <summary>
        /// グルメ情報リスト
        /// </summary>
        public List<GourmetData> GourmetList { get; set; } = new List<GourmetData>();

        #endregion

        #region 検索条件

        /// <summary>
        /// 検索用:都道府県
        /// </summary>
        public string? SearchPrefecture {  get; set; }

        /// <summary>
        /// 検索用:都道府県(整数型への変換値)
        /// </summary>
        public int? SearchPrefectureConvertInt
        {
            get
            {
                // int型へ変換可ならば変換値、変換不可ならばnull
                if (int.TryParse(SearchPrefecture, out int value))
                {
                    return value;
                };
                return null;
            }
        }

        /// <summary>
        /// 検索用:グルメ名
        /// </summary>
        public string? SearchName { get; set; }

        /// <summary>
        /// 検索用:評価上限
        /// </summary>
        public int? SearchRateMax { get; set; }

        /// <summary>
        /// 検索用:評価下限
        /// </summary>
        public int? SearchRateMin { get; set; }

        #endregion
    }
}
ViewModel用データ GourmetData.cs

更新/追加時用の必須属性を追加しています。
また、更新or追加判別用のグルメIDを追加しています。

/Web/Models/Data/GourmetData.cs(修正)
using System.ComponentModel.DataAnnotations;

namespace Web.Models.Data
{
    public class GourmetData
    {
        /// <summary>
        /// グルメID
        /// </summary>
        public int? GourmetId { get; set; }

        /// <summary>
        /// 都道府県コード
        /// </summary>
        [Required]
        public required int PrefectureCode { get; set; }

        /// <summary>
        /// グルメ名
        /// </summary>
        [Required]
        public required string GourmetName { get; set; }

        /// <summary>
        /// 評価
        /// </summary>
        public required int? Rate { get; set; }
    }
}
コントローラー GourmetController.cs

初期表示を担う Index、検索フォームから呼び出される Search、更新ボタンから呼び出される Saveの3つに整理。
Saveでは追加と更新を「対象データ内にIDを保持しているか」で判別して両立させています。

/Web/Controllers/GourmetController.cs(修正)
using Database;
using Database.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Text.Json;
using Web.Models;
using Web.Models.Data;

namespace Web.Controllers
{
    public class GourmetController : Controller
    {
        private readonly GourmetDbContext _context;
        private readonly string _viewModelKey = "ViewModel";
        
        public GourmetController(GourmetDbContext context)
        {
            _context = context;
        }

        /// <summary>
        /// 初期表示処理
        /// </summary>
        /// <remarks>
        /// 初回アクセス用、検索条件なしでDBから情報取得
        /// </remarks>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Index()
        {
            // ViewModel取得・生成
            // - TempDataにViewModelの設定があれば取得して使う、なければ生成する
            GourmetViewModel? tempViewModel = null;
            if (TempData[_viewModelKey] != null)
            {
                tempViewModel = JsonSerializer.Deserialize<GourmetViewModel>(TempData[_viewModelKey] as string ?? string.Empty);
            }
            var viewModel = tempViewModel ?? new GourmetViewModel();

            // 初期表示用データ取得
            viewModel.PrefectureSelectList = GetPrefectureSelectList();
            viewModel.GourmetList = GetGourmetList(viewModel);

            return View(viewModel);
        }

        /// <summary>
        /// 検索処理
        /// </summary>
        /// <remarks>
        /// 検索条件ありでDBから情報取得
        /// </remarks>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [HttpPost]
        public IActionResult Search(GourmetViewModel viewModel)
        {
            // Indexへリダイレクト
            TempData[_viewModelKey] = JsonSerializer.Serialize(viewModel);
            return RedirectToAction(nameof(Index));
        }

        /// <summary>
        /// 保存処理
        /// </summary>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [HttpPost]
        public IActionResult Save(GourmetViewModel viewModel)
        {
            /* 入力値検証 */
            if (!ModelState.IsValid)
            {
                viewModel.PrefectureSelectList = GetPrefectureSelectList();
                // NOTE: Viewで返却した場合、返却後のURLが /Gourmet/Save となります。ユーザーへ表示するURLを揃えたい場合はModelStateの持ち運びやメソッド名の変更を検討してください。
                return View(nameof(Index), viewModel);
            }

            /* データ保存 */
            // DBから更新対象データの既存登録内容取得
            var idList = viewModel.GourmetList.Select(g => g.GourmetId);
            var gourmetDataList = _context.Gourmet.Where(g => idList.Contains(g.GourmetId)).ToList();

            foreach (var newData in viewModel.GourmetList)
            {
                // DBに同じIDのデータが存在するか確認
                var entity = gourmetDataList.Where(g => g.GourmetId == newData.GourmetId).FirstOrDefault();
                if (entity != null)
                {
                    // データが存在する場合は更新
                    _context.Entry(entity).CurrentValues.SetValues(newData);
                }
                else
                {
                    // データが存在しない場合は新規追加
                    var addGourmet = new Gourmet
                    {
                        GourmetName = newData.GourmetName,
                        PrefectureCode = newData.PrefectureCode,
                        Rate = newData.Rate
                    };
                    _context.Gourmet.Add(addGourmet);
                }
            }

            // DBに反映
            _context.SaveChanges();

            // Indexへリダイレクト
            TempData[_viewModelKey] = JsonSerializer.Serialize(viewModel);
            return RedirectToAction(nameof(Index));
        }

        /// <summary>
        /// 都道府県選択用セレクトリスト取得
        /// </summary>
        /// <returns></returns>
        private List<SelectListItem> GetPrefectureSelectList()
        {
            return _context.Prefecture
                .Select(p => new SelectListItem
                {
                    Value = p.PrefectureCode.ToString(),
                    Text = p.PrefectureName
                }).ToList();
        }

        /// <summary>
        /// グルメ情報リスト取得
        /// </summary>
        /// <param name="viewModel">検索条件格納済みViewModel</param>
        /// <returns>グルメ情報リスト</returns>
        private List<GourmetData> GetGourmetList(GourmetViewModel? viewModel)
        {
            var gourmetQuery = _context.Gourmet.AsQueryable();

            // 都道府県が指定されている場合絞り込み
            if (viewModel?.SearchPrefectureConvertInt != null)
            {
                gourmetQuery = gourmetQuery.Where(g => g.PrefectureCode == viewModel.SearchPrefectureConvertInt);
            }

            // グルメ名が指定されている場合絞り込み
            if (!string.IsNullOrEmpty(viewModel?.SearchName))
            {
                gourmetQuery = gourmetQuery.Where(g => g.GourmetName.Contains(viewModel.SearchName));
            }

            // 評価上限が指定されている場合絞り込み
            if (viewModel?.SearchRateMax != null)
            {
                gourmetQuery = gourmetQuery.Where(g => g.Rate <= viewModel.SearchRateMax);
            }

            // 評価下限が指定されている場合絞り込み
            if (viewModel?.SearchRateMin != null)
            {
                gourmetQuery = gourmetQuery.Where(g => viewModel.SearchRateMin <= g.Rate);
            }

            // 構築クエリに対して並び替え(評価降順、都道府県コード昇順)し、必要な情報を取得
            return gourmetQuery
                .OrderByDescending(x => x.Rate)
                .ThenBy(x => x.PrefectureCode)
                .Select(x => new GourmetData
                {
                    GourmetId = x.GourmetId,
                    PrefectureCode = x.PrefectureCode,
                    GourmetName = x.GourmetName,
                    Rate = x.Rate
                }).ToList();
        }
    }
}
画面表示 Index.cshtml

検索フォームの設置、テーブル内各項目を入力できるように変更、行追加用のテンプレート追加などを行っています。

/Web/Views/Gourmet/Index.cshtml(修正)
@* ViewModelを指定する *@
@model GourmetViewModel
@{
    ViewData["Title"] = "グルメ";
}

<div>各地のグルメリストを表示する画面です。</div>

<form class="card my-4" asp-controller="Gourmet" asp-action="Search" method="post">
    <div class="card-header">
        検索条件
    </div>
    <div class="card-body">
        <div class="row gy-3">
            <div class="col-3 col-lg-2 col-xl-1">
                <label asp-for="SearchPrefecture" class="col-form-label">都道府県</label>
            </div>
            <div class="col-9 col-lg-4 col-xl-5">
                @* 都道府県入力用 *@
                <select asp-for="SearchPrefecture" asp-items="Model.PrefectureSelectList" class="form-control">
                    <option value="">都道府県選択</option>
                </select>
            </div>
            <div class="col-3 col-lg-2 col-xl-1">
                <label asp-for="SearchName" class="col-form-label">グルメ名</label>
            </div>
            <div class="col-9 col-lg-4 col-xl-5">
                @* 名前入力用 *@
                <input type="text" asp-for="SearchName" class="form-control">
            </div>
            <div class="col-3 col-lg-2 col-xl-1">
                <label class="col-form-label">評価</label>
            </div>
            <div class="col-9 col-lg-4 col-xl-5">
                @* 評価入力用 *@
                <div class="input-group mb-3">
                    <span class="input-group-text">最小</span>
                    <input type="number" asp-for="SearchRateMin" class="form-control">
                    <span class="input-group-text">最大</span>
                    <input type="number" asp-for="SearchRateMax" class="form-control">
                </div>
                
            </div>
            <div class="col-12 text-end">
                <button class="btn btn-primary">検索</button>
            </div>
        </div>
    </div>
</form>

<form class="mt-5 mb-4" asp-controller="Gourmet" asp-action="Save" method="post">
    <div class="my-2 pe-3 text-end">
        <button class="btn btn-primary me-1">更新</button>
        <button type="button" id="ButtonAddRow" class="btn btn-secondary">行を追加</button>
    </div>
    <table id="TableGourmet" class="table">
        <thead>
            <tr>
                <th class="col-3">都道府県</th>
                <th class="col-7">グルメ名</th>
                <th class="col-2">評価</th>
            </tr>
        </thead>
        <tbody>
            @* テーブルのデータ行をグルメ情報の個数分繰り返し出力 *@
            @* NOTE: foreachで変数に変換するとname属性が変わってしまいコントローラーで情報を受け取ることができません。forでのカウントを添字として使用してください。 *@
            @for(var i = 0; i < Model.GourmetList.Count; i++)
            {
                <tr>
                    <td>
                        <input type="hidden" asp-for="GourmetList[i].GourmetId"/>
                        <select asp-for="GourmetList[i].PrefectureCode" asp-items="Model.PrefectureSelectList" class="form-control">
                            <option hidden></option>
                        </select>
                        <span asp-validation-for="GourmetList[i].PrefectureCode"></span>
                    </td>
                    <td>
                        <input type="text" asp-for="GourmetList[i].GourmetName" class="form-control" />
                        <span asp-validation-for="GourmetList[i].GourmetName"></span>
                    </td>
                    <td>
                        <input type="number" asp-for="GourmetList[i].Rate" class="form-control" />
                        <span asp-validation-for="GourmetList[i].Rate"></span>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</form>

@* テーブル行新規追加用テンプレート *@
<template id="TemplateAddRow">
    <tr>
        <td>
            <select asp-items="Model.PrefectureSelectList" name="TemplatePrefectureCode" class="form-control">
                <option hidden></option>
            </select>
        </td>
        <td>
            <input type="text" name="TemplateGourmetName" class="form-control" />
        </td>
        <td>
            <input type="number" name="TemplateRate" class="form-control" />
        </td>
    </tr>
</template>

@section Scripts{
    <script src="/js/Gourmet.js" asp-append-version="true"></script>
}
画面内動作スクリプト Gourmet.js

行を追加ボタンを押した際の処理を担います。
JavaScriptファイルの新規作成は「追加」→「新しい項目」から行うことができます。

/Web/wwwroot/js/Gourmet.js(新規作成)
class Gourmet {
    constructor() {
        this.Init();
    }

    Init() {
        /* 行を追加ボタン押下時、テーブルに対して行追加 */
        const addRowButton = document.querySelector('main button#ButtonAddRow');
        const targetTbody = document.querySelector('main table#TableGourmet tbody');
        const template = document.querySelector('main template#TemplateAddRow');

        addRowButton.addEventListener('click', () => {
            // 既存行数取得
            const count = targetTbody.querySelectorAll('tr').length;
            // テンプレートからdeepコピー
            const addRow = template.content.cloneNode(true);
            // name属性書き換え
            addRow.querySelector('[name=TemplatePrefectureCode]').setAttribute('name', `GourmetList[${count}].PrefectureCode`);
            addRow.querySelector('[name=TemplateGourmetName]').setAttribute('name', `GourmetList[${count}].GourmetName`);
            addRow.querySelector('[name=TemplateRate]').setAttribute('name', `GourmetList[${count}].Rate`);
            // 末尾に追加
            targetTbody.append(addRow);
        });
    }
}
new Gourmet();

参考記事

統合言語クエリ (LINQ) - Microsoft Learn
https://learn.microsoft.com/ja-jp/dotnet/csharp/linq/

基本の SaveChanges - Microsoft Learn
https://learn.microsoft.com/ja-jp/ef/core/saving/basic

DbContext クラス - Microsoft Learn
https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.entityframeworkcore.dbcontext?view=efcore-8.0

PRG(Post Redirect Get)パターンのために、ModelStateやViewDataを持ち回りたい(ASP.NET 6 CORE MVC) - 3日坊主ITエンジニア
https://stayg.jpn.org/wp/?page_id=108

ASP.NET Core MVC および Razor Pages でのモデルの検証 - Microsoft Learn
https://learn.microsoft.com/ja-jp/aspnet/core/mvc/models/validation?view=aspnetcore-8.0

次回の記事

記事更新次第追記します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0