Spring BootからJavaDB(Apache Derby)へ接続した旧環境の保守記録です。JavaDBはOracle JDK 7・8に同梱されていました。Apache Derbyは開発が終了しています。以下では、JDK 17とDerby 10.16.1.1を固定し、JDBCで2つの接続形態を確認します。
データの投入
サンプルは一時ディレクトリ内に空のDBを作り、sample_user に1行だけ入れます。組み込みとネットワーク用のDBは別々に作るため、結果がどちらのモードから返ったか確認できます。実サービスの認証情報やDBファイルは使いません。
Derbyにアクセス
実行スクリプトはMaven Centralから指定版のJARを取得し、JDK 17でコンパイルします。ネットワークモードでは、同じマシンの127.0.0.1にテスト用のDerbyサーバーを別プロセスで起動します。
#!/bin/sh
set -eu
cd -- "$(dirname -- "$0")"
version=10.16.1.1
base=https://repo.maven.apache.org/maven2/org/apache/derby
deps=target/dependency
mkdir -p "$deps" target/classes
for artifact in derby derbyshared derbynet derbyclient derbytools; do
jar=$deps/$artifact-$version.jar
if [ ! -f "$jar" ]; then
curl -fLsS --max-time 30 "$base/$artifact/$version/$artifact-$version.jar" \
-o "$jar.tmp"
mv "$jar.tmp" "$jar"
fi
done
javac -cp "$deps/*" -d target/classes DerbyModesExample.java DerbyStartupCheck.java
java -cp "target/classes:$deps/*" DerbyModesExample
java -cp "target/classes:$deps/*" DerbyStartupCheckimport java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import org.apache.derby.drda.NetworkServerControl;
public class DerbyModesExample {
private static final String HOST = "127.0.0.1";
public static void main(String[] args) throws Exception {
Path home = Files.createTempDirectory("derby-demo-");
System.setProperty("derby.system.home", home.toString());
try {
runEmbedded(home.resolve("embedded"));
runNetwork(home.resolve("server"), findAvailablePort());
} finally {
removeTree(home);
}
}
private static void runEmbedded(Path database) throws SQLException {
String url = "jdbc:derby:" + database;
try {
insertAndRead(url, "embedded");
} finally {
try {
DriverManager.getConnection(url + ";shutdown=true").close();
} catch (SQLException e) {
if (!"08006".equals(e.getSQLState())) {
throw e;
}
}
}
}
private static int findAvailablePort() throws IOException {
try (var socket = new ServerSocket(0, 1, InetAddress.getByName(HOST))) {
return socket.getLocalPort();
}
}
static void runNetwork(Path serverHome, int port) throws Exception {
Files.createDirectories(serverHome);
InetAddress host = InetAddress.getByName(HOST);
Path serverLog = serverHome.resolve("server.log");
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
var server = new ProcessBuilder(java,
"-Duser.language=en", "-Duser.country=US",
"-Dderby.system.home=" + serverHome,
"-cp", System.getProperty("java.class.path"),
"org.apache.derby.drda.NetworkServerControl",
"start", "-h", HOST, "-p", Integer.toString(port))
.redirectErrorStream(true)
.redirectOutput(serverLog.toFile())
.start();
try {
waitForServerLog(server, serverLog, port);
new NetworkServerControl(host, port).ping();
if (!server.isAlive()) {
throw new IllegalStateException("Derby Network Server stopped after startup");
}
String url = "jdbc:derby://" + HOST + ":" + port + "/sample";
insertAndRead(url, "network");
if (!Files.isDirectory(serverHome.resolve("sample"))) {
throw new AssertionError("network database was not created under server home");
}
} finally {
server.destroy();
if (!server.waitFor(5, TimeUnit.SECONDS)) {
server.destroyForcibly().waitFor();
}
}
}
private static void waitForServerLog(Process server, Path log, int port)
throws IOException, InterruptedException {
String ready = "started and ready to accept connections on port " + port;
for (int attempt = 0; attempt < 60 && server.isAlive(); attempt++) {
if (Files.readAllLines(log).stream().anyMatch(line -> line.strip().endsWith(ready))) {
return;
}
Thread.sleep(100);
}
throw new IllegalStateException("Own Derby Network Server did not start on port " + port);
}
private static void insertAndRead(String url, String value) throws SQLException {
try (var connection = DriverManager.getConnection(url + ";create=true");
var sql = connection.createStatement()) {
sql.executeUpdate("CREATE TABLE sample_user (id INT PRIMARY KEY, name VARCHAR(20))");
try (var insert = connection.prepareStatement("INSERT INTO sample_user VALUES (?, ?)")) {
insert.setInt(1, 1);
insert.setString(2, value);
if (insert.executeUpdate() != 1) {
throw new AssertionError("expected one inserted row");
}
}
}
try (var connection = DriverManager.getConnection(url);
var sql = connection.createStatement();
var rows = sql.executeQuery("SELECT name FROM sample_user WHERE id = 1")) {
if (!rows.next() || !value.equals(rows.getString(1)) || rows.next()) {
throw new AssertionError("expected one " + value + " row");
}
}
System.out.println(value + ": 1 row read");
}
private static void removeTree(Path directory) throws IOException {
try (var paths = Files.walk(directory)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
}接続前に、自分が起動した子プロセスのログから待受開始を確認します。次の検証コードで、同じポートが既に使われている場合に他のプロセスへ接続しないことを確かめます。
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
public class DerbyStartupCheck {
public static void main(String[] args) throws Exception {
Path home = Files.createTempDirectory("derby-occupied-port-");
try (var listener = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))) {
listener.setSoTimeout(250);
try {
DerbyModesExample.runNetwork(home, listener.getLocalPort());
throw new AssertionError("unexpected startup success on occupied port");
} catch (IllegalStateException expected) {
if (!expected.getMessage().startsWith("Own Derby Network Server did not start")) {
throw expected;
}
}
try (var unexpected = listener.accept()) {
throw new AssertionError("the sample contacted another process on the port");
} catch (SocketTimeoutException expected) {
System.out.println("occupied port: child rejected; no connection attempted");
}
} finally {
try (var paths = Files.walk(home)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
}
}sh run.sh
# embedded: 1 row read
# network: 1 row read
# occupied port: child rejected; no connection attemptedDerbyの起動モード
| モード | JDBC URLの形 | DBの格納先 |
|---|---|---|
| 組み込み | jdbc:derby:/path/to/sample | 接続するJavaプロセスから見たパス。DBサーバーは不要。 |
| ネットワーク | jdbc:derby://127.0.0.1:1527/sample | DerbyサーバーのDBホーム。アプリ側のカレントディレクトリとは別。 |
Spring Bootの接続設定を読むときは spring.datasource.url の形と、実行環境の依存関係を確認します。上のサンプルはJDBCの接続と読み書きまでを検証したもので、Spring Bootアプリの起動やJPAマッピングは別の確認です。
はまりポイント
DBを置く場所 組み込みはアプリの実行ユーザー、ネットワークはDerbyサーバーの実行ユーザーが書き込める場所を使います。
別プロセスで起動するとDBが見つからない ネットワーク側のDB名が指す場所はサーバーで決まります。例は子プロセスの起動引数で derby.system.home を指定し、意図した格納先にDBができることまで確かめます。
No identifier specified for entity これは接続とは別のJPAマッピングエラーです。当時の環境なら javax.persistence.Id、Jakarta Persistenceの環境なら jakarta.persistence.Id を、使用しているバージョンに合わせて確認します。
