データベースコンテキストなど、主要な要素を理解する
データベースコンテキスト - Data/RazorPagesFootmarkContext.csファイル
モデルクラスとデータベースを結び付けるのは、データベースコンテキストです。DAOパターンにおけるDTOとDAOの関係のようなものです。データベースコンテキストのクラスファイルは、プロジェクトのルートフォルダ直下のDataフォルダに作成されます。
…略…
public class RazorPagesFootmarkContext : DbContext (1)
{
public RazorPagesFootmarkContext (DbContextOptions<RazorPagesFootmarkContext> options) (2)
: base(options)
{
}
public DbSet<RazorPagesSample.Models.Footmark> Footmark { get; set; } (3)
}
(1)で、データベースコンテキストクラスであるRazorPagesFootmarkContextをDbContextクラスを継承して宣言しています。この名前は、dotnet-aspnet-codegeneratorコマンドで-Dcオプションで指定したものです。クラスには、コンストラクタ(2)とプロパティ(3)があります。このうちコンストラクタはアプリケーションの生成時にProgram.csから呼び出されますが、重要なのはプロパティです。プロパティであるFootmarkは、モデルであるFootmarkクラスによるジェネリクスのDbSet型です。DbSet型なので、データセットを持つのだということがわかります。つまり、データベースコンテキストオブジェクトのFootmarkプロパティを介してデータの取得などが行えるわけです。
なお、RazorPagesFootmarkContextクラス自体の定義は非常にシンプルですが、それは、基本的な実装は親クラスであるDbContextクラスで行われているためです。
アプリケーションのエントリポイント - Program.csファイル
Program.csは、第2回で紹介したように、アプリケーションの開始時に実行されるエントリポイントです。Scaffoldingによって、このファイルにもコードが追加されます。ここでは、追加されたコード部分を見てみます。
…略…
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesFootmarkContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesFootmarkContext"))); (1)
var app = builder.Build();
…略…
(1)が追加されたコードです。意味は以下の通りです。
- 接続文字列として"RazorPagesFootmarkContext"を使う
- データベースプロバイダにSQLiteを使う(UseSqlite)
- RazorPagesFootmarkContext型のデータベースコンテキストを追加する
これで、Footmarkモデルを使うためのデータベースコンテキストが作成されて、ページモデルから利用可能になります。
アプリケーションの設定 - appsettings.jsonファイル
Program.csファイルでは接続文字列が登場しましたが、これはappsettings.jsonファイルに記述されています。これも、Scaffoldingで自動的に追加されます。
…略…
"AllowedHosts": "*",
"ConnectionStrings": { (1)
"RazorPagesFootmarkContext": "Data Source=RazorPagesFootmarkContext-7671a142-….db" (2)
}
…略…
(1)は接続文字列のためのブロックで、(2)が接続文字列本体です。接続文字列は(1)の中に複数記述でき、「接続文字列名: 接続文字列」の形で記述します。この場合はSQLiteを使うので、データソースにデータベースファイル名(これも自動的に決められる)が指定されているのみです。
なお、Development環境ではappsettings.Development.json、Staging環境ではappsettings.Staging.json、Production環境ではappsettings.Production.jsonというように、設定ファイルを用意することで、環境ごとにデータベースを切り替えることができます。これらの設定ファイル内の項目は、appsettings.jsonに優先して適用されます。
また、Program.csファイル中でapp.Environment.IsDevelopment()メソッド、app.Environment.IsStaging()メソッド、app.Environment.IsProduction()メソッドを用いた切り替えを行うことも可能です。以下は、Development環境ならSQLiteを、それ以外ならMySQLを使用する例です(MySQLを使う場合の接続文字列も別途必要ですが、ここでは割愛しています)。
if (app.Environment.IsDevelopment())
{
// Development環境ならSQLiteを使う
builder.Services.AddDbContext<RazorPagesFootmarkContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesFootmarkContext")));
}
else
{
// それ以外の環境ならMySQLを使う
builder.Services.AddDbContext<RazorPagesFootmarkContext>(options =>
options.UseMySql(builder.Configuration.GetConnectionString("RazorPagesFootmarkContextMySQL"))); // 接続文字列の定義は省略
}
ページモデル - Pages/Footmarks/Create.cshtml.csファイル
ページモデルの例として、作成(C)を司るPages/Footmarks/Create.cshtml.csファイルがどうなっているか見てみましょう。
…略…
namespace RazorPagesSample.Pages.Footmarks
{
public class CreateModel : PageModel
{
// データベースコンテキストを保持するフィールド
private readonly RazorPagesFootmarkContext _context; (1)
// データベースコンテキストを受け取るコンストラクタ
public CreateModel(RazorPagesFootmarkContext context) (2)
{
_context = context;
}
// GETメソッドによるリクエストの処理(元のページを表示)
public IActionResult OnGet() (3)
{
return Page();
}
// データバインドするプロパティFootmarkの宣言
[BindProperty] (4)
public Footmark Footmark { get; set; }
// POSTメソッドによるリクエストの処理
public async Task<IActionResult> OnPostAsync() (5)
{
// 検証に問題があれば元のページを表示
if (!ModelState.IsValid) (6)
{
return Page();
}
// データベースコンテキストにモデルデータを追加
_context.Footmark.Add(Footmark); (7)
await _context.SaveChangesAsync(); (8)
// 一覧ページにリダイレクト
return RedirectToPage("./Index"); (9)
}
}
}
(1)はデータベースコンテキストの保持のためのフィールド、そして(2)はコンストラクタで引数にデータベースコンテキストを受け取って(1)にセットしています。ページモデルのコンストラクタはページ生成時に呼び出されますが、その際にあらかじめ作成されているデータベースコンテキストが渡されます(以下のNOTEも参照)。
(3)では、ページがHTTP GETメソッドで呼び出された場合に、Page()メソッドの戻り値を返してページを表示します。Page()メソッドは、ページオブジェクトを返すメソッドで、この場合はCreate.cshtmlそのものになります。つまり、HTTP GETでは具体的な処理は何もせずに、作成ページをそのまま再表示するわけです。
(4)からの2行は、データバインドの指定です。第2回で、データバインドとはビューとページモデル(ビューモデル)を結び付けるものとしましたが、ここではFootmarkをデータバインドするプロパティとしています。これにより、ビューでこのプロパティを参照し、かつビューで変更されたこのプロパティをページモデルで受け取ることが可能になります。
(5)は、ビューでフォームが送信されたとき、すなわちHTTP POSTメソッドで呼び出された場合の処理を行うメソッドです。async演算子が付いているので、このメソッドは非同期実行となります。このメソッドのように、データベースの更新など比較的時間のかかる処理を含むときは、非同期実行を指定しておきます。
(6)は、入力データの検証に問題があった場合の処理をModelState.IsValidで判定しています。問題ない場合は、(7)でビューから受け取ったFootmarkプロパティをもとにデータベースコンテキストにデータを保存し、(8)で変更を反映し、(9)で一覧ページにリダイレクトしています。このとき、(8)ではawait演算子を付けて、変更処理が終了するまでいったんOnPostAsync()メソッドを待機状態にすることを指定しています。
[NOTE]依存性注入(DI)
CreateModelでは、コンストラクタにデータベースコンテキストが渡されています。このように、依存性のあるオブジェクトなどを動的に渡す仕組みは、依存性注入(DI; Dependency Injection)と呼ばれます。ページモデルに、最初から特定のデータベースにアクセスする仕組みを入れてしまうと、そのデータベースに対して高い依存性を持つことになり、汎用性に欠けてしまいます。この場合は、どのデータベースを使うか? ということはデータベースコンテキストに任せて、ページモデルはデータベースコンテクストだけを受け取ることで、データベースに対する依存性を下げているのです。
ビュー - Pages/Footmarks/Create.cshtmlファイル
最後にビューについて、同じく作成(C)のPages/Footmarks/Create.cshtmlファイルがどうなっているか見てみましょう。
<!-- POSTメソッドで送信するフォーム -->
<form method="post">
<!-- 検証メッセージの概要を表示するブロック -->
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<!-- フォームの各要素はform-groupクラスでまとめられる -->
<div class="form-group">
<!-- ラベルに展開 -->
<label asp-for="Footmark.YourName" class="control-label"></label> (1)
<!-- 入力要素に展開 -->
<input asp-for="Footmark.YourName" class="form-control" /> (2)
<!-- 個々の入力要素の検証メッセージをここに表示する -->
<span asp-validation-for="Footmark.YourName" class="text-danger"></span> (3)
</div>
…略…
<!-- 送信ボタン -->
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
作成ページのフォーム部分だけの抜粋です。(1)と(2)は、それぞれフォームにおけるラベルと入力要素ですが、asp-for属性に注目してください。asp-forはタグヘルパーと呼ばれ、要素に応じた属性などの自動生成をサポートします。ここでの属性値は、ページモデルにおけるプロパティ(ここではFootmark)です。ビューでは、このようにプロパティを参照できるのは第2回で紹介したとおりです。<label>要素では、ラベルとして表示されるリテラル、そしてfor属性を生成し、<input>要素ではname属性とvalue属性を生成します。(3)は検証メッセージの表示を行う<span>要素です。
(2)では、name属性とvalue属性がasp-for属性から生成されることで、プロパティの値をvalue属性として初期表示し、name属性によってvalue属性とともに送信するという動きになります。これが、ページモデルにおけるデータバインドとして機能しているわけです。
図2:asp-forタグヘルパーとデータバインディング
これらのマークアップは、具体的には以下のようにレンダリングされます。<label>要素のfor属性とリテラル、<input>要素のname属性とvalue属性がどのようになっているか確認してください(value属性は、プロパティの初期値が空なので、同じく空になります)。
<div class="form-group"> <!-- 検証に問題なければメッセージはレンダリングされない --> <!-- <label>要素に展開 --> <label class="control-label" for="Footmark_YourName">YourName</label> <!-- <input>要素に展開 --> <input class="form-control" type="text" data-val="true" data-val-required="The YourName field is required." id="Footmark_YourName" name="Footmark.YourName" value="" /> <!-- <span>要素に展開されるが初期状態では空 --> <span class="text-danger field-validation-valid" data-valmsg-for="Footmark.YourName" data-valmsg-replace="true"></span> </div>
