- 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
- 例如,如果application的data source配置在JNDI,可以將其wire到Spring中
- jndi-name attribute用來指定JNDI中resource的名字
- 如果run在JAVA application上,則需要將resource-ref 設定為true,這樣給訂的jndi-name會自動加入java:comp/env/ prefix
- 使用Java configuration(這部分會比XML冗長)
- 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
- Java config
- 其他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
- XML
- 和具備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配置
- 可以不配置<jdbc:script也可以配置多個
- 當需要javax.sql.DataSource的時候,就可以injects dataSource bean
- Java configuration
- Using profiles to select a data source
- 如果在dev env需要使用 <jdbc:embedded-database>,而在QA需要使用DBCP’s BasicDataSource,production env需要<jee:jndi-lookup>
- 需要做的就是,將每個data source配置在不同的profile中
- XML:
- 接著可以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
- 事實上,這些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非常容易
- 這裡的dataSource可以是javax.sql.DataSource的任意implementation
- 接著可以將JdbcTemplate wire到Repository中並使用它來access db
| @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
- 因此,新增data可以簡化如下
- 當call update()時,Jdbctemplate會獲取connect、create statement並執行insert sql,且會處理所有可能拋出SQLException,並將common的SQLException重新threw更明確的exception
- Read Data
- 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
- 也可以使用method reference:
- 在addSpitter()時,update()內要保持parameter的順序,否則會insert data錯誤
- 可以使用named parameter,這方法可以給SQL中的每個parameter一個明確的名字
- 假設SQL_INSERT_SPITTER query:
- 使用named parameter順序就不重要了
- 宣告,與JdbcTemplate幾乎相同
- 代替JdbcTemplate的addSpitter()寫法
- value是透過java.util.Map綁定
<jee:jndi-lookup>
<jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS" resource-ref="true" />
@Bean public JndiObjectFactoryBean dataSource() { JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean(); jndiObjectFB.setJndiName("jdbc/SpittrDS"); jndiObjectFB.setResourceRef(true); jndiObjectFB.setProxyInterface(javax.sql.DataSource.class); return jndiObjectFB; }
<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" />
@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; }
@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; }
<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="" />
<?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>
@Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }
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 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>
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 } } }
@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }
@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; } ... }
@Bean public SpitterRepository spitterRepository(JdbcTemplate jdbcTemplate) { return new JdbcSpitterRepository(jdbcTemplate); }
public void addSpitter(Spitter spitter) { jdbcOperations.update(INSERT_SPITTER, spitter.getUsername(), // 注意順序 spitter.getPassword(), spitter.getFullName(), spitter.getEmail(), spitter.isUpdateByEmail()); }
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"));} }
public Spitter findOne(long id) { return jdbcOperations.queryForObject( SELECT_SPITTER_BY_ID, (rs, rowNum) -> { return new Spitter( rs.getLong("id"), rs.getString("username"), rs.getString("password"), rs.getString("fullName"), rs.getString("email"), rs.getBoolean("updateByEmail")); }, id); }
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")); }
private static final String SQL_INSERT_SPITTER = "insert into spitter (username, password, fullname) " + "values (:username, :password, :fullname)";
@Bean public NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource){ return new NamedParameterJdbcTemplate(dataSource); }
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); }
沒有留言:
張貼留言