image

概要

プログラミング初級の方が、実践の場でプログラミングをする際に、
必ずと言って作るであろうCRUD(クラッド)機能。

  • Create(生成)
  • Read(読取)
  • Update(更新)
  • Delete(削除)

これら4つの機能をまとめてCRUD(クラッド)と呼ばれています。

この記事では、ASP.NET MVC5のプロジェクトにて、CRUD機能の
を作成する方法をまとめています。

今回は、商品データをデータベース上で管理して表示、追加、編集できる簡単なシステムを開発していきます。
作成する画面は、以下の通りです。

No 画面名 CRUD 画面詳細
1 商品一覧画面 Read 商品データを読込、一覧として表示する画面
2 商品登録画面 Create 商品データを新規に登録する画面
3 商品編集画面 Update/Delete 商品データの内容を更新または削除する画面

事前準備

使用ツール

・Visual Studio Community 2019
こちらでダウンロード可能です。(無料)

image

・SQL Server 2019 Developer エディション
こちらでダウンロード可能です。(無料)

image

使用プロジェクト

こちらの記事で作成したログイン機能のプロジェクトを使用して解説していきます。

  • Entity FrameWorkのバージョン:6.0.2
  • bootstrapのバージョン:3.4.1

データベース構築

今回は、Microsoft SQL Server でデータベースを作成して、商品テーブル(T_COM)を作成します。
SQL Serverのインストールに関しては、ここでは割愛します。

商品テーブルのカラム情報

No. 論理名 物理名 データ型 Not Null デフォルト 備考
1 商品ID com_id bigint identity Yes (PK)
2 商品名 com_name nvarchar(100)
3 商品詳細 com_detail nvarchar(max)
4 削除フラグ del_flag tinyint Yes 未削除:0 削除済:1
5 登録日時 create_date datetime Yes
6 登録ユーザー create_user nvarchar(50)
7 更新日時 update_date datetime Yes
8 更新ユーザー update_user nvarchar(50)

SQL Server上で、以下のSQLを実行してください。

商品テーブル(T_COM)

CREATE TABLE [dbo].[T_COM](
    [com_id] [bigint] IDENTITY(1,1) NOT NULL,
    [com_name] [nvarchar](100) NULL,
    [com_detail] [nvarchar](max) NULL,
    [del_flag] [tinyint] NOT NULL,
    [create_date] [datetime] NOT NULL,
    [create_user] [nvarchar](50) NULL,
    [update_date] [datetime] NOT NULL,
    [update_user] [nvarchar](50) NULL,
 CONSTRAINT [PK_COM] PRIMARY KEY CLUSTERED 
(
    [com_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

商品テーブル(T_COM)の初期データ

INSERT INTO [dbo].[T_COM] ([com_name], [com_detail], [del_flag], [create_date], [create_user], [update_date], [update_user]) VALUES ('テスト商品1', 'テスト商品1の詳細です。', 0, GETDATE(), 'admin', GETDATE(), 'admin')

INSERT INTO [dbo].[T_COM] ([com_name], [com_detail], [del_flag], [create_date], [create_user], [update_date], [update_user]) VALUES ('テスト商品2', 'テスト商品2の詳細です。', 0, GETDATE(), 'admin', GETDATE(), 'admin')

INSERT INTO [dbo].[T_COM] ([com_name], [com_detail], [del_flag], [create_date], [create_user], [update_date], [update_user]) VALUES ('テスト商品3', 'テスト商品3の詳細です。', 0, GETDATE(), 'admin', GETDATE(), 'admin')

INSERT INTO [dbo].[T_COM] ([com_name], [com_detail], [del_flag], [create_date], [create_user], [update_date], [update_user]) VALUES ('テスト商品4', 'テスト商品4の詳細です。', 0, GETDATE(), 'admin', GETDATE(), 'admin')

INSERT INTO [dbo].[T_COM] ([com_name], [com_detail], [del_flag], [create_date], [create_user], [update_date], [update_user]) VALUES ('テスト商品5', 'テスト商品5の詳細です。', 0, GETDATE(), 'admin', GETDATE(), 'admin')

image
SQL Server Management StudioでSQLを実行

Entity Data Model更新

データベースでテーブルの追加、初期データ追加ができたら、プロジェクト上でADO.NET Enitiy Data Modelも更新して、
データベースの情報をマッピングする為のEDMファイルを作成します。
(Visual Studio上でEDMファイルを作成する方法は、こちらの記事を参照ください。)

対象のEDMファイルのデザイナー画面を開き、右クリック→「データベースからモデルを更新」を選択します。

image

image

image

EDMのデザイナー画面に、商品テーブル(T_COM)が追加された事を確認できたら、上書き保存します。

一覧表示機能作成

EDMに商品テーブル(T_COM)を追加したので、商品一覧を表示する一覧画面を作っていきましょう。

Modelの作成

まずは一覧画面のデータを表示する際にマッピングするためのモデルを作成します。
プロジェクト内の「Model」ディレクトリにComModel.csを新規作成してください。

image

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

namespace Mvc5LoginSample1.Models
{
    public class ComModel
    {
        [DisplayName("商品ID")]
        public long Com_Id { get; set; }

        [DisplayName("商品名")]
        [Required(ErrorMessage = "商品名は必須入力です。")]
        public string ComName { get; set; }

        [DisplayName("詳細")]
        public string ComDetail { get; set; }
    }
}

一覧には、商品名は必須入力にする為、12行目で
アノテーションの設定(必須、エラーメッセージ)をしておきます。

Controllerの作成

次に商品テーブル(商品)からデータを取得する処理を記述する為、
プロジェクト内の「Controller」ディレクトリにComController.csを新規作成してください。

image

using Mvc5LoginSample1.DAL;
using Mvc5LoginSample1.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

namespace Mvc5LoginSample1.Controllers
{
    public class ComController : Controller
    {
        /// <summary>
        /// 商品一覧
        /// </summary>
        /// <returns></returns>
        public ActionResult Index()
        {
            var comModelList = new List<ComModel>();
            using (var context = new SAMPLEDB01Entities())
            {
                // DBから商品一覧取得
                comModelList = context.T_COM.AsNoTracking()
                                .Where(x => x.del_flag == 0)
                                .Select(x =>
                                new ComModel
                                {
                                    Com_Id = x.com_id,
                                    ComName = x.com_name,
                                    ComDetail = x.com_detail,
                                }).ToList();
            }
            return View(comModelList);
        }
    }
}

ポイントは商品テーブル(T_COM)から削除フラグが0(未削除)のデータを、作成したComModelクラス型のインスタンスに代入します。
取得した結果をViewに送ります。
※SAMPLEDB01Entitiesは、私がEDMファイルを作成した際に設定したエンティティの名前の為、ご自身で設定したエンティティ名に変更してください。

Viewの作成・修正

続いては一覧画面のViewを作成する為、
プロジェクト内の「View」ディレクトリにComというディレクトリを新規作成し、
その中にindex.cshtmlを新規に作成してください。

image

@model IEnumerable<Mvc5LoginSample1.Models.ComModel>

<h2>商品一覧</h2>
@Html.ActionLink("新規登録", "Create", null, new {@class = "btn btn-warning" })
<hr>

<table class="table table-hover">
    <thead>
        <tr>
            <th scope="col">
                @Html.DisplayNameFor(model => model.Com_Id)
            </th>
            <th scope="col">
                @Html.DisplayNameFor(model => model.ComName)
            </th>
            <th scope="col">
                @Html.DisplayNameFor(model => model.ComDetail)
            </th>
            <th scope="col">
                修正
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var m in Model)
        {
            <tr name="row">
                @*商品ID*@
                <td>
                    @Html.DisplayFor(modelItem => m.Com_Id)
                </td>
                @*商品名*@
                <td>
                    @Html.DisplayFor(modelItem => m.ComName)
                </td>
                @*商品詳細*@
                <td>
                    @Html.DisplayFor(modelItem => m.ComDetail)
                </td>
                <td>
                    @Html.ActionLink("編集", "Edit", new { id = m.Com_Id }, new { @class = "btn btn-primary" })
                </td>
            </tr>
        }
    </tbody>
</table>

次に商品一覧画面に遷移する為のリンクを設定する為、ヘッダーメニューの_Layout.cshtmlに
リンクを追加します。

    @*↑省略*@
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("商品管理システム", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("商品一覧", "Index", "Com")</li>
                </ul>
                @if (Request.IsAuthenticated)
                {
                    <ul class="nav navbar-nav navbar-right">
                        <li>@Html.ActionLink("ログアウト", null, null, null, new { id = "LogoutLink" })</li>
                    </ul>
                }
                else
                {
                    <ul class="nav navbar-nav navbar-right">
                        <li>@Html.ActionLink("ログイン", "Login", "Auth")</li>
                    </ul>
                }
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - 商品管理システム</p>
        </footer>
    </div>
    @*↓省略*@

10、35行目は、商品管理システムなど適当なシステム名を設定しておきましょう。
14行目で商品一覧画面に遷移するリンクを追加します。
ASP.NET MVCプロジェクトのデフォルトで入っているホーム、詳細、問い合わせのリンクは、使用しない為、削除しました。

新規登録機能の作成

次に商品データを新規登録(Create)する機能を作っていきます。

Controllerの修正

一覧機能(Read)で作成したComController.csに新規登録に必要なコードを追加します。

/* ↑ 省略  */

        /// <summary>
        /// 新規登録 表示
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public ActionResult Create()
        {
            var model = new ComModel();
            return View(model);
        }

        /// <summary>
        /// 新規登録
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(ComModel model)
        {
            try
            {
                // 入力チェック
                if (!this.ModelState.IsValid)
                {
                    return View(model);
                }
                // 登録処理
                using (var context = new SAMPLEDB01Entities())
                {
                    DateTime now = DateTime.Now;
                    string userID = User.Identity.Name;
                    T_COM targetData = new T_COM();
                    targetData.com_name = model.ComName;
                    targetData.com_detail = model.ComDetail;
                    targetData.create_date = now;
                    targetData.create_user = userID;
                    targetData.update_date = now;
                    targetData.update_user = userID;

                    context.T_COM.Add(targetData);
                    context.SaveChanges();
                }
                // 一覧画面にリダイレクト
                return RedirectToAction("Index");
            }
            catch (Exception)
            {
                ModelState.AddModelError("", "問題が発生しました。");
            }
            return View(model);
        }

同じCreateメソッドになっていますが、
画面表示時は、GETの方のメソッドを使用して、登録時はPOSTの方のメソッドを使用しています。

POST時の処理の流れとしては、
入力チェック ⇒ 登録処理 ⇒ 一覧画面にリダイレクトになっています。
※登録処理で商品IDの記載がないですが、商品IDはテーブルの設定で自動採番するようにしている為、登録時に記載する必要がありません。

Viewの作成

一覧画面(Read)で作成したComディレクトリ上に、Create.cshtmlを新規に作成してください。

@model Mvc5LoginSample1.Models.ComModel

<h2>商品登録</h2>

@using (@Html.BeginForm("Create", "Com", FormMethod.Post, new { @id = "comCreateForm" }))
{
    @Html.AntiForgeryToken()
    <div class="text-danger">
        @Html.ValidationSummary(false)
    </div>
    <hr />
    <div class="form-horizontal">
        <div class="form-group">
            <label for="name" class="col-xs-1 control-label">@Html.DisplayNameFor(m => m.ComName)</label>
            <div class="col-xs-11">
                @Html.TextBoxFor(m => m.ComName, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            <label for="name" class="col-xs-1 control-label">@Html.DisplayNameFor(m => m.ComDetail)</label>
            <div class="col-xs-11">
                @Html.TextAreaFor(m => m.ComDetail, 5, 500, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-xs-offset-1 col-xs-11">
                <input type="submit" value="登録" class="btn btn-primary" /> |
                @Html.ActionLink("戻る", "Index", null, new { @class = "btn btn-primary" })
            </div>
        </div>
    </div>
}

編集機能(更新・削除)の作成

新規作成画面が完成したら、最後に更新、削除機能を作成します。

新規作成画面でContollerとViewを作成した際に、
Viewのファイル名とContorollerのメソッド名は、一致している事に気が付きましたか?
その流れで更新・削除処理を実装すると、Viewを更新用、削除用の2つ作成しないといけません。

image

実務の場合、更新、削除の画面が、別々に分かれているCRUDアプリは、ほぼないでしょう!
その為、今回はViewは1つのファイルにして更新、削除ができるようにします。

image

1つのViewファイルから処理するメソッドをSubmitした時に分ける方法がないか?
探していたところ、こちらのサイトに方法が書かれていました。
Contorollerのメソッドに、ActionNameとButtonのカスタムのアノテーションを設定することで可能になります。

しかし、カスタムアノテーションの為のクラスを別途作成する必要があります。
別の画面でも共通で使用できるようにする為、共通のBaseContollerのファイルを作成して、
ComContollerの継承元をBaseContollerに修正します。

Contollerの修正

BaseContollerの作成

まずは、プロジェクト内の「Controller」ディレクトリにBaseController.csを新規作成してください。

using System.Reflection;
using System.Web.Mvc;

namespace Mvc5LoginSample1.Controllers
{
    public class BaseController : Controller
    {
        protected class ButtonAttribute : ActionMethodSelectorAttribute
        {
            public string ButtonName { get; set; }
            public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
            {
                return controllerContext.Controller.ValueProvider.GetValue(ButtonName) != null;
            }
        }
    }
}

Contollerの修正

次にComControllerの継承元クラスをControllerからBaseControllerに修正。
編集画面表示、更新用、削除用のメソッドを追加します。

using Mvc5LoginSample1.DAL;
using Mvc5LoginSample1.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

namespace Mvc5LoginSample1.Controllers
{
    public class ComController : BaseController
    {
        /* 新規作成関連の記載省略しています。 */

        /// <summary>
        /// 編集 表示
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet]
        public ActionResult Edit(int? id)
        {
            var model = new ComModel();

            if (id == null)
            {
                return RedirectToAction("Create");
            }

            T_COM comDetail = null;
            using (var context = new SAMPLEDB01Entities())
            {
                comDetail = context.T_COM.AsNoTracking().Where(x => x.com_id == id).FirstOrDefault();
            }
            if (comDetail == null)
            {
                return HttpNotFound();
            }

            model.Com_Id = comDetail.com_id;
            model.ComName = comDetail.com_name;
            model.ComDetail = comDetail.com_detail;

            return View(model);
        }

        /// <summary>
        /// 編集
        /// </summary>
        /// <param name="comModel"></param>
        /// <returns></returns>
        [HttpPost]
        [ActionName("Edit")]
        [Button(ButtonName = "Save")]
        [ValidateAntiForgeryToken]
        public ActionResult Save(ComModel model)
        {
            try
            {
                // 入力チェック
                if (!this.ModelState.IsValid)
                {
                    return View(model);
                }
                // 更新処理
                T_COM com = null;
                using (var context = new SAMPLEDB01Entities())
                {
                    var now = DateTime.Now;
                    string userID = User.Identity.Name;

                    com = context.T_COM.Where(x => x.com_id == model.Com_Id).FirstOrDefault();
                    com.com_name = model.ComName;
                    com.com_detail = model.ComDetail;
                    com.update_date = now;
                    com.update_user = userID;

                    context.SaveChanges();
                }
                // 一覧画面にリダイレクト
                return RedirectToAction("Index");
            }
            catch (Exception)
            {
                ModelState.AddModelError("", "問題が発生しました。");
            }
            return View(model);
        }

       /// <summary>
        /// 削除
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        [ActionName("Edit")]
        [Button(ButtonName = "Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult Delete(ComModel model)
        {
            try
            {
                // 削除処理
                T_COM com = null;
                using (var context = new SAMPLEDB01Entities())
                {
                    var now = DateTime.Now;
                    string userID = User.Identity.Name;

                    com = context.T_COM.Where(x => x.com_id == model.Com_Id).FirstOrDefault();
                    com.del_flag = 1;
                    com.update_date = now;
                    com.update_user = userID;

                    context.SaveChanges();
                }
                // 一覧画面にリダイレクト
                return RedirectToAction("Index");
            }
            catch (Exception)
            {
                ModelState.AddModelError("", "問題が発生しました。");
            }
            return View(model);
        }
    }
}

メソッド名の上に、
[ActionName("Edit")]
[Button(ButtonName = "Save")]
を設定することで、Submit時の処理を分岐しています。

削除処理に関しては、データを直接削除するわけでなく、
商品テーブル(T_COM)の削除フラグを1(削除済)に更新して、
1(削除済)の商品データに関しては、一覧画面で読込する際に、
取得しないようにする事で、削除されたように見せています。
※こういったフラグの値を変更して削除する方法を論理削除と呼ばれています。

Viewの作成

Create.cshtmlと同じようにComディレクトリ上に、Edit.cshtmlを新規に作成してください。
内容は、Create.cshtmlとほぼ同じですが、
タイトルと配置しているボタンが異なるだけで、他は変わりません。

@model Mvc5LoginSample1.Models.ComModel

<h2>商品編集</h2>

@using (@Html.BeginForm("Edit", "Com", FormMethod.Post, new { id = "comEditForm" }))
{
    @Html.AntiForgeryToken()
    <div class="text-danger">
        @Html.ValidationSummary(false)
    </div>
    <hr />
    <div class="form-horizontal">
        <div class="form-group">
            <label for="name" class="col-xs-1 control-label">@Html.DisplayNameFor(m => m.ComName)</label>
            <div class="col-xs-11">
                @Html.TextBoxFor(m => m.ComName, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            <label for="name" class="col-xs-1 control-label">@Html.DisplayNameFor(m => m.ComDetail)</label>
            <div class="col-xs-11">
                @Html.TextAreaFor(m => m.ComDetail, 5, 500, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-xs-offset-1 col-xs-11">
                <input type="submit" value="削除" name="Delete" id="comDelete" class="btn btn-danger" /> |
                <input type="submit" value="更新" name="Save" class="btn btn-primary" /> |
                @Html.ActionLink("戻る", "Index", null, new { @class = "btn btn-primary" })
                @Html.HiddenFor(m => m.Com_Id)
            </div>
        </div>
    </div>
}

※27、28行目の削除、更新ボタンのname属性は、ControllerのButtonNameで記載した文字列と同じにしてください。

JSファイルの修正

これで削除処理が実行されるようになりましたが、
このままでは削除ボタンをクリック後、問答無用に削除処理が実行されてしまい、
誤操作などした場合に、取り返しがつかなくなります。
その為、確認ダイアログを表示して、「OK」をクリックした場合のみ削除処理が実行されるようにします。

※今回はcommon.jsというjsファイルに記載します。
新規にcommon.jsを作成・読込の方法は、こちらの記事で書いてあります。

$(function () {

    $("#comDelete").click(function () { 
        var ret = confirm("削除してもよろしいですか?");
        if (!ret) {
            return false;
        }
    });

})

まとめ

ここまで実装できたら、早速実行してみましょう!

【実行結果】

image
初期表示

image
商品名が未入力の場合、バリデーションエラー発生!

image
商品名を入力して登録!

image
登録した商品が表示されている事を確認

image
編集画面で、内容を更新!

image
更新された内容が、反映されている事を確認

image
商品5のデータを削除

image
商品5のデータが削除されている事を確認

今回はかなりボリュームのある内容でしたが、如何でしたか?
現場に入って多くの初心者が、まず初めに任されるであろう機能が、
こういったCRUD機能の実装です。

これができるか?できないか?で
次のステップに進める登竜門のような機能です。

自分で一度作成した事があれば、
言語が変わろうが、やることは一緒です!
是非、練習と思って取り組んでみてください(^^)

それでは、今回もありがとうございました!

参考

ASP.NET MVC: Using ActionMethodSelectorAttribute to Handle Multiple Submit Buttons