きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

Hangfireを使ってみる (1):とりあえず触ってみる

最近 Hangfire について調査・検証しているのでメモ。

Hangfireとは

Hangfireは.NET向けのバックグランド・タスクのライブラリです。

www.hangfire.io

ストーレージにタスクをキューイングして、非同期に実行させることができます。即時実行だけでなく、cronっぽい感じで定期実行したりもできる模様。 OWIN Middleware としてASP.NETで利用できるほか、コンソール/WPF/Windows Form/Windows サービス等など……でも使えます。

 

今回から予定している一連のエントリは『ASP.NETのWebアプリケーションから重たい処理(例:集計帳票作成処理)を非同期実行させる』という観点での調査検証に基づいています。ですので、ある程度方より(?)がある点はご了承ください。

Hangfireを構成するコンポーネント

Hangfireはざっくりと以下のようなコンポーネントで構成されています。

コンポーネント 役割 典型的な実装
Hangfire Client Jobをキューへ登録 ASP.NET等, ユーザーが操作するアプリ
Job Storage Jobのキュー,その他管理情報を格納 SQL Server / Redis 等
Hangfire Server キュー上のJobの実行 ASP.NET(バックグラウンド) / Windowsサービス等

 

Hangfire ClientとHangfire Serverは同一のプロセス上で実装可能です*1。 そのほか、ASP.NET用にビルトインされていてるダッシュボード機能があり、jobの状況のモニタリングや再実行などの操作を行うことができます。

とりあえず試してみる

チュートリアル的に一番シンプルで試しやすい構成として、『ClientとServerの双方を同一のASP.NET Webアプリケーションに実装』するパターンを試してみます。

f:id:kiyokura:20170803113448p:plain:w300

https://github.com/kiyokura/HangfireSample01/tree/step01

基本構成

とりあえずこんな感じでやってみました。

  • Visual Studio 2015
  • ASP.NET MVC 5
  • SQL Server 2016

実装手順

1. SQL ServerでHangfire用のDBを作成

Jobストレージとして利用するデータベースが必要なので適当に作成しておきます。データベース内にはHangfireスキーマといくつか専用のテーブルが作成されます*2

2. ASP.NET MVCのプロジェクトを作成する

Visual Studio 2015で適当にASP.NET MVCのプロジェクトを作成しました。 f:id:kiyokura:20170803115757p:plain:w300

3. NuGetでHangfireのパッケージを取得

NuGetでHangfireを取り込みます。コマンドラインからだとInstall-Package HangfireでOK。

f:id:kiyokura:20170803120224p:plain

4. OWINスタートアップを記述

Hangire の最低限必要な設定をOWINスタートアップで行うように記述します。Webアプリケーションプロジェクトの直下にOWIN Startupクラス「Startup.cs」を追加して、以下のように書きました。

using Hangfire;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(HangfireSample01.Startup))]

namespace HangfireSample01
{
  public class Startup
  {
    public void Configuration(IAppBuilder app)
    {
      // 接続文字列は環境に応じて変えること
      var constr = @"Data Source=localhost;Initial Catalog=HangfireJob;Integrated Security=True";
      GlobalConfiguration.Configuration.UseSqlServerStorage(constr);

      app.UseHangfireDashboard();
      app.UseHangfireServer();
    }
  }
}
5. Jobの登録処理を記述

最後に実際にjobをキューに登録する処理を書きます。既存のHomeControllerのIndexアクションメソッドに、とりあえずコンソールに文字を出力するだけの単純な処理をjobとして登録するように書いてみました。

using Hangfire;
using System;
using System.Web.Mvc;

namespace HangfireSample01.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      // Hangfireでjobをキューに登録
      BackgroundJob.Enqueue(() => Console.WriteLine("Simple Job"));
      return View();
    }

    public ActionResult About()
    {
      ViewBag.Message = "Your application description page.";
      return View();
    }

    public ActionResult Contact()
    {
      ViewBag.Message = "Your contact page.";

      return View();
    }
  }
}

実行して確認する

準備が終わったので実行してみます。

1.WebアプリケーションをF5で実行

F5でデバッグ実行を開始すると見慣れた(?)ASP.NET MVCのテンプレートのホーム画面が表示されます。このとき、Home/Indexアクションメソッドが実行されているので裏ではキューにjobが登録され、そして実行されているはずです。

次の手順でそのあたりを確認してみます。

2.Hangfireダッシュボードを表示

Hangfire組み込みのダッシュボードを利用してjobの状況を確認してみます。hangfireダッシュボードは、アプリケーションルート+/hangfireというURLでアクセスできます。今回のようにIIS Expressでローカル実行している場合は例えば以下のようになります

  • http://localhost:61828/hangfire (※ポート番号は環境によって変わります)

アクセスできるとこんな感じのダッシュボードが表示されます。 f:id:kiyokura:20170803122544p:plain

3.jobの実行状況を確認

ダッシュボードで実際のjobがどのようになっているかを確認します。 メニューから[jobs]-[Succeeded]とたどっていくと、成功したジョブ一覧に先ほど登録したjobが並んでいるのが見えました。

f:id:kiyokura:20170803122848p:plain

さらにjob名をクリックすると詳細情報が見れます。 f:id:kiyokura:20170803123351p:plain

4.データベース(Job Storage)を確認

最後に、Job Storageとなっているデータベースを簡単に確認してみました。

SSMSで該当のデータベースを開いてみると、こんな感じでテーブルが作成され、例えばHangfire.Jobテーブルにはこんな形でJobが格納されているのが分かります。 f:id:kiyokura:20170803123619p:plain

ここまでのまとめ

Hangfireのクイックスタート的な感じでとりあえず手元で基本的な動作をさせてみました。というか、オフィシャルのドキュメントのQuick Startにあるそのままです。

なお、ここまでのサンプルコードは以下にあります

github.com

このHangfire ServerもASP.NETのインプロセスで動かすモデルの場合、ASP.NETの標準仕組の HostingEnvironment.QueueBackgroundWorkItem と似ているようにも見えます。しかしキューが永続化されているためjobのトレースやリトライ等QueueBackgroundWorkItemでは実現困難なことが簡単に実現できる点や将来的なスケールアウトの容易さ等、差別化のポイントはいくつもあるように思います。 (一方QueueBackgroundWorkItemはインプロセス前提なのでHttpContext等ASP.NETの実行インスタンスに依存する情報をHangfire よりもよりシンプルに扱うことができる等のメリット?はあかなーと思ったりもします)

次はClientとServerを別プロセスでやってみたいと思います。

*1:もちろん別のプロセスに分離できます。スケールなど考えるとそちらのほうが良いケースは少なくないでしょう

*2:各オブジェクトは初回実行時に勝手に作成されるのでこのタイミングで手動で作成する必要はありません

SQL Server Data Toolsのユニットテスト実行前にLocalDBのインスタンスを破棄・再作成する

SQL Server Data Tools (SSDT)のユニットテスト機能ネタです。

テスト実行前の自動デプロイは便利だが失敗するケースがある

以前の記事(これとかこれ)でも触れた、SSDTの ユニットテスト実行前にDBインスタンスに自動でデプロイを行う機能 は非常に便利です。DBプロジェクト側で変更を普通に保存した後、テストランナーでテストを走らせるだけで環境を最新にして実行してくれます。

便利なのですが、この自動デプロイが必ず失敗するケースがあります。

例えばデータが入っているテーブルの列を削除したり列の型を変更するなどデータ消失が発生する可能性があるような変更がある場合です*1

もちろん一旦手動で削除してから実行することはできますが、手間ですしCIでの自動テストの障害にもなります。

DBプロジェクトの発行機能では発行プロファイルの設定でデータベースを毎回再作成するオプションがありますが、どうやらテスト実行時の発行には利用できないようです。

再実行時に単純に削除して再作成すればよいのであれば*2、SQL Server Express LocalDB(以下LocalDB)と組み合わせて以下のような方法を思いつきました。

 SQL LocalDB Wrapper を使ってデプロイ前にLocalDBのインスタンスを削除・再作成する

LocalDBの場合、簡単にインスタンスを削除するすることができます。

またSQL Server LocalDBのインスタンスを削除してもユーザーデータベースのファイルが残る でも紹介した SQL LocalDB Wrapperを使うと、LocalDBのインスタンスをSSDTの自動デプロイで作成されたデータベースごときれいに削除できます。SQL LocalDB Wrapperにはインスタンスを再作成する機能もあります。

これらを利用すると、テストの初期化時、データベースのデプロイが始まる前にLocalDBのインスタンスを破棄・再作成が簡単に行えます。

組み込み例

組み込み方は非常に簡単です。テストプロジェクトにNuGetでSQL LocalDB Wrapperを取り込んだ後、例えば、適当に以下のようなメソッドを作ってやります。

/// <summary>
///  LocalDBのインスタンスの再作成
/// </summary>
/// <remarks>
/// SQL LocalDB Wrapper経由でネイティブAPIを通して操作する
/// https://www.nuget.org/packages/System.Data.SqlLocalDb/
/// </remarks>
internal void RecreateLocalDbInstance(string instanceName, string version)
{
  // インスタンスが存在したらインスタンスを停止してからユーザーDBも同時に削除してから作成
  if (SqlLocalDbApi.GetInstanceNames().Contains(instanceName))
  {
    SqlLocalDbApi.StopInstance(instanceName, StopInstanceOptions.KillProcess, new TimeSpan(0, 0, 30));
    SqlLocalDbApi.DeleteInstance(instanceName, true);
  }
  SqlLocalDbApi.CreateInstance(instanceName, version);
}

余談ですが、SqlLocalDbApi.CreateInstance(String, String)の第二引数を渡さない(オーバーロードのSqlLocalDbApi.CreateInstance(String)を呼ぶ)と、作成されるインスタンスのバージョンは環境にインストールされている最新のLocalDBにものになるようです(ざっとソースを見た感じ)。なので指定しなくても問題はないのですが、たとえば複数人による開発で実行環境にばらつきがあるような場合は、ここはあえて指定するようにしておいたほうが無難です。この詳細はまた別途改めて書きたいと思っています。*3

あとは先ほど作ったRecreateLocalDbInstanc()をSqlDatabaseSetup.csのSqlDatabaseSetup.InitializeAssemblyメソッドの頭のほうで呼んでやればOKです。

  [TestClass()]
  public class SqlDatabaseSetup
  {
    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
        RecreateLocalDbInstance("SomeDbInstance","13.0");

     // ...既存の処理はそのまま
    }
  }

まとめ

SQL LocalDB Wrapperを使うとC#のコード中からLocalDBを簡単に操作出来て便利ですね。 まだぼんやりとしか考えてないのですが、アプリケーションのユニットテストでDBに接続する必要があるようなテストでもこれを利用していい感じにできるんじゃないかとか思ったりしてます。

*1:テスト自体にトランザクションを利用してデータが残らないような状態にキープしておくのであればこの問題は発生しません。しかし、例えばバッチ更新のストアドのテストで失敗したときにテーブルの状態を確認したい場合や、値が原則変わらないマスタ類をテスト開始時に投入ておきたいなどの要件と相容れません

*2:単体テストなのでそうあるべきとは思う

*3:何時になるかわからないので簡単にメモしておきます。SQL Server 2014以前のデータベースのコンポーネントが入っている環境で、バージョン13.0(SQL Server 2016相当)のLocalDBのインスタンスへのテスト実行前自動デプロイが失敗することがありました。SQL Server 2016のSSMSをインストールしたり、LocalDBのインスタンスのバージョンを12.0で作成するとうまくいくことから、13.0のLocalDBに対して12.0までしか対応していないコンポーネントを使って接続しようとして失敗してるように見えました。もう少し状況を絞りたいので現状はこれ以上は追加調査できていません。

SQL Server LocalDBのインスタンスを削除してもユーザーデータベースのファイルが残る

f:id:kiyokura:20161220002431p:plain

TL;DR

  • SQL Server Express LocalDBのインスタンスをSQL Server Data Tools (SSDT)のSQL Server オブジェクトエクスプローラーやコマンドラインユーティリティー SqlLocalDb.exeで削除すると、ユーザーデータベースのmdf/ldfファイルは残る
  • この状態で同名のインスタンスを再度作成後、もと合ったものと同名のデータベースを作成しようとするとエラーが発生する
  • これを回避/回復するには以下の方法がある
    • 事前にユーザーデータベースを削除した上でインスタンスを削除する
    • インスタンスを削除済みの場合はファイルシステム上の実体を手動で削除する
      • SSDT関連のツールで作成した場合は%USERPROFILE%\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances にあるインスタンス名のフォルダが対象
      • CREATE DATABASE ~の規定値で作成した場合は%USERPROFILE%\Documents配下に作成さる
    • SSDT関連ツールで作成したデータベースであれば、.NETから簡単に利用できるライブラリ SQL LocalDB Wrapper を使ってコード中から削除できる
  • 手元で試した環境は SQL Server LocalDB 13.0.2151.0 および SQL Server Data Tools 14.0.61021.0

 

2016/12/20 追記

全体的にSSDT関連の機能で作成した場合を前提に書いてしまっていました(ここのところずっとSSDT絡みでLocalDBを使っていたので……)。 SSDT関連のツールでデータベースを作成するとファイルの実体は%USERPROFILE%\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances にある各インスタンスのフォルダ内に作成されます。 しかし、CREATE DATABASE ~で規定値で作成した場合は%USERPROFILE%\Documentsに作成されます。ですので、ここを手動で削除する必要があります。このケースでは、SQL LocalDB Wrapperでも対応できません。事前にDROP DATABASEするか、事後に手動で削除する必要があります。

SQL Server Express LocalDBのインスタンスを削除してもユーザーDBファイルは残る

SQL Server Express LocalDB(以下、LocalDB)では、コマンドラインユーティリティー SqlLocalDb.exe やSQL Server Data Tools(以下、SSDT)のSQL Server オブジェクトエクスプローラー(以下、SSOE)を使ってインスタンスを削除することができます。

これらの方法でインスタンスの削除を行った場合、(事前にユーザーデータべースを削除していない限り)ファイルシステム上にはユーザーデータベースのファイル(mdf/ldf)が残ります。 うっかり削除してしまった場合でもデータ損失を起こさないで済みますから、これは自体は悪い挙動ではないと思います。しかし、この挙動を知っていないと少しはまることもあると思います(というかはまった)。

試してみる

では、少し試してみます。 ツールとしてSSOEとSqlLocalDb.exe を使います。

1. LocalDBの作成

まずはSqlLocalDb.exe で、以下のようにLocalDBを作ります。 SqlLocalDb.exeは%ProgramFiles%\Microsoft SQL Server\<version>\Tools\Binn\SqlLocalDB.exe あたりにあると思います*1

C:\> SqlLocalDB create MyLocalDb01

f:id:kiyokura:20161220002440p:plain

(※バージョンはインストールされているLocalDBのバージョンおよびパスの優先順位などが影響します)

2.SQL Serverオブジェクトエクスプローラーで接続

Visual Studio を立ち上げ、作成したインスタンスにSSOEから接続します*2

最近のSSDTなら参照タブのLocalノードにLocalDBのインスタンスが列挙されるので、選択して接続するだけでOKです。

f:id:kiyokura:20161220003907p:plain

3.データベースを作成する

接続出来たら、データベースを作成してみます。接続したLocalDBのデータベースノードのコンテキストメニューでデータベースを追加します。とりあえず名前はなんでもOKです。

f:id:kiyokura:20161220002614p:plain

こんな感じでデータベースが作成出来たら準備完了

f:id:kiyokura:20161220002621p:plain

4.インスタンスを削除する

さっそくインスタンスを削除してみます。

SSOEの場合は、インスタンスを右クリックしてコンテキストメニューから「インスタンスの削除*3」。SqlLocalDbで行う場合は、SqlLocalDb delete <instance name> です。

f:id:kiyokura:20161220002633p:plain

あっさり削除できたと思います。先ほど削除したMyLocalDb01はもうありません。

f:id:kiyokura:20161220002640p:plain

接続しようとしても、リストにも出てきません。

f:id:kiyokura:20161220002649p:plain

これで一見、何事もなくきれいに削除できたかのように見えます。

ファイルは残っている

ですが、冒頭に述べた通り、ファイルの実体は残っています。LocalDBのインスタンスの実体は、以下のパスに格納されています。Explorerで次のパスを表示してみましょう。

%USERPROFILE%\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances

先ほど削除したインスタンスのフォルダが残ってますね。

f:id:kiyokura:20161220002708p:plain

中を見てみましょう。作成したユーザーDBのデータベースとログファイルが残っているのが分かります。

f:id:kiyokura:20161220002715p:plain

ファイルが残っていると何か問題が?

消したインスタンスを二度と使わないのであれば特に問題はありません(ディスクを占有はしますが)。 顕著に問題が出てくるのは、再度同名のインスタンスを作成したときです。

やってみましょう。 先ほどと同名のインスタンスをまたコマンドラインで作成し、SSOEで接続します。

f:id:kiyokura:20161220002737p:plain

ここまでは何の問題もなく操作できると思います。では、ここでまた先ほど同名のデータベースを追加しようとすると……。

f:id:kiyokura:20161220002741p:plain

Cannot create file 'C:\Users\<UserName>\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MyLocalDb01\MyDataBase.mdf' because it already exists. Change the file path or the file name, and retry the operation. CREATE DATABASE failed. Some file names listed could not be created. Check related errors.

ということで、作ろうとしたファイル名がすでにあるので作成できません。

これは、データベースプロジェクトの発行機能やユニットテスト実行時のデプロイなどを行おうとしたときにも発生し得ます。

回避策・対応策

これを回避または対応するための方法として、以下を思いつきました*4

  • あらかじめユーザーデータベースを削除してからインスタンスを削除する
  • インスタンス削除後にファイルの実体を削除する
  • (SSDT関連ツールで作成したデータベースであれば)SQL LocalDB Wrapper を使ってプログラムを作成して実行する

他二つは自明だと思うので、「SQL LocalDB Wrapperを使ってプログラムを作成して実行する 」について少し補足しておきます。

SQL LocalDB Wrapperを利用してインスタンスを削除する

LocalDBにはこれを管理・運用するためにネイティブAPIが用意されています。これを.NETから簡単に利用できるようにしたラッパーライブラリが SQL LocalDB Wrapperです。

www.nuget.org

このラッパーライブラリが備えている SqlLocalDbApi.DeleteInstanceメソッドにファイルを削除するオプションがあり、これを使うとサクっと削除できました%USERPROFILE%¥AppData\~配下にあるインスタンスのフォルダごと削除してくれます

    SqlLocalDbApi.DeleteInstance(instanceName, true);

なので、SSDT関連のツールで作った場合など、%USERPROFILE%\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances配下に実体を配置しているケースではとても有効だと思います。 頻繁にこういう操作をする場合はこれを実行するだけのコマンドラインツールでも作っておくとよいかもしれませんね。

以下余談

この「ラッパーライブラリで用意されているということは当然ネイティブのAPIでも削除するオプションがあるに違いない」ともうかもしれません。……が、実はありません*5。 リファレンスのLocalDBDeleteInstance 関数を見てみると、第二引数に DWORD dwFlags とかあって「お、これにフラグ指定したら削除されるんちゃうん!?」と思わせてくれます。が、よくよく見ると、

dwFlags [入力] 将来の使用のために予約されています。 現時点では、0 に設定する必要があります。

と書いてあります。Oh...。

さらに余談

では先ほどのSQL LocalDB Wrapperではどうやってるんだろう、と思ってソースを見てみると……自力で削除してました:p

DeleteInstanceFiles メソッドあたりを参照

まとめ

安全側に倒しているのは理解できるのですが、標準で用意されているツールからだとさくっと消す方法が無い&表面的には消えている(ようにも)見えるので、LocalDBの仕組みを知らないとはまってしまう可能性があるように思います。というかは少しまりました。

将来的にユーティリティーにファイル削除するオプションをつけたり、SQL Server オブジェクトエクスプローラーでもファイルを削除するかダイアログで聞いてから消してくれるようになるとかすると嬉しいですね。

参考

*1:SSDTインストール時にパスが通ってる気もします

*2:SSOE使うだけなら特にプロジェクトは開かなくても大丈夫です

*3:手元も環境だと今SSDTが英語版になってるので[Delete Instance...]になってる

*4:他にも良い方法があるかもしれません。ご存知の方がおられてたらぜひ教えてくださいm(__)m

*5:今のところ

真・SQL Server Data Toolsのユニットテスト実行前に複数DBにデプロイする

前回の続きです。

前回のエントリに対して、@ahiru_sp さんからアドバイスいただきました。

なるほど。リファレンスにあるprotected static な'DeployDatabaseProject(String, String, String, String)'のを使えばいいのではということですね。

SqlDatabaseTestService.DeployDatabaseProject Method (Microsoft.Data.Tools.Schema.Sql.UnitTesting)

 

SqlDatabaseTestServiceの派生クラスを作成する

ということで、そのままやってみます。

まず以下のような Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestService のは派生クラスを作り、DeployDatabaseProject(String, String, String, String)を呼ぶメソッドを一つ作っておきます。

internal class CustomSqlDatabaseTestService : SqlDatabaseTestService
{
  /// <summary>
  /// DBのデプロイ
  /// </summary>
  /// <param name="projectFile">プロジェクトファイルのパス(相対パス)</param>
  /// <param name="projectConfiguration">プロジェクトファイルの構成設定</param>
  /// <param name="connectionString">接続文字列</param>
  public void DeployDatabaseProjectEx(string projectFile, string projectConfiguration, string connectionString)
  {
    SqlDatabaseTestService.DeployDatabaseProject(projectFile, projectConfiguration, "System.Data.SqlClient", connectionString);
  }
}

引数に何を渡すかは、app.configSqlUnitTestingセクションをみれは推測はできますね。なお、第三引数はリファレンスに This must be System.Data.SqlClient. って明記されてるので埋め込んでおきます(まあどうせSQL Serverだし)。

あとは、昨日のやつと同様、InitializeAssemblyから呼んでやればOKです。例えばこんな感じ。

using System;
using System.Configuration;
using Microsoft.Data.Tools.Schema.Sql.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Xml;

namespace BletillaTestsNext
{
  [TestClass()]
  public class SqlDatabaseSetup
  {
    // ターゲットデータベース
    const string TARGET_DB_PROJECT = "..\\..\\..\\ MainDb\\MainDb.sqlproj";
    const string TARGET_DB_CONFIGURATION = "Release";
    const string TARGET_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列>";
   
    // 依存データベース
    const string DEPEND_DB_PROJECT = "..\\..\\..\\SubDb\\SubDb.sqlproj";
    const string DEPEND_DB_CONFIGURATION = "Release";
    const string DEPEND_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列> ";

    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
      var testService = new CustomSqlDatabaseTestService();

      // テスト実行ターゲットをデプロイ
      testService.DeployDatabaseProjectEx(TARGET_DB_PROJECT, TARGET_DB_CONFIGURATION, TARGET_DB_CONNECTION_STRING  );
 
      // 依存DBをデプロイ
      testService.DeployDatabaseProjectEx(DEPEND_DB_PROJECT, DEPEND_DB_CONFIGURATION , DEPEND_DB_CONNECTION_STRING);

      // もともとあったこれは現在は使われてないので削除して問題ない
      //SqlDatabaseTestClass.TestService.GenerateData();
    }
  }
}

実際に試してみたら、あっさりそのまま動きました。なんだかすっきりしましたし無理やりapp.configを書き換えるとかするよりもこちらの方が断然スマートですね。

@ahiru_sp さん、ありがとうございました!

SQL Server Data Toolsのユニットテスト実行前に複数DBにデプロイする

SQL Server Data Tools (SSDT)のユニットテスト機能では、テスト実行前にソリューション内のデータベースプロジェクトをテスト実行対象のデータベースに自動でデプロイしてくれる機能があります。

これは非常に便利な機能です。しかし、複数のデータベースにまたがるような開発していると、依存関係があるなどの理由で複数のデータベースプロジェクトの内容をデプロイしたいことが出てきたりします。

SSDTの設定を調べて見ましたがそういった機能は用意されていなさそうで、一旦は諦めていました。が、別件で思案してたところでふと思いついたことを試してみた……ら、なんとか実現できなのでメモしておきます。

 

SSDTがユニットテスト実行前にデプロイしている仕組み

まず、SSDTがユニットテスト実行前にどうやってデプロイしているかを見てみます。

SSDTで作成されるテストプロジェクトでは、テストが実行されるとまずSqlDatabaseSetupクラスInitializeAssemblyメソッドが呼ばれます。 このメソッドはMsTest(Microsoft.VisualStudio.QualityTools.UnitTestFrameworkアセンブリ)のAssemblyInitialize属性がついた属性です。

この中で実行されているの処理は二つ。 そのうちのSqlDatabaseTestClass.TestService.DeployDatabaseProject();がデプロイ処理であることは名前からして疑う余地はなさそうです。

f:id:kiyokura:20161213232318p:plain

この処理が実装されているMicrosoft.Data.Tools.Schema.Sql.UnitTestingアセンブリのソースは公開されていませんので具体的にどのような処理をしているのかはわかりませんが、MSDNには一応載っていました。

SqlDatabaseTestService.DeployDatabaseProject Method

この DeployDatabaseProjectメソッドは、上記のMSDN

Deploys the database project by using the settings of the user in the app.config file.

と記載されている通り、app.confgのSqlUnitTestingセクションに記載された設定に従ってDBをデプロイします。この記載内容は、テストプロジェクト作成時や「SQL Server テスト構成」のダイアログで設定する内容です。

f:id:kiyokura:20161213232418p:plain

 

複数のDB・プロジェクトをデプロイする方法(強引)

追記:もっと適切だと思う方法がありましたので、ぜひそちらを参照してください kiyokura.hateblo.jp

SSDTが生成するユニットテストプロジェクトで自動デプロイが行われている仕組みをもう一度整理すると、こういうことです。

  • AssemblyInitialize属性の追加メソッド内でDeployDatabaseProjectメソッドでデプロイが実施される
  • DeployDatabaseProjectメソッド`はapp.configの設定に従って処理を行う

 

……ここまでくればピンと来たかもおられるでしょう。 そうです。

app.configを書き換えて DeployDatabaseProjectを呼ぶ

ようにすればいくらでもデプロイできそうじゃありませんか(

 

……ということで、例えば以下のようにapp.configを書き換えて実行してやることで、複数のデータベース/プロジェクトをすんなりデプロイすることができました。

using System;
using System.Configuration;
using Microsoft.Data.Tools.Schema.Sql.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Xml;

namespace BletillaTestsNext
{
  [TestClass()]
  public class SqlDatabaseSetup
  {
    // ターゲットデータベース
    const string TARGET_DB_PROJECT = "..\\..\\..\\ MainDb\\MainDb.sqlproj";
    const string TARGET_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列>";
    
    // 依存データベース
    const string DEPEND_DB_PROJECT = "..\\..\\..\\SubDb\\SubDb.sqlproj";
    const string DEPEND_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列> ";

    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
      // テスト実行ターゲットをデプロイ
      RewriteConfig(TARGET_DB_PROJECT, TARGET_DB_CONNECTION_STRING);
      SqlDatabaseTestClass.TestService.DeployDatabaseProject();
 
      // 依存DBをデプロイ
      RewriteConfig(DEPEND_DB_PROJECT, DEPEND_DB_CONNECTION_STRING);
      SqlDatabaseTestClass.TestService.DeployDatabaseProject();
 
      // テスト実行用に元に戻しておく
      RewriteConfig(TARGET_DB_PROJECT, TARGET_DB_CONNECTION_STRING);


      SqlDatabaseTestClass.TestService.GenerateData();
    }
    
    private static void RewriteConfig(string projectFile, string connectionString)
    {
      // デプロイは特権コンテキスト(PrivilegedContext)で行われるのでそちらだけ書き換えればよい
      var xmlDoc = new XmlDocument();
      xmlDoc.Load(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
      xmlDoc.SelectSingleNode("//SqlUnitTesting/DatabaseDeployment").Attributes["DatabaseProjectFileName"].Value = projectFile;
      xmlDoc.SelectSingleNode("//SqlUnitTesting/PrivilegedContext").Attributes["ConnectionString"].Value = connectionString;
      xmlDoc.Save(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
      ConfigurationManager.RefreshSection("SqlUnitTesting");
    }
  }
}

見てのとおり、app.configを読み込んで、

  • SqlUnitTesting/DatabaseDeploymentノードDatabaseProjectFileName属性に対象のプロジェクトファイルを
  • SqlUnitTesting/PrivilegedContextノードConnectionString属性にでデプロイ先のプロジェクトファイルを

書き込んで反映、念のためリフレッシュしています。

なお、SqlUnitTestingセクションで接続文字列を宣言してる箇所としてPrivilegedContextとExecutionContextの二つがありますが、この最初のでデプロイで利用されるのはPrivilegedContextですのでそこは間違えないようにする必要があります。 またプロジェクトファイルのパスは、このテストプロジェクトのアセンブリ出力パス(bin\Debug)からの相対パスで指定しています。

そのほかこの例ではプロジェクトのパスや接続文字列を直書きしてますが、実際にはapp.configにAppSettingsセクションを追加したりして管理するといいかもしれません。

 

まとめ

多少強引感もありますが、割とすんなり動いてる感があります。というか標準で複数デプロイ先へのデプロイに標準で対応してくれればもっと嬉しいのですが:p