【问题标题】:Saving records to database using threads使用线程将记录保存到数据库
【发布时间】:2022-01-28 01:41:19
【问题描述】:

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

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

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();
    }
}

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

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 类中执行它的方式

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

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

【问题讨论】:

  • 请在问题中包含Database类的完整实现

标签: java multithreading jdbc


【解决方案1】:

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

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


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

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

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

【讨论】:

    【解决方案2】:

    DataSource

    我同意Answer by Stephen C,除了主张使用连接池的部分。恕我直言,对连接池的需求通常被超卖,风险/问题被低估了。至于他的主要观点,我同意:您在尝试共享 Connection 对象时可能会遇到麻烦。我刚看到static Connection 就感到担心,因为应该没有必要通过static 保留Connection

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

    您可以使用数据库信息(例如数据库服务器地址、数据库用户名和密码等)对 DataSource 对象进行硬编码。或者,您可以将这些详细信息外部化,以便管理员在运行时在directory/naming service(如LDAP 服务器或Jakarta EE 服务器中配置)。在Java中使用JNDI获取外部化的DataSource对象。

    例如,对于H2 Database Engine,使用DataSourceorg.h2.jdbcx.JdbcDataSource 的捆绑实现。下面是一些代码,展示了如何硬编码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 ) {
                … 
            }
    }
    

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

    执行者服务

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

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

    无论哪种方式,使用Executors 实用程序类来获取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 任务类尽可能简单。它的工作是简单地保存信息直到需要时,它的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 对象的同时访问。正如斯蒂芬 C 在其他答案中解释的那样,这基本上是一种错误的方法,充满了危险。希望我已经表明,这种方法也是不必要的和不必要的复杂。

    JavaFX

    以上所有讨论都是针对 Java 的。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多