我无法在打包的 java 应用程序中连接到我的 sqlite 数据库。它在开发中完美运行。我正在使用 gradle jlink 和 jpackage 创建 Windows 可执行文件。应用程序安装成功,但我收到以下错误:
错误日志
SEVERE: path to 'jrt:/com.scholarly/assets/databases/local_database.db': 'C:\Users\DELL\AppData\Local\Scholarly CBT\jrt:' does not exist
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/org.sqlite.SQLiteConnection.open(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/org.sqlite.SQLiteConnection.<init>(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/org.sqlite.jdbc3.JDBC3Connection.<init>(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/org.sqlite.jdbc4.JDBC4Connection.<init>(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/org.sqlite.JDBC.createConnection(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/org.sqlite.JDBC.connect(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: java.sql/java.sql.DriverManager.getConnection(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: java.sql/java.sql.DriverManager.getConnection(Unknown Source)
Nov 09, 2023 3:47:30 PM com.scholarly.MainApplication log
SEVERE: [email protected]/com.scholarly.data.util.DbConnection.getDbConnection(Unknown Source)
这是抛出错误的代码:
private static final String TAG = "DbConnection: ";
private static final String location = MainApplication.class.getResource("/assets/databases/local_database.db").getPath();
private static final String DATABASE_URL = "jdbc:sqlite:"+location;
private static Connection connection;
public static Connection getDbConnection() {
try {
if (connection == null) {
MainApplication.logInfo("Database Connecting..");
connection = DriverManager.getConnection(DATABASE_URL, "", "");
System.out.println(TAG + "Connection created successfully: " + connection.toString());
MainApplication.logInfo(TAG + "Connection created successfully: " + connection.toString());
} else {
System.out.println(TAG + "Retrieved existing Connection : " + connection);
MainApplication.logInfo(TAG + "Retrieved existing Connection : " + connection);
}
} catch (Exception e) {
System.out.println(e.getMessage());
MainApplication.log(e);
}
return connection;
}
如果您的 SQLite 数据库是资源,那么 JDBC URL 应类似于:
jdbc:sqlite::resource:assets/databases/local_database.db
假设您使用的是 https://github.com/xerial/sqlite-jdbc 驱动程序。请参阅
USAGE.md
了解更多信息。如果您的代码是模块化的,请确保您的数据库资源的包是 opens
无条件。
也就是说,资源是只读的。如果您想要一个可写的数据库,那么您必须:
在运行时提取数据库。确保仅在数据库尚未提取时才执行此操作,以免覆盖它并丢失数据。
让 SQLite 驱动程序在运行时创建一个新数据库。如果我没记错的话,如果该文件尚不存在(至少默认情况下),则会自动创建该文件。但是您必须在每次启动应用程序时运行一次初始化 SQL/脚本(例如,
CREATE TABLE IF NOT EXISTS ...
),或者仅在该文件以前不存在时运行一次。
这两个选项都要求您有外部数据库文件的“已知位置”。现在,您可以尝试工作目录,但这假设工作目录在应用程序实例之间可写且不变。需要一个通常可写且更具确定性的位置。跨操作系统工作的一个常见位置是用户主目录中的目录。您可以使用
System.getProperty("user.home")
获取主目录。尽管由于您使用 jpackage
进行部署,您可能希望添加一种方法来配置更合适的特定于操作系统的位置,例如 Windows 上 %LOCALAPPDATA%
下的目录。
这是一个在需要时提取数据库资源的类的示例。它还确保在任何给定时间只有一个
Connection
是打开的,并鼓励非并行使用数据库,假设您确保一次只有该类的一个实例。
package com.example;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.sqlite.SQLiteDataSource;
public class SQLiteDatabase implements AutoCloseable {
private static final String DB_RESOURCE_NAME = "/assets/databases/local_database.db";
@FunctionalInterface
public interface SQLFunction<R> {
R execute(Connection connection) throws SQLException;
}
private final Lock lock = new ReentrantLock();
private final Path dbFile;
private final SQLiteDataSource source;
private Connection connection;
private boolean open = true;
private boolean extracted;
public SQLiteDatabase(Path dbFile) {
this.dbFile = dbFile.toAbsolutePath().normalize();
extracted = Files.exists(dbFile);
source = new SQLiteDataSource();
source.setUrl("jdbc:sqlite:" + this.dbFile);
source.setEnforceForeignKeys(true);
}
public <R> R execute(SQLFunction<R> function) throws SQLException {
Objects.requireNonNull(function);
lock.lock();
try {
ensureOpen();
extractDatabase();
if (connection == null || connection.isClosed()) {
connection = source.getConnection();
}
return function.execute(connection);
} finally {
lock.unlock();
}
}
private void ensureOpen() {
if (!open) {
throw new IllegalStateException("SQLiteDatabase has been closed.");
}
}
private void extractDatabase() {
if (!extracted) {
try (var input = SQLiteDatabase.class.getResourceAsStream(DB_RESOURCE_NAME)) {
Files.copy(input, dbFile);
extracted = true;
} catch (FileAlreadyExistsException ex) {
extracted = true;
} catch (IOException ex) {
throw new UncheckedIOException("Unable to extract SQLite database resource", ex);
}
}
}
@Override
public void close() throws SQLException {
lock.lock();
try {
if (open) {
open = false;
if (connection != null) {
connection.close();
connection = null;
}
}
} finally {
lock.unlock();
}
}
}
使用上面的类可能看起来像:
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class Main {
/*
* WARNING: This code will create a directory in your user home directory and does
* not make any attempt to delete it. The directory name will be ".app_name"
* unless the code is modified. Inside the ".app_name" directory will be a
* "local_database.db" file.
*
* The SQLiteDatabase class assumes there's a SQLite database resource located
* at "/assets/databases/local_database.db". If that resource does not exist then
* this code will fail.
*/
public static void main(String[] args) throws Exception {
var dbFile = Path.of(System.getProperty("user.home"), ".app_name", "local_database.db");
Files.createDirectories(dbFile.getParent()); // parent directories must exist
try (var db = new SQLiteDatabase(dbFile)) {
List<?> data = db.execute(conn -> {
try (var stat = conn.createStatement()) {
var entities = new ArrayList<>();
var set = stat.executeQuery("SELECT * FROM xxx");
while (set.next()) {
// get attributes, create entity instance, add to entities list
}
return entities;
}
});
data.forEach(System.out::println);
}
}
}
尽管理想情况下,您可以将
SQLiteDatabase
的使用隐藏在存储库或数据访问对象 (DAO) 接口后面。当然,使用迁移工具/库、JDBC 包装库和/或 ORM 框架总体上可能会让您的生活更轻松。