《Spring 3.0就這麼簡單》——1.4 持久層
本節書摘來自異步社區《Spring 3.0就這麼簡單》一書中的第1章,第1.4節,作者: 陳雄華 , 林開雄著,更多章節內容可以訪問雲棲社區“異步社區”公眾號查看
1.4 持久層
持久層負責數據的訪問和操作,DAO類被上層的業務類調用。Spring本身支持多種流行的ORM框架。這裏使用Spring JDBC作為持久層的實現技術,關於Spring JDBC的詳細內容,請參見第4章的內容。為方便閱讀,會對本章涉及的相關知識點進行必要的介紹,所以相信讀者在不了解Spring JDBC的情況下,也可以輕鬆閱讀以下的內容。
1.4.1 建立領域對象
領域對象(Domain Object)也稱為實體類,它代表了業務的狀態,一般來說,領域對象屬於業務層,但它貫穿展現層、業務層和持久層,並最終被持久化到數據庫中。領域對象使數據庫表操作以麵向對象的方式進行,為程序的擴展帶來了更大的靈活性。領域對象不一定等同於數據庫表,不過對於簡單的應用來說,領域對象往往都擁有對應的數據庫表。
持久層的主要工作就是從數據庫表中加載數據並實例化領域對象,或將領域對象持久化到數據表中。由於持久層需要用到領域對象,所以將本屬於業務層的領域對象提前到持久層來說明。
景區網站登錄模塊需要涉及兩個領域對象:User和LoginLog,前者代表用戶信息,後者代表日誌信息,分別對應t_user和t_login_log數據表,領域對象類的包為com.smart.domain。
用戶領域對象
用戶信息領域對象很簡單,可以看成是對t_user表的對象翻譯,每個字段對應一個對象屬性。User類主要有兩類信息,分別為用戶名/密碼(userName/password)以及最後一次登錄的信息(lastIp、lastVisit),其代碼如代碼清單1-3所示。
代碼清單1-3 User.java領域對象
package com.smart.domain;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable{ ③
private int userId;
private String userName;
private String password;
private String lastIp;
private Date lastVisit;
//省略get/setXxx方法_
…
}
登錄日誌領域對象
用戶每次登錄成功後,記錄一條登錄日誌,該登錄日誌包括3個信息,分別是用戶ID、登錄IP地址和登錄時間。一般情況下,還包括退出時間。為了簡化實例,僅記錄登錄時間,登錄日誌的領域對象如代碼清單1-4所示。
代碼清單1-4 LoginLog.java
package com.smart.domain;
import java.io.Serializable;
import java.util.Date;
public class LoginLog implements Serializable {
private int loginLogId;
private int userId;
private String ip;
private Date loginDate;
//省略get/setXxx方法_
…
}
1.4.2 UserDao
首先定義訪問User的DAO,它包括以下3個方法。
getMatchCount():根據用戶名和密碼獲取匹配的用戶數。等於1表示用戶名/密碼正確,等於0表示用戶名或密碼錯誤(這是最簡單的用戶身份認證方法,在實際應用中需要采用諸如密碼加密等安全策略)。
findUserByUserName():根據用戶名獲取User對象。
updateLoginInfo():更新用戶積分、最後登錄IP地址以及最後登錄時間。
下麵通過Spring JDBC技術實現這個DAO類,如代碼清單1-5所示。
代碼清單1-5 UserDao
package com.smart.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.stereotype.Repository;
import com.smart.domain.User;
@Repository ①
public class UserDao {
@Autowired ②
private JdbcTemplate jdbcTemplate;
public int getMatchCount(String userName, String password) {
String sqlStr = " SELECT count(*) FROM t_user "
+ " WHERE user_name =? and password=? ";
return jdbcTemplate.queryForInt(sqlStr, new Object[] { userName, password });
}
…
}
在Spring 1.5以後,可以調用注解的方式定義Bean,較之於XML配置方式,注解配置方式的簡單性非常明顯,已經被廣泛接受,成為一種趨勢。所以除非沒有辦法,否則都應盡量采用注解的配置方式。
這裏用@Repository定義了一個DAO Bean,使用@Autowired將Spring容器中的Bean注入進來。關於Spring的注解配置,將會在第2章詳細討論。
傳統的JDBC API太底層,即使用戶執行一條最簡單的數據查詢操作,都必須執行如下的過程:獲取連接→創建Statement→執行數據操作→獲取結果→關閉Statement→關閉結果集→關閉連接,除此之外還需要進行異常處理的操作。如果使用傳統JDBC API進行數據訪問操作,可能會有1/3以上單調乏味的重複性代碼像蒼蠅一樣驅之不散。
Spring JDBC對傳統的JDBC API進行了薄層的封裝,將樣板式的代碼和那些必不可少的代碼進行了分離,用戶僅需要編寫那些必不可少代碼,剩餘的單調乏味的重複性工作則交由Spring JDBC框架處理。簡單來說,Spring JDBC通過一個模板類org.springframework. jdbc.core.JdbcTemplate封裝了樣板式的代碼,用戶通過模板類就可以輕鬆地完成大部分數據訪問的操作。
例如,對於 getMatchCount()方法,僅提供了一個查詢 SQL 語句,直接調用模板的queryForInt()方法就可獲取查詢,用戶不用擔心獲取連接、關閉連接、異常處理等繁瑣的事情。
通過JdbcTemplate的支持,可以輕鬆地實現UserDao的另外兩個接口,如代碼清單1-6所示。
代碼清單1-6 UserDao另外兩個方法
package com.smart.dao.jdbc;
…
@Repository
public class UserDao {
…
public User findUserByUserName(final String userName) {
String sqlStr = " SELECT user_id,user_name " ①
+ " FROM t_user WHERE user_name =? ";
final User user = new User();
jdbcTemplate.query(sqlStr, new Object[] { userName },
new RowCallbackHandler() {②
public void processRow(ResultSet rs) throws SQLException {**
user.setUserId(rs.getInt("user_id"));
user.setUserName(userName);
}
});
return user;
}
public void updateLoginInfo(User user) {
String sqlStr = " UPDATE t_user SET last_visit=?,last_ip=? "
+ " WHERE user_id =? ";
jdbcTemplate.update(sqlStr, new Object[] { user.getLastVisit(),
user.getLastIp(),user.getUserId()});
}
}
findUserByUserName()方法稍微有點複雜。這裏使用到了JdbcTemplate#query()方法,該方法的簽名為query(String sql,Object[] args, RowCallbackHandler rch),它有3個入參。
sqlStr:查詢的SQL語句,允許使用帶“?”的參數占位符。
args:SQL語句中占位符對應的參數數組。
RowCallbackHandler:查詢結果的處理回調接口,該回調接口有一個方法processRow (ResultSet rs),負責將查詢的結果從ResultSet裝載到類似於領域對象的對象實例中。
在②處,findUserByUserName()通過匿名內部類的方式定義了一個RowCallbackHandler回調接口實例,將ResultSet轉換為User對象。
updateLoginInfo()方法比較簡單,主要通過JdbcTemplate#update(String sql,Object[])進行數據的更新操作。
實戰經驗
在編寫SQL語句時,由於SQL語句比較長,一般會采用多行字符串的方式進行構造,如代碼清單1-6的①處所示。在編寫多行SQL語句時,由於上下行最終會組成一行完整的SQL語句,這種拚接方式很容易產生錯誤的SQL組合語句:假設在①處第一行的user_name後不加空格,第二行的FROM之前也無空格,組合的SQL將為“... user_nameFROM ...”,由於FROM保留字和user_name連在一起,就產生了非法的SQL語句。以下是一個值得推薦的編程習慣:在每一行SQL語句的句前和句尾都加一個空格,這樣就可以避免分行SQL語句組合後的錯誤。
1.4.3 LoginLogDao
LoginLogDao負責記錄用戶的登錄日誌,它僅有一個insertLoginLog()接口方法,與UserDao相似,其實現類也通過JdbcTemplate#update(String sql ,Object[] args)方法完成插入登錄日誌的操作,如代碼清單1-7所示。
代碼清單1-7 LoginLogDao
package com.smart.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.smart.domain.LoginLog;
@Repository
public class LoginLogDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void insertLoginLog(LoginLog loginLog) {
String sqlStr = "INSERT INTO t_login_log(user_id,ip,login_datetime) "
+ "VALUES(?,?,?)";
Object[] args = { loginLog.getUserId(), loginLog.getIp(),loginLog.getLoginDate() };
jdbcTemplate.update(sqlStr, args);
}
}
1.4.4 在Spring中裝配DAO
在編寫DAO接口的實現類時,大家也許有一個問題:在以上兩個DAO實現類中都沒有打開/釋放Connection的代碼,DAO類究竟如何訪問數據庫呢?前麵說過,樣板式的操作都被JdbcTemplate封裝起來了,JdbcTemplate本身需要一個DataSource,這樣它就可以根據需要從DataSource中獲取或返回連接。UserDao和LoginLog都提供了一個帶@Autowired注解的JdbcTemplate變量。所以必須事先聲明一個數據源,然後定義一個JdbcTemplate Bean,通過Spring的容器上下文自動綁定機製進行Bean的注入。
在項目工程的src\main\resources目錄下創建一個名為applicationContext.xml的Spring配置文件,配置文件的基本結構如下所示。
<?xml version="1.0" encoding="UTF-8" ?>
_<!-- 引用Spring的多個Schema空間的格式定義文件 -->_
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:p="https://www.springframework.org/schema/p"
xmlns:context="https://www.springframework.org/schema/context"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-3.1.xsd
https://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context-3.1.xsd">
…
</beans>
在IDEA中,刷新工程目錄樹,在src\main\resources文件夾下即可看到該配置文件。雙擊applicationContext.xml文件,添加如代碼清單1-8所示的配置信息。
代碼清單1-8 DAO Bean的配置
…
<beans …>
<!-- ①掃描類包,將標注Spring注解的類自動轉化Bean,同時完成Bean的注入 -->
<context:component-scan base-package="com.smart.dao"/>
<!--②定義一個使用DBCP實現的數據源-->_
<bean
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3309/sampledb"
p:username="root"
p:password="1234" />
<!--③定義JDBC模板Bean -->
<bean
p:dataSource-ref="dataSource" />
</beans>
在①處,我們使用Spring的context:component-scan掃描指定類包下的所有類,這樣在類中定義的Spring注解(如@Repository、@Autowired等)才能產生作用。
在②處,我們使用Jakarta的DBCP開源數據源實現方案定義了一個數據源,數據庫驅動器類為com.mysql.jdbc.Driver,由於我們設置的MySQL數據庫的服務端口為3309,而非默認的3306,所以數據庫URL中顯式指定了3309端口的信息。
在③處配置了JdbcTemplate Bean,將②處聲明的dataSource注入JdbcTemplate中,而這個JdbcTemplate Bean將通過@Autowired自動注入LoginLog和UserDao 的Bean中,可見Spring可以很好地將注解配置和XML配置統一起來。
這樣,我們就完成了登錄模塊持久層所有的開發工作,接下來將著手業務層的開發和配置工作,我們將對業務層的業務類方法進行單元測試,到時就可以看到DAO的實際運行效果了,現在暫時把這兩個DAO放在一邊。
最後更新:2017-05-31 14:01:50