
January 14, 2012 19:05 by
Sekine
Storage Table を利用する場合、
PartitionKeyとRowKeyをどのように決めるのか重要なポイントとなります。
あまり気にせずに作ってしまうと、テーブル内のデータ量が多くなって来た時に、
抽出時のパフォーマンスが落ち、致命的な欠陥につながります。
特にRowKeyの考え方が重要で(PartitionKeyもそうですが)
まず、Storage Tableでは、RowKeyに対して昇順のインデックス付けがされます。
→Table Storage の トランザクション – EGT
それを念頭に、実際にRowKeyをどのように組み立てれば良いかという事になるのですが、
例えば、PartitionKey、RowKey 以外の項目に where をかけるとすると、
対象テーブルの全てに走査・抽出が行われ、
データ量(レコード量)が多い場合には結果的に遅くなり、
さらにタイムアウトにより後方に存在するデータは抽出されないという事になります。
そのような事にならないように、
抽出するキーとなるものは、RowKeyに含め、RowKeyに対して where をかける
というのが原則となります。
RowKeyは、文字列型ですが、64KBの長さではなく、260文字しか有効ではありません。
(これは、PartitionKeyでも同じです)
データを表すキー項目が一つであれば、そのままRowKeyを使用するだけですが、
そんな単純なデータは多くはなく、複数のキーが有るのが普通でしょう。
そのような場合は、各キーを固定長の文字にして、連結してRowKeyとします。
例えば、店舗名、従業員名のキーが在るとしましょう。その場合、
RowKey = 店舗名20文字_従業員名20文字
のようにします。
数値が必要なら、string.Format(“0:10D”, 設定値);
日付なら、Ticks をうまく使うなどして、各項目を固定長に整形して連結します。
文字列が長い場合には、MD5ハッシュなどすれば、32文字になります。
(その場合、大文字小文字でハッシュ値が違うので注意してください。常に小文字にするなど)
このようにRowKeyを決める事で、
上の例で言えば、ある店舗名の従業員一覧が欲しい場合、
var query = from t in {TableServiceContextの従業員データ}
where
t.PartitionKey == partitionKey &&
t.RowKey.CompareTo({調べたい店舗名20char}) >= 0 &&
t.RowKey.CompareTo({調べたい店舗名20char} + "\uffff") < 0
select t;
とすることで、その店舗名で始まるRowKeyを抽出できます。
t.RowKey.CompareTo({other}) >= 0 &&
t.RowKey.CompareTo({other} + "\uffff") < 0
上の、この構文は、StartsWith 相当です。
t.RowKey.StartsWith({other}) と同等になりますが、
Storage Table (TableServiceContext) の場合、Queryプロバイダが
StartsWithに対応していませんが、上記の書き方によって同等となります。
さらに言えば、部分条件抽出は、上記 StartsWith 相当しか無いと思ったほうがよさそうです。
SubString、EndWith、(他、部分的比較命令)は使用できません。
また、それらに相当する書き方も実際うまくいかないのが現状です。
この辺は、開発ストレージで動いたとしても、実際に動かなかったりしますので
十分注意してください。
(個人的には、開発ストレージで開発しないほうが良いとも思っています)
上記のことから、RowKeyは先頭一致の抽出が可能な事と、
それを利用して、キー項目を固定長で連結することが大事です。
固定長にするのは、where の文字列を作るときに任意な長さでは曖昧になるため。
また、RowKeyの値は変更できません。
以前、論理削除の意で、
0_DATA
削除すると
1_DATA
という、先頭1charを削除フラグとして試したことがあります。
UpdateChangesは失敗し、更新できません。
これは、仕様ですので当然なのですが、
思いついた時の歓喜と、その後の落胆が大きかったです・・
7200bf14-079f-47ca-ac6e-0c7eaa02c93d|1|3.0

December 2, 2011 23:42 by
Sekine
つい先日ですが、
自分の独自ドメインのメールアドレスに、来るはずのメールが来ないことに気づき、
DNSのMXが効いていないことに気が付きました。
以前に、Azureをホストするため、CNAMEを設定するため、DNS変更を行った時に、
MX設定が、なぜか間違えた設定になっていたようでした。
さて、VALUE DOMAIN でドメインを取得している人も多いと思いますが、
Windows Azure でのWebサイトをドメインに割り当てるには、
IPアドレレスが固定されないため、Aレコードではなく、CNAMEを割り当てます。
Azure のサイトは、xxxx.cloudapp.net となっていますので、
利用したいドメイン名のCNAMEを割り当てます。
以下、一般的にありそうな設定として
1.レンタルサーバー、WEBサイト(固定IPアドレスのオンプレミスサーバー)
2.同サーバーで、メールサーバーを運用
3.同サーバーで、www、などのサブドメインで、WEBサイトを使用
4.cloud、サブドメインで、Azure Webサイトを使用
普通は、このくらいですが、他に、以下の設定もしたい人もいるでしょう。
5.*.cloud、サブドメイン・ホストで、Azure Webサイトを使用
これらを設定すると仮定します。
便宜的に、所有ドメインを、mydomain.com とします。
1.2.3.のレンタルサーバーのIPアドレスを、111.111.111.111 とします。
4.5.のAzure WebRoleのドメインは、unique.cloudapp.net とします。
メールアドレスは、**@mydomain.com を使用したいと思っていて、
サーバーアドレスは、mydomain.com と、mail.mydomain.com の設定でしたいと思います。
WEBサーバーは、Azureへ向かうドメイン以外は、固定IPのサーバーへ向ける事とします。
と、すると、
a @ 111.111.111.111
a * 111.111.111.111
mx @ 10
mx @ mail.mydomain.com 20
a www 111.111.111.111
a mail 111.111.111.111
cname cloud unique.cloudapp.net.
cname *.cloud unique.cloudapp.net.
txt @ v=spf1 ip4:111.111.111.111 ~all
a * の設定があるので、wwwやmailの設定は、実際この場合不要です。
管理の仕方として好みですが、サブドメインを細かく(別のIPにも)振るなら、
利用する分は設定しておいたほうがいいかもしれません。(私はそうしているって言うだけですが)
CNAMEで、*.cloud っていうのがありますが、
サブドメイン+ホストと言うらしいです。
***.+++.mydomain.com
のように割り当てることができます。
FC2ブログなどのアドレスで見かけるアドレスですね。
Azureに限ったことではないですが、
メモ代わりに、設定方法を書き残しておきます。
2ccb6e31-1b89-4c68-af04-978efe7e4c17|0|.0
つい先日、
MVC 3 のWebアプリケーションを Azure 上へ配置した時にハマった出来事。
普通にフォーム認証を定義し、コントローラ側のAuthorizeAttributeで承認可否をする。
Web.configは以下の様に、
<configuration>
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Login"
timeout="30"
path="/"
slidingExpiration="true"/>
</authentication>
コントローラ側は、クラス自体に”Admin”ロールのみ許可する。
[Authorize(Roles = "Admin")]
public class SecurytyController : Controller
{
public ActionResult Index()
{
// 何かの処理
return View();
}
}
まぁ、こんな感じで特に変わった事は無いのだが
loginページは、FormsAuthentication などの
ASP.NET認証チケットの発行を行い、正常的な認証処理を行う。
開発時には、正常に動作し、
いざ Azure へ上げるために、ライブラリ参照など見直し、
パッケージ化してデプロイし、動作させたところ・・・
非認証時のリダイレクトが、設定したログインページでは無く、
/Account/LogOn へリダイレクトされてしまうようになった。
/Account/LogOn ページは、MVCアプリケーションでの
デフォルトテンプレートの loginUrl 定義なので、
Web.config があやしく思い、良く見直して再度デプロイ
それでも・・やはり、/Account/LogOn へ飛ばされてしまう。
/Account/LogOn を受けるコントローラは無いので当然404エラーとなる。
そこで、ローカルの環境で実行確認をしたところ、
作成中の時は /Login ページが表示されていたのが、
ローカル実行でも、/Account/LogOn へ飛ばされるようになってしまった。
少し考え、しばらく途方に暮れた。
原因は、 「~ライブラリ参照など見直し~」 の所にあった!。
Azure WebRole は、MVC3のランタイムが無いため、
各DLLを /bin へ直接配置するために参照設定を
ローカルコピー True にする。
各DLLは、
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies の
System.Web.Mvc.dll
他、
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies 配下のすべてのDLLファイルという事なのだが・・
「すべて」は誤りで、
・WebMatrix.Data.dll
・WebMatrix.WebData.dll
は、不要!!!。
WebMatrixのアプリケーションじゃ無ければ含める必要は無い。
WebMatrix.Data.dll が読みこまれると、
<authentication>
<forms loginUrl=
の値は、/Account/LogOn に上書きされてしまい、
ログインページが意図したものでは無くなります。
WebMatrix.Data.dll
WebMatrix.WebData.dll
を参照から削除し、再度デプロイしたら、
ちゃんと、/Login ページへリダイレクトされるように直りました。
ちなみに、WebMatrix.Data.dll でも、appSettingsを定義することで、
任意URLへ変更することができます。
<configuration>
<appSettings>
<add key="loginUrl" value="~/Login"/>
</appSettings>
一応これで、ログインページを変更することは出来たのだが、
コントローラ側の、Authorize属性を付けた、HttpPostのアクションを呼ぶと、
500エラーとなり、うまく動かなかった。
何か原因はあるかと思うのだが、
WebMatrix.**の2つのDLLを、わざわざ参照する必要はないので、
これ以上の調査は今回は見送ります。
5bfc5c0f-b09c-468a-9036-5e6d6e208074|0|.0
タイトル通り、ASP NET MVC のコントローラーをDIします。
MVCとは?という人のために、ASP.NETで至極簡単にコントローラーとビューを作成します。
作成プロジェクト名 MvcApp
C#、MVC3、Razorテンプレート使用、空のアプリケーションとして作成
作成直後は、こんな構成
空のアプリケーションなので、このまま実行しても、表示するものが無いため、
エラーとなります。
Global.asax(.cs) に注目
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // ルート名
"{controller}/{action}/{id}", // パラメーター付きの URL
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Application_Start内から、RegisterRoutesメソッドを呼び、
ルーティングマップの登録を最初に行っています。
MVCでは、マップに従い、コントローラーが呼ばれます。
routes.MapRouteで、処理するコントローラーを登録しています。
初期定義では、URL指定が無い場合に、Homeコントローラーが呼ばれると思ってください。
※マップ定義を変更すれば、処理コントローラーを変更できます。
詳しくは説明しませんが、様々なルール定義をすることで、念密に振り分けることが出来ます。
初期のままの実行では、Homeコントローラーが無いため、エラーとなるので、
Homeコントローラーを作成します。
ソリューションエクスプローラ上で、
Controllers 右クリック、追加、コントローラー を選択
こんな画面が出るので、HomeController とします。
HomeController.csが作成されます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApp.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}
次に、ビューの作成
csコード内の、Indexメソッドのところで、右クリックしてサブメニューを出すと、
ビューの追加という項目があるので、選択・クリックします。
ビュー名には、メソッド名と同じく Index を指定します。
レイアウトまたはマスターページを使用する、には空白でも良いです。
追加ボタン押下で、Views/Home/Index.cshtml が作成されます。
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
これで、Homeコントローラーと、その表示ビューが作成できました。
実行します。Homeコントローラーがあるので今度はエラーにはなりません。
さて、
コントローラーとビューの関係は、コントローラー名とメソッド名、
それに、Viewsフォルダ配下のフォルダ名とファイル名とに関連付けされます。
コントローラーの戻り値にView(); として返してましたが、
引数を与えることで、別のビューを返す(=別の画面を表示させる)こともできます。
MVCの動作論理を明確に説明するには、このブログ記事では難しいので、
これ以上は割愛します。別の機会があれば書きたいと思いますが。
では、本題のコントローラーのDIについて。
MVCコントローラーは、Controllerクラス (System.Web.Mvc名前空間)を継承する必要があります。
Controllerクラスを継承さえしていれば、何処に配置しても良いのです。
※プロジェクト内のControllersフォルダに配置する必要もありません。
厳密には、IControllerインターフェースを継承すれば、コントローラーとして振る舞うことが可能ですが、
MVC内部コードでは、Controllerクラスの継承元のControllerBaseクラスの依存が、
非常に大きく、Controllerを継承するのが無難です。
コントローラーは、URLリクエストの解析(マップ定義も含め)を経て、
処理するコントローラークラスが決定されます。
この決定処理は、コントローラーファクトリが行います。
コントローラーが選ばれる条件としては、
IControllerインターフェースを継承し、クラス名がControllerで終わっていること。
即ち、コントローラーのDIは、既にコントローラーファクトリから、DIされているのです。
それでは、具体的に、
ソリューションに、クラスライブラリプロジェクトを追加します。
プロジェクト名 ImplLibrary
C# クラスライブラリ
参照設定として、
System.Web
System.Web.Routing
System.Web.Mvc
を加えます。
プロジェクトに、Controllerクラスを継承する、AboutControllerというクラスを作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
namespace ImplLibrary
{
public class AboutController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
次に、MVCプロジェクト側に、Aboutを表示するビューを追加します。
Indexメソッド付近を右クリックしても、MVCプロジェクト内ではないので、
前のようにビューの追加メニューは出てきません。
そのため、Viewsフォルダに直接フォルダとビューファイルを作成します。
Views右クリック、追加、新しいフォルダー: About フォルダーを作る。
作成した、Aboutフォルダーを右クリック、追加、新しい項目で、
MVC 3 ビューページ (Razor) を選択し、ファイル名を、Index.cshtml とします。
Index.cshtml 作成後は、内容・構造を、Home/Index.cshtml と同じにし、
文言を少し変更しましょう。
-- About/Index.cshtmlの内容 --
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
ここは、Aboutです。
MVCアプリケーションでは、ImplLibraryをプロジェクト参照します。
作成したソリューションは、以下のようになります。
実行してみます。
/Aboutへアクセスし、AboutControllerが呼ばれて、About/Index.cshtml が表示されます。
これで、コントローラーを別のプロジェクトに分離することができます。
保守性もグッと良くなります。
次に、動的DIへ向けて
生成されるモジュールは、MVCプロジェクトのモジュール(本体)と、ImplLibrary.dllですが、
ImplLibrary.dll は、/bin 配下にあります。
ASP.NETの恩恵で、/bin のアセンブリは自動的にロードされるのですが、
/bin のモジュール変更に際しては、プロセスがリサイクルされるので、
/bin以外の場所に配置して、リサイクルされないようにします。
これにより、動作中のWEBサイトを無停止でモジュール変更をします。
コントローラーをインスタンス化しているのは、コントローラーファクトリなので、
コントローラーファクトリをカスタマイズし、コントローラーをMEFに対応させ、
MEFによってインスタンス化させます。
次回へ・・・
関根:
a8f71839-09e0-4917-8538-c4e251257fe2|0|.0
前回の記事:Web Role をデプロイすると何が起こっているのか では、
Web Role 上のアプリケーションが、IIS上でどこのディレクトリ、設定で
動作しているのかを、VM上で確認した。
その後、MVCアプリケーションのViewsフォルダの物理位置を他のフォルダへ移動し、
Viewsフォルダは、仮想パスとして、移動した先のフォルダを設定した。
前回、これ等はVM上にリモートデスクトップで入って手作業で変更したので、
手間が掛かり、クラウドとして運用を考えた場合はダメダメ過ぎる。
但し、MVC動作として、Azure上でも問題無く動作出来る事が分かった事は、
収穫に値するでしょう(当たり前の事なんだけどね)。
今回は、ファイルコピー、仮想フォルダ設定を自動化させる為のモジュールを生成します。
デプロイ後に、VMへリモートデスクトップで更に設定・・というのは当然ナシ!
本題の前に、Startupタスクについて軽く説明します。
Web Role のプロジェクトで、OS設定をする方法は、幾く通りか方法があります。
ざっと思いつくのは、
・Windows サービスを入れちゃう(サービス作って、sc.exeで入れ込むとか)
・プログラム作ってゴニョゴニョするw
どちらも、Startupタスクで実行させる事は共通です。
※あるOSSで、RoleEntryPointで何かの設定をしているコードを見かけてるので、
RoleEntryPointでもゴニョゴニョ出来るかもしれません。
Windows Azure での、Startupタスクの使用方法は、
ServiceDefinition.csdef に
<Startup>
<Task commandLine="実行させたいバッチファイル.cmd" />
</Startup>
簡単に言えばこれだけ。他のオプションもあります。
例えば、実行させたいバッチを、startup.cmd としましょう。
Webプロジェクト内に、startup.cmdというテキストファイルを用意し、
中身は、**.exe でも call でも、普通に行いたいバッチを書きます。
.cmdとしましたが、.batでも良いし、.vbsとしてVBスクリプトを記述しても良いです。
デプロイを行った瞬間に、そのバッチが動くというのが、Startupタスク です。
それでは、本題に。
IISの設定を行うには、いろいろ方法があります。
設定するアプリケーションを作成するか、vbs設定、PowerShellスクリプト設定など。
私は、スクリプトはあまり得意ではないので、アプリケーションを作成します。
更に、ここで利用するモジュールは、
Microsoft.Web.Administration.dll です。
このファイルは、IISをインストールしたWindowsなら、
<システムフォルダ>\inetsrv の中に見つかるはずです。
では、IISを設定するアプリケーションを作成します。
※ デプロイするWeb Role のプロジェクト名と、ローカルストレージ名が決定している事!
プロジェクト名は、startupInfrastructure (任意)とします。
プロジェクトは、C#、コンソールアプリケーション、.NET Framework 4
コンソールでのデフォルトのフレームワークは、.NET Framework 4 Client Profile ですが、
Microsoft.Web.Administration で利用する名前空間がみつからないので、
.NET Framework 4 とします。
プログラムの流れは、
・IISサイトの確認
・ローカルストレージフォルダの確認
・フォルダ作成、ファイルコピー
・フォルダ・ファイルのアクセス制限の設定
・仮想ディレクトリの設定
となります。
また、Startupタスクの動作は、バックグラウンドとして動作させ、
この、IIS設定アプリケーションでは、設定できるまでリトライループを掛けます。
これは、Startupタスクの動きを確認すると分かるのですが、
通常(フォアグランド)で動作させると、Startupタスクのバッチが完了してから、
IIS上へデプロイされるので、IISの設定が出来ないからです。
バックグラウンド動作であれば、バッチ起動と同時にデプロイされるので、
設定ができますが、両方が一緒に動作するので、
デプロイ設定されるまで、Waitをかけたループを行います。
プロジェクトの先頭は、
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Security.Principal;
using System.Security.AccessControl;
using Microsoft.Web.Administration;
namespace startupInfrastructure
{
class Program
{
private const string LOCAL_RESOURCE_ROOT = @"C:\Resources\Directory";
private static string webProjectName = "MvcApplication1";
private static string storageName = "WebContentsStorage";
static void Main(string[] args)
{
こんな感じで始まります。
webProjectName、storageNameは、WebRoleに合わせてください。
では、IISサイトの確認のメソッド
IISSiteCheck(webProjectName); の様に呼ぶ
public static void IISSiteCheck(string siteName)
{
while (true)
{
bool isLoop = true;
try
{
ServerManager serverManager = new ServerManager();
foreach (Site site in serverManager.Sites)
{
if (site.Name.StartsWith(siteName))
{
isLoop = false;
break;
}
}
}
catch
{
isLoop = true;
}
if (isLoop)
{
Thread.Sleep(1000);
}
else
{
break;
}
}
}
ServerManagerクラスは、Microsoft.Web.Administration 内のクラスです。
IISのサイト一覧に、Web Role のプロジェクト名が現れたら、ループを抜けます。
スマートでは無いですが、Startupタスクで行うとこのようになるのは仕方がないようです。
ローカルストレージフォルダの確認
string localResourceRootPath =
GetLocalStorageDirectory(webProjectName, storageName); の様に呼ぶ
public static string GetLocalStorageDirectory(string projectName, string storageName)
{
string result = string.Empty;
while (string.IsNullOrEmpty(result))
{
try
{
string[] directorys = Directory.GetDirectories(LOCAL_RESOURCE_ROOT);
foreach (string s in directorys)
{
int find = s.IndexOf(string.Format(".{0}.{1}", projectName, storageName));
if (find >= 0)
{
result = s;
}
}
}
catch
{
}
if (string.IsNullOrEmpty(result))
{
Thread.Sleep(1000);
}
}
return result;
}
ローカルストレージにフォルダ作成
以下のような感じで、ローカルストレージ内に、Viewsフォルダを作成する。
string path = Path.Combine(localResourceRootPath, “Views”);
Directory.CreateDirectory(path);
FileSystemAccessRule rule = new FileSystemAccessRule(
new NTAccount(“Everyone”),
FileSystemRights.Modify,
InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit,
PropagationFlags.None,
AccessControlType.Allow);
DirectorySecurity security = Directory.GetAccessControl(path);
security.SetAccessRule(rule);
Directory.SetAccessControl(path, security);
次にIISのフォルダを設定します。
ServerManager serverManager = new ServerManager();
Application webApplication = null;
VirtualDirectory appDirectory = null;
VirtualDirectory viewsDirectory = null;
foreach (Site site in serverManager.Sites)
{
if (site.Name.StartsWith(webProjectName))
{
foreach (Application app in site.Applications)
{
if (app.Path == "/")
{
webApplication = app;
foreach (VirtualDirectory virtualDirectory in app.VirtualDirectories)
{
if (virtualDirectory.Path.ToLower() == "/")
{
appDirectory = virtualDirectory;
continue;
}
if (virtualDirectory.Path.ToLower() == "/views")
{
viewsDirectory = virtualDirectory;
continue;
}
}
break;
}
}
break;
}
}
string server_rootDirectoryPath = appDirectory.PhysicalPath;
string server_viewsDirectoryPath = Path.Combine(server_rootDirectoryPath, "Views");
if (Directory.Exists(server_viewsDirectoryPath))
{
// 実ディレクトリから、ローカルストレージへフォルダ階層毎にコピーする。
// コピーしたフォルダ・ファイルのアクセス制限も設定します。:ソースコードは割愛します。
}
if (viewsDirectory == null)
{
webApplication.VirtualDirectories.Add("/Views", localResourceViewsPath);
}
else
{
viewsDirectory.PhysicalPath = localResourceViewsPath;
}
serverManager.CommitChanges();
上記の、VirtualDirectory変数には、対象とするIISのフォルダ情報が入ります。
仮想ディレクトリの情報は、PhysicalPathメンバで設定します。
以上で、設定プログラムは完了。
Azureパッケージに入れるため、Web Role アプリケーションからプロジェクト参照します。
また、App.configは、startupInfrastructure.exe.configとして存在する必要があるので、
Web Roleプロジェクトに、startupInfrastructure.exe.configを作成し、
App.configの内容を転記しておきます。
Microsoft.Web.Administration.dll は、出力ディレクトリにコピーされるようにしてください。
startup.cmdの内容は、
----
@echo off
startupInfrastructure.exe
----
これだけ。
Startupタスクの定義は、
<Startup>
<Task commandLine="startup.cmd" executionContext="elevated" taskType="background" />
</Startup>
となります。
適切に設定されていれば、デプロイ完了と同時に、
Viewsフォルダがローカルストレージに配置され、仮想ディレクトリとして参照されます。
関根:
57613c86-180f-4a72-b574-f46ea5cd7d46|1|5.0
f3128c85-87b4-42b6-ada3-405d544ffc02|0|.0

April 14, 2011 17:05 by
Sekine
Blob Storageとローカルストレージを利用して、画像ローダーを作成します。
今回は、画像を表示するサンプルを記しますが、
先々行いたい事は、Managed Extensibility Framework (MEF)による、
ローカルファイルのクラスローダーによるインジェクションをするための布石です。
それでは、まず画像登録するところから、Blobへの登録を行います。
画像は、Web上からアップロード出来る仕組みにします。
※作成サイトは、ASP.NET MVC 2 / 3 です。
その前に、Global.asax側へ、おまじないを、
protected void Application_Start()
{
CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) =>
{
configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));
RoleEnvironment.Changed += (sender, arg) =>
{
if (arg.Changes.OfType<RoleEnvironmentConfigurationSettingChange>()
.Any((change) => (change.ConfigurationSettingName == configName)))
{
if (!configSetter(RoleEnvironment.GetConfigurationSettingValue(configName)))
{
RoleEnvironment.RequestRecycle();
}
}
};
});
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
では、画像アップロードの処理
コントローラ側:image_setControllerとします。
public class image_setController : Controller
{
//
// GET: /image_set/
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(HttpPostedFileBase filedata)
{
if (filedata.ContentType.ToLower().StartsWith("image/"))
{
// データ取得
byte[] buffer = new byte[filedata.ContentLength];
filedata.InputStream.Read(buffer, 0, buffer.Length);
// Blobコンテナ作成
CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudBlobClient blobClient = account.CreateCloudBlobClient();
CloudBlobContainer blobContainer = blobClient.GetContainerReference("azure-sample");
blobContainer.CreateIfNotExist();
// Blob作成
CloudBlob blob = blobContainer.GetBlobReference("images/" + filedata.FileName);
blob.DeleteIfExists();
blob.UploadByteArray(buffer);
}
return View();
}
}
ビューページ側:Views/image_set/Index.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Index
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm("Index", "image_set", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
%>
アップロードファイル
<input type="file" name="filedata" size="50" />
<input type="submit" value="送信" />
<% } %>
</asp:Content>
ビュー側のフォーム生成は、ファイル送信のために、
enctype = "multipart/form-data"を設定する必要があります。
上記の Html.BeginForm のようになります。
これにより、送信されたファイルデータは、
コントローラ側の引数で HttpPostedFileBaseクラスとして取得できます。
blobへの登録は、コードの通り
ストレージ名は、DataConnectionString として、
account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
Roles設定も定義しておいてください。(.csdef設定)
<ConfigurationSettings>
<Setting name="DataConnectionString" />
</ConfigurationSettings>
作成するコンテナ名は、azure-sample とします。
blobContainer = blobClient.GetContainerReference("azure-sample");
格納するBlob名には、プリフィックスとして、images を付けます。
blob = blobContainer.GetBlobReference("images/" + filedata.FileName);
/image_set へアクセスし、適当に画像登録すれば、
Storage Explorerなどで、アップされた事が確認できるはずです。
次に、画像を表示するページを作成します。
サンプル的に、超簡略します。
コントローラ側:image_viewControllerとします。
public class image_viewController : Controller
{
//
// GET: /image_view/
public ActionResult Index()
{
return View();
}
}
ビューページ側:Views/image_view/Index.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<img src="/images/sample.jpg" />
</asp:Content>
sample.jpgを表示するだけのページです。
なので、アップロードする画像ファイル名は、sample.jpgとしてください(^^;;
このまま、実行しても表示はされませんw 実行しても、404 not found で画像はでませんね!?
/images/sample.jpg は、Webサイト上に無いので当然です。
そこで、imagesで始まるパス用に、画像ローダーのコントローラーを作成します。
まず、マップの作成
Global.asaxのRegisterRoutesに、以下を追加します。
routes.MapRoute(
"images",
"images/{*param}",
new { controller = "image_render", action = "Index", param = UrlParameter.Optional }
);
アクセスパスが、images/ で始まるものは、
image_renderコントロールのIndex メソッドに割り当てる定義です。
では、image_renderコントロールの作成
public class image_renderController : Controller
{
//
// GET: /image_render/
public ActionResult Index(string param)
{
// Blobコンテナ
CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudBlobClient blobClient = account.CreateCloudBlobClient();
CloudBlobContainer blobContainer = blobClient.GetContainerReference("azure-sample");
blobContainer.CreateIfNotExist();
// Blob読取
CloudBlob blob = blobContainer.GetBlobReference("images/" + param);
if (blobExists(blob))
{
byte[] buffer = blob.DownloadByteArray();
// ContentTypeは拡張子毎に適切に設定すること
// ここでは、サンプル的に、jpeg固定
HttpContext.Response.ContentType = "image/jpeg";
HttpContext.Response.Flush();
HttpContext.Response.BinaryWrite(buffer);
}
else
{
// Blob上に存在しない
HttpContext.Response.ContentType = "image/jpeg";
HttpContext.Response.Flush();
// NotImage の画像データなどをレスポンスにする
// または、単に404ステータスを返しても良い
// ........
}
// レスポンス終了
HttpContext.Response.End();
// ビュー戻値は意味は無く、コントローラの戻値のためのダミー
return View();
}
public bool blobExists(CloudBlob blob)
{
try
{
blob.FetchAttributes();
return true;
}
catch (StorageClientException ex)
{
if (ex.ErrorCode == StorageErrorCode.ResourceNotFound)
{
return false;
}
else
{
throw;
}
}
}
}
レスポンスに、画像バイナリデータを出力しています。
Blobの扱いに慣れてくると、特別難しいことはしていませんが、
Blobの存在確認のメソッド、blobExists があります。
Blobを扱うクラスには、存在確認をするメソッドがありませんが、
FetchAttributes()で、意図する確認ができます。
/image_view へアクセスしてみてください。
/images/*** へのコントローラの割り当てによって、
今度は、表示されるはずです。
引数 string param には、マップによって、images が削除されたアクセスパスが来るので、
Blobの取得ファイルは images/ のプリフィックスを付けて取得します。
blob = blobContainer.GetBlobReference("images/" + param);
とりあえず、画像ローダーは完成です。
ここで、多少の問題があります。
① レスポンスは、常にデータを返すので通信データのコストが掛かる。
② 常にBlobへアクセスするので、1トランザクションのコストが掛かる。
この①については、RFC2616に従い、If や、Last-Modified など、HTTPヘッダーを解析して、
マッチする場合は、304 を返すことで対応できます。
では、②に対して、
ここで、ローカルストレージを使用します。
ローカルにファイルが無かったら、Blobからローカルにコピー
コピーしたローカルファイルのデータを、レスポンスとして返す。
という変更を行います。
以下、image_renderControllerに改修メソッドを作ります
public static object syncCheck = new object();
public static object syncCopy = new object();
public ActionResult Index2(string param)
{
byte[] buffer;
LocalResource resourse = RoleEnvironment.GetLocalResource("DataArea");
string fileName = Path.Combine(resourse.RootPath, "images", param);
string pathName = Path.GetDirectoryName(fileName);
if (!System.IO.Directory.Exists(pathName))
{
lock (syncCheck)
{
if (!System.IO.Directory.Exists(pathName))
{
System.IO.Directory.CreateDirectory(pathName);
}
}
}
if (!System.IO.File.Exists(fileName))
{
lock (syncCheck)
{
if (!System.IO.File.Exists(fileName))
{
lock (syncCopy)
{
// Blobコンテナ
CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudBlobClient blobClient = account.CreateCloudBlobClient();
CloudBlobContainer blobContainer = blobClient.GetContainerReference("azure-sample");
blobContainer.CreateIfNotExist();
// Blob読取
CloudBlob blob = blobContainer.GetBlobReference("images/" + param);
if (blobExists(blob))
{
buffer = blob.DownloadByteArray();
using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
{
stream.Write(buffer, 0, buffer.Length);
stream.Flush();
stream.Close();
}
}
}
}
}
}
if (System.IO.File.Exists(fileName))
{
using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
stream.Close();
}
HttpContext.Response.ContentType = "image/jpeg";
HttpContext.Response.Flush();
HttpContext.Response.BinaryWrite(buffer);
}
else
{
// Blob上に存在しない
HttpContext.Response.ContentType = "image/jpeg";
HttpContext.Response.Flush();
// NotImage の画像データなどをレスポンスにする
// または、単に404ステータスを返しても良い
// ........
}
// レスポンス終了
HttpContext.Response.End();
// ビュー戻値は意味は無く、コントローラの戻値のためのダミー
return View();
}
メソッドを変えたので、
Global.asaxのマップ定義は、action = "Index2"に変えてください。
ここでは、ローカルストレージを使用しています。
resourse = RoleEnvironment.GetLocalResource("DataArea");
ローカルストレージの使用には、名前定義が必要なので、
Rolesの定義に追加します。 .csdef 設定は、
<LocalResources>
<LocalStorage name="DataArea" cleanOnRoleRecycle="true" />
</LocalResources>
の様になります。
ローカルストレージのフォルダパスは、RootPath に格納されています。
resourse = RoleEnvironment.GetLocalResource("DataArea");
string fileName = Path.Combine(resourse.RootPath, "images", param);
この場合の、fileName は
{Azureが割り当てたパス名}/images/{引数param}
となります。
ローカルストレージの扱いは、パスさえ取得できれば、通常のローカルファイルと同じく利用できます。
とりあえずこれで、アクセスの度に1トランザクションのコストは緩和できます。
・・・が、更にやっかいな事に、ファイルが更新された場合、
加えて、複数のコンピューティングにデプロイされている場合に、
常に最新の画像が返せないという問題が浮上してきました。
これに対応するには ・・・・
・改修前に戻すか、
・同期する何かを仕込むか、
という別の取り組みが必要になりそうです。
関根:
37f87dd6-2fca-457b-b127-d9895d35fce3|0|.0

March 23, 2011 18:03 by
Sekine
Windows Azure Table のトランザクションについて。
Azure テーブルは、端的には自由度が高いと言える。
その反面、SQLのDBテーブルのように、実質的なスキーマは無く、
また、テーブル間のリレーション(外部制約)も無いため、逆に扱いにくく感じる面もあるでしょう。
Windows Azure Storage Table で、知っておくべき重要な点は、
・一意性を確保するためのキー項目。PartitionKey、RowKey の2つの値で一意。
・レコードのエンティティ(項目)数の上限は、255個まで(キー項目も含む)。
・1レコードのデータ上限は、1Mバイトまで。
・レコードの更新には、Timestamp値による、楽観的同時実行制御が掛かる。
テーブルを利用するにあたっては、データ型も含めて、こんなところでしょう。
複数のテーブル間(ビジネス的な)のリレーション、およびトランザクションについて考えておくべき事項は、
・文字列型は、64KBまで。PartitionKey と RowKeyは文字列型という事。
・RowKey の値により、自動的にソートされる。
・1回のバッチ処理(複数レコードの一斉書込)は、100レコード且つ4Mバイトまで。
・バッチ処理では、Entity Group Transactions(EGT)のトランザクションが利用できる。
・EGTの範囲は、同一のPartitionKey、の同じテーブルに対してのみ。
という事を、常に考えながらテーブル設計をしないといけません。
まず、簡単な説明から
ここで、→ 前回の記事
Azure Storage Tableでオートナンバーを実現する採番テーブル
で、一部利用している所がある。
10レコードを追加した後に、
> context.SaveChanges(SaveChangesOptions.Batch);
を呼び出しています。
SaveChangesメソッドには、Batchオプションをつけて呼び出している。
これは、10レコードを一気に書き込みをしていますが、途中でエラーがあった場合には、
1行も書き込まれない事を意味します。
前回の記事での書き込み例では、RowKeyの重複は無いように作成するので、
途中でエラーが起こることは通常ありえないが、
もし偶然に、他のスレッドからRowKeyが重複するレコードが書き込まれた場合には、
一意制約でエラーとなり失敗します。
業務処理をする上では、RowKeyの重複が無い前提での処理を行う場合には、
偶然的にも、あり得ないように処理をするように心がけるべきです。
次に、インデックスの観点より、
Azure Table を使ったサンプルコードを見ると、
RowKeyの生成に、
DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks
というのを良く見かけます。
例:RowKey = string.Format("{0:10}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks)
実際には、この後にも文字が続く..
Storage Table は、RowKey の値は、自動的にソートされます。というのを念頭に、
このようにすることで、新しいレコードは上位に来るという事になります。
具体的に言うと、例えば、テーブルに唯一のレコードが存在するとして、
RowKeyは、仮に、{"10文字_店舗コード5桁_店舗名"} と取り決めたとすると、
こんなデータになります。
"0000000001_00001_ショップ●●"
ショップコード"00001"のデータの取得は、
{RowKey抽出式 = SubString(11, 5).CompareTo("00001") >= 0 }.Take(1)
とすることで、最新のデータが取れます。
仮に、店舗登録が同時アクセスなどで、不整合なデータが出来たと仮定すると、
"0000000005_00001_ショップ■■"
"0000000004_00001_ショップ□□"
"0000000003_00001_ショップ△△"
"0000000002_00001_ショップ××"
"0000000001_00001_ショップ●●"
※先頭10文字は分かりやすいように記述してますが、実際は大きな数字です。
こんなデータが上から並んでいることになりますが、
最後に書き込まれたレコードは、"ショップ■■"として見なすことができます。
並び順は、上からなので効率の良い並び順となります。
これは、概念的な事を言ってますので、
たとえば、最新10件が業務に必要なデータとして、常に追加されるという様なケースでも、
利用できます。
溜まった不要データは、後でWorkerなどから削除すれば良いのです。
次に、トランザクションの話をします。
・EGTの範囲は、同一のPartitionKey、の同じテーブルに対してのみ。
という制約があります。
では、実際はどういう事なのかという例として、
2つのエンティティクラスを定義します。
論理的には、JOb(親) <-> Employee(子)のリレーションを考えたテーブルになります。
using System;
using System.Data.Services.Common;
namespace PubsStorageStore.TableEntitys
{
[DataServiceKey("PartitionKey", "RowKey")]
public class JobsEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; set; }
public string job_desc { get; set; }
public int min_lvl { get; set; }
public int max_lvl { get; set; }
}
[DataServiceKey("PartitionKey", "RowKey")]
public class EmployeeEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; set; }
public string fname { get; set; }
public string minit { get; set; }
public string lname { get; set; }
public int job_id { get; set; }
public int job_lvl { get; set; }
public string pub_id { get; set; }
public DateTime hire_date { get; set; }
}
}
RowKeyの定義は、
{"JobIDとして10桁_EmployeeIDとして10桁"}
とします。
JObのレコードを作成するには、
JobsEntity.PartitionKey = "test-pubs1";
JobsEntity.RowKey = experienceDac.CreateNumber2("Job").ToString().PadLeft(10, '0');
..その他データ
Employeeのレコードを作成するには、
employeeEntity.PartitionKey = "test-pubs1";
employeeEntity.RowKey = JobsEntity.RowKey + "_" + experienceDac.CreateNumber2("Employee").ToString().PadLeft(10, '0');
..その他データ
※ID生成には、前回の採番処理を使っています。
1つのJobのAdd
context.AddObject("Employee", JobsEntity);
for 等で、複数のEmployeeのAdd
context.AddObject("Employee", employeeEntity);
書き込み
context.SaveChanges(SaveChangesOptions.Batch);
これにより、
| PartitionKey |
RowKey |
job_desc |
min_lvl |
... |
fname |
minit |
... |
| test-pubs1 |
0000000001 |
入力値 |
入力値 |
... |
|
|
|
| test-pubs1 |
0000000001_0000000001 |
|
|
|
入力値 |
入力値 |
... |
| test-pubs1 |
0000000001_0000000002 |
|
|
|
入力値 |
入力値 |
... |
| test-pubs1 |
0000000001_0000000003 |
|
|
|
入力値 |
入力値 |
... |
| test-pubs1 |
0000000001_0000000004 |
|
|
|
入力値 |
入力値 |
... |
というようなレコードが出来上がります。
具体的には、RowKeyはもっと考慮すべき事になると思いますが・・。
AddObjectでは、両方の書き込み対象として、"Employee"に書き込まれていることで、
EGTを利用することができます。
SaveChangesのバッチ動作は、中途にエラーがあれば、全て無効ということになります。
アプリケーションは、FULL REST設計でなければ、エンティティクラスを用いるでしょうから、
物理的に、Azure Table上の同一テーブルかどうかは、意識しないはずです。
関根:
9c400514-357f-4925-8cca-74df91e6a235|0|.0
Azure Storage Table での採番を考えます。
SQL Server での identity列 相当も無く、シーケンス的なオブジェクトもありません。
テーブル設計上では、基本的に RowKey が重複しなければ良いのですが、
やはり何処かで、オートナンバーが必要になる場合があります。
方法として、登録データを取得しMAX値を得る。というのがありますが、ダメですね~
複数スレッドが同時に動く場合には、偶然同じ番号を取得しちゃいます。
lock など クリティカルセクションでも、複数サーバーだと意味がありません。
なので、専用のテーブルで実装します。
また、TableServiceContext.SaveChanges()の挙動が正しいかの検証も兼ねました。
採番テーブルは、キー名と、その現在数値を保持する唯一のレコードとします。
using System;
using System.Data.Services.Common;
namespace StorageStore.TableEntitys
{
[DataServiceKey("PartitionKey", "RowKey")]
public class ExperienceEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; set; }
public int Experience { get; set; }
}
}
PartitionKey、RowKey で唯一のキーとし、RowKeyを採番トークン名とします。
前回の記事 で、クラスアトリビュートに、[DataServiceEntity] を指定しましたが、
キー指定のため、DataServiceKey属性とします。
次に、コンテキストと実装側
using System;
using System.Linq;
using System.Data.Services.Client;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using StorageStore.TableEntitys;
namespace DataAccess.TableEntitys
{
public class ExperienceDac
{
public TableServiceContext GetContext()
{
CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudTableClient client = account.CreateCloudTableClient();
client.CreateTableIfNotExist("ExperienceTable");
return client.GetDataServiceContext();
}
private ExperienceEntity createExperienceRecord(TableServiceContext context, string keyName)
{
IQueryable<ExperienceEntity> entitys = context.CreateQuery<ExperienceEntity>("ExperienceTable");
var q = from t in entitys
where
t.PartitionKey.CompareTo("ApplicationPartition") >= 0 &&
t.RowKey.CompareTo(keyName) >= 0
select t;
ExperienceEntity result;
result = q.AsTableServiceQuery().FirstOrDefault();
if (result == null)
{
result = new ExperienceEntity()
{
PartitionKey = "ApplicationPartition",
RowKey = keyName,
Experience = 0
};
try
{
context.AddObject("ExperienceTable", result);
context.SaveChanges();
}
catch (DataServiceRequestException)
{
result = q.AsTableServiceQuery().FirstOrDefault();
}
if (result == null)
{
throw new Exception("作成エラー");
}
}
return result;
}
public int CreateNumber(string keyName)
{
TableServiceContext context = GetContext();
ExperienceEntity entity = createExperienceRecord(context, keyName);
entity.Experience++;
try
{
context.UpdateObject(entity);
context.SaveChanges();
}
catch (DataServiceRequestException)
{
throw;
}
return entity.Experience;
}
}
}
以上
int newNum = CreateNumber("注文番号");
などで呼び出します。
複数スレッドから同時にアクセスし、タイミング的にバッティングすると、
SaveChanges() が失敗してスローされます。
重複しない事が重要なので、スローされるのはアプリケーション側で対処してね。
以下、検証のためのコード
MVC アプリケーションで、HomeコントローラのView側、適当なところに
<a href="<%= Url.Action("check", "Home") %>">check</a>
コントローラ側は、
public ActionResult check()
{
ExperienceDac experienceDac = new ExperienceDac();
int newNum = experienceDac.CreateNumber("bangou");
ViewData["Message"] = string.Format("発行番号は {0} です", newNum);
return View("index");
}
ふむ・・・、これでは芸がないので、
ちょっと検証に、その2(check2)をやってみる
<a href="<%= Url.Action("check2", "Home") %>">check2</a>
public ActionResult check2()
{
CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudTableClient client = account.CreateCloudTableClient();
client.CreateTableIfNotExist("ExperienceCheck");
TableServiceContext context = client.GetDataServiceContext();
ExperienceDac experienceDac = new ExperienceDac();
for (int i = 0; i < 10; i++)
{
// ランダム秒 Wait
byte[] buf = new byte[10];
System.Security.Cryptography.RNGCryptoServiceProvider rng =
new System.Security.Cryptography.RNGCryptoServiceProvider();
rng.GetBytes(buf);
int seed = 0;
foreach (byte b in buf)
{
seed += b;
}
Random rnd = new Random(seed);
System.Threading.Thread.Sleep(rnd.Next(500, 3000)); // 0.5~ 3 秒 Wait
// 採番
int newNum = experienceDac.CreateNumber2("bangou");
// 書き込み
ExperienceCheckEntity checkEntity = new ExperienceCheckEntity()
{
PartitionKey = "test",
RowKey = string.Format("{0:D10}_{1}", newNum, Guid.NewGuid().ToString())
};
context.AddObject("ExperienceCheck", checkEntity);
}
context.SaveChanges(SaveChangesOptions.Batch);
ViewData["Message"] = "10件作成しました";
return View("index");
}
//---- ExperienceCheckEntity クラス ----
[DataServiceKey("PartitionKey", "RowKey")]
public class ExperienceCheckEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; set; }
}
説明不要かと思いますが、ランダム秒のwaitをかけて、10回書き込みます。
採番された番号を、ExperienceCheck テーブルの RowKey に含まれるように書き込みます。
RowKeyの重複を避けるため、GUIDを付けています。
別ブラウザを3-4立ち上げて、素早く!? クリック
ExperienceDac.CreateNumber での、SaveChanges() は、
高確率で、というか、ほぼ確実にスローされました(^^;;
ExperienceCheckのRowKeyを確認すると、番号の重複はありません。
やったね!
例外が出にくいように、リトライをします。
CreateNumberメソッドを改良して、
public int CreateNumber2(string keyName)
{
ExperienceEntity entity = null;
int retry = 0;
while (true)
{
try
{
TableServiceContext context = GetContext();
entity = createExperienceRecord(context, keyName);
entity.Experience++;
context.UpdateObject(entity);
context.SaveChanges();
break;
}
catch (DataServiceRequestException)
{
retry++;
if (retry >= 10)
throw;
}
}
return entity.Experience;
}
CreateNumber2メソッドを呼ぶように改良すると。
ブラウザ、5-6枚立ち上げて実行しても、例外は出ませんでした。
データを確認すると、50レコード、重複も無く順番に並んでおります。
テーブル名のキー名など、
あるいは、Blobファイル名の重複回避に利用できるね。
リトライ処置は、例外を出にくいものにするだけなので、
実際のアプリケーションでは、例外が出るものとして、対処する必要があります。
:関根
c658ad4b-d764-40b2-8f58-7ecd8d205d9a|0|.0

February 22, 2011 21:43 by
Sekine
物事は横断的に考えていこうよ っと!
Azure ストレージテーブルでデータ設計を考える。
カテゴリは、LINQです、ちょっと脱線してますが。
そもそも、データの入れ物は、単にプロパティ定義があれば良く、
Table Storage を利用するからと言って、
Microsoft.WindowsAzure.StorageClient.TableServiceEntity なんかに依存したくない。
ハイ。以下の通り。
(サンプルデータのpubs.publishersを想定)
※ System.Data.Services.Client の参照が必要
using System;
using System.Data.Services.Common;
namespace SampleStorageStore.TableEntitys
{
[DataServiceEntity]
public class PublisherEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; private set; }
public string Pub_id { get; set; }
public string Pub_name { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
}
}
クラスのアトリビュートに、DataServiceEntity を付ければ良いわけです。
Azure Table で使うには、PartitionKey, RowKey, Timestamp を定義する必要があります。
コンテキスト側のメソッドは、次のような感じ
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using SampleStorageStore.TableEntitys;
namespace SampleControlImpl
{
public class PublishersControl
{
public void AddPublisher(
string pub_id, string pub_name, string city, string state, string country)
{
var account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
CloudTableClient client = account.CreateCloudTableClient();
client.CreateTableIfNotExist("Publishers");
TableServiceContext context = client.GetDataServiceContext();
PublisherEntity publisherEntity = new PublisherEntity
{
PartitionKey = "Sample",
RowKey = "000000",
Pub_id = pub_id,
Pub_name = pub_name,
City = city,
State = state,
Country = country
};
context.AddObject("Publishers", publisherEntity);
context.SaveChanges();
}
}
}
TableServiceEntity を使った場合と全く同じです。
PublishersControl control = new PublishersControl();
control.AddPublisher("0736", "New Moon Books", "Boston", "MA", "USA");
こんな感じで挿入できます。
さて、TableServiceEntity のしがらみは無くなりました。
じゃ、どうするかと言うと、
個人的には、アスペクトの組入れに、ContextBoundObject を継承させたいのですが、
Entity Framework との融合と、ExpressionVisitorの走査手助けが欲しい・・
Entity Framework を想定すると、ObjectContext の継承と思いますが、
Azure 利用では、ExpressionVisitor が、どうしても欲しいので、
結果的には、つまり、Azure利用での継承元となるのは、ExpressionVisitorとなります。
Entity Framework の想定も外せないところなので、
ここで、POCOの登場であります。
→メモメモ POCO
http://msdn.microsoft.com/ja-jp/library/dd456853.aspx
POJO じゃなくて、POCOです、ポコ!
段々、複雑になってきた感に押されそう・・・・
リレーションには、不得意とされているAzure Tableですが、
Rowkey での自動昇順で、単的リレーションは出来る! (はず)
その前に、POCOを少し勉強しないといけないのでしょうね・・・
pubs のAzureサンプル的なものを、次回かその次回か・・にでも報告しますね。
関根:
44027e73-27ac-48cd-a379-6e9d20799b20|0|.0