データソース
では実際にテンプレート構文を利用していきましょう。そのためにはまず、「データソース」について理解する必要があります。
DioDocsのテンプレート構文ではWPFなどのデータバインディングのような機構が利用できます。その際にデータソースとしては次の3種類が利用できます。
- 変数
- カスタムオブジェクト
- データテーブル・データセット
変数
まずは「変数」から利用していきましょう。先ほどのコードを修正します。
直接セルに値を設定していた箇所をコメントアウトし、以下の通りデータソースを設定するように修正してください。
//workbook.Worksheets[0].Range["B5"].Value = "Hello, DioDocs!"; workbook.AddDataSource("CompanyName", "Hello, DioDocs!"); workbook.ProcessTemplate();
AddDataSourceの後に、ProcessTemplateメソッドを呼び出すことで、データソースの値をExcelファイルに適用します。
続いて、Excelファイルを次のように修正しましょう。
値を設定していた「B5」セルに、AddDataSourceで指定した名称を二重の波カッコで囲んで{{CompanyName}}
のように記述します。
では実行してみましょう。修正前と同じPDFが得られたはずです。
このように物理的なレイアウト情報をExcel側で指定し、論理名を利用してマッピングすることで、ExcelのテンプレートファイルとC#のコードを疎結合に保てることがテンプレート構文の最大の利点になります。
カスタムオブジェクト
さて、変数を1つずつ指定して複雑な帳票を生成することも可能ですが、実際には必要なプロパティを保持した、帳票を抽象化したオブジェクトを扱えると便利ですよね。それが「カスタムオブジェクト」です。
今回は請求書を作成します。なので請求書クラスを作成して利用してみましょう。まずは次のようにInvoiceクラスを作成してください。
namespace HelloDioDocs { public class Invoice { public string CompanyName { get; set; } } }
これを利用して帳票を生成します。先ほどの値を設定していたコードを、以下の通り修正します。
//workbook.AddDataSource("CompanyName", "Hello, DioDocs!"); var invoice = new Invoice {CompanyName = "Hello, DioDocs!"}; workbook.AddDataSource("Invoice", invoice); workbook.ProcessTemplate();
そしてExcelファイルも次のように修正します。
{{CompanyName}}
を{{Invoice.CompanyName}}
に修正します。データソースの名称とプロパティ名をピリオドでつないで記述します。
では実行して、先と同様なPDFが生成されることを確認してください。
このようにして複雑な構造のオブジェクトもテンプレート構文で扱うことが可能です。
データセット・データテーブル
データソースの最後は「データセット」と「データテーブル」です。データセットはデータテーブルを複数束ねて利用するだけなので本稿では割愛し、データテーブルのみを扱います。
データテーブルをプログラムから生成することも可能ですが、次の2つの理由からデータベースを利用してデータを取得します。
- データテーブルを利用する場合、ほとんどのユースケースでデータベースから値を取得する
- 後の大量データ生成のため
データベースのセットアップ
データベースはMicrosoftの提供するサンプルデータベース「AdventureWorks」を利用します。
データベースを利用するとは言え、Dockerさえ入っていれば何も煩わしいことはありません。いくつかのスクリプトを実行するだけです。ステップは次の通りです。
- コンテナイメージの取得
- コンテナの作成
- コンテナの起動
- 利用
- コンテナの停止
- コンテナの削除
- イメージの削除
1.コンテナイメージの取得
まずはコマンドプロンプトを開き、次の通りコンテナイメージをDockerHubから取得してください。
docker pull nuitsjp/adventureworks:latest
コンテナイメージとは、OSをインストールする際のisoファイルに置き換えて考えると理解しやすいでしょう。
2.コンテナの作成
取得したイメージからadventureworks
という名称のコンテナを作成します。ここでは、saパスワードとSQL Serverを接続するポートをあわせて指定してコンテナを作成しています。
docker create --name adventureworks -e ACCEPT_EULA=Y -e SA_PASSWORD=P@ssw0rd! -p 1433:1433 nuitsjp/adventureworks:latest
isoイメージを実際のPC(もしくは仮想PC)にインストールする感覚です。
3.コンテナの起動
続いてコンテナを起動します。
docker start adventureworks
4.利用
これでAdventureWorksが利用できるようになりました。SSMSがインストールされているのであれば、実際に試してみてもいいでしょう。
SSMSがインストールされていない場合は、特に何もしなくて構いません。すぐにコードから利用します。また、念のため停止や削除の方法も記載しておきましょう。
5.コンテナの停止
明示的に停止する場合は、次のようにコマンドを実行してください。またPCをシャットダウンしても停止し、起動時に自動的に開始されることはありません。
docker stop adventureworks
6.コンテナの削除
ローカルのリソースを削除するためにはコンテナとイメージの2つを削除する必要があります。まずはコンテナを削除しましょう。
docker rm adventureworks
7.イメージの削除
最後にイメージを削除すれば、すべて元通りです。
docker rmi nuitsjp/adventureworks:latest
Hello, DataTable!
では実際にコードからデータベースへ接続し、結果を利用してみましょう。
まずはSQL Serverへ接続するためのライブラリ「Microsoft.Data.SqlClient」をNuGetからインストールします。手順はDioDocsのライブラリをインストールしたときと同じです。そちらを参照してください。
.NET FrameworkではSQL Serverへ接続するためのライブラリは標準で含まれていました。しかし、.NET Coreではオプショナル的なパッケージは外部に切り出され、小さな単位で配備できるように分割されました。これは、たとえばスケーラブルなシステムを構築する際に、スタートアップ時間の削減などの効果が期待できるからです。
続いてSQLを用意しましょう。SQLはコードに直接記述すると読みにくいため、今回はファイルに記述します。プロジェクトを右クリックし、「追加」から「新しい項目」を選択しましょう。
検索条件に「テキスト」を入力し「テキスト ファイル」を選択してください。ファイル名に「SelectInvoices.sql」を指定して「追加」します。
追加したら「Template.xlsx」と同様に、「出力ディレクトリにコピー」プロパティの値を「新しい場合はコピーする」を選択してください。
続いてSQLファイルを開き、以下の通り記述してください。
use AdventureWorks; select Name as CompanyName from Sales.Store where BusinessEntityID = 1046
AdventureWorksデータベースのSalesスキーマのStoreテーブルから店舗を取得します。
ではこのSQLを実行してDioDocsへ渡すコードを記述しましょう。カスタムオブジェクトを設定していたコードをコメントアウトし、代わりに次のように記述しましょう。
//var invoice = new Invoice {CompanyName = "Hello, DioDocs!"}; //workbook.AddDataSource("Invoice", invoice); var connectionStringBuilder = new SqlConnectionStringBuilder { DataSource = "localhost", UserID = "sa", Password = "P@ssw0rd!" }; using var connection = new SqlConnection(connectionStringBuilder.ToString()); connection.Open(); using var command = new SqlCommand( File.ReadAllText("SelectInvoices.sql"), connection); using var dataTable = new DataTable(); dataTable.Load(command.ExecuteReader()); workbook.AddDataSource("Invoice", dataTable);
順に説明していきましょう。
まずは、次のようにして接続文字列を生成してデータベースへのコネクションを開いています。
var connectionStringBuilder = new SqlConnectionStringBuilder { DataSource = "localhost", UserID = "sa", Password = "P@ssw0rd!" }; using var connection = new SqlConnection(connectionStringBuilder.ToString()); connection.Open();
続いてSQLをファイルから読み込んで実行し、実行結果をDataTableにロードしています。
using var command = new SqlCommand( File.ReadAllText("SelectInvoices.sql"), connection); using var dataTable = new DataTable(); dataTable.Load(command.ExecuteReader());
最後にDataTableをDioDocsのデータソースとして追加しています。
workbook.AddDataSource("Invoice", dataTable);
では実行してみましょう。先ほどと名称が変わっていますが、次のような似通ったPDFが生成されます。
カスタムオブジェクトvsデータセット
さて変数はさておき、実践的な帳票生成において機能的にカスタムオブジェクトとデータセット(もしくはデータテーブル)のいずれも選択できる場合、どれを利用するべきでしょうか?
結論から言うと、高度なUIを含むアプリケーションの内部に包含するのであれば「カスタムオブジェクト」を、単一種類の帳票をシンプルなルールで出力するような専用のバッチプログラムの場合は「データセット」を選ぶというのが、私の好みに合います。
もちろん前提として、実現できる帳票に差異がなく、性能に大幅に差がない場合に限ります。次の表は、簡単なベンチマークの実行結果です。
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.836 (1903/May2019Update/19H1) Intel Core i7-7700T CPU 2.90GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=3.1.201 [Host] : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT [AttachedDebugger] DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
Method | Mean | Error | StdDev |
---|---|---|---|
DataSet | 12.66 ms | 0.482 ms | 1.384 ms |
CustomObject | 10.73 ms | 0.461 ms | 1.338 ms |
どちらも非常に高速に動作していますね。素晴らしい。CustomObjectのほうがやや高速ですが、それでも2ミリ秒の差異のため、実用上の差異は気にならないケースがほとんどかと思います。
と言うわけで、先ほどの指針で問題ないように思えます。
カスタムオブジェクトを利用する場合、データセットを利用する場合と比較して、最低でもカスタムオブジェクトの設計・実装というコストがかかります。しかし逆に言うと、帳票の元データのデータストアとビジネスルール、Excelテンプレートの3つの結合度を低く保つことが可能となります。
データセットをダイレクトに利用する場合、データストアの具体的な実装に強く依存してしまいます。その分手軽で生産性は高くなりますが、高度なUIを含むアプリケーションの内部に埋め込んでしまうと、テスト容易性や変更容易性に大きな課題を残してしまいます。
高度なUIを含むアプリケーションの内部に包含するのであればカスタムオブジェクトを、単一種類の帳票をシンプルなルールで出力するような専用のバッチプログラムの場合はデータセットを選ぶのが、第一の判断基準として良いように思います。もちろん例外は存在するでしょう。