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

xin9le.net

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

DeclarativeSql

Advent Calendar C# SQL

本記事は C# Advent Calendar 2014 最終日向けの記事です。なんとか間に合わせようと心底努力しましたが、業務上年末にシステムリリースがある関係で忙殺されていて全く余裕がなく、結局最終日に穴を空ける結果になってしまいました。ここまで繋いで下さった関係各位、大変申し訳ありません...。

さて、今回は SQL の生成支援とデータベースへの簡易なアクセスについて取り上げます。というのも、ここしばらく (気が向いたときにダラダラと、かつ結構に長々と) DeclarativeSql というライブラリを作っていたので、その紹介です。特に技術的/パラダイム的に目新しいものではなく、主に自分が業務で楽をしたくて作ったというものですので、期待はそこそこに寛大な心で読まれることをオススメします。

DeclarativeSql の概要

Declarative は「宣言的な」という意味です。宣言的な SQL ってどーゆーことよと思われるかもしれませんが、特に大きな意味はなく、短く理解しやすい記述を目指したんだと思っていただければ結構です。どのような機能があるか、簡単に列挙してみます。

  • SQL Server / Oracle / MySQL などの各種データベースへの統一的な接続
  • テーブルにマッピングするクラスのメタ情報の提供
  • 非常に簡易な SQL 文の生成
  • Dapper をベースとした超カンタンな CRUD アクセス

ソースコードは GitHub で、アセンブリは NuGet で公開しています。是非お試しください。

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

データプロバイダーを利用したデータベースへの接続

プリミティブな方法でのデータベース接続は、以下のような各種データベースへのプロバイダーを利用して行います。

データベース データプロバイダー 不変名 接続クラス
SQL Server SqlClient Data Provider System.Data.SqlClient SqlConnection
Oracle Oracle Data Provider for .NET Oracle.DataAccess.Client OracleConnection
MySQL MySQL Data Provider MySql.Data.MySqlClient MySqlConnection
PostgreSQL Npgsql Data Provider Npgsql NpgsqlConnection
SQLite SQLite Data Provider System.Data.SQLite SQLiteConnection
SQL Server Compact Microsoft SQL Server Compact Data Provider 4.0 System.Data.SqlServerCe.4.0 SqlCeConnection

これらが実装されている DLL を直接参照設定に追加して利用しても良いのですが、追加せずに動的にアセンブリを読み込む方法もあります。その方法だと各種データベース専用のプロバイダーにコードが依存しないため、ひとつのコードベースで複数のデータベースに対する接続を動的に行うことができるようになります。詳細は MSDN の記事を参照していただくことにして割愛しますが、具体的な実装は以下のように machine.config (もしくは app.config や web.config) と DbProviderFactory を利用します。

<system.data>
    <DbProviderFactories>
        <add name="SqlClient Data Provider"
             invariant="System.Data.SqlClient"
             description=".Net Framework Data Provider for SqlServer"
             type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </DbProviderFactories>
</system.data>
var factory = DbProviderFactories.GetFactory("System.Data.SqlClient");
using (var connection = factory.CreateConnection())
{
    connection.ConnectionString = "接続文字列";
    connection.Open();
    //--- 何かデータベース操作
}

見てお分かりの通り、データベースの切り分けを文字列ベースで (invariant 属性を参照することで) 行っています。この文字列はデータプロバイダーのインストール時に基本的には決まっているので、毎回書くのは非常にナンセンスです。なのでそれを DeclarativeSql では enum ベースにして共通化しました。

using (var connection = DbProvider.CreateConnection(DbKind.SqlServer, "接続文字列"))
{
    connection.Open();
    //--- 何かデータベース操作
}

たったこれだけですがかなり安心感が増しますし、どの種類のデータベースに繋いでいるのかが明快になります。あと、ほんの少しだけですが接続文字列の設定も楽になります。

テーブルマッピングクラスとメタデータ

Dapper のような Micro ORM を利用してテーブルをマッピングする場合、基本的にはプロパティ名と列名を一致させるクラスを作ると思います。それだけでも全然問題ないのですが、以下のように Entity Framework ライクに属性を付けることでもう少し付加価値を付けることができます。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DeclarativeSql.Annotations;

namespace SampleApp
{
    [Table("Person", Schema = "dbo")]  //--- テーブル名の指定
    public class Person
    {
        [Key]  //--- 主キー
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]  //--- 自動採番
        public int Id { get; set; }

        [Required]        //--- NotNull
        [Column("名前")]  //--- 列名の指定
        public string Name { get; set; }

        [Required]
        [Sequence("AgeSeq", Schema = "dbo")]  //--- シーケンスの利用 (Oracleなどで)
        public int Age { get; set; }

        [NotMapped]  //--- マッピングしない
        public int Sex { get; set; }
    }
}

DeclarativeSql はこの属性を参照してメタデータを提供します。TableMappingInfo.Create<T>() メソッドを起点として、以下のような値を取得できます。

public sealed class TableMappingInfo
{
    public Type Type { get; }      //--- マッピングするクラスの型情報
    public string Schema { get; }  //--- テーブルのスキーマ名
    public string Name { get; }    //--- テーブル名
    public IReadOnlyList<ColumnMappingInfo> Columns { get; }  //--- 列マッピング情報
}

public sealed class ColumnMappingInfo
{
    public string PropertyName { get; }  //--- プロパティ名
    public Type PropertyType { get; }    //--- プロパティのデータ型
    public string ColumnName { get; }    //--- 列名
    public DbType ColumnType { get; }    //--- 列のデータ型
    public bool IsPrimaryKey { get; }    //--- 主キーかどうか
    public bool IsNullable { get; }      //--- NULL許可かどうか
    public bool IsIdentity { get; }      //--- 自動採番をするどうか
    public SequenceMappingInfo Sequence { get; }  //--- 設定されているシーケンス情報
}

public sealed class SequenceMappingInfo
{
    public string Schema { get; }  //--- シーケンスのスキーマ名
    public string Name { get; }    //--- シーケンス名
}

このメタデータはライブラリ内部で型情報と紐付けてキャッシュしているため、初回以降は高速に取得することができます。また、テーブル名や列名は属性が指定されていなくてもクラス名やプロパティ名が設定されます。

プリミティブな SQL の自動生成

先のメタデータを利用して定型の SQL を自動生成する機能も提供しています。PrimitiveSql クラスを利用して以下のように行います。

//--- 指定の列のみを対象として全レコード取得
var sql = PrimitiveSql.CreateSelect<Person>(x => x.Id, x => x.Name);

/*
select
    Id as Id,
    名前 as Name
from dbo.Person
*/
//--- SQL Serverに対してレコード挿入
var sql = PrimitiveSql.CreateInsert<Person>(DbKind.SqlServer);

/*
insert into dbo.Person
(
    名前,
    Age
)
values
(
    @Name,
    next value for dbo.SampleSeq
)
*/
//--- 指定の列のみを対象として全レコードを更新
var sql = PrimitiveSql.CreateUpdate<Person>(DbKind.SqlServer, x => x.Name);

/*
update dbo.Person
set
    名前 = @Name
*/

この他にもいくつかのオーバーロードや、Delete / Count / Truncate などの機能も提供します。

超カンタン CRUD 操作

DeclarativeSql では、先の SQL 自動生成機能と Dapper を利用して定型的な CRUD 操作を完全に自動化します。式木を使ったタイプセーフな列指定だけでなく、一部の二項演算子 (==, !=, <, >, <=, >=) を利用したレコードのフィルターにも対応しています。実際のところ、コレがしたくてこのライブラリを作ったようなものです。

using (var connection = DbProvider.CreateConnection(DbKind.SqlServer, "接続文字列"))
{
    connection.Open();

    var p1 = connection.Select<Person>();                                        //--- 全件取得
    var p2 = connection.Select<Person>(x => x.Id, x => x.Name);                  //--- 指定した列に絞って全件取得
    var p3 = connection.Select<Person>(x => x.Id == 3);                          //--- ID = 3 のレコードのみ取得
    var p4 = connection.Select<Person>(x => x.Id == 3, x => x.Id, x => x.Name);  //--- ID = 3 のレコードを指定の列に絞って取得

    var p5 = connection.Insert(new Person { Name = "xin9le", Age = 30 });  //--- 指定されたデータを挿入
    var p6 = connection.Insert(new[]  //--- 複数のレコードでもOK
    {
        new Person { Name = "yoshiki", Age= 49, },
        new Person { Name = "suzuki",  Age= 30, },
        new Person { Name = "anders",  Age= 54, },
    });

    var p7 = connection.Update(new Person  //--- 条件に一致するレコードを更新
    {
        Name = "test",
        Age = 23
    }, x => x.Age == 30);

    var p8  = connection.Delete<Person>();                  //--- 全レコード削除
    var p9  = connection.Delete<Person>(x => x.Age != 30);  //--- 条件に一致するレコードを削除

    var p10 = connection.Truncate<Person>();  //--- テーブルの切り捨て

    var p11 = connection.Count<Person>();                       //--- レコード数を取得
    var p12 = connection.Count<Person>(x => x.Name == "test");  //--- 条件に一致するレコードの数を取得
}

メソッド名はとても直感的で分かりやすいですし、機能と書き心地を両立した落とし所なんじゃないかなーと思います。もちろん非同期バージョンも提供しています

作ってみた感想

ここまででき上がってみると、「それ何ていう Entity Framework?」と思ったりもしますが、Micro-ORM (Dapper) を利用して開発をしている自分にとってはとても楽になるソリューションになりました。簡単な SQL を何度も書くのはイヤだし、機能過多なフレームワークもちょっと採用するか悩む。SQL を書くのが得意なメンバーに囲まれているのであれば、複雑だったりパフォーマンスが求められる SQL だけは手書きすれば良いやって思い切り割りきるのも大事かな、と。そんな気持ちで作りました。

今後のタスクと目標

手元に SQL Server しかないこともあり、動作確認は SQL Server でしか行ってないのが致命的な問題。近いうちに Oracle や MySQL、SQLite などのメジャーどころでの動作確認をしたいなと思っております。もしそれらのデータベースで「動かない」などの問題があれば @xin9le までお知らせください。あとは、データベースからテーブル情報を抽出して自動で C# のコードを生成する機能とかあると便利かな、なんてボンヤリ思っています。とはいえ、そこまでやったら本当に Entity Framework になってしまう気がしますが...。

ということで年末ギリギリになってしまいましたが、以上 DeclarativeSql の紹介でした!