torutkのブログ

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

Windowsマシン上でのLinux環境は混沌と

PC上に直接Linux OSをインストールして起動する(デュアルブート)環境は対象外とします。 Windows OSの動作を前提に設計されたノートPCでは、Linux OS向けドライバが存在しないデバイスが搭載されていることが多く、Windows OSの上でLinux環境を動かすことになります。

Windows 7の頃は、Windows OS上でLinux環境を動かすには、VMwareVirtualBoxなどの仮想マシンソフトウェアを入れて、その上にLinux OSをインストールするか、CygwinなどのLinuxエミュレーションソフトウェアを入れていました。CentOSFedoraUbuntuなど特定のLinuxディストリビューションを動かしたいときは仮想マシンを使い、Linuxコマンド環境を手軽に利用したい(ファイルシステムWindowsと共用したい)ときはCygwinを使う、といった使い分けをしていました。なお、CygwinでもXサーバーのGUI環境を使うことはできました。

Windows 10になると、Proエディションでは標準で仮想マシンHyper-Vが搭載されました。また、WSL(Windows Subsystem for Linux)がWindows 10 1709版からHomeエディションを含めて利用可能となりました。これで、VMware、Virtual Box、Cygwinは不要になるか、と思われましたが、なかなかそうもいきません。

Hyper-Vですが、この上にLinuxを入れてLinuxデスクトップを動かすと動作がもっさりして快適とは程遠いレスポンスです(細い回線でリモートデスクトップ接続して操作している感じ)。さらにHyper-Vを入れると他の仮想マシンソフトウェア(VMwareVirtualBox)が動かなくなるという問題もあります(ハードウェアの仮想化機構 VT-xをHyper-Vが占有するため。今後解消に向かう模様ですが)。

WSLは、利用可能なLinuxディストリビューションが少ない(UbuntuOpenSUSESUSE Enterprise、Kali、有償のWLinux、Fedora Remix、他)ほか、ファイルシステムWindowsと共有することに制約が大きく(ファイルを壊す等)、またファイルI/Oが致命的に遅い問題があります(以前日記に記載)。 WSL(Windows Subsystem for Linux)とHyper-V上のLinuxとの重さの違い - torutkのブログ

来年のWindows 10アップデートで計画されているWSL2は、WSLの問題点を解決すると期待していましたが、なんとHyper-V上でLinuxカーネルを動かす環境となるので、WSLとはコンセプトが随分と変わっています。

というところで、Windows上でLinuxを動かすには選択肢は多くなったものの、これぞという決定打にかけている状況です。

RubyMineでRedmineプラグイン開発をする(Windows環境)

はじめに

これまでは、Windowsマシン上でRedmineプラグイン開発環境を用意するのに、仮想マシンHyper-Vなど)にLinuxを入れてその上でLinuxベースの開発環境を整える方法を取っていました。ただしCUI環境が主です。Hyper-VGUI環境を動かすとかなり動作がもっさりして快適とは程遠いので(キー入力も一呼吸待たされる)、開発環境として使う気になれないほどです。 そこで、WSL(Windows Subsystem for Linux)でLinux環境を使う方法を試してみました。こちらはRailsサーバーの起動が著しく重く、やはり開発環境としては使いたくないほどです。また、Windows環境(ツール)からWSL上のファイルを編集するとパーミッションがおかしくなって崩壊することがあります。

そのため、RubyMineが宝の持ち腐れとなっていました。今回、Windows上にRubyを展開して素のWindowsのみでRedmineプラグイン開発環境を用意してみようとおもいました。

インストール

Rubyのインストール

Windows上のRubyは、次のサイトから入手しました。 https://rubyinstaller.org/downloads/

[with Devkit]と書かれている開発キット込みのRubyをインストールします。2019年10月14日現在、2.6系の最新は

rubyinstaller-devkit-2.6.5-1-x64.exe

です。

インストーラーを実行し、ライセンス受諾し、インストール先を指定、PATH、関連付け、UTF-8デフォルトをチェックして(最初の2つはデフォルトでチェック)、MSYS2 development toolchainをチェックして(デフォルトでチェック)インストールします。

f:id:torutk:20191014152015p:plain

f:id:torutk:20191014152028p:plain

インストール完了するときに、MSYS2セットアップを実行するチェックを付けます。

f:id:torutk:20191014152331p:plain

すると、コマンドプロンプトでMSYS2セットアップが実行されます。

f:id:torutk:20191014152523p:plain

インストールするコンポーネントはデフォルトの[1,2,3]を実行(ENTERキーのみ入力でよい)します。

インストールが終わると、ENTERのみ入力してコマンドプロンプトを閉じます。

SQLite3のインストール

Windows用のSQLite3は次のサイトから入手しました。 https://www.sqlite.org/download.html

まず、Precompiled Binaries for Windows にある次のファイルを入手します。ここにはDLLファイルが含まれます。

sqlite-dll-win64-x64-3300100.zip

このDLLファイルを環境変数PATHの通っているディレクトリにコピーします。ここでは暫定でruby.exeのある場所(C:\tools\Ruby26-x64\bin\)に置きました。

次に、Source Codeにある次のファイルを入手します。開発用のヘッダーファイル、ソースファイルが含まれます。

sqlite-amalgamation-3300100.zip

このうちヘッダーファイル(sqlite3.h、sqlite3ext.h)を、rubyのincludeディレクトリ(ここではC:\tools\Ruby26-x64\include\ruby-2.6.0\)にコピーします。

RubyMineでRedmineのセットアップ

Ruby SDKの設定

RubyMineを起動し、「Welcome to RubyMine」画面の右下[Configure]をクリックし、ドロップダウンリストから[Settings]を選択します。 「Settings for New Projects」画面の左側ペインで[Languages & Frameworks] > [Ruby SDK and Gems]を選択、[+]をクリックし、先ほどインストールしたRubyを追加します。環境変数PATHにruby.exeがあれば候補に登場するのでそれを選択します。

RedmineGithubからクローンしプロジェクト作成

「Welcome to RubyMine」画面の[Check out from Version Control]をクリックし、リポジトリURLとクローン先のローカルディレクトリを指定します。

database.ymlの編集

config\database.ymlファイルを作成し、SQLite3の設定を記述します。

production:
  adapter: sqlite3
  database: db/redmine.sqlite3

development:
  adapter: sqlite3
  database: db/redmine.sqlite3

test:
  adapter: sqlite3
  database: db/redmine_test.sqlite3

bundlerでGemをインストール

Redmineが必要とするGemファイル群をプロジェクト配下にインストールします。

[Tools]メニュー > [Bundler] > [Install]をクリックし、「Bundle Install」ダイアログでオプション

--path vendor/bundler

を追記して実行します。

sqlite3でエラー
Fetching sqlite3 1.4.1
Installing sqlite3 1.4.1 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

current directory:
D:/work/trunk_redmine/vendor/bundler/ruby/2.6.0/gems/sqlite3-1.4.1/ext/sqlite3
d:/tools/Ruby26-x64/bin/ruby.exe -I d:/tools/Ruby26-x64/lib/ruby/2.6.0 -r
./siteconf20191014-17576-1unxgm.rb extconf.rb
checking for sqlite3.h... yes
checking for pthread_create() in -lpthread... yes
checking for -ldl... no
checking for sqlite3_libversion_number() in -lsqlite3... no
sqlite3 is missing. Install SQLite3 from http://www.sqlite.org/ first.
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=d:/tools/Ruby26-x64/bin/$(RUBY_BASE_NAME)
        --with-sqlcipher
        --without-sqlcipher
        --with-sqlite3-config
        --without-sqlite3-config
        --with-pkg-config
        --without-pkg-config
        --with-sqlcipher
        --without-sqlcipher
        --with-sqlite3-dir
        --without-sqlite3-dir
        --with-sqlite3-include
        --without-sqlite3-include=${sqlite3-dir}/include
        --with-sqlite3-lib
        --without-sqlite3-lib=${sqlite3-dir}/lib
        --with-pthreadlib
        --without-pthreadlib
        --with-dllib
        --without-dllib
        --with-sqlcipher
        --without-sqlcipher
        --with-sqlite3lib
        --without-sqlite3lib

bundle installでは、--with-sqlite3-libのオプションを指定しても認識できない模様なので、直接gem installで指定してみました。ただしgem installなのでシステム共通のgemとしてインストールされてしまいます。

>gem install sqlite3 --platform=ruby -- --with-sqlite3-include=c:/tools/ruby26-x64/include/ruby-2.6.0 --with-sqlite3-lib=c:/tools/ruby26-x64/bin
Fetching sqlite3-1.4.1.gem
Temporarily enhancing PATH for MSYS/MINGW...
Installing required msys2 packages: mingw-w64-x86_64-sqlite3
Building native extensions with: '--with-sqlite3-include=c:/tools/ruby26-x64/include/ruby-2.6.0 --with-sqlite3-lib=c:/tools/ruby26-x64/bin'
This could take a while...
Successfully installed sqlite3-1.4.1
Parsing documentation for sqlite3-1.4.1
Installing ri documentation for sqlite3-1.4.1
Done installing documentation for sqlite3 after 1 seconds
1 gem installed

セッション秘密鍵の生成

RubyMineからの実行方法が分からないので、コマンドプロンプトから実行しました。

bundle exec rails generate_secret_token

データベーススキーマの生成

bundle exec rails db:migrate

Railsサーバーの実行

RubyMineの[Run]メニュー > [Run] で [Development: trunk_redmine]を選択すると、デフォルトサーバーが起動します。

めでたく起動しました

No Rails とエラーが出てRailsサーバーが実行できない場合

コマンドプロンプト上でbundle installを実行した後に、Runメニューから実行したらエラーとなってしまいました。その後、RubyMineの[Tools]メニューからbundle installを再実行し、Runメニューから実行したら実行できました。

今月のJava読書会は「The Java Module System(洋書)」を読みます

Java読書会の新しい本

毎月1回川崎市Javaに関する技術書籍の読書会を開催しているJava読書会BOFでは、 先月で「Java 11 and 12 - New Features」が読了し、新しい課題図書のWeb投票を実施ました。 その結果得票数4票(!)で第1位となったのがThe Java Module System です。

本書は、Java SE 9で導入されたJava Platform Module Systemについて丸々1冊をかけて述べています。

書籍の入手

Amazon日本サイトでは、紙の書籍が2909円(送料無料)で販売されています。販売はBook Depositoryというところでイギリスにある会社です。どうやら海を渡って送られてくるようです。 電子書籍はと探してみると出版社のMANNINGでPDF/ePub/Kindle/libeBookの形式データが$39.99で販売されています。日本円では4300円ほどでしょうか。

Manning | The Java Module System

ちょっと悩んで安いAmazonのペーパーバックの方を購入しました。ただちょっと時間がかかります。9月21日に注文して、手元に届いたのが10月1日と10日間かかっていました。

予習

読書会の日までにちょっとずつ読んでおこうと思います。 (まえがき:forewordからちょっと難しいぞ)

ResultSetがStreamになったら嬉しいかも

はじめに

JDBCでデータベース検索のプログラムを書いていると、テーブル毎に似たような、そしてちょっとずつ異なるコードを書くことになります。 異なる部分は、カラム名、データ型、そして取り出したバラバラの値をJavaのデータクラス(ドメインクラスであったり、DTOであったり)に詰める処理です。

典型的には以下のようなプログラミングになるかと思います。

class DbAccessor {
    Connection conn;

    void initialize() {
        conn = DriverManager.getConnection("...");  // データベース接続用のURL・パラメータを指定しコネクションを取得
    }

    List<MyModel> getMyModelListOfDelta(int delta) {
        List<MyModel> models = new ArrayList<>();
        try (PreparedStatement statement = conn.prepareStatement("SELECT alfa,bravo,charlie,kilo FROM Phonetics WHERE delta=?")) {
            statement.setInt(1, delta);
            ResultSet result = statement.executeQuery();
            while (result.next()) {
                int alfa = result.getInt("alfa");
                String bravo = result.getNString("bravo");
                double charlie = result.getDouble("charlie");
                LocalDateTime kilo = result.getObject("kilo", LocalDateTime.class);
                models.add(new MyModel(alfa, bravo, charlie, kilo));
            }
        } catch (SQLException ex) {
            // ...
        }
        return models;
    }
}

JDBCでは、SQL検索文を発行した結果をResultSetで取得します。ResultSetはnext()でカーソルを進めて、getXXメソッドでカラムの値を取り出す仕様となっています。SQL文、PreparedStatementへのプレースホルダーの値設定、ResultSetからのカラムの取り出し、といった部分はテーブル固有の記載となります。

上述のように結構長々と書くことになり、また、同じテーブルでも検索の条件毎にSQL文とプレースホルダー、戻り値が異なるので、別々なメソッドにResultSetのイテレーション処理を記述することになります。

ResultSetをStreamにしたら

探してみると、ResultSetをjava.util.stream.Streamでラップするというブログ等がいくつか見つかりました。 また、jooq というライブラリも存在します。

ResultSetをStreamにするブログ等

  • Implementing a Spliterator for a JDBC ResultSet
    java.util.Spliterator インタフェースを使ってResultSetをStreamにするという内容。実際にはjava.util.SpliteratorsクラスのネストクラスAbstractSpliteratorを継承し、ResultSetからオブジェクトへのマッピングは著者の別ライブラリSqlMapperを使っている。Stream処理内(tryAdvanceメソッド内)で発生したSQLExceptionは、ResultSetのnextで生じたものはRuntimeExceptionにラップして投げており、ResultSetから値を取り出す際に生じたものは結果を返すオブジェクトにエラーを格納して戻り値としている。

  • 書籍 Java Closures and Lambda | Robert Fischer | Apress
    java.util.Spliterator インタフェースを使ってResultSetをStreamにする内容。こちらもAbstractSpliteratorを継承したResultSetSpliteratorクラスを定義し、ReslutSetからオブジェクトへのマッピングは、そのResultSetSpliteratorクラスの抽象メソッドprocessRowをさらに用途に応じてサブクラスで実装するというアプローチ。Stream処理内(tryAdvanceメソッド内)で発生したSQLExceptionはRuntimeExceptionにラップして投げている。

  • Against Boredom: Java 8: JDBC ResultSet to Stream AbstractSpliteratorのtryAdvanceを実装した無名クラスを生成し、ResultSetからは固定のRecordクラスにマッピングしている。Recordクラスは、カラム名をキーに、値をObject型で格納するMapをフィールドに保持する。

  • ResultSet の Stream 化 - なんとなくな Developer のメモ Spliteratorインタフェースを実装したクラスを定義している。ResultSetから任意の型のオブジェクトに変換するFunction(検査例外をスローする独自インタフェース)を受け取り、Stream処理内からSQLExceptionをスローし外側でキャッチできるようにしている。

お試し実装

AbstractSpliteratorを継承したResultSetSpliteratorを定義し、ResultSetの各行を任意の型のオブジェクトに変換するFunctionを外から渡せるようにします。上述ブログ・書籍の最後の記事にほぼ沿った実装です。

ResultSetSpliterator クラス
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class ResultSetSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
    public static final int CHARACTERISTICS = Spliterator.IMMUTABLE | Spliterator.NONNULL;
    private final ResultSet resultSet;
    private TryFunction<ResultSet, T, SQLException> converter;

    public ResultSetSpliterator(
            ResultSet resultSet, TryFunction<ResultSet, T, SQLException> converter
    ) {
        this(resultSet, converter, 0);
    }

    public ResultSetSpliterator(
            ResultSet resultSet, TryFunction<ResultSet, T, SQLException> converter, int additionalCharacteristics
    ) {
        super(Long.MAX_VALUE, CHARACTERISTICS | additionalCharacteristics);
        Objects.requireNonNull(resultSet, "result set");
        this.resultSet = resultSet;
        this.converter = converter;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        Objects.requireNonNull(action, "action to be performed");
        try {
            if (resultSet.isClosed() || !resultSet.next()) {
                return false;
            }
            action.accept(converter.apply(resultSet));
            return true;
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Stream<T> stream() {
        return StreamSupport.stream(this, false);
    }
}
TryFunction<T, R, E>クラス
@FunctionalInterface
public interface TryFunction<T, R, E extends Exception> {
    R apply(T t) throws E;
}

使用例

ResultSetをStreamにしたら、次のように記述できます。

    List<MyModel> getMyModelListOfDelta(int delta) {
        try (PreparedStatement statement = conn.prepareStatement("SELECT alfa,bravo,charlie,kilo FROM Phonetics WHERE delta=?")) {
            statement.setInt(1, delta);
            ResultSet result = statement.executeQuery();
            return new ResultSetSpliterator<>(result, r -> new MyModel(
                    r.getInt("alfa"), r.getNString("bravo"), r.getDouble("charlie"), r.getObject("kilo", LocalDateTime.class)
            ).stream().collect(toList());
        } catch (SQLException ex) {
            return List.of();
        }
    }

ここでは、ResultSetからオブジェクトへの変換をラムダ式でインライン記述しています。 複数個所で同じラムダ式を記述する必要が生じた場合は、ラムダ式をフィールドに定義して再利用する等ができます。

class Xxx {
    private TryFunction<ResultSet, MyModel, SQLException> resultSet2MyModel = r -> new MyModel(
            r.getInt("alfa"), r.getNString("bravo"), r.getDouble("charlie"), r.getObject("kilo", LocalDateTime.class)
    );

    List<MyModel> getAllMyModel() {
        try (var stmt = conn.prepareStatement("SELECT alfa,bravo,charlie, kilo FROM Phonetics")) {
            var resultSet = stmt.executeQuery();
            return new ResultSetSpliterator<>(resultSet, resultSet2MyModel).stream().collect(toList());
        } catch (SQLException ex) {
            return List.of();
        }
    }
}

グラフ表示アプリケーション作成時のトピック

はじめに

次の記事の続きです。 torutk.hatenablog.jp

データベース利用グラフ表示アプリケーションを作成している過程で生じた問題解決のメモです。

RDBMSスキーマにおいて、

  • 外部キーでエンティティ間に関連を持たせる場合
  • 自動採番するIDを主キーとするテーブルに新規レコードをインサートし、採番されたIDを取得したい
  • 種類を表現する場合

JavaFXにおいて、

  • 一つの時系列データ(XYChart.Series<>)を2つのグラフに表示するには

をどう扱ったかをメモします。

外部キーでのエンティティ間に関連

先に作成したスペクトラムテーブルと関連する測定記録テーブルを定義します。 測定記録には、測定日時、測定したスペクトラム、測定条件などを記録します。測定条件には測定に使用した器材、追加の測定条件(アッテネーター、VBW、リファレンスレベル、温度、他)があります。今回は感触をつかむ初版として列を最小限とし、次の設計としました。

  • テーブル: Measurements
列名 データ型 NOT NULL制約 備考
id bigint あり 主キー(自動インクリメント)
measurement_time datetime2(3) あり 時刻帯は日本固定
spectrum_id bigint あり 外部キー
RBW real あり Resolution Band Width

このテーブルは今後、がどんどん増えてくる可能性があります。

測定時刻

日時を扱うデータ型には、SQL Serverの場合、datetime、datetime2、smalldatetime、datetimeoffsetとがあります。 どれを選択すればいいかを判断するため、それぞれの内容を調べてみました。

データ型 有効範囲 分解能 ストレージサイズ
datetime 1753年1月1日から9999年12月31日 1/300秒 8バイト
datetime2 0001年1月1日から9999年12月31日 100ナノ秒 6~8バイト(秒以下の小数部桁数による)
smalldatetime 1900年1月1日から2079年6月6日 1分 4バイト
datetimeoffset 0001年1月1日から9999年12月31日 100ナノ秒 8~10バイト(秒以下の小数部桁数による)

datetimeoffsetは、UTC時刻からのオフセット(例えば日本時間ならUTC+9:00)を持つので、時差を扱う必要があるアプリケーションで選択します。時差は不要(日本時間一択とする、UTC時刻で統一する、等)であれば、測定時刻を比較する際にミリ秒以下の精度を要するか、1753年より古い時刻を扱う必要があるならdatetime2を選択します。 それ以外であれば、datetimeとdatetime2のどちらがいいかは微妙なところです。 smalldatetimeはストレージサイズは小さいですが、60年後には有効範囲が切れるので考えものです。

今回は、日本時刻固定で扱うこととし、精度は秒以下の小数点桁を3桁(ミリ秒)としてdatetime2(3)を採用しました。

スペクトラムテーブルへの参照

エンティティ(テーブル)の関連は、関連先のテーブルの一意となる列の値を関連元の列に格納する外部キーと言われる方法となります。外部キーは、また、制約としてテーブルのメタデータに保管します。

今回、測定記録テーブルからスペクトラムテーブルへの関連を持たせるので、測定記録テーブルの列にスペクトラムテーブルの一意な列(ここでは主キーたるSpectrumsテーブルのid列)の値を格納するため、同じデータ型(bigint)で、名前をspectrum_idとした列を設けます。

測定記録を新規作成しデータベースに登録する際困った事

測定記録に新規登録するレコードを用意するには、外部キーとして参照しているスペクトラムテーブルの関連付けするレコードのid(主キー)が必要です。

通常のユースケースでは、

となります。ここで、スペクトラムテーブルのid列は自動採番(identity)としているので、スペクトラムテーブルへのINSERT時にアプリケーション側ではidが分からず、INSERTしたレコードのidを何とかして取ってくる必要があります。

直前にINSERTしたレコードの自動採番idを取得する方法の調査

SQL ServerSQLで実現方法を調べたところ、次の3つの方法がありました。

  • @@IDENTITY → 使うな!危険!のコメントが付いていることが多い
  • IDENT_CURRENT 指定したテーブルのIDENTITYな値を取る(SQL Serverではidentityによる自動インクリメントな列は1個だけ設定可)
  • SCOPE_IDENTITY

一方、JDBCAPIに自動で生成される値を取り出す方法が用意されていました。

次の引数バリエーションのprepareStatementを使用します。

PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException

2場面の引数には、Statement.RETURN_GENERATED_KEYS を指定します。 そして、通常通りINSERTのSQLを実行した後に、preparedStatementオブジェクトのgetGeneratedKeys()メソッドを実行しResultSetを戻り値として得ます。 ResultSetのnext()を呼んでカレント行を示し、自動採番のキーの列の型に合わせてResultSetから値を取得します。(int型の列が自動採番ならgetInt(1)のように)

    Connection conn;
    :
    try (var stmt = conn.prepareStatement(
            "INSERT INTO Spectrums (name, spectrum, startFrequency, stopFrequency) VALUES (?, ?, ?, ?)",
            Statement.RETURN_GENERATED_KEYS
    )) {
        stmt.setNString(1, spectrum.getName());
        stmt.setBytes(2, spectrum.getData());
        stmt.setDouble(3, spectrum.getStartFrequency());
        stmt.setDouble(4, spectrum.getStopFrequency());
        stmt.executeUpdate();
        ResultSet generatedKeys = stmt.getGeneratedKeys();
        if (generatedKeys.next()) {
            spectrum.setId(generatedKeys.getInt(1));
        } else {
            throw new SQLException("No identity id obtained");
        }
    }

種類をもたせる

Javaでいえば、enum型を定義したいときがあります。Javaenumの解説で良く出てくるのがトランプのスイート(スペード、ダイヤ、ハート、クラブ)です。

RDMBSでも、MySQLにはENUM型があり、SQL Server等では代替としてCHECK制約で格納可能な文字列を指定するといったことがあります。しかし、種類の一覧をとりだすといったことが手間であるなどデメリットが多く、ここでは種類の一覧だけ格納する別テーブルを設けます。

種類が文字列で一意なら、種類文字列を1つだけ列に持つテーブルを定義し、主キーを種類文字列にします。 これだと、外部キーが文字列になるので、外部キーを持つテーブルだけをSELECTで閲覧しても意味が分かる列となります。

JavaFXのチャート2つに同一のXYChart.Seriesを貼ったら

折れ線グラフが二つのウィンドウにあって、同一データ(XYChart.Series)をそれぞれに貼ったところ、おかしな挙動となってしまいました。一つのXYChart.Seriesは、一つのChartにしか使用できません。

そこで、中のデータをコピーしたSeriesを作って回避しました。

XYChart.Series<Float, Float> copy(XYChart.Series<Float, Float> series) {
        var dupSeries = new XYChart.Series<Float, Float>();
        series.getData().stream()
                .map(data -> new XYChart.Data<>(data.getXValue(), data.getYValue()))
                .collect(Collectors.collectingAndThen(
                        Collectors.toCollection(FXCollections::observableArrayList),
                        l -> dupSeries.getData().addAll(l)));
       return dupSeries;
}

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

はじめに

次の記事の続きです。

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 は非推奨となっています。

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

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