変更データを検出する
データベース内のデータに変更が発生したときに、サーバーへのポーリングやトリガを使ったりすることなく変更内容をサイトマッププロバイダに通知できたら最高です。実は、SQL Server 2005ではASP.NET 2.0で導入された新しいSqlCacheDependencyクラスによってこれが実現されています。この新しいテクノロジを実装するには、4つのステップを実行しなければなりません。
- 環境を構成する
- データベース接続を初期化する
- SqlCacheDependencyを実装する
- コールバックロジックを実装する
環境を構成するには、データベースを設定して「Web.config」を変更します。マニュアルを読むと、データベースの設定は簡単で、次のステートメントを発行して、コールバックを処理するサービスブローカをオンにするだけのようです。
ALTER DATABASE Northwind SET ENABLE_BROKER
残念ながら、他にも細かい要件がたくさんあります。SQL Serverデータベースのownerプロパティに値が入っていなければなりません。このプロパティを見つけるには、データベースを右クリックし、[Properties]を選択して、[Properties]ダイアログから[Files]を選択します。大抵の場合、ownerプロパティはsaに設定します。また、データベースの[Compatibility Level]は90でなければなりません。このプロパティも、データベースの[Properties]ダイアログの[Options]カテゴリの下で設定できます。さらに、アプリケーションの実行アカウントが管理者アカウントでない場合は(そうではないことを願います!)、大量のアクセス許可を付与しなければなりません。必要なアクセス許可を付与するSQLスクリプトは次のとおりです。
GRANT CREATE PROCEDURE to [ASPNET]; GRANT CREATE QUEUE to [ASPNET]; GRANT CREATE SERVICE to [ASPNET]; GRANT REFERENCES ON CONTRACT::[http://schemas.microsoft.com/SQL/Notifications/ PostQueryNotification] to [ASPNET]; GRANT VIEW DEFINITION to [ASPNET]; EXEC sp_addrole 'sql_dependency_subscriber' GRANT SUBSCRIBE QUERY NOTIFICATIONS TO [ASPNET] GRANT RECEIVE ON QueryNotificationErrorsQueue TO [ASPNET] GRANT REFERENCES on CONTRACT::[http://schemas.microsoft.com/SQL/Notifications/ PostQueryNotification] to [ASPNET] EXEC sp_addrolemember 'sql_dependency_subscriber', '[ASPNET]' GRANT SELECT TO [ASPNET]
また、SqlCacheDependencyを使うことをASP.NETに通知するために「Web.config」ファイルを変更しなければなりません。このためには、<system.Web>要素の中に次のコードを追加します。
<caching> <sqlCacheDependency enabled="true" /> </caching> </system.Web>
構成が完了したら、カスタムサイトマッププロバイダはSqlDependency.Start()呼び出しによってデータベース接続を初期化しなければなりません。この初期化はアプリケーションの実行期間中に1回実行しなければならないので、言うまでもなく、次のようにInitialize()メソッド内で実行します。
public override void Initialize(string name, NameValueCollection attributes) { base.Initialize(name, attributes); string strConnectionName = attributes["connectionStringName"]; if (String.IsNullOrEmpty("connectionStringName")) throw new ProviderException(); mstrConnectionString = WebConfigurationManager.ConnectionStrings[ strConnectionName].ConnectionString; SqlDependency.Start(mstrConnectionString); }
このInitialize()メソッドは「Web.config」からconnectionStringName属性を取得し、それを使ってconnectionStringsセクションから実際の接続文字列を取得します。SqlCacheDependency.Start呼び出しは、データベースが依存関係情報を受け取るための準備を行います。
3つ目のステップとなるSqlCacheDependencyの実装では、クラスをインスタンス化し、SqlCommandを渡します。この処理は接続を開いてコマンドを使う前に行わなければなりません。なぜなら、ASP.NET 2.0では依存関係情報はコマンドのSQLまたはストアドプロシージャと一緒に送られるからです(SQL Server 2005はSQLステートメントまたはストアドプロシージャのいずれかをモニタすることができます)。
SqlConnection cn = new SqlConnection(mstrConnectionString); SqlCommand cm = new SqlCommand( "SELECT CategoryId, CategoryName FROM dbo.Categories," cn); SqlCacheDependency dependency = new SqlCacheDependency(cm); cn.Open(); SqlDataReader sdr = cm.ExecuteReader();
SqlCommandが使うSQLには特定の制約が課せられます。1つ目は、それぞれの列を指定しなければならないので、SELECT *はオプションではありません。2つ目は、テーブル名には2つの値からなる名前を使わなければなりません。例えば、テーブルが(先ほどの例のように)dboスキーマに属する場合、SQLはテーブル名を2つの値からなるdbo.<tablename>として指定しなければなりません。
SqlCacheDependencyの実装を完了するには、SqlCacheDependencyをHttpRuntime.Cacheに挿入する必要があります。
HttpRuntime.Cache.Insert( "CategoriesDependency," new object(), dependency, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, new CacheItemRemovedCallback(OnSiteMapChanged) );
このコードでは、CategoriesDependencyという単純なオブジェクトがCacheに挿入されます。このCacheは、データベース内の情報が変更されたときのみクリアされます。データベース内の情報が変更されると、SQL Server 2005はASP.NET 2.0に通知し、ASP.NET 2.0はオブジェクトを削除してコールバック関数OnSiteMapChanged()を呼び出します。
最後のステップはコールバックロジックの実装です。mnodeRootがnullのときにBuildSiteMapを再作成する場合、OnSiteMapChanged()の実装は簡単です。
public void OnSiteMapChanged(string strKey, object item, CacheItemRemovedReason reason) { lock (mobjLock) { if (strKey.Equals("CategoriesDependency") && reason.Equals( CacheItemRemovedReason.DependencyChanged)) { Clear(); mnodeRoot = null; } } }
これでプロセスは完了です。日付をノードに挿入し、テストする前にすべて問題なく動作することを確認してください。完全なクラスについては、リスト2を参照してください。
using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using System.Web.Configuration; using System.Data.SqlClient; using System.Web.Caching; using System.Security.Permissions; using System.Collections.Specialized; using System.Configuration.Provider; /// <summary> /// Summary description for CustomSiteMapProvider /// </summary> namespace AutomatedArchitecture.NestedLoopsNorthwind { [SqlClientPermission(SecurityAction.Demand, Unrestricted = true)] public class CustomSiteMapProvider : StaticSiteMapProvider { private SiteMapNode mnodeRoot = null; private readonly object mobjLock = new object(); private string mstrConnectionString; public CustomSiteMapProvider() : base() { } // Initialize is called only once throughout the lifetime // of the application public override void Initialize(string name, NameValueCollection attributes) { base.Initialize(name, attributes); string strConnectionName = attributes["connectionStringName"]; if (String.IsNullOrEmpty("connectionStringName")) throw new ProviderException( "connectionStringName attribute is required."); mstrConnectionString = WebConfigurationManager.ConnectionStrings[ strConnectionName].ConnectionString; // this call notifies the database to allow sql cache dependencies SqlDependency.Start(mstrConnectionString); } public override SiteMapNode BuildSiteMap() { // if the site map is already built then return the // previously built root node bool blnSiteMapAlreadyBuilt = (mnodeRoot != null); if (blnSiteMapAlreadyBuilt) return mnodeRoot; // if we need to re-build the sitemap, then only allow // a single thread to execute the following code at a time // since CustomSiteMapProvider is a singleton lock (mobjLock) { Clear(); // create a new root node (one and only one is required) mnodeRoot = new SiteMapNode(this, "Root", "Categories_Search.aspx", "Categories", "Categories Search"); AddNode(mnodeRoot); // add any additional nodes string strAddTitle = String.Format("Add ({0:M/d h:m})", DateTime.Now); SiteMapNode nodeAdd = new SiteMapNode(this, "Categories_Add.aspx", "Categories_Add.aspx", strAddTitle, "Add Category"); AddNode(nodeAdd, mnodeRoot); SqlConnection cn = new SqlConnection(mstrConnectionString); // for SqlCacheDependency to work no "*" is allowed in select // statement and table names must be fully qualified. SqlCommand cm = new SqlCommand("SELECT CategoryId, CategoryName " + "FROM dbo.Categories", cn); SqlCacheDependency dependency = new SqlCacheDependency(cm); // open the connection AFTER attaching the SqlCacheDependency cn.Open(); try { SqlDataReader sdr = cm.ExecuteReader(); // add nodes for each category in northwind while (sdr.Read()) { SiteMapNode node = GetNodeFromReader(sdr); AddNode(node, mnodeRoot); } sdr.Close(); // insert into the cache an empty object that expires ONLY // when the data in the database has changed. When it does, // change, call the OnSiteMapChanged callback function HttpRuntime.Cache.Insert( "CategoriesDependency", new object(), dependency, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, new CacheItemRemovedCallback(OnSiteMapChanged) ); return mnodeRoot; } finally { cn.Close(); } } } // simply returns a SiteMapNode given an open SqlDataReader pointing // to Categories in Northwind private SiteMapNode GetNodeFromReader(SqlDataReader sdr) { string strCategoryId = sdr.GetInt32(0).ToString(); string strUrl = "Categories_ViewDetail.aspx?CategoryID=" + strCategoryId; string strCategoryName = sdr.GetString(1); return new SiteMapNode(this, strCategoryId, strUrl, strCategoryName); } // this is called frequently. BuildSiteMap will handle caching. protected override SiteMapNode GetRootNodeCore() { return BuildSiteMap(); } // when the categories table in northwind changes remove the root node // to force a refresh public void OnSiteMapChanged(string strKey, object item, CacheItemRemovedReason reason) { // make sure to lock any code that changes state since // CustomSiteMapProvider is a singleton lock (mobjLock) { if (strKey.Equals("CategoriesDependency") && reason.Equals(CacheItemRemovedReason.DependencyChanged)) { Clear(); mnodeRoot = null; } } } } }
簡単なデバッグのヒント
このプロセスにはかなりの数の要素が関係し、すべて非同期で発生するので、プロセスのトラブルシューティングは難しいかもしれません。問題が発生した場合に最初にチェックすべき点は、SQL Server内で通知が正しく設定されているかどうかです。このチェックを実行するには、次のコマンドを呼び出します。
select * from sys.dm_qn_subscriptions
コマンドが異常終了して何も返さなかった場合は、エラーの原因が記述されているかもしれないのでSQL Serverのログをチェックします。このことは、『SQL Server Books Online』の「Troubleshooting Query Notifications」セクションに記載されています。
ASP.NET 2.0以前のサイトナビゲーションは、開発に時間を要したどころか、サイトに変更があった場合は保守にもかなりの時間がかかりました。しかし、ASP.NET 2.0の新機能のおかげで、基本的な初期セットアップから、セキュリティなどの高度な機能まで、あらゆることを驚くほど簡単に実装、保守できるようになりました。この2回シリーズで紹介した9つのソリューションを習得すれば、Webサイト管理者の業務、つまりおそらく皆さんの業務はかなり楽になることでしょう。
関連記事
- Examining ASP.NET 2.0's Site Navigation, Scott Mitchell
- Examining ASP.NET 2.0's Membership, Roles, and Profile, Scott Mitchell
- The SQL Site Map Provider You've Been Waiting For, Jeff Prosise
- Membership and Role Providers in ASP.NET 2.0 Part I, Scott Allen
- Query Notifications in ADO.NET 2.0, Bob Beauchemin
- Dealing with concurrency issues in custom SiteMapProviders, Danny Chen
- SQL Server 2005 Query Notifications Tell .NET 2.0 Apps When Critical Data Changes
- Letting Java in on SQL Server Notifications
- Get Started Using SQL Server 2005 Notification Services
- Writing A Custom Membership Provider for your ASP.NET 2.0 Web Site
