チュートリアル: ASP.NET MVC で Entity Framework を使用して CRUD 機能を実装する
前のチュートリアルでは、Entity Framework (EF) 6 と LocalDB を使用してデータを格納および表示する MVC アプリケーションSQL Server作成しました。 このチュートリアルでは、MVC スキャフォールディングによってコントローラーとビューで自動的に作成される作成、読み取り、更新、削除 (CRUD) コードを確認してカスタマイズします。
Note
コントローラーとデータ アクセス層の間に抽象化レイヤーを作成するためにリポジトリ パターンを実装することは、よく行われることです。 これらのチュートリアルをシンプルに保ち、EF 6 自体の使用方法を教えることに重点を置くために、リポジトリは使用しません。 リポジトリを実装する方法については、「 ASP.NET Data Access Content Map」を参照してください。
作成する Web ページの例を次に示します。
このチュートリアルでは、次の作業を行いました。
- [詳細の作成] ページ
- [作成] ページを更新する
- HttpPost Edit メソッドを更新する
- [削除] ページを更新する
- データベース接続を閉じる
- トランザクションを処理する
前提条件
[詳細の作成] ページ
Students Index
ページのスキャフォールディングされたコードは、そのプロパティがコレクションを Enrollments
保持するため、プロパティを省略しました。 ページで Details
、コレクションの内容を HTML テーブルに表示します。
Controllers\StudentController.cs では、ビューのアクション メソッドDetails
は Find メソッドを使用して 1 つのStudent
エンティティを取得します。
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
キー値は パラメーターとして id
メソッドに渡され、[インデックス] ページの [詳細] ハイパーリンクのルート データから取得されます。
ヒント: データをルーティングする
ルート データは、ルーティング テーブルで指定された URL セグメントでモデル バインダーが見つけたデータです。 たとえば、既定のルートでは、controller
action
および id
セグメントを指定します。
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
次の URL では、既定のルートは controller
として としてaction
Index
、1 は としてid
マップInstructor
されます。これらはルート データ値です。
http://localhost:1230/Instructor/Index/1?courseID=2021
?courseID=2021
はクエリ文字列値です。 モデル バインダーは、 をクエリ文字列値として渡 id
した場合にも機能します。
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
URL は、Razor ビューの ステートメントによって ActionLink
作成されます。 次のコードでは、 パラメーターは id
既定のルートと一致するため id
、 がルート データに追加されます。
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
次のコードでは、 courseID
は既定のルートのパラメーターと一致しないため、クエリ文字列として追加されます。
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
[詳細] ページを作成するには
Views\Student\Details.cshtml を開きます。
次の例に示すように、各フィールドはヘルパーを使用して
DisplayFor
表示されます。<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
フィールドの
EnrollmentDate
後、終了</dl>
タグの直前に、次の例に示すように、強調表示されたコードを追加して登録の一覧を表示します。<dt> @Html.DisplayNameFor(model => model.EnrollmentDate) </dt> <dd> @Html.DisplayFor(model => model.EnrollmentDate) </dd> <dt> @Html.DisplayNameFor(model => model.Enrollments) </dt> <dd> <table class="table"> <tr> <th>Course Title</th> <th>Grade</th> </tr> @foreach (var item in Model.Enrollments) { <tr> <td> @Html.DisplayFor(modelItem => item.Course.Title) </td> <td> @Html.DisplayFor(modelItem => item.Grade) </td> </tr> } </table> </dd> </dl> </div> <p> @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) | @Html.ActionLink("Back to List", "Index") </p>
コードを貼り付けた後にコードのインデントが正しくない場合は、CtrlK キー、CtrlDキー++を押して書式を設定します。
このコードは、
Enrollments
ナビゲーション プロパティ内のエンティティをループ処理します。 プロパティのエンティティごとにEnrollment
、コースのタイトルと成績が表示されます。 コースタイトルは、エンティティのCourse
ナビゲーション プロパティEnrollments
にCourse
格納されているエンティティから取得されます。 このデータはすべて、必要なときにデータベースから自動的に取得されます。 言い換えると、ここで遅延読み込みを使用しています。 ナビゲーション プロパティに一括読み込みをCourses
指定していないため、学生を取得したのと同じクエリで登録が取得されませんでした。 代わりに、ナビゲーション プロパティに初めてアクセスEnrollments
しようとすると、新しいクエリがデータベースに送信され、データが取得されます。 遅延読み込みと一括読み込みの詳細については、このシリーズの後半の 「関連データの読み取り 」チュートリアルを参照してください。プログラムを開始して (Ctrl F5 キーを押+し)、[学生] タブを選択し、Alexander Carson の [詳細] リンクをクリックして、[詳細] ページを開きます。 (Ctrl キー+を押すとDetails.cshtml ファイルが開いている間に F5、HTTP 400 エラーが発生します。これは、Visual Studio が [詳細] ページを実行しようとしたが、表示する学生を指定するリンクからアクセスできなかったためです。その場合は、URL から "Student/Details" を削除してやり直すか、ブラウザーを閉じてプロジェクトを右クリックし、[ブラウザーでビューを表示>] をクリックします)。
選択した学生のコースと成績の一覧が表示されます。
ブラウザーを閉じます。
[作成] ページを更新する
Controllers\StudentController.cs で、action メソッドをHttpPostAttribute
Create
次のコードに置き換えます。 このコードでは、ブロックをtry-catch
追加し、スキャフォールディングされたメソッドの BindAttribute 属性から を削除ID
します。[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student) { try { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator."); } return View(student); }
このコードは、
Student
ASP.NET MVC モデル バインダーによって作成されたエンティティをStudents
エンティティ セットに追加し、変更をデータベースに保存します。 モデル バインダー は、フォームによって送信されたデータを簡単に操作できるようにする ASP.NET MVC 機能を指します。モデル バインダーは、ポストされたフォーム値を CLR 型に変換し、パラメーターでアクション メソッドに渡します。 この場合、モデル バインダーは、コレクションのプロパティ値をStudent
使用してエンティティをForm
インスタンス化します。Bind 属性から削除した
ID
理由は、 が行の挿入時に自動的に設定SQL Server主キー値であるためID
です。 ユーザーからの入力では、値はID
設定されません。セキュリティ警告 - 属性は
ValidateAntiForgeryToken
、クロスサイト要求フォージェリ攻撃を防ぐのに役立ちます。 ビューには対応するHtml.AntiForgeryToken()
ステートメントが必要です。後で説明します。属性は
Bind
、作成シナリオでの 過剰転記 から保護する 1 つの方法です。 たとえば、エンティティにStudent
、この Web ページをSecret
設定しないプロパティが含まれているとします。public class Student { public int ID { get; set; } public string LastName { get; set; } public string FirstMidName { get; set; } public DateTime EnrollmentDate { get; set; } public string Secret { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } }
Web ページにフィールドがない
Secret
場合でも、ハッカーは fiddler などのツールを使用したり、JavaScript を記述してフォーム値をSecret
投稿したりすることができます。 モデル バインダーがインスタンスの BindAttribute 作成時Student
に使用するフィールドを制限する属性がない場合、 モデル バインダーはそのSecret
フォーム値を取得し、それを使用してエンティティ インスタンスをStudent
作成します。 その場合、Secret
フォーム フィールドに対してハッカーが指定した値はすべて、データベースで更新されます。 次の図は、fiddler ツールがフィールド (値 "OverPost" を含む) を投稿されたフォーム値に追加Secret
する方法を示しています。値 "OverPost" は挿入される行の
Secret
プロパティに正常に追加されますが、Web ページがそのプロパティを設定できることは意図したものではありません。属性と共
Bind
に パラメーターをInclude
使用して、フィールドを明示的に一覧表示することをお勧めします。 パラメーターを使用Exclude
して、除外するフィールドをブロックすることもできます。 その理由Include
は、エンティティに新しいプロパティを追加すると、新しいフィールドがリストによってExclude
自動的に保護されないためです。編集シナリオでの過剰ポスティングを防ぐには、まずデータベースからエンティティを読み取り、次に を呼び出
TryUpdateModel
して、明示的に許可されたプロパティ リストを渡します。 これは、これらのチュートリアルで使用されるメソッドです。多くの開発者が推奨する過剰ポスティングを防ぐ別の方法は、モデル バインドを持つエンティティ クラスではなく、ビュー モデルを使用することです。 更新するプロパティのみをビュー モデルに含めます。 MVC モデル バインダーが完了したら、必要に応じて AutoMapper などのツールを使用して、ビュー モデルのプロパティをエンティティ インスタンスにコピーします。 db を使用します。エンティティ インスタンスの状態を Unchanged に設定し、Property("PropertyName") を設定するエントリ。ビュー モデルに含まれる各エンティティ プロパティで IsModified を true に変更します。 この方法は、編集シナリオと作成シナリオの両方で利用できます。
属性以外の
Bind
ブロックは、try-catch
スキャフォールディングされたコードに対して行った唯一の変更です。 変更を保存するときに、DataException から派生した例外がキャッチされた場合は、汎用的なエラー メッセージが表示されます。 DataException 例外は、プログラミング エラーではなくアプリケーション外の何かが原因で発生する場合があるので、再試行することをお勧めします。 このサンプルでは実装されていませんが、運用品質のアプリケーションでは例外をログに記録します。 詳細については、「Monitoring and Telemetry (Building Real-World Cloud Apps with Azure)」(監視とテレメトリ (Azure での実際のクラウド アプリの構築)) の「Log for insight」(洞察のためのログ) セクションをご覧ください。Views\Student\Create.cshtml のコードは Details.cshtml で見たものと似ていますが
EditorFor
、 とValidationMessageFor
ヘルパーは の代わりにDisplayFor
各フィールドに使用されます。 関連するコードを次に示します。<div class="form-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName) </div> </div>
Create.cshtml には も含まれています
@Html.AntiForgeryToken()
。これは、クロスサイト要求フォージェリ攻撃を防ぐためにコントローラーの 属性と連携ValidateAntiForgeryToken
します。Create.cshtml では変更は必要ありません。
プログラムを開始し、[ 学生 ] タブを選択し、[ 新規作成] をクリックして、ページを実行します。
名前と無効な日付を入力し、[ 作成 ] をクリックしてエラー メッセージを表示します。
これは、既定で取得するサーバー側の検証です。 後のチュートリアルでは、クライアント側検証用のコードを生成する属性を追加する方法について説明します。 次の強調表示されたコードは、 Create メソッドのモデル検証チェックを示しています。
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }
日付を有効な値に変更し、 [Create] をクリックして、新しい学生が [Index] ページに表示されることを確認します。
ブラウザーを閉じます。
HttpPost Edit メソッドを更新する
アクション メソッドを HttpPostAttribute
Edit
次のコードに置き換えます。[HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public ActionResult EditPost(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var studentToUpdate = db.Students.Find(id); if (TryUpdateModel(studentToUpdate, "", new string[] { "LastName", "FirstMidName", "EnrollmentDate" })) { try { db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } return View(studentToUpdate); }
Note
Controllers\StudentController.cs では、
HttpGet Edit
メソッド (属性のないHttpPost
メソッド) は、 メソッドをFind
使用して選択したStudent
エンティティをDetails
取得します。これは、 メソッドで確認したとおりです。 このメソッドを変更する必要はありません。これらの変更では、 オーバーポストを防ぐためのセキュリティのベスト プラクティスが実装されています。スキャフォールディングによって属性が生成
Bind
され、モデル バインダーによって作成されたエンティティが Modified フラグを持つエンティティ セットに追加されました。 このコードは、パラメーターにBind
リストされていないフィールド内の既存のデータが属性によってクリアされるため、Include
推奨されなくなりました。 今後、MVC コントローラー スキャフォールディングが更新され、Edit メソッドの属性が生成Bind
されなくなります。新しいコードは、既存のエンティティを読み取り、 を呼び出 TryUpdateModel して、投稿されたフォーム データのユーザー入力からフィールドを更新します。 Entity Framework の自動変更追跡では、エンティティに EntityState.Modified フラグが設定されます。 SaveChanges メソッドが呼び出されると、 フラグによって Modified Entity Framework によって SQL ステートメントが作成され、データベース行が更新されます。 コンカレンシーの競合 は無視され、ユーザーが変更しなかった列を含め、データベース行のすべての列が更新されます。 (後のチュートリアルでは、コンカレンシーの競合を処理する方法を示します。データベースで個々のフィールドのみを更新する場合は、エンティティを EntityState.Unchanged に設定し、個々のフィールドを EntityState.Modified に設定できます)。
オーバーポストを防ぐために、[編集] ページで更新可能にするフィールドがパラメーターに
TryUpdateModel
一覧表示されます。 現在、他に保護しているフィールドはありませんが、モデル バインダーでバインドしたいフィールドをリストに入れておくと、後でデータ モデルにフィールドを追加した場合に、ここでフィールドを明示的に追加するまで、自動的にフィールドを保護できます。これらの変更の結果、HttpPost Edit メソッドのメソッド シグネチャは HttpGet 編集メソッドと同じです。そのため、EditPost メソッドの名前を変更しました。
ヒント
エンティティの状態と Attach メソッドと SaveChanges メソッド
データベース コンテキストは、メモリ内のエンティティがデータベースの対応する行と同期しているかどうかを追跡しており、この情報により、
SaveChanges
メソッドを呼び出したときの処理が決まります。 たとえば、新しいエンティティを Add メソッドに渡すと、そのエンティティの状態は にAdded
設定されます。 次に、 SaveChanges メソッドを呼び出すと、データベース コンテキストによって SQLINSERT
コマンドが発行されます。エンティティは、次のいずれかの 状態になります。
Added
. エンティティはまだデータベースに存在しません。 メソッドはSaveChanges
ステートメントを発行するINSERT
必要があります。Unchanged
.SaveChanges
メソッドはこのエンティティに対し何も行う必要はありません。 データベースからエンティティを読み取ると、エンティティはこの状態で開始します。Modified
. エンティティのプロパティ値の一部またはすべてが変更されています。 メソッドはSaveChanges
ステートメントを発行するUPDATE
必要があります。Deleted
. エンティティには削除のマークが付けられています。 メソッドはSaveChanges
ステートメントを発行するDELETE
必要があります。Detached
. エンティティはデータベース コンテキストによって追跡されていません。
デスクトップ アプリケーションにおいて、通常、状態の変更は自動的に設定されます。 デスクトップの種類のアプリケーションでは、エンティティを読み取り、そのプロパティ値の一部を変更します。 そのエンティティの状態は自動的に
Modified
に変更されます。 次に、 を呼び出SaveChanges
すと、変更した実際のプロパティのみを更新する SQLUPDATE
ステートメントが Entity Framework によって生成されます。Web アプリの切断された性質では、この連続したシーケンスは許可されません。 エンティティを読み取る DbContext は、ページのレンダリング後に破棄されます。
HttpPost
Edit
アクション メソッドが呼び出されると、新しい要求が行われ、DbContext の新しいインスタンスが作成されるため、エンティティの状態を手動で に設定するModified.
必要があります。 を呼び出SaveChanges
すと、コンテキストで変更したプロパティが認識できないため、Entity Framework によってデータベース行のすべての列が更新されます。ユーザーが実際に変更したフィールドのみを SQL
Update
ステートメントで更新する場合は、元の値 (非表示フィールドなど) を何らかの方法で保存して、メソッドの呼び出し時HttpPost
Edit
に使用できるようにします。 次に、元の値を使用してエンティティを作成Student
し、その元のバージョンのエンティティで メソッドを呼び出Attach
し、エンティティの値を新しい値に更新してから、 を呼び出しますSaveChanges.
。詳細については、「 Entity states and SaveChanges andLocal Data」を参照してください。Views\Student\Edit.cshtml の HTML コードと Razor コードは、Create.cshtml で見たものと似ていますが、変更は必要ありません。
プログラムを起動し、[ 学生 ] タブを選択し、[ 編集 ] ハイパーリンクをクリックしてページを実行します。
データをいくつか変更し、 [Save] をクリックします。 [インデックス] ページに変更されたデータが表示されます。
ブラウザーを閉じます。
[削除] ページを更新する
Controllers\StudentController.cs のメソッドのテンプレート コードHttpGetAttributeDelete
では、 メソッドと Edit
メソッドで確認したように、 メソッドを使用Find
して選択したStudent
エンティティをDetails
取得します。 ただし、SaveChanges
の呼び出しが失敗したときのカスタム エラー メッセージを実装するには、何らかの機能とその対応するビューをこのメソッドに追加します。
更新および作成操作で見たように、削除操作にも 2 つのアクション メソッドが必要です。 GET 要求に応答して呼び出される メソッドには、削除操作を承認または取り消す機会をユーザーに提供するビューが表示されます。 ユーザーが操作を承認すると、POST 要求が作成されます。 その場合、 HttpPost
Delete
メソッドが呼び出され、そのメソッドは実際に削除操作を実行します。
データベースの更新時に発生するHttpPostAttributeDelete
可能性のあるエラーを処理するブロックを メソッドに追加try-catch
します。 エラーが発生した場合、メソッドは HttpPostAttributeDelete
メソッドを HttpGetAttributeDelete
呼び出し、エラーが発生したことを示すパラメーターを渡します。 次に、 メソッドは HttpGetAttributeDelete
確認ページとエラー メッセージを再表示し、ユーザーにキャンセルまたは再試行を行う機会を提供します。
action メソッドを HttpGetAttribute
Delete
、エラー報告を管理する次のコードに置き換えます。public ActionResult Delete(int? id, bool? saveChangesError=false) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator."; } Student student = db.Students.Find(id); if (student == null) { return HttpNotFound(); } return View(student); }
このコードでは、変更の保存に失敗した後にメソッドが呼び出されたかどうかを示す 省略可能なパラメーター を受け取ります。 このパラメーターは、
false
メソッドが以前のHttpGet
Delete
エラーなしで呼び出された場合です。 データベース更新エラーに応答して メソッドによってHttpPost
Delete
呼び出されると、 パラメーターが になりtrue
、エラー メッセージがビューに渡されます。HttpPostAttribute
Delete
アクション メソッド (という名前DeleteConfirmed
) を、実際の削除操作を実行し、データベース更新エラーをキャッチする次のコードに置き換えます。[HttpPost] [ValidateAntiForgeryToken] public ActionResult Delete(int id) { try { Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges(); } catch (DataException/* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. return RedirectToAction("Delete", new { id = id, saveChangesError = true }); } return RedirectToAction("Index"); }
このコードは、選択したエンティティを取得し、 Remove メソッドを呼び出してエンティティの状態を に
Deleted
設定します。SaveChanges
が呼び出された場合、SQL のDELETE
コマンドが生成されます。 また、アクション メソッドの名前をDeleteConfirmed
からDelete
に変更しています。 メソッドに一意のシグネチャをHttpPost
Delete
与える メソッドDeleteConfirmed
という名前のHttpPost
スキャフォールディング されたコード。 (CLR では、異なるメソッド パラメーターを持つオーバーロードされたメソッドが必要です)。シグネチャが一意になったので、MVC 規則に従い、 メソッドとHttpGet
delete メソッドに同じ名前をHttpPost
使用できます。大量のアプリケーションでのパフォーマンスの向上が優先される場合は、 メソッドと
Remove
メソッドを呼び出すFind
コード行を次のコードに置き換えることで、不要な SQL クエリを回避して行を取得できます。Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
このコードでは、
Student
主キー値のみを使用してエンティティをインスタンス化し、エンティティの状態を にDeleted
設定します。 エンティティを削除するために Entity Framework に必要なものは主キーの値だけです前に示したように、
HttpGet
Delete
メソッドはデータを削除しません。 GET 要求に応答して削除操作を実行すると (つまり、編集操作、作成操作、またはデータを変更するその他の操作を実行する)、セキュリティ 上のリスクが生じます。 詳細については、「 ASP.NET MVC ヒント #46 - Stephen Walther のブログにセキュリティ ホールが作成されるため、リンクの削除を使用しないでください 」を参照してください。Views\Student\Delete.cshtml で、次の例に示すように、見出しと見出しの
h3
間h2
にエラー メッセージを追加します。<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
プログラムを起動し、[ 学生 ] タブを選択し、[削除] ハイパーリンクをクリックしてページを実行 します 。
[ 削除 しますか?] というページで [ 削除] を選択します。
[インデックス] ページが表示され、学生は削除されません。 ( 同時実行のチュートリアルでは、動作中のエラー処理コードの例が表示されます)。
データベース接続を閉じる
データベース接続を閉じ、保持しているリソースをできるだけ早く解放するには、コンテキスト インスタンスの処理が完了したら破棄します。 そのため、スキャフォールディングされたコードは、次の例に示すように、StudentController.cs のクラスのStudentController
末尾に Dispose メソッドを提供します。
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
基底 Controller
クラスは既に インターフェイスを IDisposable
実装しているため、このコードは単純に メソッドにオーバーライドを追加して Dispose(bool)
、コンテキスト インスタンスを明示的に破棄します。
トランザクションを処理する
既定では、Entity Framework はトランザクションを暗黙的に実装します。 複数の行またはテーブルに変更を加えて から を呼び出す SaveChanges
シナリオでは、Entity Framework によって、すべての変更が成功するか、すべて失敗するかを自動的に確認します。 一部の変更が完了した後でエラーが発生した場合、それらの変更は自動的にロールバックされます。 より多くの制御が必要なシナリオ (たとえば、Entity Framework の外部で実行される操作をトランザクションに含める場合) については、「 トランザクションの操作」を参照してください。
コードを取得する
その他の資料
これで、エンティティに対 Student
して単純な CRUD 操作を実行するページの完全なセットが作成されました。 MVC ヘルパーを使用して、データ フィールドの UI 要素を生成しました。 MVC ヘルパーの詳細については、「 HTML ヘルパーを使用したフォームのレンダリング 」を参照してください (この記事は MVC 3 用ですが、MVC 5 にはまだ関連しています)。
他の EF 6 リソースへのリンクは、「 ASP.NET データ アクセス - 推奨リソース」にあります。
次のステップ
このチュートリアルでは、次の作業を行いました。
- [詳細] ページを作成しました
- Create ページを更新した
- HttpPost Edit メソッドを更新しました
- Delete ページを更新した
- データベース接続を閉じた
- 処理されたトランザクション
次の記事に進み、プロジェクトに並べ替え、フィルター処理、ページングを追加する方法について説明します。