JavaFX – Subproceso en segundo plano para consulta SQL

Me pregunto si alguien puede ayudarme con un problema bastante molesto con respecto a la creación de un hilo de fondo en JavaFX. Actualmente tengo varias consultas SQL que agregan datos a la UI que se ejecuta actualmente en el subproceso de la aplicación JavaFX (ver ejemplo a continuación). Sin embargo, cuando cada una de estas consultas se ejecuta, se congela la UI porque no se está ejecutando en una cadena de fondo. He analizado varios ejemplos que utilizan Tarea y los entiendo, pero no puedo hacer que funcionen al hacer consultas de bases de datos, algunos de los cuales tardan unos segundos en ejecutarse.

Este es uno de los métodos que ejecuta una consulta:

public void getTopOrders() { customerOrders.clear(); try { Connection con = DriverManager.getConnection(connectionUrl); //Get all records from table String SQL = "EXEC dbo.Get_Top_5_Customers_week"; ResultSet rs; try (Statement stmt = con.createStatement();) { rs = stmt.executeQuery(SQL); while (rs.next()) { double orderValue = Double.parseDouble(rs.getString(3)); customerOrders.add(new CustomerOrders(rs.getString(1), rs.getString(2), "£" + formatter.format(orderValue), rs.getString(4).substring(6, 8) + "/" + rs.getString(4).substring(4, 6) + "/" + rs.getString(4).substring(0, 4))); } } } catch (SQLException | NumberFormatException e) { } } 

Cada registro procesado se agrega a un ObservableList que está vinculado a un TableView, o gráfico o simplemente establece el texto en una etiqueta (depende de la consulta). ¿Cómo puedo ejecutar la consulta en una cadena de fondo y aún dejar la interfaz libre para usar y actualizar desde las consultas?

Gracias por adelantado

Creé una solución de muestra para usar una Tarea (como se sugiere en el comentario de Alexander Kirov) para acceder a una base de datos en un subproceso que se ejecuta simultáneamente con el subproceso de la aplicación JavaFX.

Las partes relevantes de la solución de muestra se reproducen a continuación:

 // fetches a collection of names from a database. class FetchNamesTask extends DBTask> { @Override protected ObservableList call() throws Exception { // artificially pause for a while to simulate a long // running database connection. Thread.sleep(1000); try (Connection con = getConnection()) { return fetchNames(con); } } private ObservableList fetchNames(Connection con) throws SQLException { logger.info("Fetching names from database"); ObservableList names = FXCollections.observableArrayList(); Statement st = con.createStatement(); ResultSet rs = st.executeQuery("select name from employee"); while (rs.next()) { names.add(rs.getString("name")); } logger.info("Found " + names.size() + " names"); return names; } } // loads a collection of names fetched from a database into a listview. // displays a progress indicator and disables the trigge button for // the operation while the data is being fetched. private void fetchNamesFromDatabaseToListView( final Button triggerButton, final ProgressIndicator databaseActivityIndicator, final ListView listView) { final FetchNamesTask fetchNamesTask = new FetchNamesTask(); triggerButton.setDisable(true); databaseActivityIndicator.setVisible(true); databaseActivityIndicator.progressProperty().bind(fetchNamesTask.progressProperty()); fetchNamesTask.setOnSucceeded(new EventHandler() { @Override public void handle(WorkerStateEvent t) { listView.setItems(fetchNamesTask.getValue()); } }); fetchNamesTask.runningProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Boolean wasRunning, Boolean isRunning) { if (!isRunning) { triggerButton.setDisable(false); databaseActivityIndicator.setVisible(false); } }; }); databaseExecutor.submit(fetchNamesTask); } private Connection getConnection() throws ClassNotFoundException, SQLException { logger.info("Getting a database connection"); Class.forName("org.h2.Driver"); return DriverManager.getConnection("jdbc:h2:~/test", "sa", ""); } abstract class DBTask extends Task { DBTask() { setOnFailed(new EventHandler() { @Override public void handle(WorkerStateEvent t) { logger.log(Level.SEVERE, null, getException()); } }); } } // executes database operations concurrent to JavaFX operations. private ExecutorService databaseExecutor = Executors.newFixedThreadPool( 1, new DatabaseThreadFactory() ); static class DatabaseThreadFactory implements ThreadFactory { static final AtomicInteger poolNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable runnable) { Thread thread = new Thread(runnable, "Database-Connection-" + poolNumber.getAndIncrement() + "-thread"); thread.setDaemon(true); return thread; } } 

Tenga en cuenta que una vez que comienza a hacer cosas al mismo tiempo, su encoding y su UI se vuelven más complicadas que el modo predeterminado sin Tareas cuando todo tiene un solo hilo. Por ejemplo, en mi ejemplo, deshabilité el botón que inicia la Tarea para que no pueda ejecutar varias Tareas en el fondo haciendo lo mismo (este tipo de procesamiento es similar al del mundo web donde puede desactivar un botón de envío de formulario para evitar formulario que se publica por partida doble). También agregué un indicador de progreso animado a la escena mientras se ejecutaba la tarea de la base de datos de larga ejecución para que el usuario tenga una indicación de que algo está sucediendo.

Ejemplo de salida de progtwig que demuestra la experiencia de UI cuando una operación de base de datos larga está en progreso (obsérvese que el indicador de progreso está animando durante la captura, lo que significa que la UI responde aunque la captura de pantalla no lo muestra):

databasefetcher

Para comparar la complejidad y la funcionalidad adicionales de una implementación con tareas simultáneas frente a una implementación que ejecuta todo en el subproceso de la aplicación JavaFX, puede ver otra versión del mismo ejemplo que no utiliza tareas . Tenga en cuenta que en mi caso con una base de datos local, la complejidad adicional de la aplicación basada en tareas es innecesaria porque las operaciones de bases de datos locales se ejecutan tan rápido, pero si se conectaba a una base de datos remota grande usando consultas complejas de larga ejecución, El enfoque es útil ya que brinda a los usuarios una experiencia de IU más fluida.

Logré resolver usando la solución provista por jewelsea. Vale la pena señalar que si implementa este método cuando no utiliza listas, tablas y / o listas observables donde necesita actualizar un elemento en la interfaz de usuario, como un campo de texto o etiqueta, simplemente agregue el código de actualización dentro de Platform.runLater. A continuación se muestran algunos fragmentos de código que muestran mi solución de trabajo.

Código:

 public void getSalesData() { try { Connection con = DriverManager.getConnection(connectionUrl); //Get all records from table String SQL = "EXEC dbo.Order_Information"; try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(SQL)) { while (rs.next()) { todayTot = Double.parseDouble(rs.getString(7)); weekTot = Double.parseDouble(rs.getString(8)); monthTot = Double.parseDouble(rs.getString(9)); yearTot = Double.parseDouble(rs.getString(10)); yearTar = Double.parseDouble(rs.getString(11)); monthTar = Double.parseDouble(rs.getString(12)); weekTar = Double.parseDouble(rs.getString(13)); todayTar = Double.parseDouble(rs.getString(14)); deltaValue = Double.parseDouble(rs.getString(17)); yearPer = yearTot / yearTar * 100; monthPer = monthTot / monthTar * 100; weekPer = weekTot / weekTar * 100; todayPer = todayTot / todayTar * 100; //Doesn't update UI unless you add the update code to Platform.runLater... Platform.runLater(new Runnable() { public void run() { todayTotal.setText("£" + formatter.format(todayTot)); weekTotal.setText("£" + formatter.format(weekTot)); monthTotal.setText("£" + formatter.format(monthTot)); yearTotal.setText("£" + formatter.format(yearTot)); yearTarget.setText("£" + formatter.format(yearTar)); monthTarget.setText("£" + formatter.format(monthTar)); weekTarget.setText("£" + formatter.format(weekTar)); todayTarget.setText("£" + formatter.format(todayTar)); yearPercent.setText(percentFormatter.format(yearPer) + "%"); currentDelta.setText("Current Delta (Week Ends): £" + formatter.format(deltaValue)); } }); } } } catch (SQLException | NumberFormatException e) { } } public void databaseThreadTester() { fetchDataFromDB(); } private void fetchDataFromDB() { final testController.FetchNamesTask fetchNamesTask = new testController.FetchNamesTask(); databaseActivityIndicator.setVisible(true); databaseActivityIndicator.progressProperty().bind(fetchNamesTask.progressProperty()); fetchNamesTask.setOnSucceeded(new EventHandler() { @Override public void handle(WorkerStateEvent t) { } }); fetchNamesTask.runningProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Boolean wasRunning, Boolean isRunning) { if (!isRunning) { databaseActivityIndicator.setVisible(false); } } ; }); databaseExecutor.submit(fetchNamesTask); } abstract class DBTask extends Task { DBTask() { setOnFailed(new EventHandler() { @Override public void handle(WorkerStateEvent t) { } }); } } class FetchNamesTask extends testController.DBTask { @Override protected String call() throws Exception { fetchNames(); return null; } private void fetchNames() throws SQLException, InterruptedException { Thread.sleep(5000); getTopOrders(); getSalesData(); } } 

La única cosa que no parece funcionar con esta implementación es la siguiente, no estoy seguro de por qué no funciona, pero no dibuja el gráfico.

 public void addCricketGraphData() { yearChart.getData().clear(); series.getData().clear(); series2.getData().clear(); try { Connection con = DriverManager.getConnection(connectionUrl); //Get all records from table String SQL = "...omitted..."; try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(SQL)) { while (rs.next()) { Platform.runLater(new Runnable() { @Override public void run() { try { series.getData().add(new XYChart.Data(rs.getString(1), Double.parseDouble(rs.getString(7)))); series2.getData().add(new XYChart.Data(rs.getString(1), Double.parseDouble(rs.getString(8)))); } catch (SQLException ex) { Logger.getLogger(testController.class.getName()).log(Level.SEVERE, null, ex); } } }); } } } catch (SQLException | NumberFormatException e) { } yearChart = createChart(); } protected LineChart createChart() { final CategoryAxis xAxis = new CategoryAxis(); final NumberAxis yAxis = new NumberAxis(); // setup chart series.setName("Target"); series2.setName("Actual"); xAxis.setLabel("Period"); yAxis.setLabel("£"); //Add custom node for each point of data on the line chart. for (int i = 0; i < series2.getData().size(); i++) { nodeCounter = i; final int value = series.getData().get(nodeCounter).getYValue().intValue(); final int value2 = series2.getData().get(nodeCounter).getYValue().intValue(); int result = value2 - value; Node node = new HoveredThresholdNode(0, result); node.toBack(); series2.getData().get(nodeCounter).setNode(node); } yearChart.getData().add(series); yearChart.getData().add(series2); return yearChart; }