SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

ASP.NETサイトナビゲーションの9つの問題のソリューション:パート2

ASP.NET 2.0における高度なサイトマップソリューション

変更データを検出する

 データベース内のデータに変更が発生したときに、サーバーへのポーリングやトリガを使ったりすることなく変更内容をサイトマッププロバイダに通知できたら最高です。実は、SQL Server 2005ではASP.NET 2.0で導入された新しいSqlCacheDependencyクラスによってこれが実現されています。この新しいテクノロジを実装するには、4つのステップを実行しなければなりません。

  1. 環境を構成する
  2. データベース接続を初期化する
  3. SqlCacheDependencyを実装する
  4. コールバックロジックを実装する

 環境を構成するには、データベースを設定して「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を参照してください。

リスト2 CustomSiteMapProviderクラス
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サイト管理者の業務、つまりおそらく皆さんの業務はかなり楽になることでしょう。

関連記事

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Lee Richardson(Lee Richardson)

Automated Architecture Inc.の創設者かつCEO。Blue InkアプリケーションおよびJAGソフトウェア開発方法論の作成者でもある。現在はHeadstrongコンサルティング社でシニアコンサルタントとして勤務し、高速アプリケーション開発(Rapid Application Development)テクノロジを専門とする。ソフトウェア開発経験は9年におよび、Microsoft認定ソリューションデベロッパー(MCSD)およびプロジェクトマネージメントプロフェッショナル(Project Management Professional:PMP)の資格を持つ。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/585 2006/09/29 00:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー