Mysql
 sql >> Database >  >> RDS >> Mysql

Utilizzo dei thread per effettuare richieste di database

Regole di threading per JavaFX

Esistono due regole di base per i thread e JavaFX:

  1. Qualsiasi codice che modifica o accede allo stato di un nodo che fa parte di un grafico di scena deve essere eseguito sul thread dell'applicazione JavaFX. Alcune altre operazioni (ad esempio la creazione di un nuovo Stage s) sono vincolati anche da tale norma.
  2. Qualsiasi codice la cui esecuzione potrebbe richiedere molto tempo dovrebbe essere eseguito su un thread in background (cioè non sul thread dell'applicazione FX).

Il motivo della prima regola è che, come la maggior parte dei toolkit dell'interfaccia utente, il framework viene scritto senza alcuna sincronizzazione sullo stato degli elementi del grafico della scena. L'aggiunta della sincronizzazione comporta un costo in termini di prestazioni e questo risulta essere un costo proibitivo per i toolkit dell'interfaccia utente. Quindi solo un thread può accedere in modo sicuro a questo stato. Poiché il thread dell'interfaccia utente (FX Application Thread per JavaFX) deve accedere a questo stato per eseguire il rendering della scena, FX Application Thread è l'unico thread su cui è possibile accedere allo stato del grafico della scena "live". In JavaFX 8 e versioni successive, la maggior parte dei metodi soggetti a questa regola esegue controlli e genera eccezioni di runtime se la regola viene violata. (Questo è in contrasto con Swing, dove puoi scrivere codice "illegale" e può sembrare che funzioni bene, ma in realtà è soggetto a errori casuali e imprevedibili in momenti arbitrari.) Questa è la causa di l'IllegalStateException stai vedendo :stai chiamando courseCodeLbl.setText(...) da un thread diverso dal thread dell'applicazione FX.

Il motivo della seconda regola è che il thread dell'applicazione FX, oltre ad essere responsabile dell'elaborazione degli eventi utente, è anche responsabile del rendering della scena. Pertanto, se esegui un'operazione di lunga durata su quel thread, l'interfaccia utente non verrà renderizzata fino al completamento dell'operazione e non risponderà agli eventi dell'utente. Anche se ciò non genererà eccezioni o causerà uno stato dell'oggetto corrotto (come viola la regola 1), (nella migliore delle ipotesi) crea un'esperienza utente scadente.

Pertanto, se si dispone di un'operazione di lunga durata (come l'accesso a un database) che deve aggiornare l'interfaccia utente al completamento, il piano di base consiste nell'eseguire l'operazione di lunga durata in un thread in background, restituendo i risultati dell'operazione quando è completare e quindi pianificare un aggiornamento dell'interfaccia utente nel thread dell'interfaccia utente (applicazione FX). Tutti i toolkit dell'interfaccia utente a thread singolo hanno un meccanismo per farlo:in JavaFX puoi farlo chiamando Platform.runLater(Runnable r) per eseguire r.run() sul thread dell'applicazione FX. (In Swing, puoi chiamare SwingUtilities.invokeLater(Runnable r) per eseguire r.run() sul thread di invio degli eventi AWT.) JavaFX (vedi più avanti in questa risposta) fornisce anche alcune API di livello superiore per la gestione della comunicazione al thread dell'applicazione FX.

Buone pratiche generali per il multithreading

La procedura migliore per lavorare con più thread è strutturare il codice che deve essere eseguito su un thread "definito dall'utente" come un oggetto che viene inizializzato con uno stato fisso, dispone di un metodo per eseguire l'operazione e al termine restituisce un oggetto che rappresenta il risultato. L'utilizzo di oggetti immutabili per lo stato inizializzato e il risultato del calcolo è altamente auspicabile. L'idea qui è di eliminare il più possibile la possibilità che qualsiasi stato mutevole sia visibile da più thread. L'accesso ai dati da un database si adatta bene a questo idioma:puoi inizializzare il tuo oggetto "lavoratore" con i parametri per l'accesso al database (termini di ricerca, ecc.). Esegui la query del database e ottieni un set di risultati, utilizza il set di risultati per popolare una raccolta di oggetti di dominio e restituisci la raccolta alla fine.

In alcuni casi sarà necessario condividere lo stato mutabile tra più thread. Quando questo deve assolutamente essere fatto, è necessario sincronizzare attentamente l'accesso a quello stato per evitare di osservare lo stato in uno stato incoerente (ci sono altre questioni più sottili che devono essere affrontate, come la vitalità dello stato, ecc.). Il consiglio forte quando è necessario è utilizzare una libreria di alto livello per gestire queste complessità per te.

Utilizzo dell'API javafx.concurrent

JavaFX fornisce una API di concorrenza progettato per eseguire codice in un thread in background, con API specificamente progettata per aggiornare l'interfaccia utente JavaFX al completamento (o durante) dell'esecuzione di quel codice. Questa API è progettata per interagire con java.util.concurrent API , che fornisce funzionalità generali per la scrittura di codice multithread (ma senza hook dell'interfaccia utente). La classe chiave in javafx.concurrent è Task , che rappresenta una singola unità di lavoro destinata a essere eseguita su un thread in background. Questa classe definisce un unico metodo astratto, call() , che non accetta parametri, restituisce un risultato e può generare eccezioni verificate. Task implementa Runnable con il suo run() metodo semplicemente invocando call() . Task ha anche una raccolta di metodi che garantiscono l'aggiornamento dello stato sul thread dell'applicazione FX, come updateProgress(...) , updateMessage(...) , ecc. Definisce alcune proprietà osservabili (ad es. state e value ):gli ascoltatori di queste proprietà riceveranno una notifica delle modifiche sul thread dell'applicazione FX. Infine, ci sono alcuni metodi utili per registrare i gestori (setOnSucceeded(...) , setOnFailed(...) , eccetera); tutti i gestori registrati tramite questi metodi verranno richiamati anche nel thread dell'applicazione FX.

Quindi la formula generale per recuperare i dati da un database è:

  1. Crea un Task per gestire la chiamata al database.
  2. Inizializza il Task con qualsiasi stato necessario per eseguire la chiamata al database.
  3. Implementare call() dell'attività metodo per eseguire la chiamata al database, restituendo i risultati della chiamata.
  4. Registra un gestore con l'attività per inviare i risultati all'interfaccia utente una volta completata.
  5. Richiama l'attività su un thread in background.

Per l'accesso al database, consiglio vivamente di incapsulare il codice effettivo del database in una classe separata che non sa nulla dell'interfaccia utente ( Modello di progettazione dell'oggetto di accesso ai dati ). Quindi fai in modo che l'attività invochi i metodi sull'oggetto di accesso ai dati.

Quindi potresti avere una classe DAO come questa (nota che qui non c'è il codice dell'interfaccia utente):

public class WidgetDAO {

    // In real life, you might want a connection pool here, though for
    // desktop applications a single connection often suffices:
    private Connection conn ;

    public WidgetDAO() throws Exception {
        conn = ... ; // initialize connection (or connection pool...)
    }

    public List<Widget> getWidgetsByType(String type) throws SQLException {
        try (PreparedStatement pstmt = conn.prepareStatement("select * from widget where type = ?")) {
            pstmt.setString(1, type);
            ResultSet rs = pstmt.executeQuery();
            List<Widget> widgets = new ArrayList<>();
            while (rs.next()) {
                Widget widget = new Widget();
                widget.setName(rs.getString("name"));
                widget.setNumberOfBigRedButtons(rs.getString("btnCount"));
                // ...
                widgets.add(widget);
            }
            return widgets ;
        }
    }

    // ...

    public void shutdown() throws Exception {
        conn.close();
    }
}

Il recupero di un gruppo di widget potrebbe richiedere molto tempo, quindi qualsiasi chiamata da una classe dell'interfaccia utente (ad esempio una classe controller) dovrebbe pianificarla su un thread in background. Una classe controller potrebbe assomigliare a questa:

public class MyController {

    private WidgetDAO widgetAccessor ;

    // java.util.concurrent.Executor typically provides a pool of threads...
    private Executor exec ;

    @FXML
    private TextField widgetTypeSearchField ;

    @FXML
    private TableView<Widget> widgetTable ;

    public void initialize() throws Exception {
        widgetAccessor = new WidgetDAO();

        // create executor that uses daemon threads:
        exec = Executors.newCachedThreadPool(runnable -> {
            Thread t = new Thread(runnable);
            t.setDaemon(true);
            return t ;
        });
    }

    // handle search button:
    @FXML
    public void searchWidgets() {
        final String searchString = widgetTypeSearchField.getText();
        Task<List<Widget>> widgetSearchTask = new Task<List<Widget>>() {
            @Override
            public List<Widget> call() throws Exception {
                return widgetAccessor.getWidgetsByType(searchString);
            }
        };

        widgetSearchTask.setOnFailed(e -> {
           widgetSearchTask.getException().printStackTrace();
            // inform user of error...
        });

        widgetSearchTask.setOnSucceeded(e -> 
            // Task.getValue() gives the value returned from call()...
            widgetTable.getItems().setAll(widgetSearchTask.getValue()));

        // run the task using a thread from the thread pool:
        exec.execute(widgetSearchTask);
    }

    // ...
}

Nota come la chiamata al metodo DAO (potenzialmente) di lunga durata è racchiusa in un Task che viene eseguito su un thread in background (tramite l'accessor) per impedire il blocco dell'interfaccia utente (regola 2 sopra). L'aggiornamento dell'interfaccia utente (widgetTable.setItems(...) ) viene effettivamente eseguito nuovamente sul thread dell'applicazione FX, utilizzando il Task metodo di callback di convenienza setOnSucceeded(...) (soddisfacente regola 1).

Nel tuo caso, l'accesso al database che stai eseguendo restituisce un singolo risultato, quindi potresti avere un metodo come

public class MyDAO {

    private Connection conn ; 

    // constructor etc...

    public Course getCourseByCode(int code) throws SQLException {
        try (PreparedStatement pstmt = conn.prepareStatement("select * from course where c_code = ?")) {
            pstmt.setInt(1, code);
            ResultSet results = pstmt.executeQuery();
            if (results.next()) {
                Course course = new Course();
                course.setName(results.getString("c_name"));
                // etc...
                return course ;
            } else {
                // maybe throw an exception if you want to insist course with given code exists
                // or consider using Optional<Course>...
                return null ;
            }
        }
    }

    // ...
}

E poi il codice del tuo controller sarebbe simile a

final int courseCode = Integer.valueOf(courseId.getText());
Task<Course> courseTask = new Task<Course>() {
    @Override
    public Course call() throws Exception {
        return myDAO.getCourseByCode(courseCode);
    }
};
courseTask.setOnSucceeded(e -> {
    Course course = courseTask.getCourse();
    if (course != null) {
        courseCodeLbl.setText(course.getName());
    }
});
exec.execute(courseTask);

I documenti API per Task avere molti altri esempi, incluso l'aggiornamento del progress proprietà dell'attività (utile per le barre di avanzamento..., ecc.