torutkのブログ

ソフトウェア・エンジニアのブログ

データベースを利用するアプリケーション(データベース接続)

はじめに

次の記事の続きです。

torutk.hatenablog.jp

データベースの作成が終わったら、いよいよアプリケーションからデータベースへ接続してデータの読み書きをします。

JDBC API

Java SEではRDBMSを利用する標準APIとしてJDBCJava DataBase Connectivity)が提供されています。これは利用するRDBMS製品には依存しておらず、利用するRDBMSを交換可能とする仕様となっています。

このJDBC APIを使って特定のRDBMS製品へ接続するためには、RDBMS製品固有のJDBCドライバーをSPI(Service Provider Interface)を介して組み込みます。SPIの実現には、これもJava SEの標準APIであるServiceLoaderが使われています。APIとSPIを使った実装提供により、アプリケーションからは標準のAPIのみを使ってプログラミングし、実装の切り替えはSPIで行うということが可能になります。

Microsoft SQL Server JDBCドライバー

Microsoft SQL ServerJDBCで利用する際は、Microsoftから提供されるSQL ServerJDBCドライバーを使います。このJDBCドライバーはJARファイルで提供されており、これをクラスパスまたはモジュールパスに指定することで利用できるようになります。2019年8月現在、SQL ServerJDBCドライバーはバージョン7.4.1が提供され、Java SE 8用、Java SE 11用、Java SE 12用のそれぞれのJARファイルが別々に用意されています。

今回は、Java SE 11でアプリケーションを作成・実行するので、Java SE 11用のJARファイルを使います。

プログラミング

JDBCAPIでは、データベースへ接続するためのクラスが2種類あります。

  1. DataSource
    JNDI(Java Naming and Directory Interface)を使って、外部のディレクトリ・サーバーに置かれたデータベース接続情報・ドライバーを取り出してデータベースへ接続します。アプリケーションサーバー上で動作するプログラム(Java EE環境)では一般的ですが、スタンドアロン・プログラム(Java SE環境)ではディレクトリ・サーバー相当を動かすことがないので利用しづらい方法です。

  2. DriverManager
    データベース接続情報をURL(文字列等)で指定し、ServiceLoaderで取得したJDBCドライバーを使用してデータベースへ接続します。Java SE環境ではこちらを使うことが多いです。

データベース接続の取得

java.sql.DriverManagerクラスのgetConnectionメソッドに接続情報をURL形式で指定して接続(java.sql.Connection)を取得します。

Connection conn;
  :
try {
    conn = DriverManager.getConnection("jdbc:sqlserver://localhost;databaseName=Island;user=foo;password=var");
} catch (SQLException ex) {
    // ...
}

接続情報に記載されたjdbc:sqlserverの情報から、SQL Server用のJDBCドライバーがSPIで利用可能となります。プログラム上では明示的な指定は不要です。

接続情報をハードコーディングするのは実用的ではないので、外部のファイル、環境変数等に定義してプログラムでその情報を取り込みます。DriverManagerのgetConnectionメソッドには、引数のバリエーションがいくつか用意されています。

Connection getConnection(String url) throws SQLException
Connection getConnection(String url, String user, String password) throws SQLException
Connection getConnection(String url, Properties info) throws SQLException

外部ファイルに接続情報を記述して読み込むならば、Javaのプロパティファイル形式で記述し読み込み、3番目のシグニチャでプロパティを指定するのがよさそうです。

ここで取得したConnectionインスタンスは最後にclose()する必要があります。

Spectrumテーブルからidを指定して1件のレコードを取得

Spectrumテーブルから、idを指定して、3つの列(名前、周波数の開始値、周波数の終了値)を取り出します。

public Spectrum getSpectrum(long id) {
    try (PreparedStatement statement = conn.prepareStatement("SELECT name, startFrequency, stopFrequency FROM Spectrums WHERE id=?")) {
        statement.setLong(1, id);
        ResultSet result = statement.executeQuery();
        if (result.next()) {
           String name = result.getNString("name");
           double startFrequency = result.getDouble("startFrequency");
           double stopFrequency = result.getDouble("stopFrequency");
           return new Spectrum(name, startFrequency, stopFrequency);
        }
    } catch (SQLException ex) {
        // ...
    }

安全性から、SQLはPreparedStatementで用意します。プレースホルダーでWHERE句のidの値を設定します。プレースホルダーは複数設けられるので設定に際しては何番目かを示す数値と、埋め込む値を指定します。 PreparedStatementが完成したら、executeQueryで検索系のSQLを実行します。結果はResultSet型のオブジェクトで返ります。 ResultSetの中身を取り出すには、最初にnext()を呼びます。複数レコードが結果として返ってきた場合は、1レコード毎に最初にnext()を呼びます。 ResultSetの中身を取り出すには、列名を指定するか、序数を指定します。序数は、SELECT文の場合、SELECTの次に指定する列名(カンマ区切り)の列挙の順番を示します(最初が1)。最初は序数で指定していましたが、SELECTで取り出す列名を変えたりしておりそのたびに序数を付け直すのが手間でした。そこで、列名で指定するようにしました。列名は定数で一か所に定義しておくのがよいでしょう。

PreparedStatementは実行後にcloseする必要があります。try-with-resource構文で使用すれば自動でcloseを呼び出します。

データベースを利用するアプリケーション(データベース作成)

はじめに

続きです。

torutk.hatenablog.jp

今回は、データベースにテーブルを定義し、アプリケーションから利用できるようにします。 利用するデータベースは先の日記に記載のとおり、SQL Serverです。

データベースの作成ツール

SQL Server Express Edition(またはDeveloper Edition)がインストールされた環境で開発を進めます。 データベースの新規作成、テーブルの新規作成は、SSMS(SQL Server Management Studio)から行えます。 作成後のカラムのデータ型変更などもできるので、設計が固まるまではSSMSで作業します。

ER図を試行錯誤で作成しながらテーブルの設計をしましたが、今回はER図は頭の整理のためのスケッチとしての役割に留まっています。

SQL Serverのコマンド環境は、SQL Serverをインストールしたマシンであれば、コマンドプロンプトからsqlcmdコマンドを実行すると利用できます。対話モードとSQLファイルを実行するモードがあります。

データベース作成

データベース領域の作成

テーブルを定義するために、まずデータベースを1個作成します。ここにテーブル群を定義していきます。

スペクトラムデータのテーブル

まずはデータファイルの内容をデータベースのテーブルに定義します。項目としては次を入れます。

  • 名前(元のファイル名)
  • 周波数の開始値
  • 周波数の終了値
  • 電力値の点列

かなり絞った項目としています。データの取得日時、取得に使用した計測器・配線などの計測系、取得した信号の素性、などなど盛り込みたい情報があります。計測条件、計測対象、計測記録はそれぞれ別テーブル(別エンティティ)として定義する方針とします。

テーブルのスキーマ(現時点の結果)

列名 データ型 NOT NULL制約 備考
id bigint あり 主キー(自動インクリメント)
name nvarchar(50) なし スペクトラム
startFrequency real あり 周波数の開始値
stopFrequency real あり 周波数の終了値
spectrum varbinary(max) あり スペクトラム点列

先に挙げた項目の他に、idを追加しています。RDBMSでは、レコードを一意に識別する主キーが必要です。名前を主キーにするとしたら、名前は任意に付与できるので一意を保つにはアプリケーション側(ユーザー側)で頑張る必要があります。これは運用がやっかいなので避けたいところです。また、他の項目でレコードを一意に表現できるものはありません。そこで、サロゲートキーを設けることとします。

NOT NULL制約は、データを成立させるのに不可欠な項目に付与します。

idの設計

idは、整数でインサートごとに自動でインクリメントして採番します。データ型は最初int(32bit符号付整数)としていましたが、intだとレコード数が21億( 2^{31}-1)を超えられないので、多数のデータを保持する可能性のあるテーブルではbigintにしておきます。 自動インクリメントにするには、SSMS上では列のプロパティから、IDENTITYの指定でIDであるを[いいえ]から[はい]に変更します。

nameの設計

スペクトラムデータの名称を保持します。SQL Serverの文字列は、ユニコード文字と非ユニコード文字とそれぞれのデータ型が用意されています。nchar、nvarcharがユニコード文字のデータ型で、char、varcharが非ユニコード文字のデータ型です。

日本語Windows環境でデフォルトで構築した場合、非ユニコードはデフォルト(既定)ではコードページ932(Windows_31J)で扱われます。アプリケーション側の文字コードユニコードなので、データベース側で非ユニコードのデータ型を使うと文字コード変換が生じていろいろ面倒になりそうです。そこで、ASCII文字に限定できない場合はユニコード文字を扱うデータ型とします。

nvarcharでサイズ50を指定した場合は、最大50文字(UCS-2の範囲)となります。補助文字を使用する照合順序を指定した場合は、UTF-16となるので、サロゲートペア文字が使われた場合は50文字より少なくなります。

なお、SQL Serverでは、文字列を格納するデータ型にtext、ntextもありますが、これらは非推奨となっています。

周波数の開始値、終了値

小数点の数値は、float、real、decimal のデータ型が使えます。(numeric型もありますが、SQL Serverではdecimalと同一です。)

floatとrealは浮動小数点数のデータ型で、decimalは固定長の有効桁数(10進数)と小数点以下の桁数を指定したデータ型です。 floatは有効桁数(仮数部のbit数)を1~53で指定しますが、内部では24もしくは53のどちらかで扱います。 realは、float(24)と同義です。 周波数の値はそれほど有効桁数を必要としないので、今回はストレージ上のサイズが小さくてすむrealを使用します。

spectrumの設計

スペクトラムデータの点列を保持します。バイナリデータですが点数がまちまちのため可変長のバイナリとします。サイズを数値指定する場合は1~8000の範囲なので最大8000バイトとなります。しかし、格納したい点列は1万点を超えることがあるので、この場合はサイズにmaxを指定します。maxを指定すると、最大で  2^{31} - 1 バイトまで格納可能です。

なお、binary、image は非推奨となっています。

計測記録テーブルへの参照を含めるか

後で作成する計測記録のテーブルには、スペクトラムデータを参照する外部キーを設ける予定です。 その逆方向としてスペクトラムのテーブルに計測記録への外部キーを設けるかどうか悩みました。 双方向の関連を持つか、片方向の関連のみとするか、現時点では明確な根拠を持てないでいます。必要性を感じるまでは、スペクトラムのテーブルには計測記録への外部キーは持たせないでおきます。

Microsoft SQL Server JDBCドライバーにmodule-info定義を追加してモジュール対応

はじめに

Microsoft SQL Server JDBCドライバーのモジュール対応(module-info.classの組み込み)を試みました。経過をメモします。

経緯

SQL Server JDBCドライバーは、自動モジュール対応までしており、モジュールアプリケーションから利用できます。自動モジュールとは、JARファイルにはmodule-info.classが組み込まれておらず、マニフェストファイルのAutomatic-Module-Name属性にモジュール名が定義されたJARファイルです。

しかし、アプリケーションの配布イメージをJLinkで作成する際、自動モジュールを使用しているアプリケーションではエラーとなってしまいます。JLinkは、自動モジュールに対応していないためです。

そこで、後付けでmodule-info.classをSQL Server JDBCドライバーのJARファイルに突っ込み、モジュール対応したものとしてJLinkにかけてみます。

module-info.classの生成とJARファイルへの後付け(試行1回目)

まず、JDBCドライバーのJARファイル mssql-jdbc-7.4.1.jre11.jar の依存関係を抽出します。jdepsコマンドを使いますが、オプション指定にいろいろあり、何が正解か分かりませんので試した結果をメモします。

mssql-jdbc-7.4.1.jre11.jarの依存関係を調べる

--generate-module-info
D:\work> mkdir out
D:\work> jdeps --generate-module-info out mssql-jdbc-7.4.1.jre.jar
Missing dependence: D:\work\out\com.microsoft.sqlserver.jdbc\module-info.java not generated
エラー: 依存性が欠落しています
   com.microsoft.sqlserver.jdbc.KeyVaultCredential    -> com.microsoft.aad.adal4j.AuthenticationCallback    見つかりません
  :
--list-deps
D:\work> jdeps --list-deps mssql-jdbc-7.4.1.jre11.jar
   java.base
   java.logging
   java.naming
   java.security.jgss
   java.sql
   java.transaction.xa
   java.xml
--list-reduced-deps
D:\work> jdeps --list-reduced-deps mssql-jdbc-7.4.1.jre11.jar
   java.base
   java.naming
   java.security.jgss
   java.sql

--list-deps オプションと --list-reduced-deps オプションの差は、後者は推移的な依存関係のあるものはリストされないというもののようです。

module-info.javaを作成

module名は、mssql-jdbc-7.4.1.jre11.jar の自動モジュールで公開しているモジュール名 com.microsoft.sqlserver.jdbc とします。 APIとして公開するパッケージ com.microsoft.sqlserver.jdbc をexports宣言し、このAPIが実行時に最低限必要なモジュールをrequires宣言します。必要なモジュールは、先のjdepsコマンド(--list-reduced-depsオプション)で調べたものを記述します。なお、java.baseは暗黙でrequiresされるので省略します。

module com.microsoft.sqlserver.jdbc {
    exports com.microsoft.sqlserver.jdbc;
    requires java.naming;
    requires java.security.jgss;
    requires java.sql;
}

module-info.java から module-info.classを生成し、jarファイルに追加

D:\work> javac --patch-module com.microsoft.sqlserver.jdbc=mssql-jdbc-7.4.1.jre11.jar module-info.java

module-info.class が生成されます。 このmodule-info.class をJARファイルに追加します。

D:\work> jar uvf mssql-jdbc-7.4.1.jre11.jar module-info.class
module-infoが更新されました: module-info.class

JLink で使用

JDBCを使用するアプリケーションの配布イメージをjlinkで作成します。

エラー: 署名済モジュラJAR D:\work\mssql-jdbc-7.4.1.jre11.jarは現在サポートされていないため、エラーを抑止するには--ignore-signing-informationを使用します

とエラーになったので、jlinkのコマンドにオプション --ignore-signing-information を追加します。

警告: 署名済モジュラJAR D:\toru\Documents\work\job\mcc\Glowlink\spectrum_viewer\.\mssql-jdbc-7.4.1.jre11.jarは現在サポートされていません

とjlinkコマンドがエラーから警告に変わり、配布イメージが生成されました。

配布イメージのモジュールにJDBCが含まれるかを確認

D:\work\myapp\out\runtime> bin\java --list-modules
myapp
com.microsoft.sqlserver.jdbc
java.base@11.0.4
java.datatransfer@11.0.4
  :

一応生成できています。

実行

D:\work\myapp\out\runtime> bin\myapp.bat
java.sql.SQLException: No suitable driver found for jdbc:sqlserver://localhost;databaseName=spectrum;user=foo;password=bar
        at java.sql/java.sql.DriverManager.getConnection(Unknown Source)

JDBCドライバが見つからないとのエラーになってしまいました。

回避策(場当たり的な)

本来は不要ですが、プログラム中でJDBCドライバークラスを明示的にnewして登録すると実行可能となりました。

static {
    try {
        DriverManager.registerDriver(new com.microsoft.sqlserver.jdbc.SQLServerDriver());
    } catch (SQLException ex) {
        logger.log(Level.WARNING, "DriverManager cannot register", ex);
    }
}

実行時エラーの調査

JDBCドライバーが見つからないという実行時エラーの原因を探ってみます。

オリジナルのJDBCドライバーのモジュール情報

D:\libs> jar --describe-module --file mssql-jdbc-7.4.1.jre11.jar
モジュール・ディスクリプタが見つかりません。自動モジュールが導出されました。

com.microsoft.sqlserver.jdbc@7.4.1.jre11 automatic
requires java.base mandated
provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver
contains com.microsoft.sqlserver.jdbc
contains com.microsoft.sqlserver.jdbc.dataclassification
contains com.microsoft.sqlserver.jdbc.dns
contains com.microsoft.sqlserver.jdbc.osgi
contains microsoft.sql
contains mssql.googlecode.cityhash
contains mssql.googlecode.concurrentlinkedhashmap
contains mssql.security.provider

module-info.classを後付けしたJDBCドライバーのモジュール情報

D:\work> jar --describe-module --file mssql-jdbc-7.4.1.jre11.jar
com.microsoft.sqlserver.jdbc jar:file:///D:/work/mssql-jdbc-7.4.1.jre11.jar/!module-info.class
exports com.microsoft.sqlserver.jdbc
requires java.base mandated
requires java.naming
requires java.security.jgss
requires java.sql
contains com.microsoft.sqlserver.jdbc.dataclassification
contains com.microsoft.sqlserver.jdbc.dns
contains com.microsoft.sqlserver.jdbc.osgi
contains microsoft.sql
contains mssql.googlecode.cityhash
contains mssql.googlecode.concurrentlinkedhashmap
contains mssql.security.provider

モジュール情報の違いを分析

元のmssql-jdbc-7.4.1.jre11.jarと、module-info.classを後付け挿入したmssql-jdbc-7.4.1.jre11.jarとの違いを見ると、今回の実行時エラー(ドライバーが見つからない)に関係しそうな相違点は、provies項です。

provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver

各種JDBCドライバーは、SPI(Service Provider Interface)で提供されており、モジュール仕様以前のクラスパスではJARファイルの中にMETA-INF/services/java.sql.Driver ファイルを置き、このファイルの中にSPIの実装クラスのFQCNを記載します。 mssql-jdbc-7.4.1.jre11.jar の場合は、このファイルの中に次の記述があります。

com.microsoft.sqlserver.jdbc.SQLServerDriver

このJARファイルがクラスパスにあると、サービスローダーがSPIの実装クラスを認識してロードすることができます。

モジュール仕様では、SPIの実装はモジュール定義(module-info.java)にprovides宣言で記述します。

module-info.classの生成とJARファイルへの後付け(試行2回目)

module-info.javaに、provides宣言を追加し、コンパイルしたmodule-info.classをJARファイルに追加します。

module-info.javaの再記述

module com.microsoft.sqlserver.jdbc {
    exports com.microsoft.sqlserver.jdbc;
    provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver;
    requires java.naming;
    requires java.security.jgss;
    requires java.sql;
}

module-info.classの再生成

D:\work> javac --patch-module com.microsoft.sqlserver.jdbc=mssql-jdbc-7.4.1.jre11.jar module-info.java

module-info.class を mssql-jdbc-7.4.1.jre11.jar に挿入

D:\work>jar uvf mssql-jdbc-7.4.1.jre11.jar module-info.class
module-infoが更新されました: module-info.class

モジュール情報の確認

D:\work> jar --describe-module --file mssql-jdbc-7.4.1.jre11.jar
com.microsoft.sqlserver.jdbc jar:file:///D:/work/mssql-jdbc-7.4.1.jre11.jar/!module-info.class
exports com.microsoft.sqlserver.jdbc
requires java.base mandated
requires java.naming
requires java.security.jgss
requires java.sql
provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver
contains com.microsoft.sqlserver.jdbc.dataclassification
contains com.microsoft.sqlserver.jdbc.dns
contains com.microsoft.sqlserver.jdbc.osgi
contains microsoft.sql
contains mssql.googlecode.cityhash
contains mssql.googlecode.concurrentlinkedhashmap
contains mssql.security.provider

provides項が見えています。

JLinkで配布イメージ生成後実行

SQL Serverに接続して無事データベースアプリケーションとして実行できるようになりました。

まとめ

  • Microsoft SQL Server用のJDBCドライバー(ライブラリ)は、自動モジュール(Automatic module)形式のJARファイルとして配布されている(Ver. 7.0以降)
  • 自動モジュールのJARは、モジュール対応アプリケーションからモジュール指定でコンパイル・実行ができる
  • 自動モジュールのJARを使用しているモジュール対応アプリケーションは、JLinkで配布イメージを生成することができない
  • 非モジュール(自動モジュールも含めて)のJARをモジュールJARにするには、module-info.javaを記述してコンパイルした後、JARファイルに追加する
  • module-info.javaを記述するには、jdepsコマンドで依存関係を解析する

データベースを利用するアプリケーション(作成準備編)

動機

先月から折れ線グラフで表示するプログラムをJavaFXで作っています(自宅や出張中の宿で細々とです。出張先では通勤時間とか家事とかがないので捗りました)。

torutk.hatenablog.jp

スペクトラム・アナライザのデータをファイルから読み込む機能を持たせていますが、ファイルの数が増えてくると、ファイルの管理が大変になります。例えば、どの信号を解析したものか、それはいつのものか、解析に使った計測器は何か、計測時の条件はどうだったか、などなどをファイルと関連付けて管理しなくてはなりません。

ディレクトリ名とファイル名を工夫する等で多少は関連付けを表現できます。解析対象、計測器、計測日時などをディレクトリとし、条件をファイル名にする、といった工夫です。

信号種類A \ XWV8765-BA \ 20190818-02-51 \ ATT15_SPAN100M_RBW20K_GX.trace

しかし、単一ツリーのディレクトリ管理ではやがて限界に達してしまいます。特にディレクトリの底の条件、ファイル名に含んだ条件を探すときなどです。

そこで、データベースを使ってこれらのデータを系統立てて保存・管理し、表示したいデータを条件を指定してすぐに取り出せるようにします。

データは、ローカルなネットワーク上に置いて複数台のクライアントから利用可能とします。

どんなデータベースを使うか?

スペクトラム・アナライザのデータ(スペクトラム・アナライザの画面に表示されるある瞬間のデータ)は、1GHzの周波数範囲をRBW 20KHzで取得していた場合、 5万点の点列データとなります。周波数とパワー(X軸とY軸)を1点ごとに1レコードで管理すると1回分で5万レコードになります。1時間に1回計測があるとしたら、年間で1台あたり4億レコード超といった計算になります(10秒に1回だと1兆レコード超え)。

もしリアルタイムにこのレコードを記録し検索するのであれば、時系列データベースなどを使うことを検討します。

今回はスペクトラムデータを、計測条件を指定して取り出し表示するアプリケーションで使うことが主なユースケースとなります。スペクトラムデータの格納はオフラインで別途行うため、書き込みは検討外とします。 そこで、条件を指定してデータを取り出すことに向いていると考えるRDBMSを使うことにします。

ただし、点列データを1点1点レコードに入れるとサイズが増大してしまいますので、データベース設計上の課題とします。解決方針は、1回分の点列データをバイナリデータとして1つのレコードに格納します。 点列データの利用方法は、1回分のデータを一括して取り出すもので、点列データの中の特定の値だけ検索して取りだすことはないためです。(アプリケーションの機能として点列データの一部を切り取ることはありますが、データベースには要求しません)

データベースと開発ツール

RDBMSの選択

今回は、ゼロベースの選定はしません。既にある計測器の中で使われており、またローカルのネットワーク上に既にインストールしているデータベースにMicrosoft SQL Serverがあるので、それを使うこととします。

SQL Serverは、大規模(高スペックなサーバー上で稼働)用のEnterprise Edition、中規模用のStandard Editionがあり、それぞれ有償ライセンス(180日間の試用期間あり)となっています。また、小規模(1つのデータベース領域<10GB)のExpress Editionが無償ライセンスで提供されています。開発・テスト用にはDeveloper Editionが無償で提供されています。

SQL Serverの開発環境

開発用に、Express EditionあるいはDeveloper Editionを開発マシンに入れると便利です。 管理ツールとして、SQL Server Management Studio(SSMS)を合わせて入れます。

データベース設計用にはER図も使えるA5:SQL Mk-2を入れてみます。これはWindows用のみですが、無償で利用可能です。自宅環境なのでこれを使うことにしました。

仕事上であれば、有償のデータベース設計用ツールも視野に入れて、ER/Studio、ERWin、SI Object Browserなども候補に挙げるといいかと思います。

JavaからSQL Serverの利用

SQL Server用のJDBCドライバーがMicrosoftから提供されています。

JavaからRDBMSを使うときのAPI選定

JavaJava SE)からRDBMSを使うときに、APIとしていくつかの選択肢があります。

  • JDBC APIを使う JDBCドライバーを入れて、Java SE標準のJDBC APIを使いDBとデータをやり取りする方法
  • Java Persistent APIJPA)を使う JPA仕様に準拠したサードパーティ・ライブラリ(eclipselink等)を使い、JPAのO/Rマッパー経由でDBとデータをやり取りする方法
  • サードパーティ・ライブラリ独自APIを使う 標準APIではなく、サードパーティ・ライブラリが独自に提供するAPIを使います。いろいろ種類がありますが、おおざっぱに分類すると
    • JDBCをラップする比較的小さなライブラリ(iciql、jooq、speedment等)を使う
    • 独自のマッパーを提供する比較的大きなライブラリ(MyBatis、DOMA等)を使う

今回のAPI選定基準

次の観点を選定基準にします。

  • Java SE 以外のサードパーティ・ライブラリへの依存を少なくしたい
  • アプリケーションのソースコード構成を簡潔に保ちたいので自動生成のソースや設定ファイルは極力無にしたい
  • RDBMSでのテーブル(スキーマ)設計、SQLの書き方をこの機に勉強したい

ということで、JDBC APIを使う方法とします。

RDBMSの勉強を上げたのは、RDBMSアプリ開発での利用経験が少ないからです。 これまでのソフトウェア開発経験で使ったデータベースは、Microsoft AccessのJETエンジン、ObjectStore DB(OODBMS)です。OracleSQLiteMySQL、H2 Database Engine(+JPA)は開発インフラとして整備、チューニングに携わりましたが、これらDBをアプリから使う開発はあまりしていません。 ソフトウェア開発とは別に、Redmineの環境構築・運用管理でMySQLを扱っているので、RDBMSの素養はRedmineの管理経験に拠るところが大きいです。

JDBCドライバーを使ってDBアクセス

Java SEのJDBC APIでは、まず使用するデータベース用のJDBCドライバーを入れる必要があります。

SQL Server用のJDBCドライバーの入手

Microsoft SQL Serverについては、MicrosoftからType 4 JDBCドライバーが提供されています。

Microsoft SQL Server 用 JDBC Driver のダウンロード - SQL Server | Microsoft Docs

2019-08-18時点では、最新バージョン7.4.1がリリースされています。これは、

となっています。

これをダウンロードし、適切なディレクトリに展開します(リリースノートではC:\Program Files下を推奨)。今回は、@C:\Program Files\Java\sqljdbc_7.4@に展開しました。 JDK 8用、JDK 11用、JDK 12用のJARファイルが存在します。

mssql-jdbc-7.4.1.jre8.jar
mssql-jdbc-7.4.1.jre11.jar
mssql-jdbc-7.4.1.jre12.jar

JDK 11で使用する場合

Javaコンパイル、実行時にモジュールパスにJDBCドライバーのJARファイルを指定します。

C:\work> javac -p "C:\Program Files\Java\sqljdbc_7.4\mssql-jdbc-7.4.1.jre11.jar;out\myapp" ...

JDBC APIを使ったプログラミング

JDBC 4.0(Java SE 6以降)では、JDBCドライバーがクラスパス上にあれば次の2つの方法で接続します。

  • DriverManager を使う
  • DataSource を使う

JDBCのドキュメントによると、DriverManagerよりもDataSourceが推奨されています。ソースコードにデータベース接続設定(ホスト、ユーザー、パスワード等)を記述するDriverManagerよりも、JNDIで接続する外部(ディレクトリサーバー等)に設定を置くDataSourceがよいということです。アプリケーションサーバー上で実行する場合は簡単に利用可能です。

しかし、Java SE 環境では、 JNDI経由でDataSourceを使うのは敷居が高いので、DriverManagerを使う方法で実装します。

JavaFXでGUI全体のフォントをCSSで指定

はじめに

前回のブログの続きです。 torutk.hatenablog.jp

画面の国際化対応で日本語プロパティを追加してみました。その際使用される日本語のフォントはJavaのコードやCSSでコントロール毎に指定可能ですが、全体を変更することも可能です。

CSSのrootセレクタに-fx-font-familyでフォントを指定します。 ここで、どのようなフォントファミリ名を指定できるか、OpenJDK 11で試してみました。

CSSの記述例

JavaFXCSSファイルを作成、.rootセレクタに-fx-font-familyでフォント名を指定します。

.root {
    -fx-font-family: "Meiryo";
}

当初いろいろググってみたところ、「外部フォント」の指定には、@font-faceでフォントファイルのURLを記述、なおTTFファイルは有効でTTCファイルは指定できないといった記述をみかけました。 しかし、Windows OS上でOpenJDK 11をにおいては、@font-faceを指定しなくてもWindows OSのフォントとしてインストールされているフォントの指定が可能でした。

動作確認(画面キャプチャ)環境

項目 内容
OS Windows 10 64bit (1903)
JDK Scene Builder 11.0内蔵のOpenJDK 11

Scene Builderで画面を作成している場合、Scene Builderの[プレビュー]メニュー > [Sceneスタイルシート] > [スタイルシートの追加] でCSSファイルを指定します。 すると、Scene Builder上のデザイン画面、およびプレビュー画面は指定したCSSファイルの設定が反映されます。 また、CSSファイルを変更すると自動で変更を反映してくれます。ですので、CSSでの見栄え設定であれば、アプリケーションを再ビルド・実行することなくScene Builder上で確認できるのでとっても楽に確認ができました。

CSSファイルでのフォント指定とその画面

では、実際にCSSファイルのrootセレクタにフォントファミリを指定して表示を確認していきます。

まずはデフォルト(指定なし)の日本語フォント

まずは、何もフォントファミリを指定しなかった場合の日本語フォントです。最初は、OpenJDKのfontconfig.propertiesで定義されるMSゴシックが使われると思っていました。しかし実行してみるとアンチエイリアスが効いたフォントが表示されました。

f:id:torutk:20190728103937p:plain

デバッグプリントでコントロール(Label)のフォントを調べてみると、次のようになっていました。

Font[name=System Regular, family=System, style=Regular, size=12.0]

これは、javafx.scene.text.FontクラスのgetDefaultメソッドが返すフォントで、JavaFXAPIドキュメント(JavaFX 8 日本語APIドキュメント)には次の記述があります。

デフォルト・フォントのファミリはSystem、スタイルは通常はRegularとなり、サイズについては、判断可能な範囲においてユーザーのデスクトップ環境と一致するものが取得されます。

Windows 10 日本語版では、デスクトップのシステムフォントが「游ゴシック」となっているので、デフォルトで表示されるフォントが游ゴシックとなっています。 Windows 7 日本語版であれば、おそらくMSゴシックではないかと思われます。

フォントファミリにMS Gothicを指定

CSSファイルに以下を記述します。

.root {
    -fx-font-family: "MS Gothic";
}

表示は次の様になります。お馴染みのギザギザフォントですね。

f:id:torutk:20190728104948p:plain

フォントファミリ名は、英語で記述しました。日本語で「MS ゴシック」等と記述した場合は設定が反映されず、デフォルトフォントで表示されました。

フォントファミリにMeiryoを指定

.root {
    -fx-font-family: "Meiryo";
}

f:id:torutk:20190728105456p:plain

フォントファミリにMeiryo UIを指定

.root {
    -fx-font-family: "Meiryo UI";
}

f:id:torutk:20190728105532p:plain

フォントファミリにYu Minchoを指定

.root {
    -fx-font-family: "Yu Mincho";
}

f:id:torutk:20190728105609p:plain

フォントファミリにYu Mincho Demiboldを指定

.root {
    -fx-font-family: "Yu Mincho Demibold";
}

f:id:torutk:20190728105653p:plain

フォントファミリにHGMinchoEを指定(HG明朝E)

.root {
    -fx-font-family: "HGMinchoE";
}

f:id:torutk:20190728105733p:plain

JavaFXのグラフの凡例部分を操作可能にする

はじめに

先週書いた次のブログの続編です。 torutk.hatenablog.jp

一つのグラフ表示領域に、複数のグラフ(Series)を重畳表示した際に、簡単な操作でそれぞれのグラフを非表示にしたり表示したりとできると使い勝手が向上します。

グラフに凡例を表示した場合、それぞれの凡例をクリックするとその凡例に対応するグラフを表示する、非表示すると切り替えられるようにします。

課題

凡例全体のNodeは、Legendクラス(com.sun.javafx.chart.Legend)で実装されています。しかし、このcom.sun.javafx.chartパッケージは、アプリケーション側のモジュールには公開されていません。

ぐぐって検索した情報は、子ノードを辿ってLegendクラスかどうか判別してLegendを見つけ、Legendにキャストしてそのitemを取得して利用するもので、Java SE 9以降では使えません(--add-exportsオプション指定でコンパイルすれば可能)。

各凡例を表示するNodeを取得する方法

それぞれの凡例は、CSSセレクタ .chart-legend-item が指定されています。JavaFXAPIには、セレクタを指定してNodeを取得する機能(lookupおよびlookupAll)が提供されているので、これを使って凡例のNodeを取得します。

@FXML
LineChart<Float, Float> spectrumChart;
:
Set<Node> legendItems = spectrumChart.lookupAll(".chart-legend-item");

それぞれの凡例にマウスハンドラーを設定するには例えば次のようにします。

legendItems.forEach(item -> item.setOnMouseClicked(event -> System.out.println(event + " clicked")));

各凡例をクリックしたら対応するグラフを表示/非表示する

クリックされた凡例のNodeにマウスハンドラーを設定することはできましたが、次はそこから対応するグラフを取得して、その表示を切り替える方法を考えなくてはなりません。

それぞれの凡例はLabelクラスのインスタンスで、ラベルに表示されるテキストはグラフ(Series)のnameプロパティとなっています。

そこで、取得したそれぞれの凡例をLabel型にキャストしてラベルに表示されるテキストを取得し(getTextメソッド)、次にそのテキストを名前として持つSeriesをLineChartから探して取得します。

Labelにキャストしてテキストを取り出す

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
    }
}));

Label型へのキャストが可能な場合に、Labelへキャストし、getTextメソッドでラベルに表示されるテキストを取得します。

チャート(LineChart)に登録されているSeriesからテキストと同じ名前のものを取り出す

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
    }
}));

chartのdataプロパティには表示するグラフ(Series)がリストで登録されています。これを順次アクセスしSeriesのnameプロパティが凡例ラベルのテキストと一致するものを取り出します。

クリックした凡例に一致するグラフ(Series)の表示/非表示を切り替える

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
        any.ifPresent(series -> {
            boolean currentVisible = series.getNode().isVisible();
            series.getNode().setVisible(!currentVisible);
        });
    }
}));

取り出したグラフ(Series)のgetNodeメソッドで画面に表示されるグラフのNodeを取得し、その表示状態をisVisibleで取得します。そして、その真偽を反転させてsetVisibleに指定し、表示/非表示を反転させます。

グラフが非表示のときは凡例の表示を薄くしたい

実現方法を悩みましたが、CSSの疑似クラスを使って非表示の際に薄く(透過度を変えて)表示させることにしました。

まず、CSSで、.labelに疑似クラス haze を記述します。

.label:haze {
    -fx-opacity: 0.2;
}

次に、凡例ラベルのマウスハンドラー内でグラフ(Series)の表示/非表示切り替えに合わせて疑似クラスの有効・無効を凡例ラベルに設定します。

private static final PseudoClass HAZE_PSEUDO_CLASS = PseudoClass.getPseudoClass("haze");
    :
legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
        any.ifPresent(series -> {
            boolean toBeInvisible = series.getNode().isVisible();
            series.getNode().setVisible(!toBeInvisible);
            item.pseudoClassStateChanged(HAZE_PSEUDO_CLASS, toBeInvisible);
        });
    }
}));

JavaFXで折れ線グラフを出そうとしてみた

はじめに(お題)

電気信号を時間領域から周波数領域に変換し、周波数に対するエネルギー量を解析できる測定器にスペクトラム・アナライザがあります。今回、このスペクトラム・アナライザで解析した結果をファイルに保存し、別な場所にあるPC上でその結果を表示、分析したいとします。

計測器にCSVファイルで結果を保存する機能があればこれをExcelで読み込ませてグラフ化することは容易ですが、今回の計測器はバイナリデータのファイルに保存するものでした。そこで、JavaFXで保存したバイナリデータのファイルを読み込み折れ線グラフ(LineChart)で表示するプログラムを作成してみます。

LineChart

JavaFXのプロジェクトを作成し、折れ線グラフ(LineChart)を表示する画面をScene Builderで作成しました。 ファイル構成は次です。

ファイル名 内容
SimpleSpectrumApp.java Application派生クラス、プログラムのエントリ(main)
SimpleSpectrumView.fxml 画面(ビュー)のレイアウトを定義するFXMLファイル
SimpleSpectrumView.css 画面(ビュー)の見栄えを定義するCSSファイル
SimpleSpectrumViewController.java 画面(ビュー)に対応するコントローラークラス
SimpleSpectrumViewModel.java GUI内のモデルクラス

デフォルトのLineChart

Scene BuilderでLineChartを画面にポトペタすると次の表示となります。 f:id:torutk:20190706211210p:plain

縦軸は数値メモリとなっていますが、横軸は表示されていません。Scene Builderの左下ペインを見ると、LineChartの子ノードとしてNumberAxisとCategoryAxisが1つずつ置かれています。縦軸がNumberAxisで、横軸がCategoryAxisです。NumberAxisは名前の通り数値の軸を表し、CategoryAxisは何らかの分類(県名、年別、チーム別、など)の軸を表します。

Scene Builder上では、LineChartの子ノードの軸を変更する方法が見つからなかったので、FXMLファイルを直接エディタで開き修正しました。

      <LineChart BorderPane.alignment="CENTER">
        <xAxis>
-         <CategoryAxis side="BOTTOM" />
+         <NumberAxis side="BOTTOM" />
        </xAxis>
        <yAxis>
          <NumberAxis side="LEFT" />
        </yAxis>
      </LineChart>

横軸がNumberAxisに変更されました。 f:id:torutk:20190706212808p:plain

最初のコード

SimpleSpectrumApp.java
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class SimpleSpectrumApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("SimpleSpectrumView.fxml"));
        primaryStage.setTitle("Simple Spectrum");
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

JavaFXの典型的なApplication派生クラスの実装です。mainメソッドでプロセスのエントリを用意し、startメソッドでFXMLファイルを読み込みシーングラフを作成、Stageを表示させています。

SimpleSpectrumView.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.SimpleSpectrumViewController">
   <center>
      <LineChart fx:id="chart" BorderPane.alignment="CENTER">
        <xAxis>
          <NumberAxis side="BOTTOM" fx:id="xAxis" />
        </xAxis>
        <yAxis>
          <NumberAxis fx:id="yAxis" side="LEFT" />
        </yAxis>
      </LineChart>
   </center>
</BorderPane>

Scene Builderでポトペタ作成したFXMLファイルで、ビュー(画面)を定義しています。 コントローラークラスを指定しているので、このFXMLがロードされると自動でコントローラークラスのインスタンスが生成されます。

SimpleSpectrumViewController.java
package sample;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;

import java.net.URL;
import java.util.ResourceBundle;

public class SimpleSpectrumViewController implements Initializable {
    @FXML
    LineChart chart;
    @FXML
    NumberAxis xAxis;
    @FXML
    NumberAxis yAxis;

    SimpleSpectrumViewModel model = SimpleSpectrumViewModel.ofRandom();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        chart.getData().setAll(model.getSeriesList());
    }
}

FXMLに対応するコントローラークラスです。 コントローラーから操作するビュー部品(コントロール)は、@FXMLアノテーションを指定し、FXML側で指定したfx:idの識別子と同じ名前の変数名とすることで、FXMLロード時にインスタンスがインジェクションされます。

データについては、今回はデータファイルの読み込み部分が未実装なので、ランダムな値を表示するモデルを使います。

SimpleSpectrumViewModel.java
package sample;

import javafx.scene.chart.XYChart;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SimpleSpectrumViewModel {

    private List<XYChart.Series<Double, Double>> seriesList = new ArrayList<>();

    public List<XYChart.Series<Double, Double>> getSeriesList() {
        return Collections.unmodifiableList(seriesList);
    }

    public static SimpleSpectrumViewModel ofRandom() {
        var model = new SimpleSpectrumViewModel();
        model.seriesList.add(createRandomSeries("Random"));
        return model;
    }

    private static XYChart.Series<Double, Double> createRandomSeries(String name) {
        XYChart.Series<Double, Double> series = new XYChart.Series<>();
        series.setName(name);
        for (double x = 950d; x <= 1450d; x++) {
            series.getData().add(new XYChart.Data<Double, Double>(
                    x, ThreadLocalRandom.current().nextGaussian() * 5.0 - 60.0));
        }
        return series;
    }
    
}

JavaFXではモデルクラスについては特にフレームワークが用意されていませんが、コントローラーを軽くするために、ちょっとしたロジックであってもモデルクラスを設けるようにしています。

表示確認のために、データファイルを読み込む前にランダムなスペクトルを表示するモデルを作りました。

JavaFXの折れ線グラフ(LineChart)に表示する1本の連続する線は、データ系列(XYChart.Series)で定義し、複数のデータ系列を表示することができます。 データ系列は、複数の点データ(XYChart.Data)のリストとして作られています。 今回は、1つのデータ系列を用意しています。 用意するデータは、周波数軸(X軸)の950~1450までの間を1刻みで作成し、縦軸の電力は正規分布乱数で平均-60.0、分散5.0としています。

表示

プログラムを実行すると、次の画面が表示されました。

f:id:torutk:20190707043824p:plain

いろいろ改善したい点があります。

  • 縦軸、横軸に名称・単位を表示したい
  • 〇の表示(シンボル)は不要としたい
  • 折れ線を細くしたい
  • 縦軸、横軸の表示範囲を指定したい

LineChartの設定

縦軸、横軸に名称を表示

これは、Scene Builderで、軸のLabelプロパティに文字列を設定すれば表示できます。 f:id:torutk:20190707050535p:plain

縦軸、横軸の表示範囲を指定

縦軸は、-30.0~-80.0の範囲を表示し、横軸は950~1450の範囲を表示します。 表示すべきデータを読み込んだ際にデータに応じた値を設定するので、コードで制御します。

前回からのコード変更部分を次に記載します。

    @Override
    public void initialize(URL location, ResourceBundle resources) {
+       xAxis.setLowerBound(950d);
+       xAxis.setUpperBound(1450d);
+       yAxis.setLowerBound(-80d);
+       yAxis.setUpperBound(-30d);
        chart.getData().setAll(model.getSeriesList());
    }

NumberAxisには、表示する軸の値の上限値・下限値を指定するlowerBound、upperBoundがあります。 ここに表示する軸の数値範囲を指定します。この上下限値設定は、LineChartにデータ(XYChart.Series)を登録する前に実行する必要があります。あとから実行したら表示には反映されません。

ただし、これだけでは意図したとおりの動作をしません。NumberAxisのAutoRangingプロパティが有効だと、登録したデータの値に応じた軸の数値範囲に自動調整が働いてしまいます。今回はScene Builder上でAutoRangingプロパティを無効にします(次の図参照)。

f:id:torutk:20190707101033p:plain

プログラムを実行すると、次の様に意図した数値範囲でグラフが表示されました。

f:id:torutk:20190707101215p:plain

  • グラフの罫線間隔については、NumberAxisのTickUnitプロパティで制御可能です。
〇の表示(シンボル)をなくす

デフォルトでは、折れ線グラフの点データに丸いシンボルが表示されます。 データ数が少ないときは、データとデータを補完する線とを明確に表現するこの表示が適していますが、データ数が多いときは上図のようにシンボルだらけで肝心の折れ線が見えなくなってしまいます。

シンボルをなくすのであれば、LineChartのCreateSymbolプロパティを無効にします(次の図参照)。

f:id:torutk:20190707103825p:plain

プログラムを実行すると、次のとおり丸いシンボルが表示されなくなりました。

  • シンボルを消すのではなく、シンボルの形状を変更したいときは、CSSセレクターに.chart-line-symbolを指定し見栄えを記述します。

f:id:torutk:20190707104015p:plain

折れ線を細く

デフォルトでは、折れ線の幅がちょっと太いので、これを細くします。形状の変更はCSSで記述します。 セレクターに.chart-series-lineを指定し見栄えを記述します。

.chart-series-line {
    -fx-stroke-width: 0.75px;
}

CSSファイルは、アプリケーション派生クラスでSceneに設定するか、FXMLで最上位要素に指定するかの方法があります。今回は、FXML(Scene Builder)上で最上位要素のBorderPaneにCSSファイルを設定します。CSSファイルはFXMLファイルと同じ場所に置き、BorderPaneのStylesheetsプロパティに指定します(次の図参照)。

f:id:torutk:20190707105138p:plain

プログラムを実行すると、次のとおり折れ線が細くなりました。

f:id:torutk:20190707105538p:plain

凡例の記号を四角ではなく線にする

CSSファイルで凡例の記号を定義している@.chart-legend-item-symbol@の定義を変更します。

.chart-legend-item-symbol {
    -fx-background-radius: 0;
    -fx-background-insets: 0;
    -fx-shape: "M0,5 L0,7 L12,7 L12,5 Z";
    -fx-scale-shape: false;
}
.default-color0.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_1;
}
.default-color1.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_2;
}
.default-color2.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_3;
}
.default-color3.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_4;
}
.default-color4.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_5;
}
.default-color5.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_6;
}
.default-color6.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_7;
}
.default-color7.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_8;
}
  • デフォルトの凡例シンボルは枠が系列色、中が白色塗りつぶしの円です。@-fx-background-radius@と@-fx-background-insets@はデフォルトの定義を無効化するのに指定する模様です(細部よくわからない)
  • グラフはデフォルトで8つまで色分けできるようにCHART_COLOR_1からCHART_COLOR_8まで色が定義されています。svg-pathで定義した枠の中をグラフの対応する色で塗りつぶすために、@.default-colorX.chart-legend-item-symbol@を定義しています。