Як я повинен використовувати пробні ресурси з 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
У вашому другому прикладі вам не потрібен внутрішній, try (ResultSet rs = ps.executeQuery()) {оскільки об'єкт ResultSet автоматично закривається об'єктом Statement, який його генерував
Олександр Фарбер

2
@AlexanderFarber На жаль, були горезвісні проблеми з драйверами, які не змогли закрити ресурси самостійно. Школа жорстких ударів вчить нас завжди близько все ресурси JDBC явно спрощує використання примірочних з-ресурсів навколо Connection, PreparedStatementі ResultSetтеж. Немає причин, щоб це не так, оскільки спроба використання ресурсів робить це простішим і робить наш код більш самодокументованим щодо наших намірів.
Василь Бурк

Відповіді:


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 =. Отримуючи з'єднання з DataSource, яке може бути підкріплено пулом з'єднань, ми не можемо припустити, як встановлено функцію автокомісії.
Василь Бурк

1
ти зазвичай вводиш з'єднання у метод (на відміну від спеціального підходу, показаного у питанні ОП), ти можеш використовувати клас управління з’єднанням, який буде викликаний для забезпечення або закриття з'єднання (об'єднаний чи ні). у цьому менеджері ви можете вказати свою поведінку підключення
svarog

@BasilBourque ви можете перейти DriverManager.getConnection(myConnectionURL)до методу, який також встановлює прапор autoCommit і повертає з'єднання (або встановлює його в еквіваленті createPreparedStatementметоду в попередньому прикладі ...)
rogerdpack

@rogerdpack Так, це має сенс. По-власному реалізуйте те, DataSourceде getConnectionметод працює, як ви кажете, встановіть з'єднання та налаштуйте його за потребою, перейшовши на з'єднання.
Василь Бурк

1
@rogerdpack дякую за пояснення у відповіді. Я оновив це до вибраної відповіді.
Йонас

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
Ні, це висвітлено, проблема полягає в тому, що вищезазначений код викликає PrepaStatement зсередини методу, який не оголошує кидати SQLException. Також у наведеному вище коді є принаймні один шлях, де він може пройти, не закриваючи підготовлений оператор (якщо під час виклику setInt виникає SQLException)
Trejkaz

1
@Trejkaz хороший момент щодо можливості не закривати Підготовлений стан. Я не думав про це, але ти маєш рацію!
Жанна Боярський

2
@ArturoTena так - замовлення гарантовано
Жанна Боярський

2
@JeanneBoyarsky чи є інший спосіб зробити це? Якщо ні, мені потрібно створити специфічний метод createPreparedStatement для кожного речення sql
Джон Олександр Беттс

1
Щодо коментаря Trejkaz, createPreparedStatementвін небезпечний незалежно від того, яким чином ви ним користуєтесь. Щоб виправити це, вам доведеться додати пробний лов навколо setInt (...), зловити будь-який SQLException, а коли це станеться, зателефонувати ps.close () та повторно скинути виняток. Але це призвело б до коду, майже такого ж довгого і неелегантного, як код, який хотів удосконалити ОП.
Флоріан F

4

Ось стислий спосіб використання лямбда та постачальника 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

А як же створити додатковий клас обгортки?

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


Тоді в класі виклику ви можете реалізувати метод PrepaStatement як:

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
Нічого в коментарі вище ніколи не говорить, що це не так.
Трейказ

2

Як зазначають інші, ваш код в основному правильний, хоча зовнішній tryвигляд непотрібний. Ось ще кілька думок.

DataSource

Інші відповіді тут правильні і хороші, такий прийнятий Відповідь від bpgergo. Але жоден з них не показує використання DataSource, як правило, рекомендується над використанням DriverManagerсучасних Java.

Отже, для повноти, ось повний приклад, який отримує поточну дату з сервера баз даних. Тут використовується база даних 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…методів.

Синтаксичні нотатки

Зворотна крапка з комою

Зауважте, що крапка з комою, що відкладає останню заяву про ресурси, в дужках спробувати-ресурси - необов’язкова. Я включаю його до своєї роботи з двох причин: Послідовність і вона виглядає повноцінною, і це полегшує копіювання суміші рядків легше, не турбуючись про крапки з комою крапки. Ваш IDE може позначити останню крапку з комою як зайву, але немає шкоди, залишаючи її.

Java 9 - Використовуйте наявні VARS у спробах використання ресурсів

Нове в Java 9 - це вдосконалення синтаксису "пробних ресурсів". Тепер ми можемо оголошувати та заповнювати ресурси поза дужками tryзаяви. Я ще не знайшов цього корисного для ресурсів JDBC, але пам’ятайте про це у власній роботі.

ResultSet повинен закрити себе, а може і не

В ідеальному світі це ResultSetзакриється, як обіцяє документація:

Об'єкт ResultSet автоматично закривається, коли об'єкт заяви, який його створив, закривається, повторно виконується або використовується для отримання наступного результату з послідовності декількох результатів.

На жаль, у минулому деякі водії JDBC ганебно не виконали цієї обіцянки. В результаті, багато програмістів JDBC навчилися явно закрити всі свої ресурси , включаючи JDBC Connection, PreparedStatementі ResultSetтеж. Сучасний синтаксис "пробних ресурсів" зробив це простіше і з більш компактним кодом. Зауважте, що команда 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." );
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.