JDBC

從JDBC談Design Patterns

2019/05/13

Design Patterns

過去的窗戶跟現在的窗戶有什麼不一樣? 那現在程式的開發跟以前程式的開發有什麼不一樣?

從JDBC談起

這是一個典型的JDBC 1.0的範例:

public void connectToAndQueryDatabase(String username, String password) {
    Class.forName("com.mysql.jdbc.Driver");
    Connection con = DriverManager.getConnection(
                         "jdbc:myDriver:myDatabase",
                         username,
                         password);

    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM Table1");

    while (rs.next()) {
        int x = rs.getInt("a");
        String s = rs.getString("b");
        float f = rs.getFloat("c");
    }
}

我那時候看到範例,第一個問題就是什麼是Class.forName,為什麼要用這麼奇怪的做法?

一般而言,產生物件的方式是直接在需要產生物件的時候把物件給new出來,以JDBC為例:

Driver d = new com.mysql.jdbc.Driver();

現在幾乎都沒有人這樣寫,主要的原因是,如果更換資料庫的Driver,那就要改很多的地方。例如,我以前在Indiana University教書的時候,用的是Oracle,後來回到台灣,改用mySQL,幸好,JDBC不是這樣設計,否則,我以前寫的程式就不能用了。

JDBC是怎麼用的呢? 是使用Reflection的方式來產生物件。

Class<?> c = Class.forName("YourPluginName"); 
Plugin plugin = (Plugin) c.newInstance(); 

以JDBC為例:

Class.forName("com.mysql.jdbc.Driver");
con = DriverManager.getConnection(url);

好處是,driver跟url都可以寫成常數:

pubic interface JdbcConstants {
  public static final String DRIVER = "com.mysql.jdbc.Driver";
  public static final String URL = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
}

使用的時候,直接implements interface,就可以使用這些常數了。

public class DAOCustomer implements JdbcConstants {
  public void connectToAndQueryDatabase(String username, String password) {  
    Class.forName(DRIVER);
    con = DriverManager.getConnection(URL);

而且可以在執行的時候才會去決定在使用哪個資料庫的Driver,所以,JDBC4.0之前,都會使用Class.forName來設定使用哪個資料庫的Driver,不過,好像沒有用到newInstance(),因為這個部分已經被藏在DriverManager裡了 (可參考: Connecting to MySQL Using the JDBC DriverManager Interface / DriverManager by Oracle)。

後來,有些書會建議使用System.setProperty來取代Class.forName,整個專案也只要設定一次。

System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");

在JDBC 4.0之後,就不利用這個方式了,現在則是利用DataSource來處理,而不是利用Class.forName。

如果需要動態換資料庫的話,會採用factory(Java DataSource JDBC DataSource ExampleWorking with Multiple Databases in Spring)。大家會說,Class.forName的寫法簡單多了,可以直接換Driver,不過,別忘了通常不只是資料庫的Driver要換,所對應的URL通常也都要換。(詳參: Identify the Java statements required to connect to a database using JDBC )

以JDBC的演化過程為例,開發的方式因為要面對的問題(為了改變而準備)而逐步演化,將這些解決方案取名字,就形成Design Patterns了。

除了DriverManager之外,在JDBC的設計中其實就套用了一些Design Patterns (Using Abstractions and the Bridge Pattern in JavaJava Abstract Factory Pattern exampleDesign a class that uses the Factory design patternDatabase Applications with JDBC):

The Complete JDBC architecture implements bridge design pattern, an abstract concept that decouples abstraction from implementation so that both can vary independently.

Other design patterns also involved in the JDBC API. DriverManager.getConnection Implements static factory method pattern, connection and statement implements Transactional pattern and proxy pattern, ResultSet iterator and data mapper pattern.

  • Factory method,就如同前面的說明,當我們使用Class.forName載入任何一個Driver的時候,Driver會去跟DriverManager登錄,如果不是使用DriverManager的話,只要換driver,很多程式碼都要改寫 (比較 PHP中使用MySQLi或PDO: Open a Connection to MySQL)。
// Factory method. Loads the driver by given classname. It actually returns a 
// concrete Class<Driver>. However, we don't need it here, so we just ignore it.
// It can be any driver class name. The MySQL one here is just an example.
// Under the covers, it will do DriverManager.registerDriver(new Driver()).
Class.forName("com.mysql.jdbc.Driver");
  • Abstract factory
    • 為了讓Driver跟程式切割,JDBC利用DriverManager根據所指定的Driver產生實體的物件給Connection (Connection是個interface,connection會指向所指定的Driver所產生的connection,如:com.mysql.jdbc.Connection),DriverManager所使用的是Class (Static) Factory pattern (利用DriverManager裡的static method:getConnection)
// Abstract factory. This lets the driver return a concrete connection for the
// given URL. You can just declare it against java.sql.Connection interface.
// Under the covers, the DriverManager will find the MySQL driver by URL and call
// driver.connect() which in turn will return new ConnectionImpl().
Connection connection = DriverManager.getConnection(url);
    • JDBC利用Connection(interface)的變數connection產生實體的物件給Statement
// Abstract factory. This lets the driver return a concrete statement from the
// connection. You can just declare it against java.sql.Statement interface.
// Under the covers, the MySQL ConnectionImpl will return new StatementImpl().
Statement statement = connection.createStatement();
    • JDBC利用Statement(interface)的變數statement產生實體的物件給ResultSet
// Abstract factory. This lets the driver return a concrete result set from the
// statement. You can just declare it against java.sql.ResultSet interface.
// Under the covers, the MySQL StatementImpl will return new ResultSetImpl().
ResultSet resultSet = statement.executeQuery(sql);
public void insertRecord(Customer customer) throws SQLException {
        String INSERT_USERS_SQL = "INSERT INTO users (name, address, weight) VALUES (?, ?, ?)";
        // Step 1: Establishing a Connection
        try (Connection connection = DriverManager
            .getConnection(URL, USERNAME, PASSWORD);

            // Step 2:Create a statement using connection object
            PreparedStatement preparedStatement = connection.prepareStatement(INSERT_USERS_SQL)) {
            preparedStatement.setString(1, customer.getName());
            preparedStatement.setString(2, customer.getAddress());
            preparedStatement.setInt(3, customer.getAge());

            // Step 3: Execute the query or update query
            preparedStatement.executeUpdate();
        } catch (SQLException e) {

            // print SQL exception information
            printSQLException(e);
        }

        // Step 4: try-with-resource statement will auto close the connection.
    }

Spring還針對JDBC提供了jdbcTemplate,jdbcTemplate裡運用了template method pattern,利用這樣的作法,可以把前面的所提到的Connection、Statement都藏到jdbcTemplate裡了(詳參: Using JdbcTemplate) 。比起上面的程式碼,以下的程式碼更簡潔了!

//jdbcTemplate 
 public int insert(Customer customer) {
  return jdbcTemplate.update(
    "insert into customer (name, address, weight) values(?, ?, ?)",
    customer.getName(), customer.getAddress(), customer.getWeight());
 }

 public int update(Customer customer) {
  return jdbcTemplate.update(
    "update customer set name=?, address=?, weight=? where id =?",
    customer.getName(), customer.getAddress(), customer.getWeight(), customer.getId());
 }

 public int delete(Long id) {
  return jdbcTemplate.update(
    "delete from customer where id =?", id);
 }

讀資料雖然稍微複雜一點,但是,也是相對過去的寫法簡潔多了:

 public Customer findOne(Long id) {
     return this.jdbcTemplate.queryForObject( "select id, name, address, weight from customer where id = ?", new Object[]{id}, new CustomerMapper());
 }

 public List<Customer> findAll() {
     return this.jdbcTemplate.query( "select id, name, address, weight from customer", new CustomerMapper());
 }

 private static final class CustomerMapper implements RowMapper<Customer> {

     public Customer mapRow(ResultSet rs, int rowNum) throws SQLException {
         Customer customer = new Customer();
         customer.setId(rs.getLong("id"));
         customer.setName(rs.getString("name"));
         customer.setAddress(rs.getString("address"));
         customer.setWeight(rs.getInt("weight"));
         return customer;
     }
 }

Java的Java Persistence API(JPA)及微軟的Entity Framework也套用了不少的Design Pattern (Design Patterns for JPA and Hibernate / [.NET][Design Patterns] 無辜的 Repository Pattern)。

我們來比較一下PHP的經典資料庫存取範例。