読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

DeclarativeSql v0.2 リリース

C# SQL

もう半年以上前から構想はしていたけれど、案の定 (?) 放置し続けてきた自作のデータベースアクセス補助ライブラリ DeclarativeSql。搭載したかった機能の中でも特に重要度が高かった機能を詰め込んでアップデートしました。この記事を書いている今現在はまだ RC 版としてのリリースですが、近々正式版を公開します(たぶん。

すでに Pre Release パッケージとして NuGet からダウンロードして利用することができますので、ご興味のある方はぜひお試しください。ソースコードは vNext ブランチに入っています。

PM> Install-Package DeclarativeSql.Core
PM> Install-Package DeclarativeSql.Dapper

2016/01/22 : 追記

vNext ブランチを master ブランチにマージし、0.2.0 を正式リリースしました!

What's DeclarativeSql

端的に言うと、属性とクラス定義を利用した超簡単なデータベースアクセスを提供します。

  • Dapper は軽量で素晴らしい ORM だけど、SQL は全部直書き
  • Entity Framework はフルサポートの ORM で超便利、だけど仰々しい
  • 基本的には Dapper でいいんだけど、定型的な SQL を書くのは凄まじくダルい
  • パフォーマンスが必要だったりちょっとテクぃことをしたい場合は直書きでいい

と思っている方にオススメです。自分自身前職では全面的に使っていたし、先日別の方からも「超便利でプロダクションで使っています!」と言ってくださったりもしています。以下の記事は v0.1 ベースの情報ではありますが、雰囲気を掴むには参考になると思います。

バルクインサートのサポート

今回のアップデートで最も作りたかった機能がコレです。バッチ処理などでは短時間に大量のデータをインサートすることはよくありますが、そんなときに 1 レコードずつチマチマとインサートしていては日が暮れてしまいます。そこで使われるのがバルクインサートですが、ターゲットにしているデータベースによって書き方が全然違うんですね。しかもどのデータベースもクセがあって、不親切なぐらい複雑でめんどくさい!でも DeclarativeSql はそれを全部吸収し、メソッドひとつで様々なデータベースに対して透過的に書けるようにします。

using (var connection = DbProvider.CreateConnection(DbKind.SqlServer, "ConnectionString"))
{
    connection.BulkInsert(new []
    {
        new Person { Id = 1, Name = "yoshiki", Age= 49 },
        new Person { Id = 2, Name = "suzuki",  Age= 30 },
        new Person { Id = 3, Name = "anders",  Age= 54 },
    });
}

正直なところ、これよりもコードの意図が明確で分かりやすく、簡単に書けるバルクインサートはないと思う...。DbKindOracleMySQL に書き換えるだけで対象とするデータベースを変更して実行できます。もうこれを使わない理由がない...!(キリッ

ちなみにたいていのデータベースに対応していますが、PostgreSql には対応していません。手元に環境がないのと、まずまず自分が使ったことがなくてあまりモチベーションが上がらず...。Pull Request は常にお待ちしておりますので、PostgreSql ユーザー様は是非よろしくお願い致します m( )m

Insert 時に Auto Increment な ID を取得

このライブラリに初めて飛んできた Issue が「インサートしたときに自動採番の ID 取れないんだけど」というものでした。

当時は Oracle をターゲットに開発していて、シーケンスという Auto Increment より先進的な (?) 機能を使っていました。シーケンスを使うときも「先に自分で採番してからインサート」という方法を取っていたので、ID を返してもらう必要性を感じていなかったんですね。でも「確かにそれじゃダメだ」と思い直し、実装しました。

var person = new Person { Name = "xin9le", Age = 31 };
var id = connection.InsertAndGet(person);  //--- これで自動採番の ID を取得

これもまたデータベースごとに実装が違うのでなかなかに厄介ですが、全部綺麗に吸収しています。そして、やっぱりあるとメッチャ便利...w

Transaction サポート

もうひとつ飛んできた Issue は「Transaction 指定ができなんだけど」というものでした。

実際 Oracle だと BeginTransaction さえしておけば IDbTransaction を渡してコマンドを実行する必要がないんですね (しなくても動いていた)。そして TransactionScope を使っている場合も不要。

using (var connection = new OracleConnection()))
{
    connection.ConnectionString = "ConnectionString";
    connection.Open();
    using (var transaction = connection.BeginTransaction())
    {
        try
        {
          //connection.Execute("sql", model);  //--- 少なくとも Oracle では必要なかった
            connection.Execute("sql", model, transaction);  //--- 本来はこんな感じで IDbTransaction を指定したい
            transaction.Commit();
        }
        catch
        {
            transaction.Rollback();
        }
    }
}

なので「IDbTransaction を指定できる必要ないよね」と全く気にしていなかったのですが、その他のデータベースではそうは行かないのでしょう。ということで、IDbTransaction のサポートのために大きく手を入れました。

using (var connection = DbProvider.CreateConnection(DbKind.SqlServer, "ConnectionString"))
using (var transaction = connection.StartTransaction())  //--- トランザクション開始 (コネクションが開いてなければ自動で開く)
{
    //--- 指定のトランザクションに対する CRUD 操作
    transaction.Insert(new Person { Name = "anders",  Age= 54 });

    //--- TransactionScope ライクなトランザクション処理
    //--- ここでは完了をマークするだけで、Commit/Rollback は Dispose のタイミングで行われます
    transaction.Complete();
}

これまでは IDbConnection に対してしか CRUD 操作は提供されていなかったのですが、IDbTransaction に対しても書けるようになりました。IDbConnection に対して CRUD 操作を書けばトランザクションの指定なし、IDbTransaction に対して書けばトランザクションの指定ありとなります。何とも分かりやすい。

2016/01/22 : 追記

さらに、以下のような「Query とかも書けるようにしてほしい」という Issue をいただいたので対応しました。

Dapper が提供する IDbConnection への拡張メソッドと同様の記述ができるようになっています。以下の 2 つは同じ意味になります。

using (var connection = DbProvider.CreateConnection(DbKind.SqlServer, "ConnectionString"))
using (var transaction = connection.StartTransaction())
{
    //--- for IDbConnection
    connection.Execute("insert into ...", model, transaction);
    
    //--- for IDbTransaction (a little short)
    transaction.Execute("insert into ...", model);
}

Timeout サポート

先のトランザクションの話と同じなのですが、これまではタイムアウトの指定もできませんでした。これもまた、致命的な欠陥でしたね...。

var person = new Person { Name = "anders",  Age= 54 };
connection.Insert(person, 60);  //--- タイムアウト時間を 60 秒に設定

提供しているすべての操作に対してタイムアウト時間を設定できます。

Managed ODP.NET サポート

v0.1 系における Oracle のサポートは Unmanaged な ODP.NET をベースとしたものでした。最近気付いたのですが、これを Managed な形で作り直したがものがリリースされているんですね。しかもインストールも NuGet パッケージでダウンロードするだけ!これまでとは比べものにならないくらい簡単に ODP.NET を導入できるようになりました。

Oracle さんも今後は上記のパッケージを使うことを推奨しているのだと思いますので、Managed な ODP.NET をサポートしました。また、それに合わせて Unmanaged な ODP.NET のサポートを Obsolete としました。

ODP.NET v0.1 v0.2
Unmanaged DbKind.Oracle DbKind.UnmanagedOracle
Managed 未サポート DbKind.Oracle

DbKind.Oracle で対応している ODP.NET が v0.1 と v0.2 で異なります。対応は上記のようになっていますので、v0.1 系で DbKind.Oracle をターゲットにしていた方はご注意ください。

可変長引数 を Obsolete 化

主に Select メソッドと Update メソッドですが、可変長引数 params による列指定を止めました。

//--- 全レコードの ID 列と Name 列を抽出
connection.Select(x => x.Id, x => x.Name);

//--- ID = 1 のレコードの Name 列と Age 列を更新
var person = new Person { Name = "anders", Age = 54 };
connection.Update(person, x => x.Id == 1, x => x.Name, x => x.Age);

当初はコレが最高に書き心地が良いと思っていたのですが、やはり相当に拡張性を殺していました。params 引数はメソッドの末尾にのみ指定できる仕様のため、後々メソッドを拡張しようとしたときに

  • 後ろに引数を追加できない
  • 前に入れると既存コードに破壊的変更を加えてしまう

という至極当然な問題にぶつかりました。v0.1 リリース当初、@hidori さんから「params じゃなくて匿名型でプロパティ指定したい」と言われていたのですが、ここに来て言う事を聞いておくべきだったと反省していますw v0.2 からは以下のように書きます。

//--- 単列指定の場合は匿名型にする必要はない
connection.Select(x => x.Name);

//--- 複数列指定の場合は匿名型で
connection.Select(x => new { x.Id, x.Name });

//--- 列のフィルタと組み合わせるとこんな感じ
var person = new Person { Name = "anders", Age = 54 };
connection.Update(person, x => x.Id == 1, x => new { x.Name, x.Age });

これまでの params 引数による指定は残してあるので、そのままでも動きます。

vNext 計画

次はデータベースからモデルクラスを起こすツールを作りたいなぁと思っています。特に難しくなさそうですし!たくさんのデータベースを相手にするのは結構にめんどくさくてココロが砕かれるので、次またやる気が出れば...w