ฉันควรใช้ลองกับทรัพยากรกับ JDBC อย่างไร


148

ฉันมีวิธีรับผู้ใช้จากฐานข้อมูลด้วย 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;
}

ฉันควรใช้ Java 7 ลองกับทรัพยากรเพื่อปรับปรุงรหัสนี้อย่างไร

ฉันได้ลองใช้โค้ดด้านล่าง แต่ใช้หลายtryบล็อกและไม่ปรับปรุงการอ่านได้มากนัก ฉันควรใช้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;
}

5
ในตัวอย่างที่สองของคุณคุณไม่จำเป็นต้องใช้ Inner try (ResultSet rs = ps.executeQuery()) {เพราะวัตถุ ResultSet ถูกปิดโดยอัตโนมัติโดยวัตถุ Statement ที่สร้างมันขึ้นมา
Alexander Farber

2
@AlexanderFarber แต่น่าเสียดายที่มีปัญหาฉาวโฉ่กับไดรเวอร์ที่ล้มเหลวในการปิดทรัพยากรด้วยตัวเอง โรงเรียนของ Hard Knocks สอนให้เราเสมอใกล้ทรัพยากรทั้งหมด JDBC อย่างชัดเจนทำให้ง่ายขึ้นโดยใช้ลองกับทรัพยากรรอบConnection, PreparedStatementและResultSetมากเกินไป ไม่มีเหตุผลที่จะไม่ทำจริงๆเพราะลองกับทรัพยากรทำให้มันง่ายมากและทำให้รหัสของเราเอกสารด้วยตนเองมากขึ้นตามความตั้งใจของเรา
Basil Bourque

คำตอบ:


85

ไม่จำเป็นต้องลองภายนอกในตัวอย่างของคุณดังนั้นอย่างน้อยคุณสามารถลงจาก 3 เป็น 2 และคุณไม่จำเป็นต้องปิด;ท้ายรายการทรัพยากร ข้อได้เปรียบของการใช้บล็อกการลองสองแบบคือโค้ดทั้งหมดของคุณมีอยู่แล้วดังนั้นคุณไม่จำเป็นต้องอ้างถึงวิธีอื่น:

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
คุณโทรมาConnection::setAutoCommitอย่างไร ไม่อนุญาตให้มีการโทรภายในtryระหว่างcon = และps =ถึง เมื่อรับการเชื่อมต่อจากแหล่งข้อมูลที่อาจถูกสำรองข้อมูลด้วยพูลการเชื่อมต่อเราไม่สามารถสันนิษฐานได้ว่าตั้งค่า autoCommit อย่างไร
Basil Bourque

1
คุณมักจะฉีดการเชื่อมต่อเข้าไปในวิธีการ (ซึ่งแตกต่างจากวิธี ad-hoc ที่แสดงในคำถามของ OP) คุณสามารถใช้คลาสการจัดการการเชื่อมต่อที่จะถูกเรียกว่าให้หรือปิดการเชื่อมต่อ (ไม่ว่าจะรวมหรือไม่) ในการจัดการที่คุณสามารถระบุพฤติกรรมการเชื่อมต่อของคุณ
Svarog

@BasilBourque คุณสามารถย้ายDriverManager.getConnection(myConnectionURL)ไปยังวิธีการที่ตั้งค่าสถานะ autoCommit และส่งคืนการเชื่อมต่อ (หรือตั้งค่าเทียบเท่าcreatePreparedStatementวิธีในตัวอย่างก่อนหน้านี้ ... )
rogerdpack

@rogerdpack ใช่ว่าเหมาะสมแล้ว เตรียมการใช้งานของคุณเองในDataSourceที่ซึ่งgetConnectionวิธีการทำตามที่คุณพูดรับการเชื่อมต่อและกำหนดค่าตามที่ต้องการจากนั้นส่งผ่านการเชื่อมต่อ
Basil Bourque

1
@rogerdpack ขอบคุณสำหรับการชี้แจงในคำตอบ ฉันได้อัปเดตสิ่งนี้เป็นคำตอบที่เลือก
Jonas

187

ฉันรู้ว่าสิ่งนี้ได้รับคำตอบมานานแล้ว แต่ต้องการแนะนำวิธีการเพิ่มเติมที่หลีกเลี่ยงการบล็อกซ้อนแบบลองกับทรัพยากร

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
ไม่มันได้รับการครอบคลุมปัญหาคือรหัสข้างต้นกำลังเรียก prepareStatement จากภายในวิธีที่ไม่ได้ประกาศว่าจะโยน SQLException นอกจากนี้โค้ดด้านบนมีอย่างน้อยหนึ่งพา ธ ที่สามารถล้มเหลวได้โดยไม่ต้องปิดคำสั่งที่เตรียมไว้ (หาก SQLException เกิดขึ้นขณะเรียกใช้ setInt.)
Trejkaz

1
@ Trejkaz จุดที่ดีเกี่ยวกับความเป็นไปได้ที่จะไม่ปิด PreparedStatement ฉันไม่คิดอย่างนั้น แต่คุณพูดถูก!
Jeanne Boyarsky

2
@ArturoTena ใช่ - รับประกันการสั่งซื้อ
Jeanne Boyarsky

2
@JeanneBoyarsky มีวิธีอื่นในการทำเช่นนี้หรือไม่? ถ้าไม่ใช่ฉันจะต้องสร้างเมธอด createPreparedStatement เฉพาะสำหรับแต่ละประโยค sql
John Alexander Betts

1
เกี่ยวกับความคิดเห็นของ Trejkaz createPreparedStatementไม่ปลอดภัยไม่ว่าคุณจะใช้งานอย่างไร หากต้องการแก้ไขคุณจะต้องเพิ่มการลองรอบ setInt (... ) จับใด ๆSQLExceptionและเมื่อมันเกิดขึ้นโทร ps.close () และ rethrow ข้อยกเว้น แต่นั่นจะส่งผลให้โค้ดเกือบยาวและไม่เหมาะกับโค้ดที่ OP ต้องการปรับปรุง
Florian F

4

นี่เป็นวิธีที่กระชับโดยใช้ lambdas และผู้จำหน่าย JDK 8 เพื่อให้พอดีกับทุกสิ่งในการทดลองด้านนอก:

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
นี่จะกระชับกว่า "วิธีการแบบคลาสสิก" ตามที่อธิบายโดย @bpgergo? ฉันไม่คิดอย่างนั้นและรหัสนั้นยากที่จะเข้าใจ ดังนั้นโปรดอธิบายข้อดีของวิธีนี้
rmuller

ฉันไม่คิดว่าในกรณีนี้คุณต้องรับ SQLException อย่างชัดเจน เป็นจริง "ตัวเลือก" บนลองกับทรัพยากร ไม่มีคำตอบอื่น ๆ ที่พูดถึงเรื่องนี้ ดังนั้นคุณอาจทำให้เรื่องนี้ง่ายขึ้นอีก
djangofan

เกิดอะไรขึ้นถ้า DriverManager.getConnection (JDBC_URL, เสา) ผลตอบแทนที่เป็นโมฆะ?
gaurav

2

สิ่งที่เกี่ยวกับการสร้างคลาส wrapper เพิ่มเติม?

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


จากนั้นในคลาสการเรียกคุณสามารถใช้วิธีการเตรียมความพร้อมเป็น:

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
ไม่มีอะไรในความคิดเห็นข้างต้นไม่เคยบอกว่ามันไม่ได้
Trejkaz

2

ตามที่คนอื่น ๆ ระบุไว้รหัสของคุณนั้นถูกต้องแล้วแม้ว่าส่วนนอกtryนั้นไม่จำเป็น นี่คือความคิดอีกไม่กี่

DataSource

คำตอบอื่น ๆ ที่นี่ถูกต้องและดีเช่นคำตอบที่ยอมรับโดย bpgergo แต่ไม่มีการใช้งานDataSourceที่แนะนำโดยทั่วไปแนะนำให้ใช้มากกว่าDriverManagerในจาวาที่ทันสมัย

ดังนั้นเพื่อความสมบูรณ์นี่คือตัวอย่างที่สมบูรณ์ที่ดึงข้อมูลวันที่ปัจจุบันจากเซิร์ฟเวอร์ฐานข้อมูล ฐานข้อมูลที่ใช้ที่นี่เป็นPostgres ฐานข้อมูลอื่นจะทำงานในทำนองเดียวกัน คุณจะแทนที่การใช้org.postgresql.ds.PGSimpleDataSourceด้วยการใช้งานDataSourceที่เหมาะสมกับฐานข้อมูลของคุณ อาจมีการนำไปใช้โดยไดรเวอร์เฉพาะหรือพูลการเชื่อมต่อหากคุณไปเส้นทางนั้น

DataSourceการดำเนินการต้องไม่ถูกปิดเพราะมันจะไม่“เปิด” A DataSourceไม่ใช่ทรัพยากรไม่ได้เชื่อมต่อกับฐานข้อมูลดังนั้นจึงไม่ถือการเชื่อมต่อเครือข่ายหรือทรัพยากรบนเซิร์ฟเวอร์ฐานข้อมูล A DataSourceเป็นเพียงข้อมูลที่จำเป็นเมื่อทำการเชื่อมต่อกับฐานข้อมูลด้วยชื่อเครือข่ายหรือที่อยู่ของเซิร์ฟเวอร์ฐานข้อมูลชื่อผู้ใช้รหัสผ่านผู้ใช้และตัวเลือกต่าง ๆ ที่คุณต้องการระบุเมื่อทำการเชื่อมต่อในที่สุด ดังนั้นDataSourceวัตถุที่ใช้งานของคุณจะไม่เข้าไปในวงเล็บลองกับทรัพยากร

ซ้อนกันลองกับทรัพยากร

รหัสของคุณใช้อย่างเหมาะสมกับคำสั่งลองกับทรัพยากรที่ซ้อนกัน

โปรดสังเกตในรหัสตัวอย่างด้านล่างว่าเรายังใช้ไวยากรณ์ลองกับทรัพยากรสองครั้งซ้อนกันภายในอีก ด้านนอกtryกำหนดสองทรัพยากร: และConnection PreparedStatementภายในtryกำหนดResultSetทรัพยากร นี่คือโครงสร้างรหัสทั่วไป

หากมีข้อผิดพลาดถูกโยนออกมาจากภายในและไม่ถูกจับที่นั่นResultSetทรัพยากรจะถูกปิดโดยอัตโนมัติ (ถ้ามีอยู่จะไม่เป็นโมฆะ) หลังจากนั้นPreparedStatementจะปิดและสุดท้ายConnectionปิด ทรัพยากรถูกปิดโดยอัตโนมัติในลำดับย้อนกลับที่ประกาศในคำสั่งลองกับทรัพยากร

ตัวอย่างโค้ดที่นี่เรียบง่ายเกินไป ตามที่เขียนไว้มันสามารถดำเนินการได้ด้วยคำสั่งลองกับทรัพยากร แต่ในการทำงานจริงคุณอาจทำงานได้มากขึ้นระหว่างการtryโทรที่ซ้อนกัน ตัวอย่างเช่นคุณอาจกำลังดึงค่าจากส่วนติดต่อผู้ใช้หรือ POJO ของคุณแล้วส่งค่าเหล่านั้นเพื่อเติม?เต็มตัวยึดตำแหน่งภายใน SQL ของคุณผ่านการเรียกไปยังPreparedStatement::set…เมธอด

บันทึกไวยากรณ์

อัฒภาคต่อท้าย

ขอให้สังเกตว่าเครื่องหมายอัฒภาคต่อท้ายคำสั่งทรัพยากรล่าสุดภายในวงเล็บของ try-with-resources เป็นทางเลือก ฉันรวมไว้ในงานของฉันเองด้วยเหตุผลสองประการ: ความสอดคล้องและดูสมบูรณ์และทำให้การคัดลอกวางการผสมของบรรทัดง่ายขึ้นโดยไม่ต้องกังวลเกี่ยวกับอัฒภาคปลายเส้น IDE ของคุณอาจตั้งค่าเซมิโคลอนล่าสุดเป็นฟุ่มเฟือย แต่ไม่มีอันตรายใด ๆ

Java 9 - ใช้ vars ที่มีอยู่ในลองกับทรัพยากร

ใหม่ใน Java 9คือการปรับปรุงไวยากรณ์ลองกับทรัพยากร ตอนนี้เราสามารถประกาศและเติมทรัพยากรที่อยู่นอกวงเล็บของtryคำสั่ง ฉันยังไม่พบสิ่งนี้มีประโยชน์สำหรับทรัพยากร JDBC แต่จำไว้ในการทำงานของคุณเอง

ResultSet ควรปิดตัวเอง แต่อาจไม่

ในโลกอุดมคติที่ResultSetจะปิดตัวเองเป็นเอกสารสัญญา:

วัตถุ ResultSet จะถูกปิดโดยอัตโนมัติเมื่อวัตถุ Statement ที่สร้างมันถูกปิดดำเนินการใหม่หรือใช้เพื่อดึงผลลัพธ์ต่อไปจากลำดับของผลลัพธ์หลายรายการ

น่าเสียดายที่ในอดีตไดรเวอร์ JDBC บางตัวล้มเหลวอย่างน่าอับอายที่จะทำตามสัญญานี้ เป็นผลให้โปรแกรมเมอร์ JDBC จำนวนมากได้เรียนรู้อย่างชัดเจนใกล้ทรัพยากรทั้งหมดของพวกเขารวมทั้ง JDBC Connection, PreparedStatementและResultSetมากเกินไป ไวยากรณ์ try-with-resources ที่ทันสมัยทำให้การทำง่ายขึ้นและใช้โค้ดขนาดกะทัดรัดยิ่งขึ้น ขอให้สังเกตว่าทีม Java ไปยุ่งเกี่ยวResultSetกับการทำเครื่องหมายเป็นAutoCloseableและฉันขอแนะนำให้เราใช้มัน การใช้การลองกับทรัพยากรรอบ ๆ ทรัพยากร JDBC ทั้งหมดของคุณจะทำให้โค้ดของคุณจัดทำเอกสารด้วยตนเองมากขึ้นตามความตั้งใจของคุณ

ตัวอย่างรหัส

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." );
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.