Wie soll ich Try-with-Resources mit JDBC verwenden?

148

Ich habe eine Methode zum Abrufen von Benutzern aus einer Datenbank mit JDBC:

public List<User> getUser(int userId) {
    String sql = "SELECT id, name FROM users WHERE id = ?";
    List<User> users = new ArrayList<User>();
    try {
        Connection con = DriverManager.getConnection(myConnectionURL);
        PreparedStatement ps = con.prepareStatement(sql); 
        ps.setInt(1, userId);
        ResultSet rs = ps.executeQuery();
        while(rs.next()) {
            users.add(new User(rs.getInt("id"), rs.getString("name")));
        }
        rs.close();
        ps.close();
        con.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}

Wie sollte ich Java 7 Try-with-Resources verwenden , um diesen Code zu verbessern?

Ich habe es mit dem folgenden Code versucht, aber er verwendet viele tryBlöcke und verbessert die Lesbarkeit nicht wesentlich. Soll ich anders verwenden try-with-resources?

public List<User> getUser(int userId) {
    String sql = "SELECT id, name FROM users WHERE id = ?";
    List<User> users = new ArrayList<>();
    try {
        try (Connection con = DriverManager.getConnection(myConnectionURL);
             PreparedStatement ps = con.prepareStatement(sql);) {
            ps.setInt(1, userId);
            try (ResultSet rs = ps.executeQuery();) {
                while(rs.next()) {
                    users.add(new User(rs.getInt("id"), rs.getString("name")));
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}
Jonas
quelle
5
In Ihrem zweiten Beispiel benötigen Sie das Innere nicht, try (ResultSet rs = ps.executeQuery()) {da ein ResultSet-Objekt automatisch von dem Statement-Objekt geschlossen wird, das es generiert hat
Alexander Farber
2
@AlexanderFarber Leider gab es berüchtigte Probleme mit Treibern, die Ressourcen nicht selbst schließen konnten. Die School of Hard Knocks lehrt uns immer in der Nähe aller JDBC - Ressourcen explizit, erleichtert Anprobe mit-Ressourcen um Connection, PreparedStatementund ResultSetauch. Kein Grund, dies nicht wirklich zu tun, da das Ausprobieren mit Ressourcen es so einfach macht und unseren Code in Bezug auf unsere Absichten selbstdokumentierender macht.
Basil Bourque

Antworten:

85

In Ihrem Beispiel ist der äußere Versuch nicht erforderlich, sodass Sie mindestens von 3 auf 2 sinken können und auch nicht ;am Ende der Ressourcenliste schließen müssen. Der Vorteil der Verwendung von zwei Try-Blöcken besteht darin, dass der gesamte Code im Voraus vorhanden ist, sodass Sie nicht auf eine separate Methode verweisen müssen:

public List<User> getUser(int userId) {
    String sql = "SELECT id, username FROM users WHERE id = ?";
    List<User> users = new ArrayList<>();
    try (Connection con = DriverManager.getConnection(myConnectionURL);
         PreparedStatement ps = con.prepareStatement(sql)) {
        ps.setInt(1, userId);
        try (ResultSet rs = ps.executeQuery()) {
            while(rs.next()) {
                users.add(new User(rs.getInt("id"), rs.getString("name")));
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}
bpgergo
quelle
5
Wie rufst du an Connection::setAutoCommit? Ein solcher Anruf ist tryzwischen dem con = und nicht zulässig ps =. Wenn Sie eine Verbindung von einer DataSource abrufen, die möglicherweise mit einem Verbindungspool gesichert ist, können wir nicht davon ausgehen, wie autoCommit festgelegt ist.
Basil Bourque
1
Normalerweise fügen Sie die Verbindung in die Methode ein (im Gegensatz zu dem in der Frage von OP gezeigten Ad-hoc-Ansatz). Sie können eine Verbindungsverwaltungsklasse verwenden, die aufgerufen wird, um eine Verbindung bereitzustellen oder zu schließen (ob gepoolt oder nicht). In diesem Manager können Sie Ihr Verbindungsverhalten angeben
svarog
@BasilBourque Sie könnten DriverManager.getConnection(myConnectionURL)in eine Methode wechseln , die auch das AutoCommit-Flag setzt und die Verbindung zurückgibt (oder es in der Entsprechung der createPreparedStatementMethode im vorhergehenden Beispiel setzen ...)
Rogerdpack
@rogerdpack Ja, das macht Sinn. Lassen Sie sich selbst implementieren, DataSourcewo die getConnectionMethode funktioniert, stellen Sie die Verbindung her, konfigurieren Sie sie nach Bedarf und geben Sie die Verbindung weiter.
Basil Bourque
1
@rogerdpack danke für die Klarstellung in der Antwort. Ich habe dies auf die ausgewählte Antwort aktualisiert.
Jonas
187

Mir ist klar, dass dies vor langer Zeit beantwortet wurde, aber ich möchte einen zusätzlichen Ansatz vorschlagen, der den verschachtelten Doppelblock "Versuch mit Ressourcen" vermeidet.

public List<User> getUser(int userId) {
    try (Connection con = DriverManager.getConnection(myConnectionURL);
         PreparedStatement ps = createPreparedStatement(con, userId); 
         ResultSet rs = ps.executeQuery()) {

         // process the resultset here, all resources will be cleaned up

    } catch (SQLException e) {
        e.printStackTrace();
    }
}

private PreparedStatement createPreparedStatement(Connection con, int userId) throws SQLException {
    String sql = "SELECT id, username FROM users WHERE id = ?";
    PreparedStatement ps = con.prepareStatement(sql);
    ps.setInt(1, userId);
    return ps;
}
Jeanne Boyarsky
quelle
24
Nein, es wird behandelt, das Problem ist, dass der obige Code prepareStatement aus einer Methode heraus aufruft, die nicht deklariert, SQLException auszulösen. Außerdem hat der obige Code mindestens einen Pfad, in dem er fehlschlagen kann, ohne die vorbereitete Anweisung zu schließen (wenn beim Aufrufen von setInt eine SQLException auftritt)
Trejkaz
1
@ Trejkaz guter Punkt auf die Möglichkeit, das PreparedStatement nicht zu schließen. Daran habe ich nicht gedacht, aber du hast recht!
Jeanne Boyarsky
2
@ ArturoTena ja - die Bestellung ist garantiert
Jeanne Boyarsky
2
@ JeanneBoyarsky gibt es eine andere Möglichkeit, dies zu tun? Wenn nicht, müsste ich für jeden SQL-Satz eine bestimmte createPreparedStatement-Methode erstellen
John Alexander Betts
1
Der Kommentar von Trejkaz createPreparedStatementist unsicher, unabhängig davon, wie Sie ihn verwenden. Um dies zu beheben, müssten Sie einen try-catch um setInt (...) hinzufügen, einen abfangen SQLExceptionund in diesem Fall ps.close () aufrufen und die Ausnahme erneut auslösen. Dies würde jedoch zu einem Code führen, der fast so lang und unelegant ist wie der Code, den das OP verbessern wollte.
Florian F
4

Hier ist eine kurze Möglichkeit, Lambdas und JDK 8 Supplier zu verwenden, um alles in den äußeren Versuch zu integrieren:

try (Connection con = DriverManager.getConnection(JDBC_URL, prop);
    PreparedStatement stmt = ((Supplier<PreparedStatement>)() -> {
    try {
        PreparedStatement s = con.prepareStatement("SELECT userid, name, features FROM users WHERE userid = ?");
        s.setInt(1, userid);
        return s;
    } catch (SQLException e) { throw new RuntimeException(e); }
    }).get();
    ResultSet resultSet = stmt.executeQuery()) {
}
inder
quelle
5
Dies ist prägnanter als der von @bpgergo beschriebene "klassische Ansatz"? Ich glaube nicht und der Code ist schwieriger zu verstehen. Erklären Sie daher bitte den Vorteil dieses Ansatzes.
Müller
Ich denke in diesem Fall nicht, dass Sie die SQLException explizit abfangen müssen. Bei einem Versuch mit Ressourcen ist dies tatsächlich "optional". Keine anderen Antworten erwähnen dies. Sie können dies also wahrscheinlich weiter vereinfachen.
Djangofan
Was ist, wenn DriverManager.getConnection (JDBC_URL, prop); gibt null zurück?
Gaurav
2

Was ist mit dem Erstellen einer zusätzlichen Wrapper-Klasse?

package com.naveen.research.sql;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public abstract class PreparedStatementWrapper implements AutoCloseable {

    protected PreparedStatement stat;

    public PreparedStatementWrapper(Connection con, String query, Object ... params) throws SQLException {
        this.stat = con.prepareStatement(query);
        this.prepareStatement(params);
    }

    protected abstract void prepareStatement(Object ... params) throws SQLException;

    public ResultSet executeQuery() throws SQLException {
        return this.stat.executeQuery();
    }

    public int executeUpdate() throws SQLException {
        return this.stat.executeUpdate();
    }

    @Override
    public void close() {
        try {
            this.stat.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}


Anschließend können Sie in der aufrufenden Klasse die prepareStatement-Methode wie folgt implementieren:

try (Connection con = DriverManager.getConnection(JDBC_URL, prop);
    PreparedStatementWrapper stat = new PreparedStatementWrapper(con, query,
                new Object[] { 123L, "TEST" }) {
            @Override
            protected void prepareStatement(Object... params) throws SQLException {
                stat.setLong(1, Long.class.cast(params[0]));
                stat.setString(2, String.valueOf(params[1]));
            }
        };
        ResultSet rs = stat.executeQuery();) {
    while (rs.next())
        System.out.println(String.format("%s, %s", rs.getString(2), rs.getString(1)));
} catch (SQLException e) {
    e.printStackTrace();
}

Naveen Sisupalan
quelle
2
Nichts im obigen Kommentar sagt jemals, dass dies nicht der Fall ist.
Trejkaz
2

Wie andere gesagt haben, ist Ihr Code grundsätzlich korrekt, obwohl der äußere trynicht benötigt wird. Hier noch ein paar Gedanken.

DataSource

Andere Antworten hier sind richtig und gut, wie die akzeptierte Antwort von bpgergo. Aber keine der Shows zeigt die Verwendung von DataSource, allgemein empfohlen gegenüber der Verwendung DriverManagerin modernem Java.

Der Vollständigkeit halber finden Sie hier ein vollständiges Beispiel, das das aktuelle Datum vom Datenbankserver abruft. Die hier verwendete Datenbank ist Postgres . Jede andere Datenbank würde ähnlich funktionieren. Sie würden die Verwendung von org.postgresql.ds.PGSimpleDataSourcedurch eine Implementierung ersetzen, die DataSourceIhrer Datenbank entspricht. Eine Implementierung wird wahrscheinlich von Ihrem bestimmten Treiber oder Verbindungspool bereitgestellt, wenn Sie diesen Weg gehen.

Eine DataSourceImplementierung muss nicht geschlossen werden, da sie niemals „geöffnet“ wird. A DataSourceist keine Ressource, ist nicht mit der Datenbank verbunden und enthält daher weder Netzwerkverbindungen noch Ressourcen auf dem Datenbankserver. A DataSourcesind lediglich Informationen, die beim Herstellen einer Verbindung zur Datenbank benötigt werden, mit dem Netzwerknamen oder der Netzwerkadresse des Datenbankservers, dem Benutzernamen, dem Benutzerkennwort und verschiedenen Optionen, die angegeben werden sollen, wenn eine Verbindung hergestellt wird. Ihr DataSourceImplementierungsobjekt wird also nicht in die Klammern für den Versuch mit Ressourcen eingefügt.

Verschachtelter Versuch mit Ressourcen

Ihr Code verwendet verschachtelte Try-with-Resources-Anweisungen ordnungsgemäß.

Beachten Sie im folgenden Beispielcode, dass wir die Try-with-Resources-Syntax auch zweimal verwenden , wobei eine in die andere verschachtelt ist. Das Äußere trydefiniert zwei Ressourcen: Connectionund PreparedStatement. Das Innere trydefiniert die ResultSetRessource. Dies ist eine gängige Codestruktur.

Wenn eine Ausnahme von der inneren ausgelöst und dort nicht abgefangen wird, wird die ResultSetRessource automatisch geschlossen (falls vorhanden, ist sie nicht null). Danach PreparedStatementwird das geschlossen und zuletzt das Connectiongeschlossen. Ressourcen werden automatisch in umgekehrter Reihenfolge geschlossen, in der sie in den Anweisungen zum Ausprobieren mit Ressourcen deklariert wurden.

Der Beispielcode hier ist zu simpel. Wie geschrieben, kann es mit einer einzelnen Try-with-Resources-Anweisung ausgeführt werden. Aber in einer echten Arbeit werden Sie wahrscheinlich mehr Arbeit zwischen den verschachtelten Anrufpaaren tryerledigen. Beispielsweise extrahieren Sie möglicherweise Werte von Ihrer Benutzeroberfläche oder einem POJO und übergeben diese dann, um ?Platzhalter in Ihrem SQL über Aufrufe von PreparedStatement::set…Methoden zu erfüllen .

Syntaxnotizen

Nachfolgendes Semikolon

Beachten Sie, dass das Semikolon hinter der letzten Ressourcenanweisung in den Klammern des Versuchs mit Ressourcen optional ist. Ich nehme es aus zwei Gründen in meine eigene Arbeit auf: Konsistenz und es sieht vollständig aus und erleichtert das Kopieren und Einfügen einer Mischung von Zeilen, ohne sich um Semikolons am Zeilenende kümmern zu müssen. Ihre IDE kennzeichnet das letzte Semikolon möglicherweise als überflüssig, aber es schadet nicht, es zu verlassen.

Java 9 - Verwenden Sie vorhandene Variablen in Try-with-Resources

Neu in Java 9 ist eine Erweiterung der Syntax zum Ausprobieren von Ressourcen. Wir können jetzt die Ressourcen außerhalb der Klammern der tryAnweisung deklarieren und füllen . Ich habe dies noch nicht für JDBC-Ressourcen nützlich gefunden, aber denken Sie bei Ihrer eigenen Arbeit daran.

ResultSet sollte sich schließen, darf aber nicht

In einer idealen Welt ResultSetwürde sich das schließen, wie die Dokumentation verspricht:

Ein ResultSet-Objekt wird automatisch geschlossen, wenn das Anweisungsobjekt, das es generiert hat, geschlossen, erneut ausgeführt oder zum Abrufen des nächsten Ergebnisses aus einer Folge mehrerer Ergebnisse verwendet wird.

Leider haben in der Vergangenheit einige JDBC-Fahrer dieses Versprechen nicht erfüllt. Viele JDBC - Programmierer gelernt explizit schließen alle JDBC - Ressourcen , einschließlich Als Ergebnis Connection, PreparedStatementund ResultSetauch. Die moderne Try-with-Resources-Syntax hat dies einfacher und mit kompakterem Code gemacht. Beachten Sie, dass sich das Java-Team die Mühe gemacht hat, ResultSetals zu markieren AutoCloseable, und ich schlage vor, dass wir davon Gebrauch machen. Durch die Verwendung eines Try-with-Resources für alle Ihre JDBC-Ressourcen wird Ihr Code in Bezug auf Ihre Absichten selbstdokumentierender.

Codebeispiel

package work.basil.example;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.Objects;

public class App
{
    public static void main ( String[] args )
    {
        App app = new App();
        app.doIt();
    }

    private void doIt ( )
    {
        System.out.println( "Hello World!" );

        org.postgresql.ds.PGSimpleDataSource dataSource = new org.postgresql.ds.PGSimpleDataSource();

        dataSource.setServerName( "1.2.3.4" );
        dataSource.setPortNumber( 5432 );

        dataSource.setDatabaseName( "example_db_" );
        dataSource.setUser( "scott" );
        dataSource.setPassword( "tiger" );

        dataSource.setApplicationName( "ExampleApp" );

        System.out.println( "INFO - Attempting to connect to database: " );
        if ( Objects.nonNull( dataSource ) )
        {
            String sql = "SELECT CURRENT_DATE ;";
            try (
                    Connection conn = dataSource.getConnection() ;
                    PreparedStatement ps = conn.prepareStatement( sql ) ;
            )
            {
                … make `PreparedStatement::set…` calls here.
                try (
                        ResultSet rs = ps.executeQuery() ;
                )
                {
                    if ( rs.next() )
                    {
                        LocalDate ld = rs.getObject( 1 , LocalDate.class );
                        System.out.println( "INFO - date is " + ld );
                    }
                }
            }
            catch ( SQLException e )
            {
                e.printStackTrace();
            }
        }

        System.out.println( "INFO - all done." );
    }
}
Basil Bourque
quelle