Tôi nên sử dụng tài nguyên thử với JDBC như thế nào?


148

Tôi có một phương thức để nhận người dùng từ cơ sở dữ liệu với 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;
}

Tôi nên sử dụng Java 7 dùng thử với tài nguyên như thế nào để cải thiện mã này?

Tôi đã thử với mã bên dưới, nhưng nó sử dụng nhiều trykhối và không cải thiện khả năng đọc nhiều. Tôi có nên sử dụng try-with-resourcestheo cách khác?

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

5
Trong ví dụ thứ hai của bạn, bạn không cần bên trong try (ResultSet rs = ps.executeQuery()) {đối tượng Kết quả được tự động đóng bởi đối tượng Statement đã tạo ra nó
Alexander Farber

2
@AlexanderFarber Thật không may, đã có những vấn đề nổi tiếng với các trình điều khiển không thể tự đóng tài nguyên. Trường Hard Knocks dạy chúng ta phải luôn luôn đóng tất cả các nguồn lực JDBC một cách rõ ràng, dễ dàng hơn sử dụng try-với-nguồn lực xung quanh Connection, PreparedStatementResultSetquá. Không có lý do gì để không thực sự, vì tài nguyên thử làm cho nó trở nên dễ dàng và làm cho mã của chúng tôi tự ghi lại nhiều hơn theo ý định của chúng tôi.
Basil Bourque

Câu trả lời:


85

Không cần thử bên ngoài trong ví dụ của bạn, vì vậy ít nhất bạn có thể giảm từ 3 xuống 2, và bạn cũng không cần đóng ;ở cuối danh sách tài nguyên. Ưu điểm của việc sử dụng hai khối thử là tất cả mã của bạn được hiển thị ở phía trước để bạn không phải tham khảo một phương pháp riêng biệt:

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

5
Bạn gọi Connection::setAutoCommitnhư thế nào Một cuộc gọi như vậy không được phép trong trykhoảng giữa con = ps =. Khi nhận được Kết nối từ Nguồn dữ liệu có thể được hỗ trợ với nhóm kết nối, chúng tôi không thể giả sử cách autoCommit được đặt.
Basil Bourque

1
bạn thường sẽ đưa kết nối vào phương thức (không giống như cách tiếp cận đặc biệt được thể hiện trong câu hỏi của OP), bạn có thể sử dụng lớp quản lý kết nối sẽ được gọi để cung cấp hoặc đóng kết nối (có được gộp chung hay không). trong trình quản lý đó, bạn có thể chỉ định hành vi kết nối của mình
svarog

@BasilBourque bạn có thể chuyển DriverManager.getConnection(myConnectionURL)sang một phương thức cũng đặt cờ autoCommit và trả về kết nối (hoặc đặt nó tương đương với createPreparedStatementphương thức trong ví dụ trước ...)
rogerdpack 17/8/2017

@rogerdpack Vâng, điều đó có ý nghĩa. Có cách thực hiện của riêng bạn về DataSourcenơi getConnectionphương thức thực hiện như bạn nói, nhận kết nối và định cấu hình nó khi cần, sau đó chuyển qua kết nối.
Basil Bourque

1
@rogerdpack cảm ơn bạn đã làm rõ trong câu trả lời. Tôi đã cập nhật điều này để trả lời được lựa chọn.
Jonas

187

Tôi nhận ra điều này đã được trả lời từ lâu nhưng muốn đề xuất một cách tiếp cận bổ sung để tránh khối kép thử tài nguyên lồng nhau.

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

24
Không, nó được bảo vệ, vấn đề là đoạn mã trên đang gọi chuẩn bịStatement từ bên trong một phương thức không khai báo để ném SQLException. Ngoài ra, đoạn mã trên có ít nhất một đường dẫn mà nó có thể thất bại mà không đóng câu lệnh đã chuẩn bị (nếu xảy ra lỗi SQLException trong khi gọi setInt.)
Trejkaz

1
@Trejkaz điểm tốt về khả năng không đóng PreparedStatement. Tôi đã không nghĩ về điều đó, nhưng bạn đã đúng!
Jeanne Boyarsky

2
@ArturoTena có - đơn hàng được đảm bảo
Jeanne Boyarsky

2
@JeanneBoyarsky có cách nào khác để làm việc này không? Nếu không, tôi sẽ cần tạo một phương thức createdPreparedStatement cụ thể cho mỗi câu sql
John Alexander Betts

1
Về nhận xét của Trejkaz, createPreparedStatementkhông an toàn cho dù bạn sử dụng nó như thế nào. Để khắc phục, bạn sẽ phải thêm một lần thử xung quanh setInt (...), bắt bất kỳ SQLExceptionvà khi nó xảy ra, hãy gọi ps.close () và suy nghĩ lại ngoại lệ. Nhưng điều đó sẽ dẫn đến một mã gần như dài và không hợp lý như mã mà OP muốn cải thiện.
Florian F

4

Đây là một cách ngắn gọn bằng cách sử dụng lambdas và Nhà cung cấp JDK 8 để phù hợp với mọi thứ trong lần thử bên ngoài:

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

5
Điều này ngắn gọn hơn "cách tiếp cận cổ điển" như được mô tả bởi @bpgergo? Tôi không nghĩ vậy và mã khó hiểu hơn. Vì vậy, hãy giải thích lợi thế của phương pháp này.
rmuller

Tôi không nghĩ rằng, trong trường hợp này, bạn bắt buộc phải nắm bắt rõ ràng nhận thức về SQLEx. Nó thực sự là "tùy chọn" trên một tài nguyên thử. Không có câu trả lời khác đề cập đến điều này. Vì vậy, bạn có thể có thể đơn giản hóa điều này hơn nữa.
djangofan

nếu DriverManager.getConnection (JDBC_URL, prop); trả về null?
gaurav

2

Điều gì về việc tạo ra một lớp bao bọc bổ sung?

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


Sau đó, trong lớp gọi, bạn có thể thực hiện phương thức Chuẩn bị như:

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


2
Không có gì trong bình luận ở trên bao giờ nói rằng nó không.
Trejkaz

2

Như những người khác đã nói, mã của bạn về cơ bản là chính xác mặc dù bên ngoài trylà không cần thiết. Dưới đây là một vài suy nghĩ.

DataSource

Các câu trả lời khác ở đây là đúng và tốt, chẳng hạn như Câu trả lời được chấp nhận bởi bpgergo. Nhưng không ai trong số cho thấy việc sử dụng DataSource, thường được khuyến nghị sử dụng DriverManagertrong Java hiện đại.

Vì vậy, để hoàn thiện, đây là một ví dụ hoàn chỉnh lấy ngày hiện tại từ máy chủ cơ sở dữ liệu. Cơ sở dữ liệu được sử dụng ở đây là Postgres . Bất kỳ cơ sở dữ liệu khác sẽ làm việc tương tự. Bạn sẽ thay thế việc sử dụng org.postgresql.ds.PGSimpleDataSourcebằng một triển khai DataSourcephù hợp với cơ sở dữ liệu của bạn. Việc triển khai có thể được cung cấp bởi trình điều khiển cụ thể của bạn hoặc nhóm kết nối nếu bạn đi theo tuyến đường đó.

Một DataSourcetriển khai không cần phải đóng, bởi vì nó không bao giờ được mở ra. A DataSourcekhông phải là tài nguyên, không được kết nối với cơ sở dữ liệu, vì vậy nó không giữ kết nối mạng cũng như tài nguyên trên máy chủ cơ sở dữ liệu. A DataSourcechỉ đơn giản là thông tin cần thiết khi thực hiện kết nối với cơ sở dữ liệu, với tên hoặc địa chỉ mạng của máy chủ cơ sở dữ liệu, tên người dùng, mật khẩu người dùng và các tùy chọn khác nhau mà bạn muốn chỉ định khi cuối cùng kết nối được thực hiện. Vì vậy, DataSourceđối tượng triển khai của bạn không đi vào trong ngoặc đơn thử tài nguyên của bạn.

Thử tài nguyên lồng nhau

Mã của bạn sử dụng đúng các câu lệnh try-with-resource lồng nhau.

Lưu ý trong mã ví dụ bên dưới rằng chúng tôi cũng sử dụng cú pháp try-with-resource hai lần , một cái được lồng bên trong cái kia. Bên ngoài tryđịnh nghĩa hai tài nguyên: ConnectionPreparedStatement. Bên trong tryxác định ResultSettài nguyên. Đây là một cấu trúc mã phổ biến.

Nếu một ngoại lệ được ném từ bên trong và không bị bắt ở đó, ResultSettài nguyên sẽ tự động bị đóng (nếu nó tồn tại, không phải là null). Sau đó, PreparedStatementsẽ được đóng lại, và cuối cùng Connectionlà đóng cửa. Các tài nguyên được tự động đóng theo thứ tự ngược lại trong đó chúng được khai báo trong các báo cáo thử tài nguyên.

Mã ví dụ ở đây là quá đơn giản. Như đã viết, nó có thể được thực thi với một câu lệnh try-with-resource duy nhất. Nhưng trong một công việc thực tế, bạn có thể sẽ thực hiện nhiều công việc hơn giữa các cặp trycuộc gọi lồng nhau . Ví dụ: bạn có thể trích xuất các giá trị từ giao diện người dùng hoặc POJO, sau đó chuyển các giá trị đó để đáp ứng các ?trình giữ chỗ trong SQL của bạn thông qua các lệnh gọi đến PreparedStatement::set…các phương thức.

Ghi chú cú pháp

Dấu chấm phẩy

Lưu ý rằng dấu chấm phẩy theo dõi câu lệnh tài nguyên cuối cùng trong ngoặc đơn của tài nguyên thử với là tài nguyên là tùy chọn. Tôi đưa nó vào công việc của mình vì hai lý do: Tính nhất quán và nó có vẻ hoàn chỉnh, và nó giúp việc sao chép một cách dễ dàng hơn mà không phải lo lắng về dấu chấm phẩy cuối dòng. IDE của bạn có thể gắn cờ dấu chấm phẩy cuối cùng là không cần thiết, nhưng không có hại gì khi để nó.

Java 9 - Sử dụng các vars hiện có trong tài nguyên dùng thử

Điểm mới trong Java 9 là sự cải tiến cho cú pháp thử tài nguyên. Bây giờ chúng ta có thể khai báo và điền vào các tài nguyên bên ngoài dấu ngoặc đơn của trycâu lệnh. Tôi chưa thấy điều này hữu ích cho các tài nguyên JDBC, nhưng hãy ghi nhớ nó trong công việc của bạn.

ResultSet nên tự đóng, nhưng có thể không

Trong một thế giới lý tưởng, ResultSetnó sẽ tự đóng lại như tài liệu hứa hẹn:

Một đối tượng result Set được tự động đóng khi đối tượng Statement tạo ra nó được đóng lại, được thực thi lại hoặc được sử dụng để lấy kết quả tiếp theo từ một chuỗi nhiều kết quả.

Thật không may, trong quá khứ, một số trình điều khiển JDBC đã thất bại trong việc thực hiện lời hứa này. Kết quả là, nhiều lập trình viên JDBC đã học cách đóng rõ ràng tất cả các tài nguyên JDBC của họ Connection, bao gồm cả PreparedStatement, và ResultSetquá. Cú pháp thử tài nguyên hiện đại đã giúp thực hiện dễ dàng hơn và với mã nhỏ gọn hơn. Lưu ý rằng nhóm Java đã đi sâu vào việc đánh dấu ResultSetAutoCloseablevà tôi khuyên chúng ta nên sử dụng điều đó. Việc sử dụng một tài nguyên thử với tất cả các tài nguyên JDBC của bạn làm cho mã của bạn tự ghi lại nhiều tài liệu hơn theo ý định của bạn.

Mã ví dụ

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." );
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.