個人向けデスクトップアプリで DB 使いたい時なんかに便利かもかも
方法 : SQL Server Compact 3.5 データベースをアプリケーションと共に配置する
極小SQL Server Compactでデータベース・アプリをお手軽作成 - @IT
再配布権の登録というのが必要らしいのでそこは気を付けよう (@IT の記事の3ページ目のコラム参照)
実践!ソフトウェアアーキテクチャ VisualStudioとASP.NETによる業務システム開発方法
この本でユニットテスト時に SMO を使ってテスト用のデータベースを構築しているのを見て、僕も取り入れてみたので自分のためにメモ。
# 本に載ってる方法とは少し違う方法。
ユニットテストだけでなく、インストーラのカスタム動作でも使えるので便利。
まず、データベース管理用のクラスライブラリプロジェクトを作成。ここでは Hoge.Databases と名付けることにする。
次に、全てのテーブルを (再) 作成するためのクエリーファイルをプロジェクト内に作成。ビルドアクションは "埋め込まれたリソース" に設定。ここでは Create.sql と名付けることにする。
SET ANSI_NULLS ON
GO
-- テーブルを削除。子テーブルを先に削除すること。
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Table2]') AND type in (N'U'))
DROP TABLE [dbo].[Table2]
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Table1]') AND type in (N'U'))
DROP TABLE [dbo].[Table1]
GO
-- テーブルを作成。親テーブルを先に作成すること。
CREATE TABLE [dbo].[Table1]
(
[PrimaryKey] [bigint] IDENTITY(1,1) NOT NULL,
[Id] [nvarchar](255) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Table1] PRIMARY KEY CLUSTERED ([PrimaryKey]),
CONSTRAINT [IX_Table1_Id] UNIQUE NONCLUSTERED ([Id])
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Table2]
(
[PrimaryKey] [bigint] IDENTITY(1,1) NOT NULL,
[Table1PrimaryKey] [bigint] NOT NULL,
[Value] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Table2] PRIMARY KEY CLUSTERED ([PrimaryKey]),
CONSTRAINT [FK_Table2_Table1] FOREIGN KEY([Table1PrimaryKey])
REFERENCES [dbo].[Table1] ([PrimaryKey])
ON UPDATE CASCADE
ON DELETE CASCADE
) ON [PRIMARY]
GO
次に、SMO を使ってデータベースを管理するクラスを作成。
using System;
using System.IO;
using System.Reflection;
using Microsoft.SqlServer.Management.Smo;
namespace Hoge.Databases
{
/// <summary>
/// Hoge データベースを管理します。
/// </summary>
public sealed class HogeDatabase
{
#region Static Members
/// <summary>
/// Hoge データベースに含めるテーブルを生成するための SQL クエリーファイルのリソースパスを取得します。
/// </summary>
private const string CreateScriptResourcePath = "Hoge.Databases.Create.SQL";
/// <summary>
/// 指定したリソースパスに含まれるリソースを文字列として読み出します。
/// </summary>
/// <param name="resourcePath">リソースパス。</param>
/// <returns></returns>
static private string ReadResourceText(string resourcePath)
{
Assembly thisAssembly = Assembly.GetExecutingAssembly();
using (Stream resourceStream = thisAssembly.GetManifestResourceStream(resourcePath))
using (StreamReader resourceReader = new StreamReader(resourceStream))
{
return resourceReader.ReadToEnd();
}
}
#endregion
#region Constructors
/// <summary>
/// HogeDatabase クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="server">SQL Server のインスタンス名。</param>
/// <param name="database">データベース名。</param>
public HogeDatabase(string instanceName, string databaseName)
{
this._server = new Server(instanceName);
this._database = new Database(this._server, databaseName);
}
#endregion
#region Fields
/// <summary>
/// SQL Server のインスタンスの管理オブジェクトを取得します。
/// </summary>
private readonly Server _server;
/// <summary>
/// データベースの管理オブジェクトを取得します。
/// </summary>
private readonly Database _database;
#endregion
#region Properties
/// <summary>
/// Hoge データベースが存在するかどうかを示す値を取得します。
/// </summary>
/// <returns>Hoge データベースが存在するかどうかを示す値。</returns>
public bool Exists
{
get
{
this._database.Refresh();
return (this._database.State == SqlSmoState.Existing);
}
}
#endregion
#region Methods
/// <summary>
/// Hoge データベース、各テーブルを作成し、ASPNET アカウントをログイン登録します。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは既に存在します。</exception>
public void Create()
{
this.CreateDatabase();
this.CreateTables();
this.CreateLoginForAspnet();
}
/// <summary>
/// Hoge データベースを作成します。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは既に存在します。</exception>
public void CreateDatabase()
{
if (this.Exists)
{
throw new InvalidOperationException("データベースは既に存在します。");
}
this._database.Refresh();
this._database.Create();
}
/// <summary>
/// Hoge データベースの各テーブルを作成します。
/// テーブルが既に存在する場合は、テーブルを削除してから作成を行います。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
public void CreateTables()
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
string createScript = ReadResourceText(CreateScriptResourcePath);
this._database.ExecuteNonQuery(createScript);
}
/// <summary>
/// 結果を返さない SQL コマンドを実行します。
/// </summary>
/// <param name="sqlCommand">SQL コマンド。</param>
/// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
public void ExecuteNonQuery(string sqlCommand)
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
this._database.ExecuteNonQuery(sqlCommand);
}
/// <summary>
/// Hoge データベースを削除します。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
public void Drop()
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
this._server.Refresh();
this._server.KillDatabase(this._database.Name);
System.Data.SqlClient.SqlConnection.ClearAllPools();
}
/// <summary>
/// IIS 5.1 上に配置された ASP.NET アプリケーションから DB にアクセス (R/W) できるよう、
/// ASPNET アカウントをログイン登録します。
/// </summary>
public void CreateLoginForAspnet()
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
string name = string.Format(@"{0}\{1}", Environment.MachineName, "aspnet");
this._server.Refresh();
if (!this._server.Logins.Contains(name))
{
Login login = new Login(this._server, name);
login.LoginType = LoginType.WindowsUser;
login.Create();
}
this._database.Refresh();
User user = new User(this._database, name);
if (!this._database.Users.Contains(name))
{
user.Login = name;
user.Create();
}
if (!user.IsMember("db_datareader"))
{
user.AddToRole("db_datareader");
}
if (!user.IsMember("db_datawriter"))
{
user.AddToRole("db_datawriter");
}
}
#endregion
}
}
ユニットテストクラスでの使用例
private const string InstanceName = @"localhost\sqlexpress";
private const string DatabaseName = "HogeDatabase_Test";
private HogeDatabase _testDatabase;
[TestFixtureSetUp]
public void CreateDatabase()
{
this._testDatabase = new HogeDatabase(InstanceName, DatabaseName);
if (this._testDatabase.Exists)
{
this._testDatabase.Drop();
}
this._testDatabase.Create();
}
[SetUp]
public void CreateTables()
{
this._testDatabase.CreateTables();
}
[TestFixtureTearDown]
public void DropDatabase()
{
if (this._testDatabase.Exists)
{
this._testDatabase.Drop();
}
}
private void InsertTable1(string id, string name)
{
const string insertQueryFormat = "INSERT INTO [dbo].[Table1] ([Id], [Name]) VALUES ('{0}', '{1}')";
string insertQuery = string.Format(insertQueryFormat, id.Replace("'", "''"), name.Replace("'", "''"));
this._testDatabase.ExecuteNonQuery(insertQuery);
}
[Test]
public void テスト1()
{
for (int i = 0; i < 3; i++)
{
InsertTable1(i.ToString(), string.Format("Test {0:000}", i));
}
// テストコード
}
// 2009/01/14
ユニットテストの SetUp と TearDown で DB の作成・削除を実行すると、2 度目以降の DB 作成に失敗してしまうため、DB の作成・削除は TestFixtureSetUp と TestTearDown で行い、テーブルの再作成だけ SetUp で行うように、HogeDatabase クラスとユニットテストを変更。
HogeDatabase クラスの実装方法も全体的に変更。
ついでに ASPNET アカウントの R/W 権限を登録するメソッドも追加。(IIS 5.1 のみ正常に機能する)
// 2009/02/02
テストケースが複数あって DB の作成・削除が2度行われる場合、SqlConnection のプールが残ってしまう関係でエラーが発生する。そのため、DB の削除時に ClearAllPools メソッドを呼び出すよう修正。
参考 : DBバックアップ→DBリストア→テーブル内容参照時にException - Insider.NET
一番の収穫はやはり LINQ to SQL。
つか、LINQ to SQL の場合、where 句のラムダ式が SQL の where 句に変換されるというのは、正直半信半疑だった。
で、帰宅して早速試してみた。
static void Main(string[] args)
{
DataContext context = new DataContext(Settings.Default.Database1ConnectionString);
var query =
from target in context.GetTable<Table1>()
where (target.Column1 == "a")
select target;
string queryText = query.ToString();
Console.WriteLine(queryText);
}
[Table(Name = "Table1")]
class Table1
{
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public int PrimaryKey;
[Column]
public string Column1;
}
実行結果
SELECT [t0].[PrimaryKey], [t0].[Column1]
FROM [Table1] AS [t0]
WHERE [t0].[Column1] = @p0
Σ(゚д゚lll)ガーン
ホントに where 句が where 句に・・・!
そういや、LINQ to SQL のラムダ式はデリゲートではなく System.Linq.Expressions.Expression オブジェクトに変換されると言っていたな。
逆コンパイルしてみると、確かに・・・つーか結構複雑なことやってるな。target.Column1 == "a" というラムダ式から、target とか Column1 とか == 演算子なんかのメタデータを採取して、それを元に Expression オブジェクトを構築するようなコードへと変換されている。
なるほどなるほど。
でもまぁこんなクエリー式はさすがに無理だろ。
var query =
from target in context.GetTable<Table1>()
where (target.Column1[1].ToString() == "a")
where (target.Column1.IndexOf("b") <= 3)
select target;
実行結果
SELECT [t0].[PrimaryKey], [t0].[Column1]
FROM [Table1] AS [t0]
WHERE ((
(CASE
WHEN (DATALENGTH(@p0) / 2) = 0 THEN CONVERT(BigInt,0)
ELSE CONVERT(BigInt,(CONVERT(Int,CHARINDEX(@p0, [t0].[Column1]))) - 1)
END)) <= @p1) AND ((CONVERT(NVarChar(MAX),CONVERT(NChar(1),SUBSTRING([t0].[Column1], @p2 + 1, 1)))) = @p3)
( ゚Д゚ ) ・・・。
日経BP書店|商品詳細 - プログラミングMicrosoft ADO.NET 2.0
定価は 8,925 円 とやや高めですが、MCP 資格を保有している人は以下のページにて 10% OFF の 8,033 円 で購入できます。
プログラミング Microsoft ADO.NET 2.0 ( 日経BPソフトプレス )
これはもう買うしかないですね。僕は財布と相談もせずに注文してしまいました ^^;
[ 情報元 ]
The road to C# master trapemiya
# あれ、Windows Live ID にサインインしていなくても、割引が適用されるんだけど・・・。
# もしかして MCP 資格持ってなくても割引価格で購入できんの・・・?