Bagaimana saya harus menggunakan sumber daya coba dengan JDBC?

148

Saya punya metode untuk mendapatkan pengguna dari database dengan 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;
}

Bagaimana saya harus menggunakan Java 7 coba-dengan-sumber daya untuk meningkatkan kode ini?

Saya telah mencoba dengan kode di bawah ini, tetapi menggunakan banyak tryblok, dan tidak banyak meningkatkan keterbacaan . Haruskah saya menggunakan try-with-resourcescara lain?

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
sumber
5
Dalam contoh kedua Anda, Anda tidak perlu bagian dalam try (ResultSet rs = ps.executeQuery()) {karena objek ResultSet secara otomatis ditutup oleh objek Pernyataan yang menghasilkannya
Alexander Farber
2
@AlexanderFarber Sayangnya, ada masalah dengan driver yang gagal menutup sumber daya sendiri. Sekolah pukulan keras mengajarkan kita untuk selalu dekat semua sumber daya JDBC eksplisit, dibuat lebih mudah dengan menggunakan try-dengan-sumber di sekitar Connection, PreparedStatementdan ResultSetjuga. Tidak ada alasan untuk tidak melakukannya, karena coba-dengan-sumber daya membuatnya sangat mudah dan membuat kode kita lebih mendokumentasikan diri sendiri tentang niat kita.
Basil Bourque

Jawaban:

85

Tidak perlu untuk percobaan luar dalam contoh Anda, jadi Anda setidaknya bisa turun dari 3 ke 2, dan Anda juga tidak perlu menutup ;pada akhir daftar sumber daya. Keuntungan menggunakan dua blok percobaan adalah bahwa semua kode Anda ada di depan sehingga Anda tidak perlu merujuk ke metode terpisah:

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
sumber
5
Bagaimana Anda menelepon Connection::setAutoCommit? Panggilan seperti itu tidak diizinkan di tryantara con = dan ps =. Saat mendapatkan Koneksi dari DataSource yang mungkin didukung dengan kumpulan koneksi, kami tidak dapat berasumsi bagaimana autoCommit diatur.
Basil Bourque
1
Anda biasanya akan menyuntikkan koneksi ke metode (tidak seperti pendekatan ad-hoc yang ditunjukkan dalam pertanyaan OP), Anda bisa menggunakan kelas pengelola koneksi yang akan dipanggil untuk menyediakan atau menutup koneksi (baik itu dikumpulkan atau tidak). di manajer itu Anda dapat menentukan perilaku koneksi Anda
svarog
@BasilBourque Anda bisa pindah DriverManager.getConnection(myConnectionURL)ke metode yang juga menetapkan flag autoCommit dan mengembalikan koneksi (atau mengaturnya dengan createPreparedStatementmetode yang sama dalam contoh sebelumnya ...)
rogerdpack
@rogerdpack Ya, itu masuk akal. Miliki implementasi Anda sendiri di DataSourcemana getConnectionmetode melakukan seperti yang Anda katakan, dapatkan koneksi dan konfigurasikan sesuai kebutuhan, kemudian meneruskan koneksi.
Basil Bourque
1
@rogerdpack terima kasih atas klarifikasi dalam jawabannya. Saya telah memperbarui ini ke jawaban yang dipilih.
Jonas
187

Saya menyadari ini sudah lama dijawab tetapi ingin menyarankan pendekatan tambahan yang menghindari blok ganda coba-dengan-sumber daya bersarang.

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
sumber
24
Tidak, sudah dibahas, masalahnya adalah kode di atas memanggil prepstatement dari dalam metode yang tidak menyatakan untuk melempar SQLException. Juga, kode di atas memiliki setidaknya satu jalur di mana ia dapat gagal tanpa menutup pernyataan yang disiapkan (jika SQLException terjadi saat memanggil setInt.)
Trejkaz
1
@Trejkaz poin bagus tentang kemungkinan tidak menutup PreparedStatement. Saya tidak memikirkan itu, tetapi Anda benar!
Jeanne Boyarsky
2
@ ArturoTena ya - pesanan dijamin
Jeanne Boyarsky
2
@ JeanneBoyarsky apakah ada cara lain untuk melakukan ini? Jika tidak, saya perlu membuat metode createPreparedStatement spesifik untuk setiap kalimat sql
John Alexander Betts
1
Mengenai komentar Trejkaz, createPreparedStatementtidak aman terlepas dari bagaimana Anda menggunakannya. Untuk memperbaikinya Anda harus menambahkan try-catch di sekitar setInt (...), menangkap apa saja SQLException, dan ketika itu terjadi panggilan ps.close () dan rethrow pengecualian. Tetapi itu akan menghasilkan kode yang hampir sepanjang dan tidak memenuhi syarat seperti yang ingin ditingkatkan oleh OP.
Florian F
4

Berikut adalah cara ringkas menggunakan lambdas dan Pemasok JDK 8 agar sesuai dengan semua yang ada di luar coba:

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
sumber
5
Ini lebih ringkas daripada "pendekatan klasik" seperti yang dijelaskan oleh @bpgergo? Saya kira tidak begitu dan kodenya lebih sulit dimengerti. Jadi tolong jelaskan manfaat dari pendekatan ini.
rmuller
Saya tidak berpikir, dalam hal ini, bahwa Anda diminta untuk menangkap SQLException secara eksplisit. Ini sebenarnya "opsional" pada coba-dengan-sumber daya. Tidak ada jawaban lain yang menyebutkan ini. Jadi, Anda mungkin dapat menyederhanakan ini lebih lanjut.
djangofan
bagaimana jika DriverManager.getConnection (JDBC_URL, prop); pengembalian nol?
gaurav
2

Bagaimana dengan membuat kelas pembungkus tambahan?

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


Kemudian di kelas panggilan Anda dapat menerapkan metode prepStatement sebagai:

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
sumber
2
Tidak ada dalam komentar di atas yang mengatakan tidak.
Trejkaz
2

Seperti yang telah dinyatakan orang lain, kode Anda pada dasarnya benar meskipun bagian luar trytidak diperlukan. Ini beberapa pemikiran lagi.

DataSource

Jawaban lain di sini benar dan bagus, seperti yang diterima oleh bpgergo. Tapi tidak ada yang menunjukkan penggunaan DataSource, lebih sering direkomendasikan daripada penggunaan DriverManagerdi Jawa modern.

Jadi demi kelengkapan, berikut adalah contoh lengkap yang mengambil tanggal saat ini dari server database. Basis data yang digunakan di sini adalah Postgres . Basis data lain mana pun akan bekerja dengan cara yang sama. Anda akan mengganti penggunaan org.postgresql.ds.PGSimpleDataSourcedengan implementasi yang DataSourcesesuai dengan database Anda. Suatu implementasi kemungkinan disediakan oleh pengemudi khusus Anda, atau kumpulan koneksi jika Anda pergi rute itu.

Sebuah DataSourceimplementasi perlu tidak ditutup, karena tidak pernah “dibuka”. A DataSourcebukan sumber daya, tidak terhubung ke database, jadi ia tidak memegang koneksi jaringan atau sumber daya pada server database. A DataSourcehanyalah informasi yang diperlukan ketika membuat koneksi ke database, dengan nama atau alamat jaringan server database, nama pengguna, kata sandi pengguna, dan berbagai opsi yang ingin Anda tentukan ketika suatu koneksi akhirnya dibuat. Jadi DataSourceobjek implementasi Anda tidak masuk ke dalam tanda kurung coba-dengan-sumber daya Anda.

Bersarang coba-dengan-sumber daya

Kode Anda digunakan dengan benar untuk pernyataan try-with-resources bersarang.

Perhatikan pada contoh kode di bawah ini bahwa kami juga menggunakan sintaks coba sumber daya dua kali , satu bersarang di dalam yang lain. Bagian luar trymendefinisikan dua sumber daya: Connectiondan PreparedStatement. Batin trymendefinisikan ResultSetsumber daya. Ini adalah struktur kode yang umum.

Jika pengecualian dilemparkan dari dalam, dan tidak ditangkap di sana, ResultSetsumber daya akan secara otomatis ditutup (jika ada, bukan nol). Setelah itu, PreparedStatementakan ditutup, dan terakhir Connectionditutup. Sumber daya secara otomatis ditutup dalam urutan terbalik yang dinyatakan dalam pernyataan coba-dengan-sumber daya.

Contoh kode di sini terlalu sederhana. Seperti yang tertulis, itu dapat dieksekusi dengan pernyataan coba-dengan-sumber daya tunggal. Tetapi dalam pekerjaan nyata Anda mungkin akan melakukan lebih banyak pekerjaan di antara pasangan trypanggilan yang bersarang . Misalnya, Anda mungkin mengekstraksi nilai dari antarmuka pengguna atau POJO, dan kemudian meneruskannya untuk memenuhi ?placeholder dalam SQL Anda melalui panggilan ke PreparedStatement::set…metode.

Catatan sintaks

Mengejar titik koma

Perhatikan bahwa koma yang membuntuti pernyataan sumber daya terakhir dalam tanda kurung sumber daya percobaan adalah opsional. Saya memasukkannya ke dalam karya saya sendiri karena dua alasan: Konsistensi dan kelihatannya lengkap, dan itu membuat copy-paste campuran garis lebih mudah tanpa harus khawatir tentang titik koma akhir-line. IDE Anda mungkin menandai titik koma terakhir sebagai berlebihan, tetapi tidak ada salahnya meninggalkannya.

Java 9 - Gunakan vars yang ada di try-with-resources

Baru di Java 9 adalah penyempurnaan dari sintaks coba sumber daya. Kami sekarang dapat mendeklarasikan dan mengisi sumber daya di luar tanda kurung trypernyataan. Saya belum menemukan ini berguna untuk sumber daya JDBC, tetapi perlu diingat dalam pekerjaan Anda sendiri.

ResultSet harus menutup sendiri, tetapi mungkin tidak

Dalam dunia yang ideal itu ResultSetakan menutup sendiri sesuai dengan yang dijanjikan dokumentasi:

Objek ResultSet ditutup secara otomatis ketika objek Pernyataan yang dihasilkannya ditutup, dijalankan kembali, atau digunakan untuk mengambil hasil berikutnya dari urutan beberapa hasil.

Sayangnya, di masa lalu beberapa driver JDBC terkenal gagal memenuhi janji ini. Akibatnya, banyak programmer JDBC belajar untuk secara eksplisit menutup semua sumber daya JDBC mereka termasukConnection , PreparedStatementdan ResultSetjuga. Sintaks coba-dengan-sumber daya modern telah membuatnya menjadi lebih mudah, dan dengan kode yang lebih ringkas. Perhatikan bahwa tim Java bersusah payah menandai ResultSetsebagai AutoCloseable, dan saya sarankan kita memanfaatkannya. Menggunakan coba-dengan-sumber daya di sekitar semua sumber daya JDBC Anda membuat kode Anda lebih mendokumentasikan diri sendiri tentang niat Anda.

Contoh kode

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
sumber