2013-02-14 18 views
10

Mi chiedo se qualcuno può aiutarmi con un problema piuttosto fastidioso relativo alla creazione di un thread in background in JavaFX! Al momento ho diverse query SQL che aggiungono dati all'interfaccia utente attualmente in esecuzione sul thread dell'applicazione JavaFX (vedere l'esempio seguente). Tuttavia, quando ognuna di queste query viene eseguita, blocca l'interfaccia utente perché non è in esecuzione su un thread in background. Ho esaminato vari esempi che utilizzano Task e li comprendono, ma non riesco a farli funzionare quando eseguo query di database, alcune delle quali richiedono alcuni secondi per essere eseguite.JavaFX - Thread in background per query SQL

Ecco uno dei metodi che esegue una query:

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) { 
    } 
} 

Ogni record elaborato viene aggiunto a un ObservableList che è collegato a un TableView, o un grafico o semplicemente imposta il testo su un'etichetta (dipende la query). Come posso eseguire la query su un thread in background e lasciano ancora l'interfaccia libera di utilizzare ed essere aggiornati dalle query

Grazie in anticipo

+1

Avete osservato le classi nel pacchetto javafx.concurrent? docs.oracle.com/javafx/2/api/javafx/concurrent/.... Penso che una classe Task dovrebbe essere particolarmente interessante per te: docs.oracle.com/javafx/2/api/javafx/concurrent/Task.html, puoi leggere la sua javadoc, per capire l'elenco completo delle opzioni. –

+0

Forse DataFX può aiutarti: http://www.javafxdata.org o http://www.guigarage.com/category/datafx/ –

risposta

14

ho creato un sample solution per l'utilizzo di un Task (come suggerito nel commento di Alexander Kirov) per accedere a un database su un thread in esecuzione contemporaneamente al thread dell'applicazione JavaFX.

Le parti rilevanti della soluzione del campione sono riprodotti di seguito:

// fetches a collection of names from a database. 
class FetchNamesTask extends DBTask<ObservableList<String>> { 
    @Override protected ObservableList<String> 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<String> fetchNames(Connection con) throws SQLException { 
    logger.info("Fetching names from database"); 
    ObservableList<String> 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<WorkerStateEvent>() { 
    @Override public void handle(WorkerStateEvent t) { 
     listView.setItems(fetchNamesTask.getValue()); 
    } 
    }); 
    fetchNamesTask.runningProperty().addListener(new ChangeListener<Boolean>() { 
    @Override public void changed(ObservableValue<? extends Boolean> 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<T> extends Task<T> { 
    DBTask() { 
    setOnFailed(new EventHandler<WorkerStateEvent>() { 
     @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; 
    } 
}  

Nota che una volta che si inizia a fare le cose contemporaneamente, il tuo codice e l'interfaccia utente diventa più complicata di quanto la modalità di default, senza compiti quando tutto è a thread singolo . Ad esempio, nel mio esempio ho disabilitato il pulsante che avvia l'attività in modo da non poter avere più attività in esecuzione in background facendo la stessa cosa (questo tipo di elaborazione è simile al mondo web in cui è possibile disabilitare un pulsante di post forma per impedire un la forma è doppia pubblicata). Ho anche aggiunto un indicatore di avanzamento animato alla scena mentre l'attività del database in esecuzione era in esecuzione in modo che l'utente abbia un'indicazione che qualcosa sta succedendo.

Esempio di output del programma che dimostra l'esperienza utente durante un'operazione di database lunga esecuzione è in corso (nota l'indicatore di avanzamento viene animando durante il recupero che significa che l'interfaccia utente è reattivo se la schermata non mostra questo):

databasefetcher

Per confrontare la complessità e la funzionalità aggiuntive di un'implementazione con attività concorrenti rispetto a un'implementazione che esegue tutto sul thread dell'applicazione JavaFX, è possibile vedere another version of the same sample which does not use tasks. Si noti che nel mio caso con un database locale giocattolo, la complessità aggiuntiva dell'applicazione basata su attività non è necessaria perché le operazioni del database locale vengono eseguite in modo così rapido, ma se ci si collega a un grande database remoto utilizzando query complesse di lunga durata, rispetto all'attività basata l'approccio è utile in quanto fornisce agli utenti un'esperienza di interfaccia utente più fluida.

+0

Grazie. Sono riuscito a utilizzare il tuo eccellente esempio per consentire alla query del mio database di essere eseguita in background e quindi aggiungere elementi in una vista tabella. Comunque ho un'altra domanda. Perché quando eseguo una query di database che modifica i valori di Labels (utilizzando label.setText (rs.getString (x)), rifiuta di funzionare? Mi manca qualcosa di semplice, presumo che il tavolo uno funzioni perché è semplicemente l'aggiunta i dati in una lista osservabile che viene utilizzata dalla vista tabella, ma non sono sicuro di quale sia il modo per fare in modo che il thread cambi le mie etichette? - vedi http://pastebin.com/TXyf00xq – Uniqum

3

Gestito per risolvere utilizzando la soluzione fornita da jewelsea. Vale la pena notare che se si implementa questo metodo quando non si utilizzano elenchi, tabelle e/o elenchi osservabili in cui è necessario aggiornare un elemento sull'interfaccia utente come un campo di testo o un'etichetta, aggiungere semplicemente il codice di aggiornamento in Platform.runLater. Di seguito sono riportati alcuni snippet di codice che mostrano la mia soluzione di lavoro.

Codice:

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<WorkerStateEvent>() { 
     @Override 
     public void handle(WorkerStateEvent t) { 
     } 
    }); 
    fetchNamesTask.runningProperty().addListener(new ChangeListener<Boolean>() { 
     @Override 
     public void changed(ObservableValue<? extends Boolean> observable, Boolean wasRunning, Boolean isRunning) { 
      if (!isRunning) { 
       databaseActivityIndicator.setVisible(false); 
      } 
     } 
    ; 
    }); 
databaseExecutor.submit(fetchNamesTask); 
} 

abstract class DBTask<T> extends Task { 

    DBTask() { 
     setOnFailed(new EventHandler<WorkerStateEvent>() { 
      @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(); 
    } 
} 

L'unica cosa che non sembra funzionare con questa implementazione è la seguente, non so perché non funziona, ma non disegna il grafico.

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<String, Number>(rs.getString(1), 
            Double.parseDouble(rs.getString(7)))); 
          series2.getData().add(new XYChart.Data<String, Number>(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<String, Number> 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; 
} 
Problemi correlati