# Spring 與 JDBC ## 永續性概念 - 應用程式中物件與資料都存在記憶體,一旦JVM重啟所有物件與狀態都會消失 - Persistence 機制 - 目的: JVM重啟,重新載入恢復上次狀態 - 實務上最常使用:關聯式資料庫系統(RDBMS) ```mermaid flowchart LR; P_prev[my product] P_new[my product] DB[(my data)] P_prev---->DB---->P_new ``` ## JDBC - 定義:Java提供能統一操作DB的API,底層抽換不同DB程式碼也不須異動 - 大部分的資料庫廠商都會提供符合JDBC規格的Driver(type4) - 如果廠商沒提供Driver可以用以下方法: - 透過ODBC(type1) - 找外部廠商實做DB Client端函式庫(type2) - 如果App不是直接連線到DB而中間透過web server取得資料(type3) - 示意圖: ```mermaid flowchart TB; App[Application] API[JDBC API] SPI[JDBC SPI] DR1[DB2 JDBC Driver] DB1[(DB2)] DR2[MySQL JDBC Driver] DB2[(mySQL)] JDOD[JDBC-ODBC] ODBC DB[(DB)] DBLIB[DB Client Library] App-->API-->SPI-->DR1-->DB1 SPI-->DR2-->DB2 SPI-->JDOD-->ODBC-->DB SPI-->DBLIB-->DB ``` ## 透過JDBC操作DB的步驟 1. 建立並註冊Driver物件 ```java Class.forName("com.mysql.jdbc.Driver"); //or use Class.forName("com.mysql.jdbc.Driver").newInstance(); ``` > 為什麼要用Class.forName產生Driver instance呢? > 想要在不改code情況下用config抽換不同Driver 2. 透過DriverManager建立資料庫連結 ```java String url = "jdbc:mysql://localhost:3306/springdb"; Connection con = DriverManager.getConnection(url,pw); ``` 3. 建立Statement物件 ```java Statement stmt = connection.createStatement(); ``` 4. 下達SQL指令 ```java ResultSet rs = stmt.executeQuery("select * from Product"); ``` 5. 取得回傳結果 ```java while(rs.next()){ ... } ``` 8. 關閉所有連線物件 ```java rs.close(); stmt.close(); con.close(); ``` ## 優化方向 1. 簡化取得連線方法 => DataSource 2. 提升系統效率 => Connection Pool 3. 固定流程不要重複撰寫 => Template Method Pattern 4. SQL在Table欄位數量增加時容易寫錯 => O-R Mapping ## DataSource - JDBC 2.0 加入新機制 - 本質是一個資料庫連線的工廠,同時儲存建立資料庫連線的資訊。 - 系統管理員設定好DataSource的資訊跟JNDI名稱,開發人員藉由JNDI名稱取得資料庫連線。 ```java! Context ctx = new InitialContext(); DataSource ds = (DataSource)ctx.lookup("jdbc/MyDataSource"); Connection con = ds.getConnection(); ``` ### DriverManagerDataSource - DataSource必須從JNDI取得,如果沒有跟Java EE應用伺服器使用,很難取得。 - POJO-based元件可以從Spring提供的DriverManagerDataSource取得DataSource。 bean-config.xml: ```xml! <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClaseName" value="..."/> <property name="url" value="..."/> <property name="username" value="..."/> <property name="password" value="..."/> </bean> ``` Client程式: ```java= ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-config.xml"); DataSource ds = (DataSource)ctx.getBean("dataSource"); Connection con = ds.getConnection(); ``` :::danger DriverManagerDataSource適合在元件開發時期單元測試使用,但沒有Connection Pool機制,每次取得連線都是建立新連線。絕對不要在Web Server上使用會讓server爆掉。 ::: ## Connection Pool 建立新的連線是最花時間也是最耗費資源的一步。 解決方法:建立**連線池**,預先建立連線。要用的時候提供,用完時就歸還。 可以採用Apache Jakarta Commons DBCP 專案的BasicDataSource,有Connection Pool機制。 > Tomcat也有內建Connection Pool的類別。 可以透過maxActive, maxIdle等屬性調效Connection Pool: ```xml! <bean id="dataSource" class="org.apache.common.dbcp.BasicDataSource"> <property name="driverClaseName" value="..."/> <property name="url" value="..."/> <property name="username" value="..."/> <property name="password" value="..."/> <property name="maxActive" value="..."/> <property name="maxIdle" value="..."/> </bean> ``` Client端程式不需異動,所使用的DataSource就升級成有connection pool版本。 ## 透過Spring JdbcTemplate簡化開發 - Thread-safe類別 - 建立JdbcTemplate實體時,需要先有一個DataSource傳入constructor ```java! JdbcTemplate template = new JdbcTemplate(dataSource); //create table template.execute("CREATE TABLE T_USER (user_id integer, name varchar(100))"); //insert data template.update("INSERT INTO T_USER(user_id,name) VALUE (?,?)",new Object[]{user.getId(), user.getName}); //query //1.回傳基本型別 int i = jdbcTemplate.queryForInt("SELECT COUNT(*) FROM T_USER"); //2.指定回傳型別 String name = (String)jdbcTemplate.queryForObject("SELECT name FROM T_USER WHERE id=?",new Object[]{id}, java.lang.String.class); //3.回傳List of Maps List row = jdbcTemplate.queryForList("SELECT * FROM T_USER"); Iterator it = row.iterator(); while(it.hasNext()){ Map userMap = (Map)it.next(); ... } ``` ## PreparedStatementCreator ```java jdbcTemplate.update(new PreparedStatementCreator(){ public PreparedStatement createPreparedStatement(Connection con) throws SQLException{ String sql = "INSERT INTO T_USER(name, age) VALUES(?,?)"; PreparedStatement ps = con.preparedStatement(sql); ps.setString(1, name); ps.setInt(2, age); return ps; } }); ``` ## PreparedStatementSetter 實作setValues方法時,jdbcTemplate會自動根據所給的SQL產生PreparedStatementCreator的實例 ```java jdbcTemplate.update("INSERT INTO T_USER(name, age) VALUES(?,?)", new PreparedStatementSetter(){ public void setValues() throws SQLException{ ps.setString(1, name); ps.setInt(2, age); } }) ``` ## RowCallbackHandler 在processRow方法,根據取得的ResultSet物件處理 ```java jdbcTemplate.query("SELECT * FROM T_USER WHERE id=?", new Object[]{id},new RowCallbackHandler(){ public void processRow(ResultSet rs) throws SQLException{ user.setName(rs.getString("name")); ... } }) ``` ## NameParameterJdbcTemplate 用有意義的名稱取代問號,減少參數對錯機率 - 單一參數 ```java! int id = 101; String sql = "SELECT * FROM T_USER WHERE id = :userId"; SqlParameterSource param = new MapSqlParameterSource("userId",id); NameParameterJdbcTemplate jdbcTemplate = new NameParameterJdbcTemplate(dataSource); List rows = jdbcTemplate.queryForList(sql, param); ``` - 多參數 ```java! String name ="Bob" int age = 10; String sql = "SELECT * FROM T_USER WHERE name = :userName and age =:userAge"; Map paramMap = new HashMap(); paramMap.put("userName", name); paramMap.put("userAge", age); NameParameterJdbcTemplate jdbcTemplate = new NameParameterJdbcTemplate(dataSource); List rows = jdbcTemplate.queryForList(sql, paramMap); ``` - Bean 屬性自動偵測機制 ```java! User user = new User(name, age); ... String sql = "INSERT INTO T_USER VALUES(:name, :age)"; SqlParameterSource param = new BeanPropertySqlParameterSource(user); NameParameterJdbcTemplate jdbcTemplate = new NameParameterJdbcTemplate(dataSource); List rows = jdbcTemplate.queryForList(sql, param); ``` ## SimpleJdbcTemplate - 支援泛型 ```java! int id = 100; String sql = "SELECT * FROM T_USER WHERE id =?"; ParameterRowMapper<User> mapper = new ParameterRowMapper(){ public User mapRow(ResultSet rs, int rowNumber)throw SQLException{ ... return user; } }; SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource); jdbcTemplate.queryForObject(sql, mapper, id); ``` ## Translated Exception - Spring 提供的Exception:都是DataAccessException的子類別,該類別是Runtime Exception子類別 (Unchecked Exception) - 不要求開發者一定要處理exception,當Runtime Exception發生時會一路向上拋(往外滲透) - 適合抽離在Exception統一處理:例外處理跟業務邏輯的程式混雜會降低可讀性,且每個開發者處理例外方式也可能會有差異 - 各項例外分類: - 資料庫連結發生錯誤:DataAccessResourceFailureException - SQL語法錯誤:BadSqlGrammerException ## 以DAO封裝資料庫存取邏輯 - 中大型的專案通常會把資料操作邏輯以DAO(Data Access Object)方式封裝 - 針對每一個領域物件都提供一個DAO介面封裝該領域物件的CRUD方法 - DAO將呼叫端跟實作端透過介面隔開,開發人員可以隨時替換DAO實作物件 ```mermaid classDiagram class Client class ProductDao { <<interface>> +insert():void +update():void +delete():void +find():void } class ProductDaoJdbcImpl class MockProductDao Client..>ProductDao ProductDao <|-- ProductDaoJdbcImpl ProductDao <|-- MockProductDao ``` ```java! public void setProductName(Product product, String name){ product.setName(name); productDao.update(product); } ``` ## JdbcDaoSupport - JdbcDaoSupport 內含setDataSource(),以及JdbcTemplate - 不需要在每個CRUD function去取得DataSource產生樣板物件 ```xml! <bean id="dataSource" class="org.apache.common.dbcp.BasicDataSource"> <property name="driverClaseName" value="..."/> <property name="url" value="..."/> <property name="username" value="..."/> <property name="password" value="..."/> <property name="maxActive" value="..."/> <property name="maxIdle" value="..."/> </bean> <bean id="productDao" class="ProductDaoJdbcImpl"> <property name="dataSource" ref="dataSource" /> </bean> ``` ```java= public class ProductDaoJdbcImpl extends JdbcDaoSupport implements ProductDao{ ... public void insert(Product product){ this.getJdbcTemplate().update("INSERT INTO T_PRODUCT (id, name) VALUES(?,?)", new Object[]{product.getId(), product.getName()}); } } ```