2017年12月27日 星期三

Java Spring - Spring JDBC

  • Spring data-access
    • Spring的目標之一就是允許在開發的時候,能夠遵循OO原則中的coding to interface。Spring對data的support也是如此
    • 為了避免persistence logic分散到各個component,最好將data access放到一個或多個專注在此任務的component中
    • 這樣的component通常稱為data access object, DAO或repository
    • 為了避免application和特定的data access strategy,良好多repository應該要用interface的方式顯示功能
      • service object ---> repository interface <--- repository implementation
    • service object本身並不會處理data access,而是會將data access delegate給repository,repository interface確保和service object decoupling
      • 好處:service object好測試,因為不需要和特定的data access implementation綁在一起,也可以為這些interface建立mock implementation,無需連接DB就能夠測試service object
      • 只有data access相關的method才透過interface exposed。這可以實現靈活設計,且切換data-persistence framework對應用程式其他部分帶來影響最小。如果將細節滲透到application的其他部分,則整個application將和data access layer couple在一去,導致僵化的設計
    • interface&Spring: 
      • interface是實現low coupling的key,並且應將其應用在application的各個layer
  • Spring’s data-access exception hierarchy
    • 使用JDBC時可以發現,如果不catch SQLException,幾乎沒辦法做任何事情。而SQLException表示在嘗試access db的時候發生問題,但這個exception卻沒辦法告訴你哪裡錯了,以及如何進行處理
    • 對於所有data access的問題都會throw SQLException,而不是對每種可能的問題都會有不同的exception type
    • 需要的是data-access exception hierarchy 有描述性,但又不能和特定的persistence framework有關聯
      • JDBC的exception hierarchy太簡單
      • Hibernate的exception hierarchy是其本身所擁有的
  • Spring所提供Platform–Agnostic persistence exceptions
    • Spring JDBC提供的data access exception hierarchy 解決了以上兩個問題
      • Spring提供多個data-access exceptionns,且描述了他們throw時的對應問題
      • 沒有和particular persistence solution相關聯,表示可以使用Spring拋出一致的exception,而不用擔心所選擇的persistence provider
      • 也表示persistence choice將會和data-access layer隔開
  • 不用寫catch
    • Spring JDBC exception都是extends DataAccessException,因為DataAccessException是一個unchecked exception,所以不需要catch data access exception
  • Templating data access
    • template method pattern: 將特定implement 相關的部分delegate給interface(行李登記櫃檯),而這個interface的不同implementation定義了過程中的具體行為,這也是Spring在data-access中所使用的pattern
    • 不管使用什麼技術,都需要一些特定的data access steps,但每種data-access method會有些不一樣
      • 例如都需要一個到data storage的connection
      • 在完成之後需要close connection
    • 會查詢不同的object會以不同方式update db,這些都是data access過程中變化的部分
    • Spring將data access過程中固定的即可變的部分明確劃分為兩個不同的class
      • template: 管理過程中固定的部分
        • 1. Prepare resources 
        • 2. Start transaction
        • 5. Commit/rollback transaction
        • 6. Close resources and handle errors
      • callback: 自定義的data access code
        • 3. Execute in transaction
        • 4. Return data
      • 在使用Spring template和repository之前,需要在Spring配置一個data source用來連結db
  • Configuring a data source
    • 無論選擇Spring的哪一種data access方式,都需要配置一個data source。Spring提供在Spring context中配置data source bean的多種方式
      • Data sources that are defined by a JDBC driver
      • Data sources that are looked up by JNDI
      • Data sources that pool connections
  • 使用JNDI data sources
    • Tomcat允許config透過JNDI獲取data source
    • 好處在於data source完全可以在application之外進行管理,這樣application只需要在access db的時候找data source就可以了
    • 在application server中管理的data source通常以pool的方式管理,從而具備更好的performance,而且還支援system administrators對他的hot-swapped
    • 利用Spring,可以像使用Spring bean那樣config JNDI中data source的引用,並將他wire到需要的class中
    • 位於jee namespace下的<jee:jndi-lookup>可以用來找JNDI中的任何object,包含data source,並將其作為Spring的bean
    • <jee:jndi-lookup>
      
    • 例如,如果application的data source配置在JNDI,可以將其wire到Spring中
    • <jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS" resource-ref="true" />
      
      • jndi-name attribute用來指定JNDI中resource的名字
      • 如果run在JAVA application上,則需要將resource-ref 設定為true,這樣給訂的jndi-name會自動加入java:comp/env/ prefix
    • 使用Java configuration(這部分會比XML冗長)
    • @Bean
      public JndiObjectFactoryBean dataSource() {
       JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
       jndiObjectFB.setJndiName("jdbc/SpittrDS");
       jndiObjectFB.setResourceRef(true);
       jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
       return jndiObjectFB;
      }
      
  • Using a pooled data source
    • 如果不能從JNDI中找到data source,下一個選擇就是直接在Spring中配置data source connection pool
    • 雖然Spring沒有提供data source connection pool,但有其他可行方案
      • Apache Commons DBCP (http://jakarta.apache.org/commons/dbcp)
      • c3p0 (http://sourceforge.net/projects/c3p0/)
      • BoneCP (http://jolbox.com/)
    • 以下是DCNP的 BasicDataSource
    • <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
                p:driverClassName="org.h2.Driver"
                p:url="jdbc:h2:tcp://localhost/~/spitter"
                p:username="sa"
                p:password=""
                p:initialSize="5"
                p:maxActive="10" />
      
    • Java config
    •  @Bean
       public BasicDataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("org.h2.Driver");
        ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
        ds.setUsername("sa");
        ds.setPassword("");
        ds.setInitialSize(5);
        ds.setMaxActive(10);
        return ds;
       }
      
    • 其他DBCP BasicDataSource的pool config attributes
      • initialSize
      • maxActive
      • maxIdle
      • maxOpenPreparedStatements
      • maxWait
      • minEvictableIdleTimeMillis
      • minIdle
      • poolPreparedStatements
  • Using JDBC driver-based data sources
    • 在Spring中,透過JDBC driver定義data soruce是最簡單的方式。Spring提供三個data source class(org.springframework.jdbc.datasource)供選擇
      • DriverManagerDataSource: 在每個connection request的時候都會return一個新的connection,沒有進行pool connection management
      • SimpleDriverDataSource: 和DriverManagerDataSource相似,但是是直接使用JDBC driver來解決特定環境下的class loader的問題,這樣的環境包含OSGi container
      • SingleConnectionDataSource: 每個connection request的時候都會return同一個connection,可以將其視為只有一個connection的pool
    • 以上這些配置如同DBCP BasicDataSource,下例是DriverManagerDataSource
    • Java config
    • @Bean
      public DataSource dataSource() {
       DriverManagerDataSource ds = new DriverManagerDataSource();
       ds.setDriverClassName("org.h2.Driver");
       ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
       ds.setUsername("sa");
       ds.setPassword("");
       return ds;
      }
      
    • XML
    • <bean id="dataSource" 
      class="org.springframework.jdbc.datasource.DriverManagerDataSource" 
      p:driverClassName="org.h2.Driver" 
      p:url="jdbc:h2:tcp://localhost/~/spitter"
      p:username="sa"
      p:password="" />
      
    • 和具備pool的data source比較,唯一的區別在於這些data source bean都沒有提供connection pool功能,所以沒有可以配置的pool相關的attribute
    • 如果要把這個使用在production還是要考慮。因為SingleConnectionDataSource不適合使用在multi thread的application中,最好只在test的時候使用。雖然SimpleDriverDataSource和DriverManagerDataSource支持multi-thread,但是在每次request connection的時候都會create新的connection,這是以performance作為代價。強烈建議應該使用data source connection pool
  • Using an embedded data source
    • embedded database作為application的一部份運行,而不是應用連接的獨立db server。對於開發和測試來說是一個可選方案。因為每次重啟application及run test的時候,能夠重新insert test data
    • Spring jdbc namespace能夠簡化 embedded data source配置
    • <?xml version="1.0" encoding="UTF-
           8"?> <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:jdbc="http://www.springframework.org/schema/jdbc"
          xmlns:c="http://www.springframework.org/schema/c"
          xsi:schemaLocation="http://www.springframework.org/schema/jdbc
      http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      ...
      <jdbc:embedded-database id="dataSource" type="H2"> // 要確保H2在classpath下
      <jdbc:script location="com/habum a/spitter/db/jdbc/schema.sql"/> //建立schema 
      <jdbc:script location="com/habuma/sp itter/db/jdbc/test-data.sql"/> // 建立測試資料
      </jdbc:embedded-database>
      ...
      </beans>
      
      • 可以不配置<jdbc:script也可以配置多個
      • 當需要javax.sql.DataSource的時候,就可以injects dataSource bean
    • Java configuration
      @Bean
      public DataSource dataSource() {
       return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
      }
      
  • Using profiles to select a data source
    • 如果在dev env需要使用 <jdbc:embedded-database>,而在QA需要使用DBCP’s BasicDataSource,production env需要<jee:jndi-lookup> 
    • 需要做的就是,將每個data source配置在不同的profile中
    • package com.habuma.spittr.config;
      
      import org.apache.commons.dbcp.BasicDataSource;
      import javax.sql.DataSource;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.Profile;
      import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
      import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;import org.springframework.jndi.JndiObjectFactoryBean;Configuration
      
      public class DataSourceConfiguration {
          @Profile("development")
          @Bean
          public DataSource embeddedDataSource() {
              return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).addScript("classpath:schema.sql")
                              .addScript("classpath:test-data.sql").build();
          }
      
          @Profile("qa")
          @Bean
          public DataSource Data() {
      
              BasicDataSource ds = new BasicDataSource();
              ds.setDriverClassName("org.h2.Driver");
              ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
              ds.setUsername("sa");
              ds.setPassword("");
              ds.setInitialSize(5);
              ds.setMaxActive(10);
              return ds;
          }
      
          @Profile("production")
          @Bean
          public DataSource dataSource() {
      
              JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
              jndiObjectFactoryBean.setJndiName("jdbc/SpittrDS");
              jndiObjectFactoryBean.setResourceRef(true);
              jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
              return (DataSource) jndiObjectFactoryBean.getObject();
          }
      }
      
    • XML:
    • <?xml version="1.0" encoding="UTF-
           8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
        xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
        xsi:schemaLocation="http://www.springframework.org/schema/jdbc
            http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
          http://www.springframework.org/schema/jee
          http://www.springframework.org/schema/jee/spring-jee-3.1.xsd
          http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd">
        <beans profile="development">
          <jdbc:embedded-database id="dataSource" type="H2">
            <jdbc:script location="com/habumuma/spitter/db/jdbc/schema.sql" />
            <jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql" />
          </jdbc:embedded-database>
        </beans>
        <beans profile="qa">
          <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
            p:driverClassName="org.h2.Driver" p:url="jdbc:h2:tcp://localhost/~/spitter"
            p:username="sa" p:password="" p:initialSize="5" p:maxActive="10" />
        </beans>
        <beans profile="production">
          <jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS"
            resource-ref="true" />
        </beans>
      </beans>
      
    • 接著可以access DB了。Spring提供了多種使用DB的方式包含JDBC、Hibernate、Java persistence API(JPA)
  • Using JDBC with Spring
    • JDBC允許使用db的所有property,這是其他framework不鼓勵甚至禁止的
    • 相對於persistence framework,JDBC能夠日我們在更低的layer上處理data
  • 應對失控的JDBC code
    • 要負責處理所有和db access相關的事情,以下這個超過20行的code只是為了對db插入一個簡單的object。且能夠發現只有20%的code是真的用來query data,80%都是boilerplate
    • private static final String SQL_INSERT_SPITTER =
       "insert into spitter (username, password, fullname) values (?, ?, ?)";
      private DataSource dataSource;
      
      public void addSpitter(Spitter spitter) {
       Connection conn = null;
       PreparedStatement stmt = null;
       try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement(SQL_INSERT_SPITTER);
        stmt.setString(1, spitter.getUsername());
        stmt.setString(2, spitter.getPassword());
        stmt.setString(3, spitter.getFullName());
        stmt.execute();
       } catch (SQLException e) {
      
       } finally {
        try {
         if (stmt != null) {
          stmt.close();
         }
         if (conn != null) {
          conn.close();
         }
        } catch (SQLException e) {
         // I'm even less sure about what to do here
        }
       }
      }
      
    • 事實上,這些boilerplate是非常重要的,close connection及處理錯誤確保data access的health。如果沒有他們,就不會發生錯誤,且resource 也會處於open狀態,會導致resource leaks。因此,不僅需要他們,還需要確保是正確的,基於這樣的原因,才會使用framework才保證這些code只寫一次,而且是正確的。
  • Working with JDBC templates
    • Spring的JDBC framework簡化的JDBC code,讓我們只需編寫讀寫data的必要code
    • 正如前面所介紹的,Spring將data access的template code抽象到template class中,Spring為JDBC提供三個template classes
      • JdbcTemplate: 最基本的Spring JDBC template,支援簡單的JDBC db access功能及基於index parameter的query
      • NamedParameterJdbcTemplate: 使用該template class執行query時可以將value以命名參數的形式綁定到SQL中,而不是使用簡單的index parameter
      • SimpleJdbcTemplate: 該template class利用Java5的特性autoboxing, generics, and variable parameter list來簡化JDBC template的使用
  • 使用JDBCTEMPLATE insert data
    • 只需要設置DataSource就可以了,這使得Spring配置JdbcTemplate非常容易
    • @Bean
      public JdbcTemplate jdbcTemplate(DataSource dataSource) {
       return new JdbcTemplate(dataSource);
      }
      
    • 這裡的dataSource可以是javax.sql.DataSource的任意implementation
    • 接著可以將JdbcTemplate wire到Repository中並使用它來access db
    • @Repository // 會在component scan的時候create
      public class JdbcSpitterRepository implements SpitterRepository {
        private JdbcOperations jdbcOperations;
        @Inject // inject jdbcOperations interface保持loose coupling
        public JdbcSpitterRepository(JdbcOperations jdbcOperations) {
          this.jdbcOperations = jdbcOperations;
        }
      ...
      }
      
    • | @Component | generic stereotype for any Spring-managed component |
      | @Repository| stereotype for persistence layer                    |
      | @Service   | stereotype for service layer                        |
      | @Controller| stereotype for presentation layer (spring-mvc) 
    • compared from here
    • 也可以將JdbcSpitterRepository宣告為Spring中的bean,讓component scan
    • @Bean
      public SpitterRepository spitterRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcSpitterRepository(jdbcTemplate);
      }
      
    • 因此,新增data可以簡化如下
    • public void addSpitter(Spitter spitter) {
          jdbcOperations.update(INSERT_SPITTER,
              spitter.getUsername(), // 注意順序
              spitter.getPassword(),
              spitter.getFullName(),
              spitter.getEmail(),
              spitter.isUpdateByEmail());
      }
      
    • 當call update()時,Jdbctemplate會獲取connect、create statement並執行insert sql,且會處理所有可能拋出SQLException,並將common的SQLException重新threw更明確的exception
  • Read Data
    • public Spitter findOne(long id) {
       return jdbcOperations.queryForObject(SELECT_SPITTER_BY_ID, new SpitterRowMapper(), id);
      }...
      
      private static final class SpitterRowMapper implements RowMapper < Spitter > {
       public Spitter mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Spitter(rs.getLong("id"), 
                           rs.getString("username"), 
                           rs.getString("password"),
                           rs.getString("fullName"), 
                           rs.getString("email"), 
                           rs.getBoolean("updateByEmail"));}
      }
      
    • queryForObject有三個parameters
      • String object: 包含要從db query的sql
      • RowMapper object: 從ResultSet中得到的data並create object
      • variable parameter: 綁定到query上的parameter
    • 注意SpitterRowMappermap到Spitter中
  • JdbcTemplate使用Lambda
    • RowMapper interface只有addRow(),因此可以使用lambda
    • public Spitter findOne(long id) {
        return jdbcOperations.queryForObject(
          SELECT_SPITTER_BY_ID,
          (rs, rowNum) -&gt; {
            return new Spitter(
              rs.getLong("id"),
              rs.getString("username"),
              rs.getString("password"),
              rs.getString("fullName"),
              rs.getString("email"),
              rs.getBoolean("updateByEmail"));
      },
      id); }
      
    • 也可以使用method reference:
    • public Spitter findOne(long id) {
        return jdbcOperations.queryForObject(
          SELECT_SPITTER_BY_ID, this::mapSpitter, id);
      }
      private Spitter mapSpitter(ResultSet rs, int row) throws SQLException {
          return new Spitter(
            rs.getLong("id"),
            rs.getString("username"),
            rs.getString("password"),
            rs.getString("fullName"),
            rs.getString("email"),
            rs.getBoolean("updateByEmail"));
      }
      
  • 在addSpitter()時,update()內要保持parameter的順序,否則會insert data錯誤
    • 可以使用named parameter,這方法可以給SQL中的每個parameter一個明確的名字
    • 假設SQL_INSERT_SPITTER query:
      • private static final String SQL_INSERT_SPITTER =
              "insert into spitter (username, password, fullname) " +
              "values (:username, :password, :fullname)";
        
    • 使用named parameter順序就不重要了
    • 宣告,與JdbcTemplate幾乎相同
    • @Bean
      public NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource){
        return new NamedParameterJdbcTemplate(dataSource);
      }
      
    • 代替JdbcTemplate的addSpitter()寫法
    • private static final String INSERT_SPITTER =
       "insert into Spitter " +
       "    (username, password, fullname, email, updateByEmail) " +
       "values " +
       "    (:username, :password, :fullname, :email, :updateByEmail)";
      public void addSpitter(Spitter spitter) {
       Map < String, Object > paramMap = new HashMap < String, Object > ();
       paramMap.put("username", spitter.getUsername());
       paramMap.put("password", spitter.getPassword());
       paramMap.put("fullname", spitter.getFullName());
       paramMap.put("email", spitter.getEmail());
       paramMap.put("updateByEmail", spitter.isUpdateByEmail());
       jdbcOperations.update(INSERT_SPITTER, paramMap);
      }
      
    • value是透過java.util.Map綁定

沒有留言:

張貼留言