打包可执行文件中的 SQLite 数据库错误,但可在调试中运行

问题描述 投票:0回答:1

我无法在打包的 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;
}
java database sqlite javafx
1个回答
2
投票

如果您的 SQLite 数据库是资源,那么 JDBC URL 应类似于:

jdbc:sqlite::resource:assets/databases/local_database.db

假设您使用的是 https://github.com/xerial/sqlite-jdbc 驱动程序。请参阅

USAGE.md
了解更多信息。如果您的代码是模块化的,请确保您的数据库资源的包是
opens
无条件

也就是说,资源是只读的。如果您想要一个可写的数据库,那么您必须:

  1. 在运行时提取数据库。确保仅在数据库尚未提取时才执行此操作,以免覆盖它并丢失数据。

  2. 让 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 框架总体上可能会让您的生活更轻松。

© www.soinside.com 2019 - 2024. All rights reserved.