Windows Azure のテーブル ストレージでのデータの追加・更新・削除と、データ項目の追加時の動作について興味があったので、ASP.NET MVC 4 なプログラムを書いてみました。なお、Windows Azure は、開発用ファブリック(Windows Azure エミュレータ)を利用しています。
プログラムは、最初に簡単なメッセージ表示を行うものを作成し、次にメッセージ更新の際にメッセージ作成者が決めたキーワードを確認するように変更を行います(データ項目としてキーワード格納用のものが追加される)。
まずは準備作業です。
テンプレートから「Cloud」を選択して、名前を「MessageBoad」として「OK」ボタンをクリックします。
「新しい Windows Azure クラウドサービス」ウィンドウが表示されるので、「ASP.NET MVC 4 Web ロール」を選択してから「>」ボタンをクリックします。「Windows Azure クラウド サービス ソリューション」側に「ASP.NET MVC 4 Web ロール」が表示されているのを確かめて「OK」ボタンをクリックします。
「新しい ASP.NET MVC 4 プロジェクト」ウィンドウが表示されるので、「インターネットアプリケーション」を選択して「OK」ボタンをクリックします。
次に接続文字列を設定します。「MessageBoad」プロジェクトのロール「MvcWebRole1」を右クリックして表示されるメニューから「プロパティ」を選択します。左に表示されているメニューから「設定」を選択して、「設定の追加」をクリックします。名前に「StorageConnection」を設定し、「種類」を「接続文字列」、「値」を「Windows Azure ストレージ エミュレーター」として「OK」ボタンをクリックします。
最初にモデルを作りますが、モデルと Azure のテーブル ストレージのエンティティでデータ項目のプロパティをそれぞれ定義することから、データ項目に変更があった際の更新漏れの可能性を減らすためにインターフェイスを利用しました。
「Models」フォルダに「IMessage.cs」を作成します。「Models」フォルダを右クリックして表示されるメニューから「追加」「新しい項目…」を選びます。テンプレートから「コード」を選択して「インターフェイス」を選択し名前を「IMessage」とします。
プロパティにはデータ項目である「MessageBody」「UserName」とキー項目となる「PartitionKey」と「RowKey」を定義します(データ項目には画面表示用の表題も設定しています)。
using System.ComponentModel.DataAnnotations;
namespace MvcWebRole1.Models
{
public interface IMessage
{
[Display(Name = "メッセージ")]
string MessageBody { get; set; }
[Display(Name = "ユーザー名")]
string UserName { get; set; }
string PartitionKey { get; set; }
string RowKey { get; set; }
}
}
次にモデル用のクラス「Message.cs」を作成します。「Models」フォルダを右クリックして表示されるメニューから「追加」「クラス…」を選び、名前を「Message」とします。
namespace MvcWebRole1.Models
{
public class Message : IMessage
{
public string MessageBody { get; set; }
public string UserName { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
}
}
次にデータアクセスを行う部分を作成します。「MvcWebRole1」プロジェクトを右クリックして表示されるメニューから「追加」「フォルダ」を選び、名前を「DAL」とします(DAL は Data Access Layer の略)。
まずは、エンティティ「MessageEntity.cs」を作成します。「DAL」フォルダを右クリックして表示されるメニューから「追加」「クラス…」を選び、名前を「Message」とします。「MessageEntity」クラスは「TableServiceEntity」クラスを継承し「IMessage」インターフェイスを実装します(PartitionKey は日付、RowKey は GUID を設定しています)。
using System;
using Microsoft.WindowsAzure.StorageClient;
using MvcWebRole1.Models;
namespace MvcWebRole1.DAL
{
public class MessageEntity : TableServiceEntity, IMessage
{
public string MessageBody { get; set; }
public string UserName { get; set; }
public MessageEntity()
{
this.PartitionKey = DateTime.UtcNow.ToString("yyyyMMdd");
this.RowKey = Guid.NewGuid().ToString();
}
}
}
次にリポジトリ用のインターフェイス「IMessageRepository.cs」を作成します(テスト用のプロジェクトは書きませんが 😉 )。「DAL」フォルダを右クリックして表示されるメニューから「追加」「新しい項目…」を選びます。テンプレートから「コード」を選択して「インターフェイス」を選択し名前を「IMessageRepository」とします(ついでに「UpdateException」も定義しておきます。この時点ではまだ使いませんが)。
using System;
using System.Collections.Generic;
using MvcWebRole1.Models;
namespace MvcWebRole1.DAL
{
public interface IMessageRepository
{
IEnumerable<IMessage> Find();
IMessage Get(string partitionKey, string rowKey);
void Add(IMessage message);
void Update(IMessage message);
void Remove(IMessage message);
}
public class UpdateException : Exception
{
public UpdateException(string message) : base(message) { }
}
}
次にリポジトリ用のクラス「MessageRepository.cs」を作成します。「DAL」フォルダを右クリックして表示されるメニューから「追加」「クラス…」を選び、名前を「MessageRepository」とします。
- EntitySetName は固定で「MessageBoad」としています。
- 接続文字列は、準備作業で設定したものになります。
using System.Collections.Generic;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;
using MvcWebRole1.Models;
namespace MvcWebRole1.DAL
{
public class MessageRepository : IMessageRepository
{
private const string EntitySetName = "MessageDoad";
private TableServiceContext _context;
public MessageRepository()
{
string connectionString =
RoleEnvironment.GetConfigurationSettingValue("StorageConnection");
var account = CloudStorageAccount.Parse(connectionString);
var client = account.CreateCloudTableClient();
client.CreateTableIfNotExist(EntitySetName);
_context = client.GetDataServiceContext();
}
public IEnumerable<IMessage> Find()
{
var items = _context.CreateQuery<MessageEntity>(EntitySetName);
var itemsList = new List<Message>();
foreach (var n in items)
{
itemsList.Add(entity2Item(n));
}
return itemsList;
}
public IMessage Get(string partitionKey, string rowKey)
{
var entity = getEntity(partitionKey, rowKey);
return entity2Item(entity);
}
public void Add(IMessage message)
{
var entity = new MessageEntity();
entity.MessageBody = message.MessageBody;
entity.UserName = message.UserName;
_context.AddObject(EntitySetName, entity);
_context.SaveChanges();
}
public void Update(IMessage message)
{
var dbEntity = getEntity(message.PartitionKey, message.RowKey);
dbEntity.MessageBody = message.MessageBody;
dbEntity.UserName = message.UserName;
_context.UpdateObject(dbEntity);
_context.SaveChanges();
}
public void Remove(IMessage message)
{
var dbEntity = getEntity(message.PartitionKey, message.RowKey);
_context.DeleteObject(dbEntity);
_context.SaveChanges();
}
private MessageEntity getEntity(string partitionKey, string rowKey)
{
return _context.CreateQuery<MessageEntity>(EntitySetName)
.Where(p => p.PartitionKey == partitionKey && p.RowKey == rowKey)
.FirstOrDefault();
}
private static Message entity2Item(MessageEntity entity)
{
var item = new Message();
item.MessageBody = entity.MessageBody;
item.UserName = entity.UserName;
item.PartitionKey = entity.PartitionKey;
item.RowKey = entity.RowKey;
return item;
}
}
}
次にコントローラーにアクションを実装します。
using System.Web.Mvc;
using MvcWebRole1.DAL;
using MvcWebRole1.Models;
namespace MvcWebRole1.Controllers
{
public class HomeController : Controller
{
private IMessageRepository _repository;
public HomeController()
{
_repository = new MessageRepository();
}
public HomeController(IMessageRepository repository)
{
_repository = repository;
}
public ActionResult Index()
{
return View(_repository.Find());
}
public ActionResult Create()
{
return View();
}
// 引数の型をインターフェイスで指定すると、ビューからのデータ受け取り時に
// インターフェイスからインスタンスを生成できない例外が発生する
[HttpPost]
public ActionResult Create(Message message)
{
if (ModelState.IsValid)
{
_repository.Add(message);
return RedirectToAction("Index");
}
return View(message);
}
public ActionResult Edit(string partitionKey, string rowKey)
{
return View(_repository.Get(partitionKey, rowKey));
}
[HttpPost]
public ActionResult Edit(Message message)
{
if (ModelState.IsValid)
{
_repository.Update(message);
return RedirectToAction("Index");
}
return View(message);
}
public ActionResult Delete(string partitionKey, string rowKey)
{
return View(_repository.Get(partitionKey, rowKey));
}
[HttpPost]
public ActionResult Delete(Message message)
{
if (ModelState.IsValid)
{
_repository.Remove(message);
return RedirectToAction("Index");
}
return View(message);
}
public ActionResult About()
{
ViewBag.Message = "アプリケーション説明ページ。";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "連絡先ページ。";
return View();
}
}
}
ビューを作る前に、ここで一度ビルドしておきましょう。
次にビューです。
まず最初に「Views」「Home」とフォルダを開いて、「Index.cshtml」ファイルを右クリックして削除します。
次に「Index」「Create」「Edit」「Delete」のアクションをそれぞれ右クリックして「ビューの追加…」を選択し、表示される「ビューの追加」ウィンドウで「厳密に型指定されたビューを作成する」にチェックを入れて、「モデル クラス」で「IMessage」を選択し、「スキャホールディング ビュー テンプレート」で、それぞれ「List」「Create」「Edit」「Delete」を選択して「追加」ボタンをクリックします。
Index.cshtml
@model IEnumerable<MvcWebRole1.Models.IMessage>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
@Html.DisplayNameFor(model => model.MessageBody)
</th>
<th>
@Html.DisplayNameFor(model => model.UserName)
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.MessageBody)
</td>
<td>
@Html.DisplayFor(modelItem => item.UserName)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { partitionKey = item.PartitionKey, rowKey = item.RowKey }) |
@Html.ActionLink("Delete", "Delete", new { partitionKey = item.PartitionKey, rowKey = item.RowKey })
</td>
</tr>
}
</table>
Create.cshtml
@model MvcWebRole1.Models.IMessage
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>MessageEntity</legend>
<div class="editor-label">
@Html.LabelFor(model => model.MessageBody)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.MessageBody)
@Html.ValidationMessageFor(model => model.MessageBody)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.UserName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.UserName)
@Html.ValidationMessageFor(model => model.UserName)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Edit.cshtml
@model MvcWebRole1.Models.IMessage
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>IMessage</legend>
<div class="editor-label">
@Html.LabelFor(model => model.MessageBody)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.MessageBody)
@Html.ValidationMessageFor(model => model.MessageBody)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.UserName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.UserName)
@Html.ValidationMessageFor(model => model.UserName)
</div>
@Html.HiddenFor(model => model.PartitionKey)
@Html.HiddenFor(model => model.RowKey)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Delete.cshtml
@model MvcWebRole1.Models.IMessage
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>このデータを削除しますか?</h3>
<fieldset>
<legend>メッセージ</legend>
<div class="display-label">
@Html.DisplayNameFor(model => model.MessageBody)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.MessageBody)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.UserName)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.UserName)
</div>
@Html.HiddenFor(model => model.PartitionKey)
@Html.HiddenFor(model => model.RowKey)
</fieldset>
@using (Html.BeginForm()) {
<p>
<input type="submit" value="Delete" /> |
@Html.ActionLink("Back to List", "Index")
</p>
}
ここまでで、簡単なメッセージ表示を行うものはできました。次にメッセージ更新の際にメッセージ作成者が決めたキーワードを確認するように変更するというところですが、長くなったので、次回へ続けます。。。