Micro-ORM Dapper の使い方
今回は Micro-ORM の Dapper を紹介するエントリです。Dapper の使用方法だけでなく、Micro-ORM とは?についても簡単に触れています。
尚、このエントリは Visual Studio 2022 Community Edition で .NET 6 以降 と C# を使用するエントリなので、C# での基本的なコーディング知識を持っている人が対象です。
目次
Dapper については日本語での解説ページも多く見つかりますし、以前、このサイトでも WPF MVVM L@bo の #5『ようこそ Dapper 至上主義の DataAccess へ』と言う記事で紹介しています。
ただ、以前の記事は連載の中の 1 エントリとして書いたので、Dapper 以外の要素も多く含まれていて、Dapper の使い方を見たいような場合等には不向きな記事だったと思います。
その反省を込めて、今回は Dapper 単体を紹介するエントリとしてリライトしました。
前提
このエントリでは Dapper の使い方を紹介しますが、DB には SQLite を使用しています。このエントリで紹介しているサンプル用 DB ファイルの中身を確認したい場合等には SQLite の環境が必要になるので、別エントリの【SQLite とメンテナンスツール】で紹介しているツール類をインストールしていただく方が良いと思います。
又、サンプルの動作には SQLite 用の ADO.NET プロパイダも必要になりますが、このエントリでは紹介していないので、別エントリの【SQLite の NuGet パッケージ】を参考に NuGet パッケージをインストールしてください。
ただ、このエントリで紹介するサンプルコードは GitHub に上げているので、それを実行するだけであれば、クローンして VS 2022 で開くと NuGet パッケージは復元されるので、自分でインストールする必要はありません。
又、このエントリのサンプルは SQLite データベースを対象にしていますが、Oracle や SQL Server、MySQL 等を使用する場合でもほとんど同じコードで動作します。但し、SQL 自体の方言はあるので、SQL 文だけは多少の修正が必要になりますが、SQLite 固有の方法はほとんど紹介していません。
SQLite 固有の機能を紹介する際には、あらかじめ宣言して紹介しています。
ADO.NET を使用した DB Access
.NET 6 をターゲットにしたプロジェクトから DB にアクセスするには一般的に src. 1 のようなコードになると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | using System; using System.Data; using System.Data.SQLite; using System.Text; using System.Windows; private async void btnAdo_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" * "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.KANA LIKE @kana "); sql.AppendLine(" AND CHR.ID <= @id "); var buf = new StringBuilder(); await con.OpenAsync(); await using (var cmd = con.CreateCommand()) { cmd.CommandText = sql.ToString(); cmd.CommandType = CommandType.Text; cmd.Parameters.AddWithValue("kana", "%くろさき%"); cmd.Parameters.AddWithValue("id", 5); await using (var reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { buf.AppendLine(reader.GetInt32("ID").ToString()); buf.AppendLine(reader.GetString("CHARACTER_NAME")); buf.AppendLine(reader.GetString("KANA")); break; } } } this.txtResult.Text = buf.ToString(); } } |
src. 1 は一応 async/await 等も使用していますが、MVVM ではなく .NET 6 の WPF コードビハインドから DB にアクセスしています。コードビハインドに書く良し悪しは置いておいて、サンプルなので極力シンプルで分かりやすい構造にしています。
src. 1 ではイベントハンドラに直接 DB を呼び出すコードを書いているので、View に何かを返すようにはなっていませんが、別エントリの『DB が見えるのは嫌なので 3 階層 に AbstractFactory したいと思います。【#4 WPF MVVM L@bo】』で紹介しているような階層構造で設計されたシステムでは DataAccess 層から View にクラスを渡すようになっている事がほとんどだと思います。
DB から取得した値をクラスに乗せ換えるには退屈で決まりきったコードを大量に書く事を強いられますが、これはクラスと言う言語仕様と、大量データを効率よく処理するためのデータベースと言う構造の違いが原因で、この構造の違いから発生する問題はインピーダンスミスマッチと呼ばれています。
インピーダンスミスマッチ
アプリケーションデータの永続化には DB を使用する事が増えてきている(業務系では昔からほぼ DB 一択でしたが…)と思いますが、DB のテーブル定義と画面等で使用するクラスの構造が一致する事はマスタ系を除くと稀だと思います。これはそれぞれの特性の違いで、以下のような違いがあります。
- DB
- アプリケーションの永続層で使用するる
- 大量のデータを保持して、その中から必要なデータを検索したり登録する目的で使用する
- SQL を使用して操作する
- オブジェクト指向言語
- View やアプリケーション層で使用する
- 現実のデータモデルに合わせた構造で設計される場合が多い
- .NET C# や Java 等のオブジェクト指向言語で作成する
このような構造の違いから発生する困難を指してインピーダンスミスマッチと呼ばれています。インピーダンスミスマッチとは元々電気工学で使用されていた言葉のようですが、詳細を書ける程理解していないので岩永信之さんの【[雑記] O/R インピーダンスミスマッチ – C# によるプログラミング入門 | ++C++; // 未確認飛行 C】を読んでください。
インピーダンスミスマッチとは何かを知らなくても、.NET ⇔ DB 間でデータをやり取りするようなコードを書いた経験があれば何となくイメージできると思っています。このようなインピーダンスミスマッチを解消する手段の 1 つとして O/R マッパーと呼ばれる仕組みがあります。
O/R マッパーとは
O/R マッパー(以下、ORM)とは、オブジェクト関係マッピング(Object-relational mapping)を意味します。これは DB と、C# や Java 等のオブジェクト指向プログラミング言語の間でデータを変換するための機能やツールを指し、以下のような機能を持つツールを指す事が多いようです。
- データベースの定義を自動的に作成・管理する機能マイグレーション
- SQLを内部で生成するクエリビルダ
- 接続やコマンド操作等の DB 操作のラッパー
- 抽出結果をオブジェクトにマッピングするマッパー
※ プロダクト毎の実装に依るので全てがサポートされている訳では無い
.NET プラットフォームで上記の機能が揃っているパッケージとしては Entity Framework(以下、EF)や NHibernate 等がありますが、管理人はどちらも使った事がありません。理由としては以下のような事情があります。
- ORM を導入しているプロジェクトに巡り合わない
- 自作アプリへの導入は手順が多くて面倒そう
- 管理人はそれなりに SQL には慣れているのでつい、SQL で設計してしまう
- 『ORM(EF)は遅い』と言うような情報を見かける事が多い
何だか使わないための言い訳を並べたような感じですが、やはり『遅い』と言うのは躊躇する理由の中で 1 番大きなウェイトを占めていると思います。
EF を使用するとクラス構造から DB のテーブルを作成したり、既存の DB 構造からクラスを生成することもできるようですが、100 ~ 200 行は当たり前の SQL を見かける事も多い、業務系システムの現場では既存の SQL と同程度の速度が保証できるかはかなり重要で気になる点です。
日本の業務系システムはホスト由来のテーブル定義を正規化もせずそのまま流用しているケースも多く、複雑怪奇な SQL でデータを取得する事も多く見かけるので、ORM の導入がなかなか進まない事は理解できなくもありません。ただ、それでもアプリケーション層や画面(View)では DB を意識せず、永続化層から取得したデータはクラスに入れて受け取りたいと言う要望も当然多く【抽出結果をオブジェクトにマッピングするマッパー】に特化した(これもプロダクトの実装に依りますが…) Micro-ORM と言うジャンルのライブラリが出て来たようです。
Micro-ORM Dapper
前章で『Micro-ORM と言うジャンル』と書きましたが、日本語で書かれた .NET プラットフォームの Micro-ORM を探してもほとんど Dapper しか見つからないのでここで取り上げるのも Dapper です。
Dapper を扱っている記事も多いので見た事がある人も多いと思いますが、Dapper は 2015 年時点で 57 億 PVあった Stack Overflow の中の人が開発して、現在(2022/8 月現在)でも Stack Overflow で使用されている ORM です。
Stack Overflow で Dapper を開発することになった経緯は『データ ポイント – Dapper、Entity Framework、およびハイブリッド アプリ』で語られています。
長々と経緯を紹介してきましたが、以降から本題の Dapper の紹介に入ります。
Dapper でできる事
Micro-ORM と言うジャンル名の通り、Dapper は ORM の一部機能のみをサポートしていて、対応している機能は以下の通りです。
- 接続やコマンド操作等の DB 操作のラッパー
- 抽出結果をオブジェクトにマッピングするマッパー
※ 上記は Dapper 単体でサポートする機能です(ヘルパライブラリ等を組み合わせると ORM に近い操作も可能なようです)
Dapper は IDbConnection の拡張メソッド群として定義されているので、IDbConnection のインスタンスさえあれば DB 接続 → クエリの実行 → オブジェクトへのマッピングまで全て行ってくれます。そして、Dapper は SQL を直接 DB に渡すので、クエリの実行速度は自分で書いた SQL に依存します。
具体的な使用方法は後述します。
Dapper のインストール
Dapper は NuGet からインストールできるので、fig. 1 のように『ソリューションの NuGet パッケージの管理』で【dapper】を検索します。
赤枠で囲んでいるパッケージをインストールすれば Dapper を使用できるようになります。
Dapper のサポート DB とパフォーマンス
Dapper の GitHub リポジトリ には【Dapper には DB 固有の実装は含まれていない】と書かれていて『SQLite, SQL CE, Firebird, Oracle, MySQL, PostgreSQL、SQL Server を含む全ての ADO.NET プロパイダで動作する』とも書かれてます。
つまり、ADO.NET のプロパイダが用意されている DB であれば動作すると考えて良いはずで、最悪 ODBC があれば動作すると思われます。
そして、パフォーマンスについては、Dapper の GitHub リポジトリ 【Performance】の章に書かれている通り、ADO.NET から DataTable にマッピングした場合とほとんど変わらないベンチマーク値を叩き出しています。
上で紹介したベンチマーク比較は、ADO.NET:『Hand Coded』。Dapper:『Dapper – QueryFirstOrDefault<T>』辺りの【Mean】を比較すると、ほとんど速度差が無い事が読み取れると思います。(つまり ADO.NET で実行した場合と変わらないパフォーマンスが予測できる)
Dapper の使い方
ここからは実際に Dapper を使用したサンプルソースを紹介します。Dapper の主な機能は DB のレコードと .NET のオブジェクト(クラス)のマッピングなので、必然的に SQL で SELECT する場合を重点的に紹介します。
以降は『BLEACH』のキャラクター情報を登録した SQLite の DB ファイルを使用します。ファイル自体は GitHub リポジトリ に上げています。
本章では src. 2 の CHARACTERS テーブルを使用します。
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE CHARACTERS ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CHARACTER_NAME TEXT NOT NULL, KANA TEXT, BIRTHDAY TEXT, PROBABLE_AGE INTEGER, SEX INTEGER NOT NULL, AFFILIATION INTEGER NOT NULL, ZANPAKUTOU INTEGER ); |
CHARACTERS テーブルには BLEACH のキャラクターデータを 40 人程度登録済みです。
dynamic 型にマッピング
src. 3 は DB から取得したレコードを dynamic 型にマッピングする例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | using System; using System.Collections.Generic; using System.Data.SQLite; using System.Linq; using System.Text; using System.Windows; using Dapper; namespace DapperSample { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { /// <summary>接続文字列ビルダ</summary> private readonly SQLiteConnectionStringBuilder connectionStringBuilder; /// <summary>コンストラクタ。</summary> public MainWindow() { InitializeComponent(); var dbPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleDb.db"); this.connectionStringBuilder = new SQLiteConnectionStringBuilder() { DataSource = dbPath }; } /// <summary>dynamic型で取得ボタンのClickイベントハンドラ。</summary> /// <param name="sender">イベントのソース。</param> /// <param name="e">イベントデータを表すRoutedEventArgs。</param> private async void btnDynamic_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" * "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.ID <= @Id "); var characters = await con.QueryAsync(sql.ToString(), new { Id = 5 }); var buf = new StringBuilder(); buf.AppendLine(this.getColumnNames<dynamic>(characters, 3, "\t")); foreach (var item in characters) { buf.AppendLine(item.ID + "\t" + item.CHARACTER_NAME + "\t" + item.KANA); } this.txtResult.Text = buf.ToString(); } } /// <summary>Dapperのマッピング結果からカラム名の一覧をから取得します。</summary> /// <typeparam name="T">Dapperでマッピングしたクラスの型を表します。</typeparam> /// <param name="values">>Dapperの取得結果を表すIEnumerable<T>。</param> /// <param name="columnCount">取得するカラムの数を表すint。</param> /// <param name="separator">各カラム名を区切る文字列。</param> /// <returns></returns> private string getColumnNames<T>(IEnumerable<T> values, int columnCount, string separator) { var columns = values.OfType<IDictionary<string, object>>().First().Select(x => x.Key).Take(columnCount); return string.Join(separator, columns); } } } |
このエントリでは Dapper の紹介が目的なので、WPF と言っても他のエントリのように MVVM ではなくコードビハインドに書いていて、実行結果は fig. 3 です。
src. 3 の中で Dapper を呼び出しているのは 44 行目のみで、Dapper の Query 拡張メソッドを使用しています。Dapper には他にも【Query】から始まる以下の 5 種類の拡張メソッドが用意されていて、取得結果によって挙動が変わります。
拡張メソッド名 | 結果 0 件 | 結果 1 件 | 結果複数 |
---|---|---|---|
Query | IEnumerable<T> | IEnumerable<T> | IEnumerable<T> |
QueryFirst | Exception | 取得オブジェクト | 先頭オブジェクト |
QuerySingle | Exception | 取得オブジェクト | Exception |
QueryFirstOrDefault | Default | 取得オブジェクト | 先頭オブジェクト |
QuerySingleOrDefault | Default | 取得オブジェクト | Exception |
上の 5 種類に加えて、src. 3 でも使用している、メソッド名に【Async】が付いた非同期版もあり、引数が異なるオーバーロードがいくつも定義されています。そしてそれぞれジェネリック版も用意されているので、数は更に多くなります。Query メソッドは、fig. 4 のように Dapper 内部で IEnumerable<dynamic(匿名クラス)> が自動で作成されるようなイメージです。
返される匿名クラスは fig. 4 のように【プロパティ名 = SELECT に指定したカラム名】になります。(プロパティ名は CREATE TABLE 時に指定した大文字・小文字の違いも含むカラム名そのまま)このように dynamic 型(匿名クラス)で受け取る場合、別途クラスを定義する必要が無いので、マッピングされたデータを手軽に取得する事が出来ます。但し、当然ですが、匿名クラスのメンバは IntelliSense が効かないと言うデメリットはあります。
バインド変数(プリペアードステートメント)へのマッピング
Dapper のもう 1 つの大きな利点は、src. 3 の 44 行目の第 2 パラメータのように dynamic 型をバインド変数にマッピングできる事です。ADO.NET でバインド変数に値をセットするには src. 4 のように DbCommand.Parameters.AddWithValue ~
等をバインド変数の数だけ指定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | private async void btnAdo_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" * "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.KANA LIKE @kana "); sql.AppendLine(" AND CHR.ID <= @id "); await con.OpenAsync(); await using (var cmd = con.CreateCommand()) { cmd.CommandText = sql.ToString(); cmd.CommandType = CommandType.Text; cmd.Parameters.AddWithValue("kana", "%くろさき%"); cmd.Parameters.AddWithValue("id", 5); await using (var reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { buf.AppendLine(reader.GetInt32("ID").ToString()); buf.AppendLine(reader.GetString("CHARACTER_NAME")); buf.AppendLine(reader.GetString("KANA")); break; } } } this.txtResult.Text = buf.ToString(); } } |
Dapper を使用すると src. 5 のように書けるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using Dapper; private async void btnAdo_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" * "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.KANA LIKE @kana "); sql.AppendLine(" AND CHR.ID <= @id "); var character = await con.QueryFirstOrDefaultAsync(sql.ToString(), new { kana = "%くろさき%", id = 5 }); var buf = new StringBuilder(); buf.AppendLine(character.ID.ToString()); buf.AppendLine(character.CHARACTER_NAME); buf.AppendLine(character.KANA); this.txtResult.Text = buf.ToString(); } } |
src. 5 のように SQL 文内に指定したバインド変数と同名のプロパティを持つ匿名クラスを渡すだけで、バインド変数にマッピングしてくれるので非常に便利です。バインド変数マッピング用のクラスをわざわざ作成する必要も無いので、匿名クラスは結果のマッピングより、バインド変数へのマッピングの方が頻度が高くなると思います。
但し、SQLite + Dapper でバインド変数にマッピングする場合には 1 点注意があります。
【SQLite 公式サイトの Binding Values To Prepared Statements】にはバインド変数に指定可能な文字として『?』『:』『@』『$』の 4 つが書かれています。
src. 4 の ADO.NET で試すと、確かに上記 4 つはいずれも実行可能でしたが、Dapper を使用した場合、以下の 2 つ以外は実行時に例外がスローされました。
- :
- @
SQLite から Dapper 経由で プリペアードステートメントを使用する場合の記号は上の 2 つのどちらかに統一して SQL を記述する必要があります。
IN 句のバインド変数にマッピング
ADO.NET でバインド変数に値を設定する場合、最も面倒なのは【IN 句】だと思いますが、Dapper を使用すると src. 6 のように List を渡すだけでマッピングできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using Dapper; private async void btnInMapping_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" * "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.ID IN @id "); var characters = await con.QueryAsync(sql.ToString(), new { id = new List<int>() { 1, 2, 3, 6, 9, 34, 36, 37 } }); var buf = new StringBuilder(); buf.AppendLine($"ID\tCHARACTER_NAME\tKANA"); foreach (var item in characters) { buf.AppendLine($"{item.ID}\t{item.CHARACTER_NAME}\t{item.KANA}"); } this.txtResult.Text = buf.ToString(); } } |
fig. 5 が src. 6 の実行結果です。
List を渡すだけで済むので非常に簡単ですが、1 点だけ注意があります。【IN 句に指定するバインド変数は()で括らない】と言う点です。11 行目の【@id】を【(@id)】と書いてしてしまうとSQL 解析エラーになるので、その点だけは注意が必要です。
カスタムクラスにマッピング
ここまでは、dynamic 型の匿名クラスにマッピングする方法を紹介してきましたが、クエリの結果はアプリケーション層や画面等で使用するため、独自で作成したクラスにマッピングする事が目的だと思います。
当然、Dapper は dynamic 型だけでなく独自に作成したクラスにもマッピングできますが、【クエリで指定したカラム名に一致したメンバにのみマッピングする】と言うルールになっているので覚えておいてください。(つまり、クラスメンバ名に一致しない値はスルーされる)
DB のカラム名と一致したメンバ名を持つクラスにマッピング
例えば、src. 7 15 行目のようなクラスを作成したとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System; namespace DapperSample { public enum Sex { 設定なし = 0, 男 = 1, 女 = 2 } } namespace DapperSample.Entities { public class Character_ColName { public int ID { get; set; } = 0; public string CHARACTER_NAME { get; set; } = string.Empty; public string KANA { get; set; } = string.Empty; public DateTime? BIRTHDAY { get; set; } = null; public Sex Sex { get; set; } = Sex.NotSet; } } |
src. 7 のようなクラスを作成すると、src. 8 のように SELECT * FROM
でクエリを実行してもマッピングできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using Dapper; using DapperSample.Entities; private async void btnColName_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" * "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.KANA LIKE @kana "); sql.AppendLine(" AND CHR.ID <= @id "); var characters = await con.QueryAsync<Character_ColName>(sql.ToString(), new { kana = "%くろさき%", id = 10 }); var buf = new StringBuilder(); buf.AppendLine($"ID\tCHARACTER_NAME\tKANA\tSEX\tBIRTHDAY"); foreach (var item in characters) { buf.AppendLine($"{item.ID}\t{item.CHARACTER_NAME}\t{item.KANA}\t{item.Sex.ToString()}\t{item.BIRTHDAY?.ToString("yyyy/MM/dd")}"); } this.txtResult.Text = buf.ToString(); } } |
fig. 6 は src. 8 の実行結果です。(誕生年は適当なのでスルーしてくださいw)
実行結果もソースコードも匿名クラスにマッピングした src. 5 とほとんど変わりませんが、メンバ名が全て大文字のクラスは気持ち悪いですね…
余談ですが、src. 7 の 5 ~ 10 行目のような Enum を定義していれば、fig. 6 のようにちゃんと Enum 型として値がセットされます。当然ですが、fig. 6 のように Enum の値に変換されるのは、DB の値が Enum の範囲内の場合のみです。(試しに DB 側を SEX = 3 に変更して実行すると fig. 6 には『3』が表示されました)
一般的なメンバ名を持つクラスにマッピング
前項では DB のカラム名と同じメンバ名のクラスを作成しましたが、通常は src. 9 15 行目のようなクラスにする事が多いと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System; namespace DapperSample { public enum Sex { 設定なし = 0, 男 = 1, 女 = 2 } } namespace DapperSample.Entities { public class Character_Normal { public int Id { get; set; } = 0; public string Name { get; set; } = string.Empty; public string Kana { get; set; } = string.Empty; public DateTime? Birthday { get; set; } = null; public Sex Sex { get; set; } = Sex.設定なし; } } |
src. 9 のクラスを指定すると src. 8 のままでは Dapper はマッピングできないので、src. 10 のように SQL 側を変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | using Dapper; using DapperSample.Entities; private async void btnNormal_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" CHR.ID AS Id "); sql.AppendLine(" , CHR.CHARACTER_NAME AS Name "); sql.AppendLine(" , CHR.KANA AS Kana "); sql.AppendLine(" , CHR.BIRTHDAY AS Birthday "); sql.AppendLine(" , CHR.SEX AS Sex "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.ZANPAKUTOU IS NOT NULL "); sql.AppendLine(" AND CHR.KANA LIKE @kana "); sql.AppendLine(" AND CHR.ID <= @id "); var characters = await con.QueryAsync<Character_Normal>(sql.ToString(), new { kana = "%くろさき%", id = 10 }); var buf = new StringBuilder(); buf.AppendLine($"ID\tCHARACTER_NAME\tKANA\tSEX\tBIRTHDAY"); foreach (var item in characters) { buf.AppendLine($"{item.Id}\t{item.Name}\t{item.Kana}\t{item.Sex.ToString()}\t{item.Birthday?.ToString("yyyy/MM/dd")}"); } this.txtResult.Text = buf.ToString(); } } |
src. 10 のように SELECT のカラムリストで【AS 句】にクラスのメンバ名を指定すれば、名前が一致するプロパティにマッピングしてくれます。
又、以下ような条件に一致する場合は【AS 句】を指定しないでマッピングする方法もあります。
- DB カラム名:URIAGE_DENPYO
プロパティ名:UriageDenpyo - DB カラム名:uriage_denpyo
プロパティ名:UriageDenpyo
上記は【DB のカラム名を一定の法則に従って変換すればプロパティ名に一致する】場合を意味します。処理のほとんどで上記のような条件に一致する環境であれば Dapper の CustomPropertyTypeMap クラスに変換ルールを設定する事でマッピングできるようですが、管理人が本業としている業務系界隈ではそう言う DB 設計をあまり見かけないのでここでは紹介しません。CustomPropertyTypeMap の使用方法については『neue cc – Micro-ORMとC#(とDapperカスタマイズ)』で紹介されているので、そちらを見てください。
自分でも試す機会があれば又紹介しようとは思っています。
JOIN した SELECT 結果のマッピング
以降は、src. 2 で紹介した CHARACTERS テーブル以外に src. 11 のテーブルも使用します。
1 2 3 4 5 6 7 8 9 10 11 12 | -- 所属テーブル CREATE TABLE AFFILIATION ( ID INTEGER PRIMARY KEY AUTOINCREMENT, AFFILIATION_NAME TEXT NOT NULL ); -- 斬魄刀テーブル CREATE TABLE ZANPAKUTOU ( ID INTEGER PRIMARY KEY AUTOINCREMENT, ZANPAKUTOU_NAME TEXT NOT NULL, BANKAI_NAME TEXT ); |
src. 11 に加えて、所属テーブルと斬魄刀テーブルをマッピングするクラスを src. 12 のように追加して、src. 9 で紹介した Character_Normal クラスも変更しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | using System; namespace DapperSample { public enum Sex { 設定なし = 0, 男 = 1, 女 = 2 } } namespace DapperSample.Entities { /// <summary>BLEACHのキャラクターを表します。</summary> public class Character_Normal { public int Id { get; set; } = 0; public string Name { get; set; } = string.Empty; public string Kana { get; set; } = string.Empty; public DateTime? Birthday { get; set; } = null; public Sex Sex { get; set; } = Sex.設定なし; public Affiliation Affiliation { get; set; } = new Affiliation(); public Zanpakuto? Zanpakuto { get; set; } = null; } /// <summary>キャラクターの所属を表します。</summary> public class Affiliation { public int Id { get; set; } = 0; public string Name { get; set; } = string.Empty; } /// <summary>斬魄刀を表します。</summary> public class Zanpakuto { public string Name { get; set; } = string.Empty; public string Bankai { get; set; } = string.Empty; } } |
1 対 1 の関係になる SELECT 結果のマッピング
Dapper で src. 12 のような 1 対 1 の関係で入れ子になった Character_Normal クラスにマッピングするには src. 13 のように指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | using Dapper; using DapperSample.Entities; private async void btnJoinOne_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" CHR.ID AS Id "); sql.AppendLine(" , CHR.CHARACTER_NAME AS Name "); sql.AppendLine(" , CHR.KANA AS Kana "); sql.AppendLine(" , CHR.SEX AS Sex "); sql.AppendLine(" , AFL.ID AS Id "); sql.AppendLine(" , AFL.AFFILIATION_NAME AS Name "); sql.AppendLine(" , ZPT.ZANPAKUTOU_NAME AS Name "); sql.AppendLine(" , ZPT.BANKAI_NAME AS Bankai "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); sql.AppendLine(" INNER JOIN AFFILIATION AFL ON "); sql.AppendLine(" CHR.AFFILIATION = AFL.ID "); sql.AppendLine(" LEFT JOIN ZANPAKUTOU ZPT ON "); sql.AppendLine(" CHR.ZANPAKUTOU = ZPT.ID "); sql.AppendLine(" WHERE "); sql.AppendLine(" CHR.AFFILIATION = @id "); sql.AppendLine(" ORDER BY "); sql.AppendLine(" CHR.KANA; "); var characters = await con.QueryAsync<Character_Normal, Affiliation, Zanpakuto, Character_Normal>(sql.ToString(), (chara, aff, zpt) => { chara.Affiliation = aff; chara.Zanpakuto = zpt; return chara; }, new { id = 15 }, splitOn:"Id,Id,Name"); ~ 以下略 ~ } } |
実行すると fig. 7 のように想定通りマッピングされます。
Dapper の Query ~ で始まるメソッドは 7 個までのクラスにマッピングできるオーバーロードも用意されていて、31 ~ 34 行目のように、ラムダ式内でマッピング先を指定します。
そして、src. 13 のような SELECT を書いた場合、肝となるのは 37 行目の【splitOn 名前付きパラメータ】です。splitOn パラメータは fig. 8 のようにクラスの切れ目になるカラム名を【,(カンマ)】で区切って指定します。
結構意地悪なカラム名のエイリアス(AS 句)を設定してみましたが、想定通りにマッピングされたクラスが 29 行目のラムダ式に渡されます。ラムダ式内でパラメータに渡されたクラスをプロパティに設定して結果を返すと、マッピングされた IEnumerable<Character_Normal> が返る流れです。
1 対多の関係になる SELECT 結果のマッピング
次は 1 対多の関係になる SELECT 結果をマッピングする方法を紹介するので、src. 12 で紹介した Affiliation クラスを src. 14 のように変更しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System.Collections.Generic; namespace DapperSample.Entities { /// <summary>キャラクターの所属を表します。</summary> public class Affiliation { public int Id { get; set; } = 0; public string Name { get; set; } = string.Empty; public List<Character_Normal> Characters => this._characters; private readonly List<Character_Normal> _characters = new List<Character_Normal>(); } } |
src. 14 のように【多】にあたる値を設定するための List 型プロパティを追加します。1 対多にマッピングする場合も 1 対 1 でマッピングする src. 13 と変わりませんが、src. 15 のように多少固有の処理が必要になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | using Dapper; using DapperSample.Entities; private async void btnJoinMany_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" AFL.ID AS Id "); sql.AppendLine(" , AFL.AFFILIATION_NAME AS Name "); sql.AppendLine(" , CHR.ID AS Id "); sql.AppendLine(" , CHR.CHARACTER_NAME AS Name "); sql.AppendLine(" , CHR.SEX AS Sex "); sql.AppendLine(" , ZPT.ZANPAKUTOU_NAME AS Name "); sql.AppendLine(" , ZPT.BANKAI_NAME AS Bankai "); sql.AppendLine(" FROM "); sql.AppendLine(" AFFILIATION AFL "); sql.AppendLine(" INNER JOIN CHARACTERS CHR ON "); sql.AppendLine(" AFL.ID = CHR.AFFILIATION "); sql.AppendLine(" LEFT JOIN ZANPAKUTOU ZPT ON "); sql.AppendLine(" CHR.ZANPAKUTOU = ZPT.ID "); sql.AppendLine(" ORDER BY "); sql.AppendLine(" AFL.ID "); sql.AppendLine(" , CHR.ID "); sql.AppendLine(" LIMIT 10 "); var affilicationDic = new Dictionary<int, Affiliation>(); var characters = await con.QueryAsync<Affiliation, Character_Normal, Zanpakuto, Affiliation>(sql.ToString(), (aff, chara, zpt) => { Affiliation? tempAff = null; if (!affilicationDic.TryGetValue(aff.Id, out tempAff)) { tempAff = aff; affilicationDic.Add(aff.Id, tempAff); } chara.Zanpakuto = zpt; tempAff.Characters.Add(chara); return tempAff; }, splitOn: "Id,Id,Name"); var buf = new StringBuilder(); buf.AppendLine($"ID\t所属\tキャラクター名\tKANA\tSEX\t斬魄刀\t卍解"); foreach (var item in affilicationDic.Values) { buf.AppendLine(item.Id + "\t" + item.Name); item.Characters.ForEach(c => buf.AppendLine($"\t\t{c.Name}\t{c.Kana}\t{c.Sex}\t{c.Zanpakuto?.Name}\t{c.Zanpakuto?.Bankai}")); } this.txtResult.Text = buf.ToString(); } } |
src. 15 は【Multi Mapping Result in Dapper Tutorial】で紹介されている方法そのままです。
src. 15 の SELECT はいわゆる【ヘッダ – 明細形式】で取得していますが、Dapper は今までと同様にレコード単位でマッピング処理を実行します。そのため、fig. 9 のようにヘッダ部のクラスは取得したレコード数分生成されます。
サンプル DB のレコード数は『AFFILIATION テーブル:18 件、CHARACTERS テーブル:46 件』なので、例えば第 2 パラメータのラムダ式内で aff.Characters.Add(chara);
のみを実行した場合、欲しいのは 18 件の Affiliation クラスですが、実際は 46 件の Affiliation クラスが返り、それぞれの Affiliation.Characters には Character_Normal クラスが 1 件だけ格納された状態になります。取得レコード数は 46 件なので、当然と言えば当然ですが…
そのような状態を避けるため、src. 15 のようにラムダ式の外に用意した Dictionary<int, Affiliation> に Affiliation クラス(ヘッダとなるクラス)を退避して、ラムダ式内で正しい親子関係を設定しています。そして結果の出力には 28 行目の affilicationDic.Values を使用(29 行目の characters 変数は読み捨てる形になる)する事で 1 対多(いわゆるヘッダ – 明細形式)のデータでも期待する形で取得する事ができます。
余談ですが、SQLite では 23 行目のように LIMIT n
を指定すると、Oracle の RowNum や SQL Server の TOP と同じように取得レコード数を絞ることができます。
単一値の取得
ここまで紹介した方法で、SELECT 結果をクラスにマッピングできますが、単一値(いわゆるスカラー値)を取得したい事も多いと思います。Dapper には Query ~ から始まるメソッド以外に、ADO.NET にもある ExecuteScalar も IDbConnection の拡張メソッドとして用意されていて、src. 16 のように呼び出す事が出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using Dapper; private async void btnScalar_Click(object sender, RoutedEventArgs e) { await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { var sql = new StringBuilder(); sql.AppendLine(" SELECT "); sql.AppendLine(" COUNT(1) "); sql.AppendLine(" FROM "); sql.AppendLine(" CHARACTERS CHR "); var count = await con.ExecuteScalarAsync<int>(sql.ToString()); var buf = new StringBuilder(); buf.AppendLine($"登録済みキャラクター数"); buf.AppendLine($"{count.ToString()}人"); this.txtResult.Text = buf.ToString(); } } |
実行すると fig. 10 のように取得できます。
ADO.NET 標準の ExecuteScalar の返り値は Object ですが、Dapper はジェネリック版なのでキャストの必要もなく使いやすいと思います。
その他の Dapper 機能
ここまでは Dapper の Query ~ メソッドで SELECT 結果をマッピングする方法を紹介してきましたが、以降では Dapper が持つマッピング以外の機能について簡単に紹介します。
ExecuteReader
Dapper は SELECT 結果をマッピングする以外にも、何故か IDataReader を返す ExecuteReader メソッドも用意されています。
Dapper を使用するとクラスにマッピングしてくれるのに IDataReader を返すメソッドの存在は不思議だったので調べてみると teratail に『Dapperで取得したデータをDataTableで取得したい。』と言うドンピシャな質問が見つかりましたw
回答を読んで納得しました。例えば、画面等で使用している DataTable 等に影響は与えたくないが、Dapper のバインド変数マッピングは使用したい等の段階的に移行するような場面に限定すれば、使用するメリットはあるかもしれません。メインで使用するメソッドでは無いと思いますが、どうしても DataTable を返したいような場合でも Dapper は使用できると言う事のようです。
INSERT、UPDATE、DELETE の呼び出し
ここまでは Dapper で SELECT 文を実行する場合しか紹介していませんでしたが、当然、SELECT 以外の INSERT や UPDATE、DELETE 等の呼び出しもサポートしています。基本的にはバインド変数にマッピングするだけなので、上で書いた【バインド変数(プリペアードステートメント)へのマッピング】の内容がそのまま利用できます。例えば、INSERT や DELETE は src. 17 のように呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using Dapper; using DapperSample.Entities; private async void btnInsert_Click(object sender, RoutedEventArgs e) { var chara = new Character_Normal() { Name = "バンビエッタ・バスターバイン", Kana = "バンビエッタバスターバイン", Sex = Sex.女, Affiliation = new Affiliation() { Id = 19 } }; var sql = new StringBuilder(); sql.AppendLine(" INSERT INTO CHARACTERS( "); sql.AppendLine(" CHARACTER_NAME "); sql.AppendLine(" , KANA "); sql.AppendLine(" , SEX "); sql.AppendLine(" , AFFILIATION "); sql.AppendLine(" ) VALUES ( "); sql.AppendLine(" @Name "); sql.AppendLine(" , @Kana "); sql.AppendLine(" , @Sex "); sql.AppendLine(" , @Affiliation "); sql.AppendLine(" ) "); await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { await con.OpenAsync(); await using (var tran = con.BeginTransaction()) { try { var count = await con.ExecuteAsync(sql.ToString(), chara, tran); await tran.CommitAsync(); } catch (Exception) { await tran.RollbackAsync(); throw; } } } } |
ここまで紹介してきた Query ~系メソッドとの違いは、28 行目の Open(Async)です。本来、Dapper の拡張メソッドを呼び出すと DB への接続が開いていない場合は、Dapper 内部で自動的に Open(Async 系メソッドでは OpenAsync)を呼び出してくれます。
33 行目の Execute(Async)でも Dapper 内部で Open を呼び出してくれますが、29 行目の BeginTransaction に Open 済みの DbConnection が必要になるため、Open を別途呼び出しています。
ですが、src. 17 を実行すると、33 行目で NotSupportedException: ‘The member Affiliation of type DapperSample.Entities.Affiliation cannot be used as a parameter value’ が Throw されます。原因は 23 行目の『@Affiliation パラメータ』です。
src. 12 で紹介した通り、Character_Normal.Affiliation プロパティはAffiliation クラス型なので『 Character_Normal.Affiliation プロパティから値が取得できない』と言う内容の例外が Throw されます。
回避方法を 2 通りほど紹介します。
クラスに読み取り専用プロパティを追加
Character_Normal.Affiliation プロパティはクラス型のプロパティなので、Affiliation クラスの値を取得するための読み取り専用プロパティを src. 18 のように別途追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; namespace DapperSample.Entities { /// <summary>BLEACHのキャラクターを表します。</summary> public class Character_Normal { public int Id { get; set; } = 0; public string Name { get; set; } = string.Empty; public string Kana { get; set; } = string.Empty; public DateTime? Birthday { get; set; } = null; public Sex Sex { get; set; } = Sex.設定なし; public Affiliation Affiliation { get; set; } = new Affiliation(); public Zanpakuto? Zanpakuto { get; set; } = null; public int AffiliationId => this.Affiliation == null ? 0 : this.Affiliation.Id; } } |
src. 18 の 22 ~ 23 行目のように新規プロパティを追加すると、src. 19 のようにバインド変数にマッピングできるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | private async void btnInsert_Click(object sender, RoutedEventArgs e) { var chara = new Character_Normal() { Name = "バンビエッタ・バスターバイン", Kana = "バンビエッタバスターバイン", Sex = Sex.女, Affiliation = new Affiliation() { Id = 19 } }; var sql = new StringBuilder(); sql.AppendLine(" INSERT INTO CHARACTERS( "); sql.AppendLine(" CHARACTER_NAME "); sql.AppendLine(" , KANA "); sql.AppendLine(" , SEX "); sql.AppendLine(" , AFFILIATION "); sql.AppendLine(" ) VALUES ( "); sql.AppendLine(" @Name "); sql.AppendLine(" , @Kana "); sql.AppendLine(" , @Sex "); sql.AppendLine(" , @AffiliationId "); sql.AppendLine(" ) "); await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { await con.OpenAsync(); var newId = 0; await using (var tran = con.BeginTransaction()) { try { var delCount = await con.ExecuteAsync("DELETE FROM CHARACTERS WHERE KANA = @kana", new { kana = "バンビエッタバスターバイン" }); var count = await con.ExecuteAsync(sql.ToString(), chara, tran); newId = await con.ExecuteScalarAsync<int>("SELECT seq FROM sqlite_sequence WHERE name = @tableName ", new { tableName = "CHARACTERS" }); await tran.CommitAsync(); } catch (Exception) { await tran.RollbackAsync(); throw; } var newChara = await con.QueryFirstOrDefaultAsync("SELECT * FROM CHARACTERS WHERE ID = @id", new { id = newId }); var buf = new StringBuilder(); buf.AppendLine($"ID\t所属\tキャラクター名\tKANA\tSEX"); buf.AppendLine($"{newChara.ID}\t{newChara.AFFILIATION}\t{newChara.CHARACTER_NAME}\t{newChara.KANA}\t{newChara.SEX}"); this.txtResult.Text = buf.ToString(); } } } |
src. 17 から 20 行目のバインド変数名を Affiliation ⇒ AffiliationId
に変更しただけですが、例外は回避する事ができます。
ついでに 34 行目の SQLite 固有機能についても紹介します。SQLite でオートナンバー型が設定されたテーブルに INSERT すると、オートナンバー型に設定される値は SQLite 内部の sqlite_sequence テーブルから取得されます。そのため、INSERT したレコードの連番を取得したい場合は、34 行目のようにテーブル名をキーに SELECT すると採番された値を取得する事が出来ます。
匿名クラスに値を乗せ換える
上では脊髄反射的にクラスにプロパティを追加しましたが、既存クラスを安易に変更できない場合も多いと思います。その場合は、【バインド変数(プリペアードステートメント)へのマッピング】で紹介した場合と同じく、src. 20 のように匿名クラスに値を乗せ換える事で回避することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | private async void btnInsert_Click(object sender, RoutedEventArgs e) { var chara = new Character_Normal() { Name = "バンビエッタ・バスターバイン", Kana = "バンビエッタバスターバイン", Sex = Sex.女, Affiliation = new Affiliation() { Id = 19 } }; var sql = new StringBuilder(); sql.AppendLine(" INSERT INTO CHARACTERS( "); sql.AppendLine(" CHARACTER_NAME "); sql.AppendLine(" , KANA "); sql.AppendLine(" , SEX "); sql.AppendLine(" , AFFILIATION "); sql.AppendLine(" ) VALUES ( "); sql.AppendLine(" @Name "); sql.AppendLine(" , @Kana "); sql.AppendLine(" , @Sex "); sql.AppendLine(" , @AffiliationId "); sql.AppendLine(" ) "); await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { await con.OpenAsync(); var newId = 0; await using (var tran = con.BeginTransaction()) { try { var delCount = await con.ExecuteAsync("DELETE FROM CHARACTERS WHERE KANA = @kana", new { kana = "バンビエッタバスターバイン" }); var count = await con.ExecuteAsync(sql.ToString(), new {Name = chara.Name, Kana = chara.Kana, Sex = chara.Sex, AffiliationId = chara.Affiliation.Id}, tran); newId = await con.ExecuteScalarAsync<int>("SELECT seq FROM sqlite_sequence WHERE name = @tableName ", new { tableName = "CHARACTERS" }); await tran.CommitAsync(); } catch (Exception) { await tran.RollbackAsync(); throw; } ~ 略 ~ } } } |
src. 20 のように匿名クラスに値を乗せ換える方が、他のソースファイルへの影響も無く柔軟に対応できます。src. 19、20 どちらの方法を採っても実行結果は fig. 11 のようになります。
src. 19、20 では Dapper から DELETE と INSERT を呼び出す方法しか紹介していませんが、UPDATE の場合も同じようにバインド変数にマッピングすれば、問題なく動作するはずです。(管理人は UPDATE をほとんど使用しないので…)
複数レコードの INSERT
src. 20 の Execute(Async)の第 2 パラメータは Object 型なので、実は src. 21 のように List 型を渡すこともできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | using Dapper; private async void btnMultiInsert_Click(object sender, RoutedEventArgs e) { var affiliation = new Affiliation() { Id = 19, Name = "見えざる帝国" }; var characters = new List<Character_Normal>() { new Character_Normal() { Name = "ユーハバッハ", Kana = "ユーハバッハ", Sex = Sex.男, Affiliation = affiliation }, new Character_Normal() { Name = "ユーグラム・ハッシュヴァルト", Kana = "ユーグラムハッシュヴァルト", Sex = Sex.男, Affiliation= affiliation } }; var insertCharacters = new List<dynamic>(); characters.ForEach(c => insertCharacters.Add(new { Name = c.Name, Kana = c.Kana, Sex = c.Sex, AffiliationId = c.Affiliation.Id})); var sql = new StringBuilder(); sql.AppendLine(" INSERT INTO CHARACTERS( "); sql.AppendLine(" CHARACTER_NAME "); sql.AppendLine(" , KANA "); sql.AppendLine(" , SEX "); sql.AppendLine(" , AFFILIATION "); sql.AppendLine(" ) VALUES ( "); sql.AppendLine(" @Name "); sql.AppendLine(" , @Kana "); sql.AppendLine(" , @Sex "); sql.AppendLine(" , @AffiliationId "); sql.AppendLine(" ) "); await using (var con = new SQLiteConnection(this.connectionStringBuilder.ConnectionString)) { await con.OpenAsync(); await using (var tran = con.BeginTransaction()) { try { var count = await con.ExecuteAsync(sql.ToString(), insertCharacters, tran); await tran.CommitAsync(); } catch (Exception) { await tran.RollbackAsync(); throw; } } } } |
サンプルなので、List<Character_Normal> を List<dynamic> に載せ替える冗長なコードになってはいますが、43 行目のように List<dynamic> をパラメータに渡すだけで、INSERT だけでなく、DELETE や UPDATE でも同様に呼び出せます。
但し、Dapper 内部ではリストの件数分、SQL を実行しているだけなので、Dapper を呼び出す外側でループする場合と変わりません。複数オブジェクトを高速に INSERT したい場合は別のアプローチを採る必要がある事は覚えておいてください。
尚、src. 21 の実行画面は貼らないので、動作が確認したい場合は GitHub リポジトリ に上げたソースをクローンする等してください。
このエントリでは紹介しない機能
Dappe は通常の SQL 文の実行に加えて、ストアドプロシージャも呼び出す事が出来ます。SQLite もストアドを登録する事が出来るようですが、現時点で管理人は SQLite のストアドを書いた事が無いのでここでは紹介しません。現時点では記事にする予定はありませんが、機会があれば書くと思います。
まとめ的な
Micro-ORM の Dapper を紹介してきましたが、管理人的には非常に便利なライブラリだと思います。
DB から取得したレコードのマッピング部分だけで言えば Dapper を使用しなくても Reflection 等を利用して自作できるので、既に自作したライブラリがある事も多いかもしれません。ですが、Dapper のような簡単バインド変数マッピングを実装したライブラリは(管理人個人的には)見た事が無いので、それだけでも入れ替える価値はありそうな気はします。
加えて、Dapper と同等のパフォーマンスを出すことや、大量アクセスに耐えた実績なども加味すれば入れ替える価値は更に高くなると個人的には思っているので、まずは使って Dapper の便利さを感じてみてください!
尚、今回紹介したサンプルコードもいつも通り GitHub リポジトリ に上げています。