Jak używać zasobów typu Try-with-Resources w JDBC?

148

Mam metodę pobierania użytkowników z bazy danych za pomocą 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;
}

W jaki sposób powinienem korzystać z zasobów Try-with- Java 7, aby ulepszyć ten kod?

Próbowałem z poniższym kodem, ale używa on wielu trybloków i nie poprawia zbytnio czytelności . Czy powinienem używać try-with-resourcesw inny sposób?

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
źródło
5
W swoim drugim przykładzie nie trzeba wewnętrzna try (ResultSet rs = ps.executeQuery()) {ponieważ obiekt ResultSet zostaje automatycznie zamknięty przez obiekt oświadczenie, że generowanego
Alexander Farber
2
@AlexanderFarber Niestety, pojawiły się notoryczne problemy ze sterownikami, które samodzielnie nie zamknęły zasobów. School of Hard puka uczy nas zawsze blisko wszystkich zasobów JDBC wyraźnie, łatwiejsze przy użyciu try-with-zasobów wokół Connection, PreparedStatementi ResultSetzbyt. Nie ma powodu, aby tego nie robić, ponieważ próba z zasobami sprawia, że ​​jest to tak łatwe i sprawia, że ​​nasz kod jest bardziej samodokumentujący co do naszych intencji.
Basil Bourque

Odpowiedzi:

85

W twoim przykładzie nie ma potrzeby wykonywania zewnętrznej próby, więc możesz przynajmniej zejść z 3 do 2, a także nie musisz zamykać ;na końcu listy zasobów. Zaletą używania dwóch bloków try jest to, że cały kod jest obecny na początku, więc nie musisz odwoływać się do oddzielnej metody:

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
źródło
5
Jak dzwonisz Connection::setAutoCommit? Takie połączenie nie jest dozwolone w okresie trymiędzy con = a ps =. Podczas uzyskiwania połączenia ze źródła danych, które może być obsługiwane przez pulę połączeń, nie możemy założyć, jak ustawiono funkcję autoCommit.
Basil Bourque
1
zazwyczaj wprowadzasz połączenie do metody (w przeciwieństwie do podejścia ad-hoc pokazanego w pytaniu OP), możesz użyć klasy zarządzania połączeniem, która zostanie wywołana w celu zapewnienia lub zamknięcia połączenia (czy to w puli, czy nie). w tym menedżerze możesz określić zachowanie połączenia
svarog
@BasilBourque możesz przejść DriverManager.getConnection(myConnectionURL)do metody, która również ustawia flagę autoCommit i zwraca połączenie (lub ustawia je w odpowiedniku createPreparedStatementmetody z poprzedniego przykładu ...)
rogerdpack
@rogerdpack Tak, to ma sens. Stwórz własną implementację tego, DataSourcegdzie getConnectionmetoda działa tak, jak mówisz, uzyskaj połączenie i skonfiguruj je zgodnie z potrzebami, a następnie przekaż połączenie.
Basil Bourque
1
@rogerdpack dzięki za wyjaśnienie w odpowiedzi. Zaktualizowałem to do wybranej odpowiedzi.
Jonas
187

Zdaję sobie sprawę, że odpowiedź była dawno temu, ale chcę zasugerować dodatkowe podejście, które pozwala uniknąć zagnieżdżonego podwójnego bloku próbującego z zasobami.

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
źródło
24
Nie, jest to omówione, problem polega na tym, że powyższy kod wywołuje metodę preparatowania z wnętrza metody, która nie deklaruje zgłaszania wyjątku SQLException. Ponadto powyższy kod ma co najmniej jedną ścieżkę, w której może się nie powieść bez zamykania przygotowanej instrukcji (jeśli wystąpi SQLException podczas wywoływania setInt.)
Trejkaz
1
@Trejkaz dobry punkt widzenia na możliwość niezamykania przygotowanego sprawozdania. Nie pomyślałem o tym, ale masz rację!
Jeanne Boyarsky
2
@ArturoTena tak - zamówienie jest gwarantowane
Jeanne Boyarsky
2
@JeanneBoyarsky czy jest inny sposób, aby to zrobić? Jeśli nie, musiałbym utworzyć specjalną metodę createPreparedStatement dla każdego zdania sql
John Alexander Betts,
1
Jeśli chodzi o komentarz Trejkaz, createPreparedStatementjest niebezpieczny, niezależnie od tego, jak go używasz. Aby to naprawić, musiałbyś dodać try-catch wokół setInt (...), złapać dowolny SQLException, a kiedy to się stanie, wywołaj ps.close () i ponownie wyrzuć wyjątek. Ale to spowodowałoby, że kod byłby prawie tak długi i nieelegancki, jak kod, który OP chciał ulepszyć.
Florian F
4

Oto zwięzły sposób wykorzystania lambd i dostawcy JDK 8, aby dopasować wszystko do zewnętrznej próby:

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
źródło
5
Jest to bardziej zwięzłe niż „klasyczne podejście” opisane przez @bpgergo? Nie sądzę, a kod jest trudniejszy do zrozumienia. Proszę więc wyjaśnić zalety tego podejścia.
rmuller
Nie sądzę, że w tym przypadku musisz jawnie wychwycić SQLException. W rzeczywistości jest to „opcjonalne” przy próbie z zasobami. Żadne inne odpowiedzi o tym nie wspominają. Więc prawdopodobnie możesz to jeszcze bardziej uprościć.
djangofan
co jeśli DriverManager.getConnection (JDBC_URL, prop); zwraca null?
gaurav
2

A co z utworzeniem dodatkowej klasy opakowania?

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


Następnie w klasie wywołującej można zaimplementować metodę readyStatement jako:

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
źródło
2
Nic w powyższym komentarzu nigdy nie mówi, że tak nie jest.
Trejkaz
2

Jak powiedzieli inni, twój kod jest w zasadzie poprawny, chociaż zewnętrzna część tryjest niepotrzebna. Oto kilka innych myśli.

DataSource

Inne odpowiedzi tutaj są poprawne i dobre, takie jak zaakceptowana odpowiedź bpgergo. Ale żaden z nich nie używa DataSource, powszechnie zalecanego używania, DriverManagerwe współczesnej Javie.

Tak więc, aby uzyskać kompletność, oto kompletny przykład, który pobiera bieżącą datę z serwera bazy danych. Użyta tutaj baza danych to Postgres . Każda inna baza danych działałaby podobnie. Zastąpiłbyś użycie org.postgresql.ds.PGSimpleDataSourceimplementacją DataSourceodpowiedniej dla Twojej bazy danych. Implementacja jest prawdopodobnie dostarczana przez określony sterownik lub pulę połączeń, jeśli wybierzesz tę trasę.

DataSourceRealizacja musi nie być zamknięte, ponieważ nigdy nie jest „otwarty”. A DataSourcenie jest zasobem, nie jest połączony z bazą danych, więc nie utrzymuje połączeń sieciowych ani zasobów na serwerze bazy danych. A DataSourceto po prostu informacje potrzebne podczas nawiązywania połączenia z bazą danych, z nazwą lub adresem sieciowym serwera bazy danych, nazwą użytkownika, hasłem użytkownika i różnymi opcjami, które mają być określone po ostatecznym nawiązaniu połączenia. Tak więc DataSourceobiekt implementacji nie jest umieszczany w nawiasach próbnych z zasobami.

Zagnieżdżone try-with-resources

Twój kod prawidłowo wykorzystuje zagnieżdżone instrukcje try-with-resources.

Zauważ, że w poniższym przykładowym kodzie używamy również składni try-with-resources dwukrotnie , jedna zagnieżdżona w drugiej. Zewnętrzny trydefiniuje dwa zasoby: Connectioni PreparedStatement. Wewnętrzna tryokreśla ResultSetzasób. To jest wspólna struktura kodu.

Jeśli wyjątek zostanie wyrzucony z wewnętrznego i nie zostanie tam przechwycony, ResultSetzasób zostanie automatycznie zamknięty (jeśli istnieje, nie jest zerowy). Następnie PreparedStatementzostanie zamknięty, a na koniec Connectionzamknięty. Zasoby są automatycznie zamykane w odwrotnej kolejności, w jakiej zostały zadeklarowane w instrukcjach try-with-resource.

Przykładowy kod jest zbyt uproszczony. Jak napisano, można to wykonać za pomocą jednej instrukcji try-with-resources. Ale w prawdziwej pracy prawdopodobnie wykonasz więcej pracy między zagnieżdżonymi parami trywywołań. Na przykład możesz wyodrębniać wartości z interfejsu użytkownika lub POJO, a następnie przekazywać je w celu wypełnienia ?symboli zastępczych w języku SQL za pośrednictwem wywołań PreparedStatement::set…metod.

Uwagi dotyczące składni

Końcowy średnik

Zwróć uwagę, że średnik kończący ostatnią instrukcję zasobu w nawiasach próby z zasobami jest opcjonalny. Uwzględniam go w mojej pracy z dwóch powodów: spójności i wygląda na kompletny, a także ułatwia kopiowanie i wklejanie mieszanki linii bez martwienia się o średniki końca wiersza. Twoje IDE może oznaczyć ostatni średnik jako zbędny, ale nie ma nic złego w pozostawieniu go.

Java 9 - Użyj istniejących zmiennych w próbach z zasobami

Nowością w Javie 9 jest ulepszenie składni try-with-resources. Teraz możemy zadeklarować i wypełnić zasoby poza nawiasami w tryinstrukcji. Nie znalazłem jeszcze tego przydatnego dla zasobów JDBC, ale miej to na uwadze w swojej własnej pracy.

ResultSet powinien się zamknąć, ale nie może

W idealnym świecie ResultSetzamknęłaby się, jak obiecuje dokumentacja:

Obiekt ResultSet jest automatycznie zamykany, gdy obiekt Statement, który go wygenerował, zostanie zamknięty, ponownie wykonany lub użyty do pobrania następnego wyniku z sekwencji wielu wyników.

Niestety, w przeszłości niektóre sterowniki JDBC niesławnie nie spełniały tej obietnicy. W rezultacie, wielu programistów JDBC nauczył się wyraźnie blisko wszystkich swoich zasobów JDBC tym Connection, PreparedStatementi ResultSetteż. Nowoczesna składnia try-with-resources sprawiła, że ​​jest to łatwiejsze i bardziej kompaktowe. Zauważ, że zespół Java zajął się oznaczaniem ResultSetjako AutoCloseablei sugeruję, abyśmy to wykorzystali. Korzystanie z zasobów typu „try-with-resources” wokół wszystkich zasobów JDBC sprawia, że ​​kod jest bardziej samodokumentujący, jeśli chodzi o intencje.

Przykład kodu

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
źródło