使用线程将记录保存到数据库

Saving records to database using threads

我必须使用线程将记录保存到 H2 数据库。

这是我的数据库 class 中用于将具有名称和描述的类别 class 保存到数据库中的方法:

public static void saveNewCategory(Category newCategory, Connection connection){
    try {
        String sql = "INSERT INTO CATEGORY(NAME, DESCRIPTION) VALUES(?, ?)";
        PreparedStatement ps = connection.prepareStatement(sql);

        ps.setString(1, newCategory.getName());
        ps.setString(2, newCategory.getDescription());

        ps.executeUpdate();

    } catch (SQLException e) {
        e.printStackTrace();
    }
}

这是我的线程class,它应该实现该原则并使用线程将其保存到数据库中

public class SaveCategoryThread implements Runnable{

    private static Connection connectToDatabase;
    private Category category;

    public SaveCategoryThread(Category categoryC) {
        category=categoryC;
    }

    @Override
    public void run() {
        try {
            openConnectionWithDatabase();

            Database.saveNewCategory(category,connectToDatabase);

        } catch (IOException | SQLException e) {
            e.printStackTrace();
        } finally {
            closeConnectionWithDatabase();
        }
    }

    public synchronized void openConnectionWithDatabase() throws IOException, SQLException {
        if (Database.activeConnectionWithDatabase) {
            try {
                wait();
                System.out.println("Connection is busy");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        connectToDatabase = Database.connectToDatabase();
    }

    public synchronized void closeConnectionWithDatabase() {
        try {
            Database.disconnectFromDatabase(connectToDatabase);
        } catch (SQLException ex) {
            ex.printStackTrace();
        }

        notifyAll();
    }
}

这就是我在接受用户输入的 JavaFX class 中执行它的方式

ExecutorService es = Executors.newCachedThreadPool();
es.execute(new SaveCategoryThread(category));

但它不工作,没有错误或任何东西,但结果没有保存到数据库中。

我认为问题出在 wait / notifyAll 代码上。

您正在 this 等待/通知。在这种情况下,这将是您提交给 ExecutorServiceRunnable 个实例。它们都是不同的对象。因此 notifyAll 通知将不会到达正在等待的 Runnable 对象。


但我认为我应该指出,您在这里尝试实施的策略(基本上)是错误的。看起来您正在尝试使用一个数据库连接并在多个线程之间共享它。实际上,所有数据库 INSERT 操作都将是 single-threaded;即数据库操作中没有并行性。

如果需要并行性,请使用由 off-the-shelf JDBC 连接池管理的多个连接。这也将避免为每个 INSERT ... 打开和关闭数据库连接的开销,正如您当前的实现 可能 所做的那样。 (我们看不到相关代码。)

但是如果您想要良好的插入性能,请不要执行由单个 INSERT 语句组成的事务。而是使用 JDBC 的批处理机制或 multi-row INSERT 语句。

DataSource

我同意,除了提倡使用连接池的部分。恕我直言,对连接池的需求通常被夸大了,而 risks/issues 被低估了。至于他的主要观点,我同意:您可能会在尝试共享 Connection 对象时遇到麻烦。我看到 static Connection 就很担心,因为应该没有必要通过 static.

来保持 Connection

与其专注于传递 Connection 对象,我建议您传递 DataSource 对象。 DataSource 包含连接到数据库所需的所有信息。或者,如果您决定使用连接池,DataSource 对象可以成为访问该池的前门。无论哪种方式,使用 DataSource 都很简单,因为它主要由方法 getConnection 组成,返回一个 Connection 对象。

您可以 hard-code 一个 DataSource 包含数据库信息的对象,例如数据库服务器地址、数据库用户名和密码等。或者您可以将这些详细信息外部化,由管理员在运行时在 Java 中的 directory/naming service like an LDAP server, or in a Jakarta EE server. Use JNDI 中配置,以获得外部化的 DataSource 对象。

例如,用H2 Database Engine, use the bundled implemantion of DataSource, org.h2.jdbcx.JdbcDataSource。这是一些代码,显示如何 hard-code DataSource 信息。

public javax.sql.DataSource configureDataSource() {
    org.h2.jdbcx.JdbcDataSource ds = Objects.requireNonNull( new JdbcDataSource() );  // Implementation of `DataSource` bundled with H2.
    ds.setURL( "jdbc:h2:/path/to/database_file;" );
    ds.setUser( "scott" );
    ds.setPassword( "tiger" );
    ds.setDescription( "An example database showing how to use DataSource." );
    return ds ;
}

保留那个 DataSource 对象以备后用。该对象仅保存连接信息(或访问连接池)。传递给您的 JDBC 代码。

顺便给你一个大提示:使用try-with-resources语法自动关闭你的资源,如ConnectionStatementResultSet

我还建议养成使用分号 (;) 正确终止 SQL 语句的习惯。

除了 SQLException 之外,添加第二个 catch,用于 DataSource#getConnection 也抛出的更具体的异常:SQLTimeoutException。此异常适用于驱动程序确定已超过 setLoginTimeout 方法指定的超时值。

public void saveNewCategory( Category newCategory, DataSource ds ){
        String sql = "INSERT INTO CATEGORY( NAME, DESCRIPTION ) VALUES( ?, ? ) ;";
        try (
            Connection conn = ds.getConnection() ;
            PreparedStatement ps = conn.prepareStatement( sql );
        ) {
            ps.setString( 1, newCategory.getName() );
            ps.setString( 2, newCategory.getDescription() );
            ps.executeUpdate();
        } catch ( SQLTimeoutException e ) {
            … 
        } catch ( SQLException e ) {
            … 
        }
}

请注意上面代码中的 try-with-resources 语法如何自动关闭 ConnectionPreparedStatement 对象(如果它们已成功打开)。我们不想让 Connection 对象保持打开状态的时间超过必要的时间。

执行者服务

至于使用线程并发,使用 DataSource 可能会显着简化您的代码。

尽早定义一个 ExecutorService 对象,并保留它。如果您希望一次执行一个任务,请使用 single-threaded 服务。如果您想要并发任务,请使用由线程池支持的服务。

无论哪种方式,请使用 Executors 实用程序 class 获取代码中所见的 ExecutorService。保留此 ExecutorService 对象,以供重复使用。

ExecutorService executorService = Executors.newCachedThreadPool();
…

当您准备好持久化您的 Category 对象之一时,定义一个任务并传递给执行程序服务。如您的代码所示,任务定义为 RunnableCallable.

但是您为 Runnable 任务选择的名称 SaveCategoryThread 表明您正在考虑管理线程。那不是 Runnable 的意思。 Runnable 是要完成的工作,不考虑线程。 Runnable 可以在同一个线程上执行,这在某些情况下是合适的。所以Runnable而不是负责线程。管理线程是 ExecutorService.

的工作
public class SaveNewCategoryTask implements Runnable {
    // Member fields. 
    DataSource dataSource;
    Category newCategory ;

    // Constructor
    public SaveNewCategoryTask ( Category newCategory , DataSource dataSource ) {
        this.newCategory = newCategory ; // Remember the passed argument.
        this.dataSource = dataSource ; // Remember the passed argument.
    }

    @Override
    public void run() {
        saveNewCategory( this.newCategory , this.dataSource ) ;
    }
}

请注意我们的 Runnable 如何不再跟踪连接或异常。 saveNewCategory 方法中包含所有数据库交换详细信息。通常最好使 Runnable 任务 class 尽可能简单。它的工作是简单地保存信息直到需要时,当它的 run 方法最终被执行时。

用法:

// Retrieve that `DataSource` object you established early on in your app’s lifecycle.
DataSource ds = … retrieve existing object … ;
// Retrieve the `ExecutorService` object you established early on in your app’s lifecycle.
ExecutorService es = … retrieve existing object … ;
es.submit( new SaveNewCategoryTask( newCategory , ds ) ) ;

我不完全确定您使用 waitnotifyAll 的意图。您似乎正在尝试管理对单个 Connection 对象的同时访问。正如 Stephen C 在其他答案中所解释的那样,这基本上是一种错误的方法,充满了危险。希望我已经证明这种方法也是不必要的,而且不必要地复杂化。

Java外汇

以上所有讨论都是针对Java的一般情况。

但是你提到了JavaFX. JavaFX has its own concurrency utilities。我不熟悉那些。希望以上代码有助于建立一些通用概念和准则,但您可能需要进行修改才能正常使用 JavaFX。