TypeScriptでgRPCしたいがいろいろあってよくわからない
用語
- grpc/grpc-node: Node.js向けの公式プロジェクト
- grpc-tools: grpc/grpc-node が提供しているパッケージ
- grpc_tools_node_protoc: grpc-toolsに含まれるコマンド
- agreatfool/grpc_tools_node_protoc_ts: grpc_tools_node_protocで吐いたコードに .d.ts を付けるサードパーティのコマンド
- improbable-eng/ts-protoc-gen: TypeScript向けのサードパーティprotocプラグイン
Node or ブラウザ
gRPCのサーバー側はどのツールもNodeを想定しているので特に問題ない。 gRPCのクライアント側はNodeで使うことを想定しているのものとブラウザで動かすことを想定しているものがあるので注意が必要
- ブラウザ上で動作することとを想定しているもの
- grpc/grpc-web
- Node用のもの
- grpc/grpc-node (grpc-toolsのgrpc_tools_node_protoc)
- 両方に対応しているもの
- improbable-eng/grpc-web
- Transportを切り替えることで両方で使えるっぽい
- improbable-eng/grpc-web
JavaScript での gRPC
次の2パターンがある。
- .protoをそのまま読み込む
- .protoからコード生成したコードを読み込む
TypeScriptから使う場合は型定義を付けたいので後者のコード生成するやり方を選ぶのが良さそうだよね。
grpc or @grpc/grpc-js
- grpcは開発終了間近
- 今後は @grpc/grpc-js を使うのが良さそう
- 両方ほとんど同じだけど @grpc/grpc-js を使う場合、protobufを生成する時に
--grpc_out=grpc_js:...
みたいなオプションが各ツールによって提供されているので適宜ドキュメントをチェックすべし
TypeScriptサポート
できるだけ公式か公式に近いものが良いなら以下の2つが良さそう。2つあるけど動作環境を考慮すると実質一択なのかな。
- grpc/grpc-webのTypeScriptサポート(experimental)
- agreatfool/grpc_tools_node_protoc_tsで公式のgrpc/grpc-node (grpc-tools) に対して .d.ts を付ける
まとめ
- grpc-webやりたいならgrpc/grpc-web
- そうでなければgrpc-tools+grpc_tools_node_protoc_tsが一番無難そう
- 両対応したいならimprobable-eng/ts-protoc-genも選択肢に上がってきそう
おまけ
個人用のscala-stewardを動かす
scala-stewardを使い始めました。
ちょっと使うだけならscala-stewardのrepos.mdにプルリクエスト送るだけで良くて便利なんですが、個人の雑多なプロジェクト全てにそれをやるのはちょっと気が引けるので自前でも動かせるようにしました。
今だとGitHub Actionsを使うのが手取り早いんだけど、使うトークンの権限の問題でちょっとややこしかったです。そのあたりは吉田さんのブログが詳しいです。
xuwei-k.hatenablog.com xuwei-k.hatenablog.com
自分は次の形にしました。
- scala-ojisanという個人用のGitHub Appを作ってそのトークンを使う
- 権限は自分のリポジトリに対してcontentとpull-requestのread/writeがあれば良さそう。
- その辺も吉田さんのブログに書いてあります。
- scala-stewardは各リポジトリに設定するのではなく、1つのリポジトリで管理する
で、試行錯誤の上なんとかscalaおじさんがプルリクくれるようになりました。
Update sbt to 1.4.7 by scala-ojisan · Pull Request #3 · tototoshi/hello.g8 · GitHub
sbtn 便利かも
sbt 1.4.0 から sbtn という機能が追加されました。
https://eed3si9n.com/sbt-1.4.0-beta
Native thin client sbt 1.4.0 adds an official native thin client called sbtn that supports all tasks. > https://github.com/sbt/sbtn-dist/releases/tag/v1.4.0-RC2
This lets you run sbt tasks from the system shell as:
$ sbtn compile
$ sbtn shutdown
The native thin client will run sbt (server) as a daemon, which avoids the JVM > spinup and loading time for the second call onwards. This could an option if you > would like to use sbt from the system shell such as Zsh and Fish.
Remember to call sbtn shutdown when you're done!
Later on, sbt script will also support --client option to run the native thin client:
$ sbt --client compile
$ sbt --client shutdown
#5620 by @eatkins
sbtn 一度起動すると sbt をデーモンとして常駐させ、以降は sbt を起動するのではなくすでに起動している sbt に接続しに行くため起動時間が短縮されます。
emacs 使ってる人には emacsclient ですって言えば通じそう。あと sbt と同じ Scala のビルドツールである mill も似たようなことやってますよね。mill 最近使い始めたばかりでよく知らないけど。
sbt は起動が遅いので対話シェルを起動させておいてそれを利用するのが普通でしたが、sbtn によってシェルを起動しっぱなしにする必要がなくなります。私は気づくと tmux のウィンドウが無限に開いていて、sbt 起動してるのはどこのウィンドウだっけ??と探して回るのを毎日やっているのですが、sbtn を使うと今いる window でコマンド打てばいいのでそれがなくなりそうです。
あと他のシェルスクリプトとの連携がしやすくなるのも良いですね。今までは
#!/bin/sh sbt compile set_up_database sbt test tear_down_database
みたいなスクリプトを書くのは sbt の起動が遅いので辛く、シェルスクリプト側の処理を sbt の方に組み込むことになり、sbt 職人が build.sbt に
lazy val setUpDatabase = taskKey[Unit]("setup_database...") lazy val tearDownDatabase = taskKey[Unit]("teardown_database...") ...
とか書いてドヤッって感じだったんですが、sbtn を使えば
#!/bin/sh sbtn compile set_up_database sbtn test tear_down_database
でもう十分ですね。build.sbtのダイエットができそうです。
追記
- project/build.properties のバージョンを上げるのに加えてsbtコマンドのバージョンも 1.4 に上げましょう。
- sbtnでPlayのプロジェクトをrunするとプロジェクトは起動するけれどsbtn側で標準入力を受け取れないっぽい。(Playプロジェクトのログを受け取る前にデタッチされてそう?)
Modules should be fast and side-effect free と Guice は言っているがPlay Frameworkはそうなってはいない話
Modules should be fast and side-effect free
Modules should be fast and side-effect free(モジュールは速くて副作用なしであるべき)と Guice の Wiki にはあります。
これは Guice モジュールの中で DB に接続したりスレッド開始したりするなという話で、これに従うのならば Guice の Module (extends AbstractModule したクラスの中とか)の他にも Module を構成する各クラスのコンストラクタの中でも副作用を起こしてはいけないことになります。
副作用を起こしてはいけない理由としては
- Modules start up, but they don't shut down.
- Modules should be tested.
- Modules can be overridden.
を Guice は挙げています。
Guice の意図に沿ったコードを書くと初期化処理、終了処理のためのメソッドをコンストラクタ以外に用意して、アプリケーションの起動、終了時になんらかの形で呼んであげることになります。
public interface Service { void start() throws Exception; void stop(); }
public static void main(String[] args) throws Exception { Injector injector = Guice.createInjector( new WebserverModule(), ... ); Service webserver = injector.getInstance( Key.get(Service.class, WebserverService.class)); webserver.start(); addShutdownHook(webserver); }
サンプルでは単純ですが徹底するとなると例えば設定ファイルを読み込むとかも禁止となるのでなかなか厳しい制限にも感じます。(実際には設定ファイル読み込みとかは OK にして、スレッド開始とかは NG にする、くらいが落とし所かも?)
Play Framework の場合
Play Framework は Guice を使っていますが、Guice のこの主張を完全に無視して副作用をバリバリにつかっています。Play を構成するクラスは大体コンストラクタでスレッド開始したり設定読み込んだりとやって、終了処理はコンストラクタ内で ApplicationLifecycle
に登録します。
@Singleton class NanrakanoService @Inject()(lifecycle: ApplicationLifecycle) { val resource = new NanrakanoBackgroundResource().start() lifecycle.addStopHook(() => Future.successful(resource.shutdown())) }
Guice の wiki を見たあとでこれを見るとギョッとするんですが実際のところそんなに困ることはないです。
とはいえやっぱり時々は困るし、Guice の推奨しない方法で Guice をヘビーに使っているというのは精神衛生上あまり良くはありません。そして実はコンストラクタで副作用を起こすようになったのは Play が Guice を使い始めた Play 2.4 からの話で、Guice を使っていなかった Play 2.3 以前の Plugin
の仕組みでは副作用のためのメソッドが分かれていたのです。
package plugins import play.api.{Plugin, Application} class MyPlugin(app: Application) extends Plugin { val myComponent = new MyComponent() override def onStart() = { myComponent.start() } override def onStop() = { myComponent.stop() } override def enabled = true }
Guice を使う前は副作用が分かれていたのに、互換性を壊してまで Guice を使い始めたら副作用起こすようになったというのがなんかチグハグだなあって思います。
PlayのCache APIでMemcachedやRedisの代わりにRDBを使う
Play frameworkはキャッシュのためのAPIを play.api.cahe
というパッケージで提供しています。play.api.cache
はそのバックエンドとして、CaffineやEhcacheが選択でき、またサードバーティのライブラリを使えばMemcached, Redisなどをバックエンドにすることができます。キャッシュと言うと高速化のためのものですが、Playのキャッシュは高速化の他に、単なるデータの一時保存のため、つまり単なるKey-Value Storeとして使われていたりもします。
Caffeineを使うとインメモリのキャッシュが簡単に実現できますが、アプリケーションサーバーのインスタンスが複数ある場合、各サーバーでキャッシュした内容を共有することができません。MemcachedやRedisを使えばインスタンス間でキャッシュした内容を共有できますが、MemcachedやRedisを管理する手間が発生します。
PlayのキャッシュAPIは使いたいけれど、MemcachedやRedisなどの新たなミドルウェアを導入したくない。という状況で、ならばRDBをバックエンドにしてしまおうと作ったのがdbcacheというライブラリです。実際のところ、MemcachedやRedisを使わなくてもRDBで中小規模のサービスでは事足りてしまいます。
https://github.com/tototoshi/dbcache
設定
テーブル追加
MySQLをバックエンドとして使う場合の例です。まずデータベースにcache_entriesテーブルを作成します。
CREATE TABLE `cache_entries` ( `cache_key` varchar(191) PRIMARY KEY, `cache_value` mediumblob NOT NULL, `expired_at` datetime, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, INDEX (`expired_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
Play2.7.xの場合
Play 2.7.xを使っている場合は dbcache-mysql
と dbcache-play
をbuild.sbtで追加します。
libraryDependencies ++= Seq( "com.github.tototoshi" %% "dbcache-mysql" % "0.3.0", "com.github.tototoshi" %% "dbcache-play" % "0.3.0" )
Play2.6.xの場合
dbcache-play
は最近追加したモジュールなので、Play 2.6.xには対応していません。Play2.6.xを使っている場合は dbcache-mysql
だけを追加し、以下のコードをアプリケーションコードに追加します。
libraryDependencies ++= Seq( "com.github.tototoshi" %% "dbcache-mysql" % "0.2.0" )
// Create adapter for play.api.cache class DBCacheApi(myCache: DBCache) extends CacheApi { def set(key: String, value: Any, expiration: Duration): Unit = myCache.set(key, value, expiration) def get[A](key: String)(implicit ct: ClassTag[A]): Option[A] = myCache.get[A](key) def getOrElse[A: ClassTag](key: String, expiration: Duration)(orElse: => A) = myCache.getOrElse(key, expiration)(orElse) def remove(key: String) = myCache.remove(key) }
DIの設定
あとはProviderを作ってDIしてあげるだけです。Scalikejdbcを使っている場合はこんな感じになります。
import java.sql.Connection import com.github.tototoshi.dbcache.ConnectionFactory import com.github.tototoshi.dbcache.mysql.MySQLCache import com.github.tototoshi.dbcache.play.DBCacheApi import javax.inject.{Inject, Provider} import play.api.Environment import play.api.cache.SyncCacheApi import scalikejdbc.DB class MySQLCacheApiProvider @Inject()(environment: Environment) extends Provider[SyncCacheApi] { private val connectionFactory = new ConnectionFactory { override def get(): Connection = { DB.autoCommitSession().connection } } override def get(): SyncCacheApi = { val mysqlCache = new MySQLCache(connectionFactory, environment.classLoader) new DBCacheApi(mysqlCache) } }
EhCacheモジュールは無効にしておきます。
play.modules.disabled += "play.api.cache.ehcache.EhCacheModule"
あとは普通にPlayのキャッシュAPIを使えばデータベースに保存されるようになります。
運用について
期限切れのキャッシュデータがDBに残るのでバッチか何か動かしてたまに掃除してあげてください。
DELETE FROM cache_entries where expired_at < ${n日前}
いつもJavaConvertersでうまくいくとは限らない
ScalaとJavaのコレクションを相互に変換するには scala.collection.JavaConverters
が便利です。例えばJavaのコレクションに .asScala
をつければ、JavaのコレクションをラップしたScalaのコレクションが作られます。ただし、いつも .asScala
で済むとは限りません。コレクションのインターフェイスではなくデータ構造が重要な場合は注意が必要です。
次のコード例は java.util.LinkedHashMap
を .asScala
に変換して、 +=
, +
の操作を行ったものです。 LinkedHashMap
は要素が挿入された順番を保持するはずですが、 +=
を使った場合は順番が保持されるものの、 +
を使った場合は順番が保持されていません。
val m = new java.util.LinkedHashMap[String, Int]() m.put("field1", 1) m.put("field2", 2) import scala.collection.JavaConverters._ val m2 = m.asScala println(m2) // Map(field1 -> 1, field2 -> 2) m2 += "field3" -> 3 m2 += "field4" -> 4 println(m2) // Map(field1 -> 1, field2 -> 2, field3 -> 3, field4 -> 4)
val m = new java.util.LinkedHashMap[String, Int]() m.put("field1", 1) m.put("field2", 2) import scala.collection.JavaConverters._ val m2 = m.asScala println(m2) // Map(field1 -> 1, field2 -> 2) val m3 = m2 + ("field3" -> 3) + ("field4" -> 4) println(m3) // Map(field1 -> 1, field3 -> 3, field2 -> 2, field4 -> 4)
+=
を使った場合、新しい要素は元のLinkedHashMap
に挿入されます。一方、 +
を使った場合は新たな Map
のインスタンスが作られます。その新たな Map
のインスタンスは JavaConverters
で定義してあるラッパークラスが作るのですが、それが LinkedHashMap
ではなく順番を保持しない HashMap
なのです。
これは実際にplay-json 2.6.11に入り込んだバグです。play-jsonはJsonのフィールドを Map
として保持していますが、その Map
をScalaのものから java.util.LinkedHashMap
のインスタンスとして生成して、 JavaConverters
でラップするという実装に変えたところ、Jsonのフィールドの順序が狂うという現象が起きました。
https://github.com/playframework/play-json/issues/236
というわけで、気をつけましょう。
JasperReportsでPDFを作成する
JavaでPDFを作成するJasperReportsの使い方について解説します。少し古いライブラリですが企業がバックについてますし、継続的にメンテナンスはされているようです。
JasperReportでPDFを作成するには、
- JasperReport Studioでテンプレートを作成する
- Javaプログラムでデータを流し込んでPDFを作成する
と言う手順を踏みます。JasperReport StudioではGUIからText、Imageといったパーツを配置しますが、裏ではjrxml形式のファイルを吐き出しています。このjrxmlをコンパイルした.jasperという拡張子のファイルをJavaのプログラムから読み込み、PDFを作成するというのが通常の手順です。
PDF出力のサンプルコードは次のようになります。大抵の場合はこれで事足りると思います。
// 入力パス、出力パス Path in = ... Path out = ... // .jasperを読み込む // // .jrxmlを直接読み込むこともできる // // JasperReport report = // JasperCompileManager.compileReport(in.toAbsolutePath().toString()); JasperReport report = (JasperReport) JRLoader.loadObject(in.toAbsolutePath().toFile()); // パラメータ、データソースの準備 Map<String, Object> parameters = new HashMap<>(); Collection<Map<String, ?>> source = new ArrayList<>(); ... // パラメータ、データソースの流し込み // ここでは JRMapCollectionDataSource を利用しているが、他にも JRBeanCollectionDataSource などがある。 // データソースが空の時は空の JRMapCollectionDataSource ではなく JREmptyDataSource を利用する。 // 空の JRMapCollectionDataSourc を利用すると JasperReports 6.7.0 では真っ白なPDFが作成され、エラーも発生しない問題があった。 JasperPrint print = JasperFillManager.fillReport(report, parameters, new JRMapCollectionDataSource(source)); // 書き出し JasperExportManager.exportReportToPdfFile(print, out.toAbsolutePath().toString());
テンプレートへ流し込むデータには、「パラメータ」と「データソース」の2種類があります。データソースはDetailセクションに流し込む繰り返し構造のデータで、CSVファイルをイメージすると良いです。パラメータは主にDetailセクション以外で使われる埋め込みデータです。
テンプレートの作成
まずは空のテンプレートを見ていきましょう。
次の画像はJasperReports Studioで作った空のテンプレートファイルです。テンプレートはTitle, Page Header, Column Header, Detail1, Column Footer, Page Footer, Summaryのセクションからなっています。Title, Page Headerなどはそのままの意味で、タイトルやヘッダーを表示するところです。Column Header, Detail1, Column Footerがデータを流し込んで、テーブルとして表示する場所です。
jrxmlの方も見てみましょう。Title,Page,Header...といった構造がjrxmlにも反映されていることがわかります。
<?xml version="1.0" encoding="UTF-8"?> <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="86d21441-0199-44fa-8d1e-8b7170bc739f"> <queryString> <![CDATA[]]> </queryString> <background> <band splitType="Stretch"/> </background> <title> <!-- bandタグで各セクションの高さを指定できる --> <band height="79" splitType="Stretch"/> </title> <pageHeader> <band height="35" splitType="Stretch"/> </pageHeader> <columnHeader> <band height="61" splitType="Stretch"/> </columnHeader> <detail> <band height="125" splitType="Stretch"/> </detail> <columnFooter> <band height="45" splitType="Stretch"/> </columnFooter> <pageFooter> <band height="54" splitType="Stretch"/> </pageFooter> <summary> <band height="42" splitType="Stretch"/> </summary> </jasperReport>
各セクションは必須ではなく、不要であったら削除しても構いません。例えば何枚かに渡るレポートを作りたい時、最初の1枚は表紙になるかもしれません。その時はTitleセクションだけにして大丈夫です。
文字の埋め込み
雰囲気を掴むためにjrxmlを直接編集してみます。
文字の埋め込みは staticText
タグを使います。
<title> <band height="79" splitType="Stretch"> <staticText> <!-- reportElementタグで表示場所などを指定できる --> <reportElement x="0" y="0" width="100" height="50"/> <text><![CDATA[Invoice]]></text> </staticText> </band> </title>
独自のフォントを埋め込む場合には設定の追加が必要です。特に日本語を利用する場合は必須です。今回はIPAフォントを利用します。
まず、クラスパス上に jasperreports_extension.properties
というファイルを作成し、フォントの読み込みのための設定と、フォント情報を記述したxmlファイルへのパス(fonts/fonts.xml)を指定します。
net.sf.jasperreports.extension.registry.factory.fonts=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory net.sf.jasperreports.extension.simple.font.families.font=fonts/fonts.xml
fonts.xmlは次のように記述します。
<?xml version="1.0" encoding="UTF-8"?> <fontFamilies> <fontFamily name="IPAexゴシック"> <normal>fonts/ipaexg.ttf</normal> <pdfEncoding>Identity-H</pdfEncoding> <pdfEmbedded>true</pdfEmbedded> </fontFamily> </fontFamilies>
その上で、jrxmlに textElement
タグでフォントの名前を指定します。これで日本語フォントが利用可能になります。
<title> <band height="79" splitType="Stretch"> <staticText> <!-- reportElementタグで表示場所などを指定できる --> <reportElement x="0" y="0" width="100" height="50"/> <textElement> <font fontName="IPAexゴシック" size="18"/> </textElement> <text><![CDATA[請求書]]></text> </staticText> </band> </title>
なおフォントの使用にあたってはライセンスに注意してください。
パラメータの埋め込み
パラメータの埋め込みは textField
タグを使います。textFieldExpression
として $P{パラメータ名}
を指定します。パラメータの名前はXMLの先頭で parameter
タグで宣言しておきます。
<?xml version="1.0" encoding="UTF-8"?> <!-- Created with Jaspersoft Studio version 6.6.0.final using JasperReports Library version 6.6.0 --> <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4_1" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="f7adae20-db6a-4207-ae6a-75eb4a16e40e"> <parameter name="title"/> <queryString> <![CDATA[]]> </queryString> <background> <band splitType="Stretch"/> </background> <title> <band height="79" splitType="Stretch"> <textField> <reportElement x="0" y="0" width="100" height="50"/> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <textFieldExpression><![CDATA[$P{title}]]></textFieldExpression> </textField> </band> </title> <pageHeader> <band height="35" splitType="Stretch"/> </pageHeader> <columnHeader> <band height="61" splitType="Stretch"/> </columnHeader> <detail> <band height="125" splitType="Stretch"/> </detail> <columnFooter> <band height="45" splitType="Stretch"/> </columnFooter> <pageFooter> <band height="54" splitType="Stretch"/> </pageFooter> <summary> <band height="42" splitType="Stretch"/> </summary> </jasperReport>
テーブルの作成
次のようなテーブルを作成してみましょう。
columnHeaderに見出しの内容が、detailにテーブルの一行分の内容が入ります。
<columnHeader>{ここに見出しの内容が入る}</columnHeader> <detail>{ここにテーブルの一行分の内容が入る}</detail>
<?xml version="1.0" encoding="UTF-8"?> <!-- Created with Jaspersoft Studio version 6.6.0.final using JasperReports Library version 6.6.0 --> <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="86d21441-0199-44fa-8d1e-8b7170bc739f"> <queryString> <![CDATA[]]> </queryString> <field name="fruit"/> <field name="price"/> <columnHeader> <band height="30" splitType="Stretch"> <staticText> <reportElement x="0" y="0" width="100" height="30"/> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <text><![CDATA[商品]]></text> </staticText> <staticText> <reportElement x="100" y="0" width="100" height="30"/> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <text><![CDATA[価格]]></text> </staticText> </band> </columnHeader> <detail> <band height="30" splitType="Stretch"> <textField isBlankWhenNull="true"> <reportElement x="0" y="0" width="100" height="30"/> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <textFieldExpression><![CDATA[$F{fruit}]]></textFieldExpression> </textField> <textField isBlankWhenNull="true"> <reportElement x="100" y="0" width="100" height="30"/> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <textFieldExpression><![CDATA[$F{price}]]></textFieldExpression> </textField> </band> </detail> </jasperReport>
テーブルに埋め込むデータは「パラメータ」ではなく主に「データソース」です。データソースはコレクション形式のデータで、そのコレクション内の一つの要素がテーブル一行分のデータとなります。コレクション内の要素はその中にいくつかのフィールドを持つことになります。 今回の例の場合、データソースは「fruit」、「price」というフィールドを持つ要素のコレクションです。
データソースの内容を埋め込むには、$F{フィールドの名前}
という記法を使います。フィールドの名前は field
タグであらかじめ宣言しておく必要があります。
これをレンダリングするJavaプログラムは次のようになります。
Path in = ...; Path out = ...; JasperReport report = (JasperReport) JRLoader.loadObject(in.toAbsolutePath().toFile()); Map<String, Object> parameters = new HashMap<>(); Collection<Map<String, ?>> source = new ArrayList<>(); Map<String, Object> row1 = new HashMap<>(); row1.put("fruit", "りんご"); row1.put("price", "100"); source.add(row1); Map<String, Object> row2 = new HashMap<>(); row2.put("fruit", "みかん"); row2.put("price", "50"); source.add(row2); Map<String, Object> row3 = new HashMap<>(); row3.put("fruit", "バナナ"); row3.put("price", "30"); source.add(row3); JasperPrint print = JasperFillManager.fillReport(report, parameters, new JRMapCollectionDataSource(source)); JasperExportManager.exportReportToPdfFile(print, out.toAbsolutePath().toString());
罫線や背景色の調整
reportElement
タグで背景色をつけたり、 box
タグで罫線を引いたりすることができます。
<columnHeader> <band height="30" splitType="Stretch"> <staticText> <reportElement x="0" y="0" width="100" height="30" mode="Opaque" backcolor="rgba(200, 200, 200, 0.5)"/> <box> <topPen lineWidth="1"/> <leftPen lineWidth="1"/> <bottomPen lineWidth="1"/> <rightPen lineWidth="1"/> </box> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <text><![CDATA[商品]]></text> </staticText> <staticText> <reportElement x="100" y="0" width="100" height="30" mode="Opaque" backcolor="rgba(200, 200, 200, 0.5)"/> <box> <topPen lineWidth="1"/> <leftPen lineWidth="1"/> <bottomPen lineWidth="1"/> <rightPen lineWidth="1"/> </box> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <text><![CDATA[価格]]></text> </staticText> </band> </columnHeader> <detail> <band height="30" splitType="Stretch"> <textField isBlankWhenNull="true"> <reportElement x="0" y="0" width="100" height="30"/> <box> <topPen lineWidth="0"/> <leftPen lineWidth="1"/> <bottomPen lineWidth="1"/> <rightPen lineWidth="1"/> </box> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <textFieldExpression><![CDATA[$F{fruit}]]></textFieldExpression> </textField> <textField isBlankWhenNull="true"> <reportElement x="100" y="0" width="100" height="30"/> <box> <topPen lineWidth="0"/> <leftPen lineWidth="1"/> <bottomPen lineWidth="1"/> <rightPen lineWidth="1"/> </box> <textElement verticalAlignment="Middle" textAlignment="Center"> <font fontName="IPAexゴシック" size="14"/> </textElement> <textFieldExpression><![CDATA[$F{price}]]></textFieldExpression> </textField> </band> </detail>
とまあここまでくるとJasperReports StudioのGUIも使いながらデザインするのが楽だと思います。
画像、線、矩形表示などのタグもありますが、基本的な考えは変わらないので割愛します。 より詳しくはドキュメントのJasperReports Library Ultimate Guideを参照してください。
https://community.jaspersoft.com/documentation
JasperReportsはテーブルが1つのPDF、例えば見積書や請求書のような書類を作成するのに特化しているようにも思いましたが、SubReportという機能を利用することでより複雑なレイアウトにも対応できそうです。