Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Threads verwenden, um Datenbankanfragen zu stellen

Threading-Regeln für JavaFX

Es gibt zwei Grundregeln für Threads und JavaFX:

  1. Jeder Code, der den Zustand eines Knotens modifiziert oder darauf zugreift, der Teil eines Szenendiagramms ist, muss auf dem JavaFX-Anwendungsthread ausgeführt werden. Bestimmte andere Operationen (z. B. das Erstellen einer neuen Stage s) sind ebenfalls an diese Regel gebunden.
  2. Jeder Code, dessen Ausführung möglicherweise lange dauert, sollte auf einem Hintergrund-Thread ausgeführt werden (d. h. nicht auf dem FX-Anwendungs-Thread).

Der Grund für die erste Regel besteht darin, dass das Framework wie die meisten UI-Toolkits ohne Synchronisierung des Zustands von Elementen des Szenendiagramms geschrieben wird. Das Hinzufügen einer Synchronisierung verursacht Leistungseinbußen, und dies stellt sich als unerschwinglicher Preis für UI-Toolkits heraus. Somit kann nur ein Thread sicher auf diesen Zustand zugreifen. Da der UI-Thread (FX-Anwendungsthread für JavaFX) auf diesen Zustand zugreifen muss, um die Szene zu rendern, ist der FX-Anwendungsthread der einzige Thread, auf dem Sie auf den „Live“-Szenendiagrammstatus zugreifen können. In JavaFX 8 und höher führen die meisten Methoden, die dieser Regel unterliegen, Überprüfungen durch und lösen Laufzeitausnahmen aus, wenn die Regel verletzt wird. (Dies steht im Gegensatz zu Swing, wo Sie "illegalen" Code schreiben können, der scheinbar problemlos läuft, aber tatsächlich zu zufälligen und unvorhersehbaren Fehlern zu willkürlichen Zeiten neigt.) Das ist die Ursache von die IllegalStateException Sie sehen :Sie rufen courseCodeLbl.setText(...) auf aus einem anderen Thread als dem FX Application Thread.

Der Grund für die zweite Regel ist, dass der FX-Anwendungs-Thread nicht nur für die Verarbeitung von Benutzerereignissen, sondern auch für das Rendern der Szene verantwortlich ist. Wenn Sie also einen lang andauernden Vorgang für diesen Thread ausführen, wird die Benutzeroberfläche nicht gerendert, bis dieser Vorgang abgeschlossen ist, und reagiert nicht mehr auf Benutzerereignisse. Während dies keine Ausnahmen generiert oder einen beschädigten Objektstatus verursacht (wie dies bei einem Verstoß gegen Regel 1 der Fall ist), führt dies (im besten Fall) zu einer schlechten Benutzererfahrung.

Wenn Sie also einen lang andauernden Vorgang haben (z. B. den Zugriff auf eine Datenbank), der die Benutzeroberfläche nach Abschluss aktualisieren muss, besteht der grundlegende Plan darin, den lang andauernden Vorgang in einem Hintergrundthread auszuführen und die Ergebnisse des Vorgangs zurückzugeben, wenn dies der Fall ist abschließen und dann eine Aktualisierung der Benutzeroberfläche im Thread für die Benutzeroberfläche (FX-Anwendung) planen. Alle Singlethread-UI-Toolkits haben dafür einen Mechanismus:In JavaFX können Sie dies tun, indem Sie Platform.runLater(Runnable r) aufrufen um r.run() auszuführen im FX-Anwendungsthread. (In Swing können Sie SwingUtilities.invokeLater(Runnable r) aufrufen um r.run() auszuführen im AWT-Event-Dispatch-Thread.) JavaFX (siehe weiter unten in dieser Antwort) bietet auch einige APIs auf höherer Ebene für die Verwaltung der Kommunikation zurück zum FX-Anwendungs-Thread.

Allgemeine gute Praktiken für Multithreading

Die beste Methode für die Arbeit mit mehreren Threads besteht darin, Code, der auf einem "benutzerdefinierten" Thread ausgeführt werden soll, als Objekt zu strukturieren, das mit einem festen Zustand initialisiert wird, über eine Methode zum Ausführen der Operation verfügt und nach Abschluss ein Objekt zurückgibt das Ergebnis darstellen. Die Verwendung unveränderlicher Objekte für den initialisierten Zustand und das Berechnungsergebnis ist höchst wünschenswert. Die Idee hier ist, die Möglichkeit auszuschließen, dass ein veränderlicher Zustand von mehreren Threads aus so weit wie möglich sichtbar ist. Der Zugriff auf Daten aus einer Datenbank passt gut zu dieser Redewendung:Sie können Ihr "Worker"-Objekt mit den Parametern für den Datenbankzugriff (Suchbegriffe usw.) initialisieren. Führen Sie die Datenbankabfrage durch und erhalten Sie eine Ergebnismenge, verwenden Sie die Ergebnismenge, um eine Sammlung von Domänenobjekten zu füllen, und geben Sie die Sammlung am Ende zurück.

In einigen Fällen ist es erforderlich, den änderbaren Zustand zwischen mehreren Threads zu teilen. Wenn dies unbedingt erforderlich ist, müssen Sie den Zugriff auf diesen Zustand sorgfältig synchronisieren, um zu vermeiden, dass der Zustand in einem inkonsistenten Zustand beobachtet wird (es gibt andere subtilere Probleme, die angegangen werden müssen, z. B. die Lebendigkeit des Zustands usw.). Die dringende Empfehlung, wenn dies erforderlich ist, ist die Verwendung einer High-Level-Bibliothek, um diese Komplexität für Sie zu verwalten.

Verwendung der javafx.concurrent-API

JavaFX bietet eine Parallelitäts-API das für die Ausführung von Code in einem Hintergrund-Thread entwickelt wurde, mit einer API, die speziell für die Aktualisierung der JavaFX-Benutzeroberfläche nach Abschluss (oder während) der Ausführung dieses Codes entwickelt wurde. Diese API wurde entwickelt, um mit java.util.concurrent API , das allgemeine Möglichkeiten zum Schreiben von Multithread-Code bietet (jedoch ohne UI-Hooks). Die Schlüsselklasse in javafx.concurrent ist Task , das eine einzelne, einmalige Arbeitseinheit darstellt, die für einen Hintergrundthread ausgeführt werden soll. Diese Klasse definiert eine einzelne abstrakte Methode, call() , die keine Parameter akzeptiert, ein Ergebnis zurückgibt und möglicherweise geprüfte Ausnahmen auslöst. Task implementiert Runnable mit seinem run() Methode, die einfach call() aufruft . Task verfügt auch über eine Sammlung von Methoden, die den Status im FX-Anwendungsthread garantiert aktualisieren, z. B. updateProgress(...) , updateMessage(...) usw. Es definiert einige beobachtbare Eigenschaften (z. B. state und value ):Listener dieser Eigenschaften werden über Änderungen im FX-Anwendungsthread benachrichtigt. Schließlich gibt es einige praktische Methoden zum Registrieren von Handlern (setOnSucceeded(...) , setOnFailed(...) , etc); Alle über diese Methoden registrierten Handler werden auch im FX-Anwendungsthread aufgerufen.

Die allgemeine Formel zum Abrufen von Daten aus einer Datenbank lautet also:

  1. Erstellen Sie eine Task um den Aufruf an die Datenbank zu verarbeiten.
  2. Initialisieren Sie die Task mit jedem Zustand, der benötigt wird, um den Datenbankaufruf durchzuführen.
  3. Implementieren Sie den call() der Aufgabe Methode, um den Datenbankaufruf durchzuführen und die Ergebnisse des Aufrufs zurückzugeben.
  4. Registrieren Sie einen Handler mit der Aufgabe, um die Ergebnisse nach Abschluss an die Benutzeroberfläche zu senden.
  5. Rufen Sie die Aufgabe in einem Hintergrund-Thread auf.

Für den Datenbankzugriff empfehle ich dringend, den eigentlichen Datenbankcode in einer separaten Klasse zu kapseln, die nichts über die Benutzeroberfläche weiß (). Entwurfsmuster für Datenzugriffsobjekte ). Lassen Sie dann einfach die Aufgabe die Methoden für das Datenzugriffsobjekt aufrufen.

Sie könnten also eine DAO-Klasse wie diese haben (beachten Sie, dass hier kein UI-Code vorhanden ist):

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

Das Abrufen einer Reihe von Widgets kann lange dauern, daher sollten alle Aufrufe von einer UI-Klasse (z. B. einer Controller-Klasse) dies in einem Hintergrund-Thread planen. Eine Controller-Klasse könnte so aussehen:

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

    // ...
}

Beachten Sie, wie der Aufruf der (möglicherweise) lang andauernden DAO-Methode in eine Task eingeschlossen ist die auf einem Hintergrundthread (über den Accessor) ausgeführt wird, um zu verhindern, dass die Benutzeroberfläche blockiert wird (Regel 2 oben). Die Aktualisierung der Benutzeroberfläche (widgetTable.setItems(...) ) wird tatsächlich wieder auf dem FX-Anwendungsthread ausgeführt, indem die Task verwendet wird 's praktische Callback-Methode setOnSucceeded(...) (Regel 1 erfüllend).

In Ihrem Fall gibt der von Ihnen durchgeführte Datenbankzugriff ein einzelnes Ergebnis zurück, sodass Sie möglicherweise eine Methode wie

haben
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 ;
            }
        }
    }

    // ...
}

Und dann würde Ihr Controller-Code so aussehen

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

Die API-Dokumentation für Task haben viele weitere Beispiele, einschließlich der Aktualisierung des progress Eigenschaft der Aufgabe (nützlich für Fortschrittsbalken... usw.).