使用线程将记录保存到数据库
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
等待/通知。在这种情况下,这将是您提交给 ExecutorService
的 Runnable
个实例。它们都是不同的对象。因此 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语法自动关闭你的资源,如Connection
、Statement
和ResultSet
。
我还建议养成使用分号 (;
) 正确终止 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 语法如何自动关闭 Connection
和 PreparedStatement
对象(如果它们已成功打开)。我们不想让 Connection
对象保持打开状态的时间超过必要的时间。
执行者服务
至于使用线程并发,使用 DataSource
可能会显着简化您的代码。
尽早定义一个 ExecutorService
对象,并保留它。如果您希望一次执行一个任务,请使用 single-threaded 服务。如果您想要并发任务,请使用由线程池支持的服务。
无论哪种方式,请使用 Executors
实用程序 class 获取代码中所见的 ExecutorService
。保留此 ExecutorService
对象,以供重复使用。
ExecutorService executorService = Executors.newCachedThreadPool();
…
当您准备好持久化您的 Category
对象之一时,定义一个任务并传递给执行程序服务。如您的代码所示,任务定义为 Runnable
或 Callable
.
但是您为 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 ) ) ;
我不完全确定您使用 wait
和 notifyAll
的意图。您似乎正在尝试管理对单个 Connection
对象的同时访问。正如 Stephen C 在其他答案中所解释的那样,这基本上是一种错误的方法,充满了危险。希望我已经证明这种方法也是不必要的,而且不必要地复杂化。
Java外汇
以上所有讨论都是针对Java的一般情况。
但是你提到了JavaFX. JavaFX has its own concurrency utilities。我不熟悉那些。希望以上代码有助于建立一些通用概念和准则,但您可能需要进行修改才能正常使用 JavaFX。
我必须使用线程将记录保存到 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
等待/通知。在这种情况下,这将是您提交给 ExecutorService
的 Runnable
个实例。它们都是不同的对象。因此 notifyAll
通知将不会到达正在等待的 Runnable
对象。
但我认为我应该指出,您在这里尝试实施的策略(基本上)是错误的。看起来您正在尝试使用一个数据库连接并在多个线程之间共享它。实际上,所有数据库 INSERT 操作都将是 single-threaded;即数据库操作中没有并行性。
如果需要并行性,请使用由 off-the-shelf JDBC 连接池管理的多个连接。这也将避免为每个 INSERT ... 打开和关闭数据库连接的开销,正如您当前的实现 可能 所做的那样。 (我们看不到相关代码。)
但是如果您想要良好的插入性能,请不要执行由单个 INSERT 语句组成的事务。而是使用 JDBC 的批处理机制或 multi-row INSERT 语句。
DataSource
我同意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语法自动关闭你的资源,如Connection
、Statement
和ResultSet
。
我还建议养成使用分号 (;
) 正确终止 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 语法如何自动关闭 Connection
和 PreparedStatement
对象(如果它们已成功打开)。我们不想让 Connection
对象保持打开状态的时间超过必要的时间。
执行者服务
至于使用线程并发,使用 DataSource
可能会显着简化您的代码。
尽早定义一个 ExecutorService
对象,并保留它。如果您希望一次执行一个任务,请使用 single-threaded 服务。如果您想要并发任务,请使用由线程池支持的服务。
无论哪种方式,请使用 Executors
实用程序 class 获取代码中所见的 ExecutorService
。保留此 ExecutorService
对象,以供重复使用。
ExecutorService executorService = Executors.newCachedThreadPool();
…
当您准备好持久化您的 Category
对象之一时,定义一个任务并传递给执行程序服务。如您的代码所示,任务定义为 Runnable
或 Callable
.
但是您为 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 ) ) ;
我不完全确定您使用 wait
和 notifyAll
的意图。您似乎正在尝试管理对单个 Connection
对象的同时访问。正如 Stephen C 在其他答案中所解释的那样,这基本上是一种错误的方法,充满了危险。希望我已经证明这种方法也是不必要的,而且不必要地复杂化。
Java外汇
以上所有讨论都是针对Java的一般情况。
但是你提到了JavaFX. JavaFX has its own concurrency utilities。我不熟悉那些。希望以上代码有助于建立一些通用概念和准则,但您可能需要进行修改才能正常使用 JavaFX。