2015-11-14 11 views
9

Oto Main.java:Czy istnieje bardziej przejrzysty sposób użycia funkcji prób z zasobami i PreparedStatement?

package foo.sandbox.db; 

import java.sql.Connection; 
import java.sql.PreparedStatement; 
import java.sql.ResultSet; 
import java.sql.SQLException; 

public class Main { 
    public static void main(String[] args) { 
     final String SQL = "select * from NVPAIR where name=?"; 
     try (
       Connection connection = DatabaseManager.getConnection(); 
       PreparedStatement stmt = connection.prepareStatement(SQL); 
       DatabaseManager.PreparedStatementSetter<PreparedStatement> ignored = new DatabaseManager.PreparedStatementSetter<PreparedStatement>(stmt) { 
        @Override 
        public void init(PreparedStatement ps) throws SQLException { 
         ps.setString(1, "foo"); 
        } 
       }; 
       ResultSet rs = stmt.executeQuery() 
     ) { 
      while (rs.next()) { 
       System.out.println(rs.getString("name") + "=" + rs.getString("value")); 
      } 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 
    } 
} 

A oto DatabaseManager.java

package foo.sandbox.db; 

import java.sql.Connection; 
import java.sql.DriverManager; 
import java.sql.PreparedStatement; 
import java.sql.SQLException; 
import java.sql.Statement; 

/** 
* Initialize script 
* ----- 
* CREATE TABLE NVPAIR; 
* ALTER TABLE PUBLIC.NVPAIR ADD value VARCHAR2 NULL; 
* ALTER TABLE PUBLIC.NVPAIR ADD id int NOT NULL AUTO_INCREMENT; 
* CREATE UNIQUE INDEX NVPAIR_id_uindex ON PUBLIC.NVPAIR (id); 
* ALTER TABLE PUBLIC.NVPAIR ADD name VARCHAR2 NOT NULL; 
* ALTER TABLE PUBLIC.NVPAIR ADD CONSTRAINT NVPAIR_name_pk PRIMARY KEY (name); 
* 
* INSERT INTO NVPAIR(name, value) VALUES('foo', 'foo-value'); 
* INSERT INTO NVPAIR(name, value) VALUES('bar', 'bar-value'); 
*/ 
public class DatabaseManager { 
    /** 
    * Class to allow PreparedStatement to initialize parmaters inside try-with-resource 
    * @param <T> extends Statement 
    */ 
    public static abstract class PreparedStatementSetter<T extends Statement> implements AutoCloseable { 
     public PreparedStatementSetter(PreparedStatement pstmt) throws SQLException { 
      init(pstmt); 
     } 

     @Override 
     public void close() throws Exception { 
     } 

     public abstract void init(PreparedStatement pstmt) throws SQLException; 
    } 

    /* Use local file for database */ 
    private static final String JDBC_CONNECTION = "jdbc:h2:file:./db/sandbox_h2.db;MODE=PostgreSQL"; 

    static { 
     try { 
      Class.forName("org.h2.Driver"); // Init H2 DB driver 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 
    } 

    /** 
    * @return Database connection 
    * @throws SQLException 
    */ 
    public static Connection getConnection() throws SQLException { 
     return DriverManager.getConnection(JDBC_CONNECTION, "su", ""); 
    } 
} 

używam bazy danych H2 dla uproszczenia, ponieważ jest to jeden plik, który oparty jest łatwo tworzyć i testować dalej.

Tak więc wszystko działa i zasoby są oczyszczane zgodnie z oczekiwaniami, ale po prostu uważam, że może istnieć czystszy sposób ustawiania parametrów PreparedStatement z bloku try-with-resources (i nie chcę używać zagnieżdżonych try/catch bloki, ponieważ te wyglądają "niezręcznie"). Być może istnieje już klasa pomocnicza w JDBC, która właśnie to robi, ale nie udało mi się jej znaleźć.

Najlepiej z funkcją lambda, aby zainicjować PreparedStatement, ale nadal wymagałoby przydziału obiektu AutoCloseable, aby mógł znajdować się w try-with-resources.

+1

Prawdopodobny duplikat [Jak powinienem używać try-with-resources z JDBC?] (Http://stackoverflow.com/questions/8066501/how-should-i-usead-the-resources-with-jdbc) –

+0

Mam nadzieję znaleźć sposób na zrobienie init dla PreparedStatement za pomocą lambda zamiast instancji klasy, prawie jak przesyłanie parametrów do PreparedStatement. – AlexC

Odpowiedz

7

Po pierwsze, klasa PreparedStatementSetter jest niewygodne:

  • jest to klasa wpisywanych ale typ nie jest używany.
  • konstruktor jawnie wywołuje metodę możliwą do zastąpienia, which is a bad practice.

Zamiast tego należy rozważyć następujący interfejs (zainspirowany wersją Spring interface o tej samej nazwie).

public interface PreparedStatementSetter { 
    void setValues(PreparedStatement ps) throws SQLException; 
} 

Interfejs ten definiuje umowę co PreparedStatementSetter ma robić: ustawienie wartości w PreparedStatement, nic więcej.

W takim przypadku lepiej byłoby utworzyć i zainicjować model PreparedStatement w ramach jednej metody. Rozważmy następujący dodatek wewnątrz klasy DatabaseManager:

public static PreparedStatement prepareStatement(Connection connection, String sql, PreparedStatementSetter setter) throws SQLException { 
    PreparedStatement ps = connection.prepareStatement(sql); 
    setter.setValues(ps); 
    return ps; 
} 

Dzięki tej metodzie statycznej można następnie napisać:

try (
    Connection connection = DatabaseManager.getConnection(); 
    PreparedStatement stmt = DatabaseManager.prepareStatement(connection, SQL, ps -> ps.setString(1, "foo")); 
    ResultSet rs = stmt.executeQuery() 
) { 
    // rest of code 
} 

zauważyć, jak PreparedStatementSetter zostało napisane z wyrażeniem lambda. Jest to jedna z zalet używania interfejsu zamiast klasy abstrakcyjnej: w rzeczywistości jest to funkcjonalny interfejs (ponieważ istnieje jedna abstrakcyjna metoda) i można go zapisać jako lambda.

+0

Wierzę, że Twój kod ma tę samą wadę, o której wspomina @Trejkaz: https://stackoverflow.com/questions/8066501/how-should-i-use -try-with-resources-with-jdbc#comment-23568725 - to wyjątek generowany przez setter.setValues ​​(ps) omija zwrot lokalnie skonstruowanego PreparedStatement, który w ten sposób nie jest zamknięty. – AjahnCharles

2

rozciągający się od @ odpowiedź Tunaki jest to również możliwe, aby czynnik-w try-with-zasobów i rs.executeQuery() taki, że DatabaseManager uchwyty to wszystko dla ciebie i tylko prosi o SQL, a PreparedStatementSetter i ResultSet obsługi .

Pozwoli to uniknąć powtarzania tego w dowolnym miejscu zapytania. Rzeczywisty interfejs API będzie zależeć od Twojego użycia - np. czy będziesz robić kilka zapytań z tym samym połączeniem?

Przypuśćmy chcesz, proponuję następujące:

public class DatabaseManager implements AutoCloseable { 

    /* Use local file for database */ 
    private static final String JDBC_CONNECTION = "jdbc:h2:file:./db/sandbox_h2.db;MODE=PostgreSQL"; 

    static { 
     try { 
      Class.forName("org.h2.Driver"); // Init H2 DB driver 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 
    } 

    private final Connection connection; 

    private DatabaseManager() throws SQLException { 
     this.connection = getConnection(); 
    } 

    @Override 
    public void close() throws SQLException { 
     connection.close(); 
    } 

    public interface PreparedStatementSetter { 
     void setValues(PreparedStatement ps) throws SQLException; 
    } 

    public interface Work { 
     void doWork(DatabaseManager manager) throws SQLException; 
    } 

    public interface ResultSetHandler { 
     void process(ResultSet resultSet) throws SQLException; 
    } 

    /** 
    * @return Database connection 
    * @throws SQLException 
    */ 
    private static Connection getConnection() throws SQLException { 
     return DriverManager.getConnection(JDBC_CONNECTION, "su", ""); 
    } 

    private PreparedStatement prepareStatement(String sql, PreparedStatementSetter setter) throws SQLException { 
     PreparedStatement ps = connection.prepareStatement(sql); 
     setter.setValues(ps); 
     return ps; 
    } 

    public static void executeWork(Work work) throws SQLException { 
     try (DatabaseManager dm = new DatabaseManager()) { 
      work.doWork(dm); 
     } 
    } 

    public void executeQuery(String sql, PreparedStatementSetter setter, ResultSetHandler handler) throws SQLException { 
     try (PreparedStatement ps = prepareStatement(sql, setter); 
      ResultSet rs = ps.executeQuery()) { 
      handler.process(rs); 
     } 
    } 
} 

To owija połączenia jako pole instancji DatabaseManager, która zajmie się cykl połączenia, dzięki realizacji AutoCloseable.

definiuje również 2 nowe interfejsy funkcjonalne (dodatkowo @ Tunaki za PreparedStatementSetter):

  • Work definiuje coś do zrobienia z DatabaseManager poprzez executeWork statycznej metody
  • ResultSetHandler określa sposób ResultSet należy obchodzić podczas wykonywania zapytania za pomocą nowej metody instancji executeQuery.

To może być wykorzystane w następujący sposób:

final String SQL = "select * from NVPAIR where name=?"; 
    try { 
     DatabaseManager.executeWork(dm -> { 
      dm.executeQuery(SQL, ps -> ps.setString(1, "foo"), rs -> { 
       while (rs.next()) { 
        System.out.println(rs.getString("name") + "=" + rs.getString("value")); 
       } 
      }); 
      // other queries are possible here 
     }); 
    } catch (Exception e) { 
     e.printStackTrace(); 
    } 

Jak widać, nie trzeba się martwić o magazynowanie więcej zasobu.

Zostawiłam SQLException obsługę poza api, ponieważ mógłbyś chcieć pozwolić mu się rozprzestrzeniać.

To rozwiązanie zostało zainspirowane przez Design Patterns in the Light of Lambda Expressions by Subramaniam.

0

znalazłem inny sposób robi to co może być pomocne dla osób:

PreparedStatementExecutor.java:

/** 
* Execute PreparedStatement to generate ResultSet 
*/ 
public interface PreparedStatementExecutor { 
    ResultSet execute(PreparedStatement pstmt) throws SQLException; 
} 

PreparedStatementSetter.java:

/** 
* Lambda interface to help initialize PreparedStatement 
*/ 
public interface PreparedStatementSetter { 
    void prepare(PreparedStatement pstmt) throws SQLException; 
} 

JdbcTriple.java:

/** 
* Contains DB objects that close when done 
*/ 
public class JdbcTriple implements AutoCloseable { 
    Connection connection; 
    PreparedStatement preparedStatement; 
    ResultSet resultSet; 

    /** 
    * Create Connection/PreparedStatement/ResultSet 
    * 
    * @param sql String SQL 
    * @param setter Setter for PreparedStatement 
    * @return JdbcTriple 
    * @throws SQLException 
    */ 
    public static JdbcTriple create(String sql, PreparedStatementSetter setter) throws SQLException { 
     JdbcTriple triple = new JdbcTriple(); 
     triple.connection = DatabaseManager.getConnection(); 
     triple.preparedStatement = DatabaseManager.prepareStatement(triple.connection, sql, setter); 
     triple.resultSet = triple.preparedStatement.executeQuery(); 
     return triple; 
    } 

    public Connection getConnection() { 
     return connection; 
    } 

    public PreparedStatement getPreparedStatement() { 
     return preparedStatement; 
    } 

    public ResultSet getResultSet() { 
     return resultSet; 
    } 

    @Override 
    public void close() throws Exception { 
     if (resultSet != null) 
      resultSet.close(); 
     if (preparedStatement != null) 
      preparedStatement.close(); 
     if (connection != null) 
      connection.close(); 
    } 
} 

DatabaseManager.java:

/** 
* Initialize script 
* ----- 
* CREATE TABLE NVPAIR; 
* ALTER TABLE PUBLIC.NVPAIR ADD value VARCHAR2 NULL; 
* ALTER TABLE PUBLIC.NVPAIR ADD id int NOT NULL AUTO_INCREMENT; 
* CREATE UNIQUE INDEX NVPAIR_id_uindex ON PUBLIC.NVPAIR (id); 
* ALTER TABLE PUBLIC.NVPAIR ADD name VARCHAR2 NOT NULL; 
* ALTER TABLE PUBLIC.NVPAIR ADD CONSTRAINT NVPAIR_name_pk PRIMARY KEY (name); 
* 
* INSERT INTO NVPAIR(name, value) VALUES('foo', 'foo-value'); 
* INSERT INTO NVPAIR(name, value) VALUES('bar', 'bar-value'); 
*/ 
public class DatabaseManager { 
    /* Use local file for database */ 
    private static final String JDBC_CONNECTION = "jdbc:h2:file:./db/sandbox_h2.db;MODE=PostgreSQL"; 

    static { 
     try { 
      Class.forName("org.h2.Driver"); // Init H2 DB driver 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 
    } 

    /** 
    * @return Database connection 
    * @throws SQLException 
    */ 
    public static Connection getConnection() throws SQLException { 
     return DriverManager.getConnection(JDBC_CONNECTION, "su", ""); 
    } 

    /** Prepare statement */ 
    public static PreparedStatement prepareStatement(Connection conn, String SQL, PreparedStatementSetter setter) throws SQLException { 
     PreparedStatement pstmt = conn.prepareStatement(SQL); 
     setter.prepare(pstmt); 
     return pstmt; 
    } 

    /** Execute statement */ 
    public static ResultSet executeStatement(PreparedStatement pstmt, PreparedStatementExecutor executor) throws SQLException { 
     return executor.execute(pstmt); 
    } 
} 

Main.java:

public class Main { 
    public static void main(String[] args) { 
     final String SQL = "select * from NVPAIR where name=?"; 
     try (
      JdbcTriple triple = JdbcTriple.create(SQL, pstmt -> { pstmt.setString(1, "foo"); }) 
     ){ 
      while (triple.getResultSet().next()) { 
       System.out.println(triple.getResultSet().getString("name") + "=" + triple.getResultSet().getString("value")); 
      } 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 
    } 
} 

Chociaż nie obsługuje przypadki, w których może być konieczne, aby powrócić identyfikator z wkładką lub transakcji, to nie oferują szybki sposób aby uruchomić kwerendę, ustawić parametry i uzyskać ResultSet, który w moim przypadku jest masą kodu DB.

Powiązane problemy