はじめに
今回は前回に引き続き、Azure Storageの各データサービスについてサンプルを交えながら紹介します。今回はAzure Tableを使ったWebアプリケーションの実装方法について説明します。
Azure TableをデータベースにしたWebアプリケーションを実装しよう
今回はAzure TableをデータベースとしたWebアプリケーションの実装を通してライブラリの使用方法を説明していきます。Webアプリケーションは登録フォームで入力したアドレスをAzure Tableに保存する画面、登録された全てのアドレス情報を閲覧できる画面などを実装します。
WebアプリケーションはASP.NET CoreのRazor PagesアプリケーションとしてVisual Studio 2022を使用して実装していきます。
Razor Pagesアプリケーション用プロジェクトの新規作成
まずはじめにプロジェクトを新規作成します。Visual Studioを開き、「新しいプロジェクトの作成」画面で「ASP.NET Core Webアプリ」を選択して「次へ」を選択します。
プロジェクト名、ソースコードの保存先、ソリューション名の入力画面が表示されるので、任意の値を入力して「次へ」を選択します。
追加情報を入力する画面では「フレームワーク」に「.NET 8.0」を、「認証の種類」は「なし」、「HTTPS用の構成」にチェックを入れた状態で「作成」ボタンを選択します。
しばらくするとプロジェクトの作成が完了します。
次に実装に必要な依存ライブラリをプロジェクトに追加していきます。今回はAzure Table操作用のライブラリである「Azure.Data.Tables」を追加します。
ソリューションエクスプローラーでプロジェクト名を右クリックし、「NuGetパッケージの管理」を選択します(①)。「参照タブ」にある検索ボックスに「Azure.Data.Tables」と入力し(②)、表示された同名のパッケージを選択します(③)。バージョンは「最新の安定版」を選択した状態でインストールを選択します(④)。
Azure Tableの接続文字列の設定
続いてWebアプリケーションからAzure Tableにアクセスするために、ストレージアカウントの接続文字列を設定します。Razor Pagesアプリケーションの場合は「appsettings.json」および「appsettings.Development.json」に設定情報を集約して管理します。今回は開発段階のため、「appsettings.Development.json」の方に以下の設定を追加します。
設定情報ファイルの編集(appsettings.Development.json)
{
・・・中略
"ConnectionString": "<ストレージアカウントの接続文字列>"
}
「ConnectionString」というキーを追加し、値にストレージアカウントの接続文字列を設定します。
エンティティクラスの実装
次にAzure Tableのレコードを表現するエンティティクラスを実装していきます。ソリューションエクスプローラーでプロジェクト名を右クリックし、「追加」→「新しいフォルダ」を選択して「Models」という名称でフォルダを作成します。
次に作成した「Models」フォルダを右クリックし、「追加」→「新しい項目」を選択して「Address.cs」という名称でファイルを作成します。
エンティティクラスの実装(Models/Address.cs)
using Azure;
using Azure.Data.Tables;
namespace AzureTableSample.Models;
public class Address : ITableEntity
{
// ITableEntityとして必須のプロパティを実装する・・・①
public string RowKey { get; set; } = Guid.NewGuid().ToString();
public string PartitionKey { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
// メールアドレス用のフィールドとプロパティ
private string _email;
public string Email
{
get { return this._email; }
set
{
// メールアドレスのドメイン部分をパーティションキーとして設定する・・・②
this.PartitionKey = value.Split('@')[1];
this._email = value;
}
}
// 姓名用のプロパティ
public string LastName { get; set; }
public string FirstName { get; set;}
}
Azure Table用のエンティティクラスは「Azure.Data.Tables.ITableEntity」インターフェースを実装するクラスにする必要があります。ITableEntityでは、
- RowKey:行キー
- PartitionKey:パーティションキー
- Timestamp:更新時刻
- ETag:ETag値
という4つのプロパティが定義されているため、これらを実装する必要があります(①)。エンティティはPartitionKey/RowKeyで一意に特定できます。「RowKey」に関しては、全データで一意となる値とするためにGUIDをデフォルトで設定しています。
また、メールアドレス用のプロパティでは、値を設定する際にメールアドレスのドメイン部分(@より後ろ)を抜き出して「PartitionKey」に設定するようにしています(②)。これにより、addressテーブルではドメインごとにパーティションを区切ってデータが保存されるようになります。
Azure Table操作用サービスクラスの実装
次にAzure Tableに対するデータの参照や追加といった操作を担うデータアクセス用のサービスクラスを実装します。ソリューションエクスプローラーでプロジェクト名を右クリックし、「追加」→「新しいフォルダ」を選択して「Services」という名称でフォルダを作成します。
次に作成した「Services」フォルダを右クリックし、「追加」→「新しい項目」を選択して「AddressService.cs」という名称でファイルを作成します。
サービスクラスの実装(Services/AddressService.cs)
using Azure.Data.Tables;
using AzureTableSample.Models;
namespace AzureTableSample.Services;
public interface IAddressService
{
Task AddAsync(Address address);
Task<IList<Address>> GetAllAsync();
}
public class AddressService : IAddressService
{
// テーブルを操作するテーブルクライアント
private readonly TableClient tableClient;
// コンストラクタ・・・①
public AddressService(TableServiceClient client)
{
// テーブルクライアントの生成・・・①-1
this.tableClient = client.GetTableClient(tableName: "address");
// テーブルが存在しなければ、テーブルを作成する・・・①-2
this.tableClient.CreateIfNotExists();
}
// データの登録・・・②
public async Task AddAsync(Address address)
{
await this.tableClient.AddEntityAsync<Address>(address);
}
// データの全件取得・・・③
public async Task<IList<Address>> GetAllAsync()
{
var addresses = new List<Address>();
// クエリを用いて全件取得し、リストに格納する・・・③-1
await foreach (var address in this.tableClient.QueryAsync<Address>(x => x.Email != string.Empty))
{
addresses.Add(address);
}
return addresses;
}
}
「AddressService.cs」は「IAddressService」インターフェースとその実装である「AddressService」クラスで構成しています。
コンストラクタではテーブルを操作するテーブルクライアントを生成します(①)。インスタンスの引数としてTableServiceClientを受け取り、テーブルの操作を行うためのテーブルクライアントを生成します(①-1)。その際、テーブル名として「address」を指定します。次にテーブルクライアントを用いてテーブルが存在しない場合はテーブルを作成します(①-2)。これによってこのサービスがインスタンス化されたタイミングでテーブルが必ず存在するようになります。
「AddAsync」メソッドではデータ1件をテーブルに追加する操作を実装しています(②)。テーブルクライアントの「AddEntityAsync」メソッドを使用することでデータをテーブルに追加することができます。
「GetAllAsync」メソッドはテーブル内の全てのデータを取得します(③)。テーブルクライアントの「QueryAsync」メソッドの引数で取得したいデータの条件を記述します(③-1)。今回は、メールアドレスが空ではないデータを取得することを条件とすることで、全件取得扱いとしています。
一覧画面の実装
続いて画面の実装に移ります。まずは一覧画面の実装です。今回は一覧画面をトップページにするので、プロジェクト作成時に作成されている「Index.cshtml」の内容を以下のように書き換えます。
一覧画面の実装(Pages/Index.cshtml)
@page
@model IndexModel
@{
ViewData["Title"] = "Address List";
}
<h1>Address List</h1>
<table class="table">
<thead>
<tr>
<th>メールアドレス</th>
<th>姓</th>
<th>名</th>
<th>更新日時</th>
</tr>
</thead>
<tbody>
@* モデルクラスのアドレス一覧を参照する・・・① *@
@foreach (var address in Model.Addresses)
{
<tr>
<td>@address.Email</td>
<td>@address.LastName</td>
<td>@address.FirstName</td>
<td>@address.Timestamp</td>
</tr>
}
</tbody>
</table>
一覧画面ではシンプルにアドレス一覧を表形式で実装しています。表示するアドレスの情報はこの後説明するモデルクラスから参照して動的に表示できるようにしています(①)。 モデルクラスは「Index.cshtml.cs」として実装します。
一覧画面用モデルの実装(Pages/Index.cshtml.cs)
using Microsoft.AspNetCore.Mvc.RazorPages;
using AzureTableSample.Models;
using AzureTableSample.Services;
namespace AzureTableSample.Pages;
public class IndexModel : PageModel
{
// データアクセス用のサービス
private readonly IAddressService _addressService;
// 画面から参照されるアドレス一覧
public IList<Address> Addresses { get; set; }
public IndexModel(IAddressService addressService)
{
// サービスのインスタンスを設定する・・・①
_addressService = addressService;
}
public async Task OnGetAsync()
{
// サービス経由でデータを全件取得する・・・②
Addresses = await _addressService.GetAllAsync();
}
}
「IndexModel」クラスのインスタンスではデータアクセス用のサービスのインスタンスをDIコンテナから注入します(①)。
「OnGetAsync」メソッドは、Webブラウザからこのサイトのトップページにアクセスがあった場合に呼び出されるメソッドです。この際にデータアクセス用サービスを経由してテーブルに存在するデータを全件取得するようにします(②)。「Addresses」プロパティにデータを設定することで、画面側でもデータを参照することができるようになります。
データ登録画面の実装
一覧画面の次はデータ登録用の画面の実装を行います。「Add.cshtml」というファイルを「Pages」ディレクトリ配下に新規作成します。
データ追加画面の実装(Pages/Add.cshtml)
@page
@model AddModel
@{
ViewData["Title"] = "Add Address";
}
<h1>Add Address</h1>
<form method="post">
<div class="form-group">
<label asp-for="Address.Email">メールアドレス</label>
<input asp-for="Address.Email" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Address.LastName">姓</label>
<input asp-for="Address.LastName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Address.FirstName">名</label>
<input asp-for="Address.FirstName" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">追加</button>
</form>
登録画面も単純なフォームで実装しています。メールアドレス、姓、名を入力できるようにしており、各inputタグにある「asp-for」要素で対応するモデルのフィールド名を指定しています。
このフォームの入力内容を受け取り、実際にデータを登録するモデルクラスを「Add.cshtml.cs」として「Pages」ディレクトリ配下に新規作成します。
データ登録用画面モデルの実装(Pages/Add.cshtml.cs)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using AzureTableSample.Models;
using AzureTableSample.Services;
namespace AzureTableSample.Pages;
public class AddModel : PageModel
{
// 画面で入力された内容を保持するプロパティ
[BindProperty]
public Address Address { get; set; }
// データアクセス用のサービス
private readonly IAddressService _addressService;
public AddModel(IAddressService addressService)
{
_addressService = addressService;
}
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
// 入力内容のチェック・・・①
if (!ModelState.IsValid)
{
// 未入力項目があれば、登録画面に戻る
return Page();
}
// サービス経由でデータを登録する・・・②
await _addressService.AddAsync(Address);
// 登録に成功したら、一覧画面に遷移する・・・③
return RedirectToPage("/Index");
}
}
フォームに入力された内容は、POSTメソッドで送信されるため「OnPostAsync」メソッドに登録処理を実装していきます。最初に入力内容のチェックを実施し、未入力項目があれば登録画面に戻るようにしています(①)。今回は簡単のため単純なチェックしか行っていませんが、入力文字の長さチェックやメールアドレス形式のチェックなどをここで行うことができます。チェックに問題がなければ、サービス経由でデータをテーブルに登録します(②)。最後に登録に成功したら一覧画面に遷移するために「RedirectToPage」メソッドを呼び出します(③)。
テンプレートの修正
最後にプロジェクト作成時のテンプレートを一部修正していきます。 まずは画面のレイアウトファイルを修正します。
レイアウトファイルの修正(Pages/Shared/_Layout.cshtml)
・・・中略
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">一覧</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Add">登録</a>
</li>
</ul>
・・・中略
ナビゲーションバーに表示する画面の名称と、遷移先を変更します。遷移先は「asp-page」要素が「/Privacy」となっていた箇所を「/Add」に変更しています。
最後にアプリケーション起動時に最初に実行される「Program.cs」を修正します。
(Program.cs)
using Azure.Data.Tables;
using AzureTableSample.Services;
var builder = WebApplication.CreateBuilder(args);
// Razor Pages用サービスの登録
builder.Services.AddRazorPages();
// Azure Table用サービスクライアントの登録・・・①
builder.Services.AddSingleton<TableServiceClient>((_) =>
{
// Azure Tableの接続文字列を設定ファイルから取得する・・・①-1
var connectionString = builder.Configuration.GetValue<string>("ConnectionString");
// TableServiceClientインスタンスの生成・・・①-2
return new TableServiceClient(connectionString);
});
// データアクセス用サービスの登録・・・②
builder.Services.AddSingleton<IAddressService, AddressService>();
var app = builder.Build();
・・・中略
「Program.cs」にはWebアプリケーション全体に関連する設定が実装されています。ここでAzure Table用のサービスクライアントとデータアクセス用サービスのインスタンスをアプリケーションのDIコンテナに登録するための記述を追加します。DIコンテナにインスタンスを追加することで、各クラスがコンストラクタでこれらのインスタンスを取得することができるようになります。
まずはAzure Tableを操作するためのサービスクライアントである「TableServiceClient」を登録します(①)。登録する際の詳細設定としてサービスクライアントが接続するAzure Tableの接続文字列を取得して(①-1)、インスタンス化する際に引数に渡します(①-2)。これによって「AddressService.cs」のコンストラクタで受け取るサービスクライアントは、あらかじめ接続文字列が設定されたものになります。
次にデータアクセス用サービスである「AddressService」を登録します(②)。これにより「Index.cshtml.cs」や「Add.cshtml.cs」のコンストラクタでサービスのインスタンスを受け取ることができるようになります。
Webアプリケーションの実行
ここまで実装ができたら、アプリケーションを実行してみましょう。Visual Studio上部にある実行ボタンを選択しアプリケーションを起動します。
ビルドに成功するとWebブラウザが表示され、一覧画面が表示されます。最初の段階では登録データがないため表は空の状態となっているかと思います。
画面上部のナビゲーションバーに表示されている「登録」を選択して登録画面を開き、データを登録してみます。