ORM核心思想在於經過創建MODEL與數據庫的映射來簡化大量重複的工做量. 對於簡單增刪改查操做來講, 經過MODEL自動轉換爲SQL語句並執行能夠節省不少工做量. 可是對於複雜的系統來講, 須要各類各樣的複雜操做, 而且SQL也須要通過高度優化, 所以經過MODEL自動執行SQL並不可行.正則表達式
Mybatis中經過所謂的半自動化解決了這一問題. 即手動書寫SQL, 自動完成映射. 本章將實現一個簡易的Mybatis.sql
Mapper文件是Mybatis中很是重要的組件, 全部的SQL操做及映射方式都在Mapper文件中. 在程序(或項目)啓動時加載全部的Mapper文件, 解析其中的SQL節點並保存. 當執行某項操做時從Mapper中找到對應的SQL, 完成參數映射後執行並返回執行結果.數據庫
在瞭解了基本的設計思路後, 下面開始對ORM框架中的組件及接口進行設計.api
SQL節點信息類, 負責保存Mapper中的SQL節點信息. 加載Mapper文件時, 將Mapper中的每一個SQL節點進行解析並保存至MappedStatement中.bash
// SQL節點信息類
public class MappedStatement {
// SQL節點ID
private String id;
// SQL語句
private String sql;
// SQL節點類型(select, insert, update, delete)
private String statementType;
// 返回值類型(類型爲select時須要)
private String resultType;
// SQL中須要被映射覆制的參數集合
private List<String> parameters;
// 實例化時必須傳入SQL_ID
public MappedStatement(String id) {
this.id = id;
}
// Getter & Setter
// ...
}
複製代碼
全局配置類, 保存SQL節點信息及其餘配置信息.app
// 全局配置(保存SQL節點信息及其餘配置)
public class Configuration {
// 保存全部的SQL節點信息. k: SQL節點ID, v: SQL節點對象
private Map<String, MappedStatement> mappedStatements = new HashMap<String, MappedStatement>();
// 根據SQL節點ID獲取SQL節點信息
public MappedStatement getMappedStatement(String id) {
return this.mappedStatements.get(id);
}
// 添加SQL節點
public MappedStatement addMappedStatement(MappedStatement s) {
return this.mappedStatements.put(s.getId(), s);
}
}
複製代碼
SQL會話組件,封裝了數據庫操做. 對外提供增刪改查的接口供使用者調用.框架
// SQL會話(提供SQL基本操做)
public class SqlSession {
// 全局配置
private Configuration config;
// 實例化時必須傳入全局配置
public SqlSession(Configuration config) {
this.config = config;
}
/**
* 查詢單條數據(查詢結果必須爲一條記錄)
*
* @param sqlId SQL節點ID
* @return 查詢結果對應的MODEL(resultType指定的類)
*/
public <T> T selectOne(String sqlId) {
return selectOne(sqlId, null);
}
/**
* 查詢單條數據(查詢結果必須爲一條記錄)
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 查詢結果對應的MODEL(resultType指定的類)
*/
public <T> T selectOne(String sqlId, Object args) {
List<T> list = selectList(sqlId, null);
if (list.size() > 1) {
throw new RuntimeException("count 1111");
}
if (list.size() == 1) {
return list.get(0);
}
return null;
}
/**
* 查詢多條數據
*
* @param sqlId SQL節點ID
* @return 查詢結果對應的MODEL(resultType指定的類)集合
*/
public <T> List<T> selectList(String sqlId) {
return selectList(sqlId, null);
}
/**
* 查詢多條數據
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 查詢結果對應的MODEL(resultType指定的類)集合
*/
public <T> List<T> selectList(String sqlId, Object args) {
// TODO
return null;
}
/**
* 插入數據
*
* @param sqlId SQL節點ID
* @return 成功被插入的數據條數
*/
public int insert(String sqlId) {
return insert(sqlId, null);
}
/**
* 插入數據
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 成功被插入的數據條數
*/
public int insert(String sqlId, Object args) {
return update(sqlId, args);
}
/**
* 刪除數據
*
* @param sqlId SQL節點ID
* @return 成功被刪除的數據條數
*/
public int delete(String sqlId) {
return delete(sqlId, null);
}
/**
* 刪除數據
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 成功被刪除的數據條數
*/
public int delete(String sqlId, Object args) {
return update(sqlId, args);
}
/**
* 更新數據
*
* @param sqlId SQL節點ID
* @return 成功被更新的數據條數
*/
public int update(String sqlId) {
return update(sqlId, null);
}
/**
* 更新數據
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 成功被更新的數據條數
*/
public int update(String sqlId, Object args) {
// TODO
return 0;
}
}
複製代碼
// SQL會話工廠類(負責建立SQL會話)
public class SqlSessionFactory {
// 全局配置
private Configuration config;
// 實例化時必須傳入全局配置
public SqlSessionFactory(Configuration config) {
this.config = config;
}
// 獲取SQL會話
public SqlSession getSession() {
return new SqlSession(config);
}
}
複製代碼
框架初始化入口, 負責解析Mapper文件並建立SQL會話工廠.工具
// 框架初始化入口(負責解析Mapper文件並建立SQL會話工廠)
public class SqlSessionFactoryBean {
private static final Pattern PARAMETER_PATTERN = Pattern.compile("#\\{(.+?)\\}");
// 全局配置
private Configuration config = new Configuration();
// Mapper文件路徑
private String mapperLocation;
// 默認構造器
public SqlSessionFactoryBean() {
}
// 經過Mapper文件路徑實例化
public SqlSessionFactoryBean(String mapperLocation) {
this.setMapperLocation(mapperLocation);
}
// 設置Mapper文件
public void setMapperLocation(String mapperLocation) {
this.mapperLocation = mapperLocation;
}
// 構建SQL會話工廠
public SqlSessionFactory build() {
// TODO
// 加載Mapper
return new SqlSessionFactory(config);
}
}
複製代碼
程序(或項目)啓動時, 實例化SqlSessionFactoryBean, 設置Mapper所在路徑, 調用build方法構建SqlSessionFactory測試
SqlSessionFactoryBean在build時根據Mapper路徑加載並解析Mapper, 將Mapper下的每一個SQL節點封裝成MappedStatement對象. 並保存在全局配置Configuration中.優化
數據訪問層(DAO)對數據庫進行操做時, 經過SqlSessionFactory獲取SqlSession, 調用其對應的方法並傳入SQL ID.
SqlSession中根據SQL ID在全局配置Configuration中找到對應的SQL節點信息對象MappedStatement.
將傳入的參數按照MappedStatement中SQL的參數規則進行參數映射後生成可執行的SQL.
執行SQL後將執行結果返回. 若是是select操做, 將查詢結果封裝成指定對象後返回
SqlSessionFactoryBean做爲框架的初始化入口, 在build方法中加載配置文件, 解析Mapper並構建SQL會話工廠.
// 構建SQL會話工廠
public SqlSessionFactory build() {
if (this.mapperLocation == null) {
throw new RuntimeException("請設置Mapper文件所在路徑");
}
// 獲取Mapper文件
List<File> mappers = getMapperFiles();
// 加載全部Mapper並解析
for (File mapper : mappers) {
parseStatement(mapper);
}
// 返回SQL會話工廠
return new SqlSessionFactory(config);
}
複製代碼
Mapper文件配置路徑支持通配符(*), 例: /m/*_mapper.xml
爲Mapper文件位於classpath下的m文件內,而且以_mapper.xml
結尾. 在加載Mapper文件時須要對通配符進行處理.
// 根據Mapper所在路徑獲取全部Mapper文件
private List<File> getMapperFiles() {
List<File> mappers = new ArrayList<File>();
String mapperDir; // Mapper文件目錄
String mapperName; // Mapper文件名稱
// 根據配置的Mapper路徑分別獲取文件目錄及文件名稱
// 是否含有文件分隔符
int lastPos = this.mapperLocation.lastIndexOf("/");
// 含有文件分隔符
if (lastPos > -1) {
mapperDir = this.mapperLocation.substring(0, lastPos);
mapperName = this.mapperLocation.substring(lastPos + 1);
}
// 無文件分隔符
// 配置路徑爲Mapper文件名
else {
mapperDir = "";
mapperName = this.mapperLocation;
}
// 獲取Mapper目錄下全部文件
String classpath = ClassLoader.getSystemResource("").getPath();
File[] allMappers = new File(classpath, mapperDir).listFiles();
// *爲通配符,將*轉換爲正則表達式通配符進行匹配
// *_mapper.xml -> .+?_mapper.xml
Pattern pattern = Pattern.compile(mapperName.replaceAll("\\*", ".+?"));
// 遍歷Mapper目錄下全部文件
for (File f : allMappers) {
// 文件是否和指定的Mapper名稱一致
if (pattern.matcher(f.getName()).matches()) {
mappers.add(f);
}
}
return mappers;
}
複製代碼
解析Mapper文件中的全部SQL節點並保存. Mapper文件格式以下:
<mapper>
<select id="selectGoods" resultType="com.atd681.xc.ssm.orm.test.Goods">
select id, goods_name goodsName, category, price from t_orm_goods
</select>
<update id="updateGoods">
UPDATE t_orm_goods
SET goods_name = #{goodsName}, price = #{price}
WHERE id = #{id}
</update>
</mapper>
複製代碼
將每一個SQL節點封裝至MappedStatement對象中. 並統一保存至全局配置中.
// 解析Mapper文件
@SuppressWarnings("unchecked")
private void parseStatement(File mapper) {
Document doc = null;
// 使用JDom解析XML
try {
doc = new SAXBuilder().build(mapper);
} catch (Exception e) {
throw new RuntimeException("加載配置文件錯誤", e);
}
// Mapper下全部SQL節點
List<Element> statementList = doc.getRootElement().getChildren();
// 遍歷Mapper下全部SQL節點
for (Element statement : statementList) {
// SQL節點ID
String sqlId = statement.getAttributeValue("id");
// SQL節點必須設置ID屬性
if (sqlId == null) {
throw new RuntimeException("SQL節點須要設置id屬性");
}
// SQL節點的ID不能重複
if (config.getMappedStatement(sqlId) != null) {
throw new RuntimeException("SQL節點id已經存在");
}
// 解析SQL節點
MappedStatement ms = new MappedStatement(sqlId);
ms.setSql(statement.getTextTrim());
ms.setStatementType(statement.getName());
ms.setResultType(statement.getAttributeValue("resultType"));
// 解析SQL中的參數
parseSqlAndParameters(ms);
// 將SQL節點信息添加至全局配置中
config.addMappedStatement(ms);
}
}
複製代碼
SQL中若是含有須要被替換的參數時, 須要對SQL進行處理. 保存參數名稱並將其替換成?, 以便使用JDBC執行時可使用PrepareStatement進行賦值.
// 解析SQL中的參數
private void parseSqlAndParameters(MappedStatement ms) {
List<String> parameters = new ArrayList<String>();
StringBuffer sql = new StringBuffer();
// 匹配SQL中的#{}
Matcher m = PARAMETER_PATTERN.matcher(ms.getSql());
// 將匹配到的#{}中的參數名稱保存, 並替換爲?
// where u_name=#{UName} and u_age=#{UAge} -> where u_name=? and u_age=?
// 執行SQL時在傳入的參數中找到UName對第一個?賦值,UAge對第二個?賦值
while (m.find()) {
parameters.add(m.group(1));
m.appendReplacement(sql, "?");
}
m.appendTail(sql);
ms.setSql(sql.toString());
ms.setParameters(parameters);
}
複製代碼
至此, Mapper文件已經解析完成. SQL會話工廠也已經建立完成. 在DAO中能夠獲取SQL會話並調用對應的數據庫操做方法執行.
執行SQL時, 根據對SQL中的參數進行賦值後執行. 在解析Mapper時已經將SQL中的參數替換爲?而且保存了參數的名稱. 賦值時只須要依次將經過參數名稱獲取對應的參數值而且經過JDBC賦值到SQL中便可.
// 對參數進行賦值
private void setParameters(PreparedStatement ps, MappedStatement ms, Object args) throws Exception {
if (args == null) {
return;
}
List<String> parameters = ms.getParameters();
if (parameters == null) {
return;
}
// 依次根據參數名稱從對應的MODEL中獲取參數值替換SQL中的?
for (int i = 0; i < parameters.size(); i++) {
Object value = BeanUtil.getValue(args, parameters.get(i));
ps.setObject(i + 1, value);
}
}
複製代碼
DAO調用SQL會話的方法傳入的參數支持如下幾種類型
在根據SQL參數名稱獲取對應值時須要對三種狀況分別進行解析.
// Bean工具類
public class BeanUtil {
// 從對象中獲取執行屬性的值
@SuppressWarnings("rawtypes")
public static Object getValue(Object bean, String name) throws Exception {
// 字符串
if (bean instanceof String) {
return bean;
}
// Map
if (bean instanceof Map) {
return ((Map) bean).get(name);
}
// Javabean調用屬性的Getter方法獲取
Class<?> clazz = bean.getClass();
Method getter = clazz.getDeclaredMethod(getGetter(name), new Class<?>[] {});
return getter.invoke(bean, new Object[] {});
}
// 獲取Getter方法名. userName -> getUserName
public static String getGetter(String name) {
return "get" + capitalize(name);
}
// 獲取Setter方法名. userName -> setUserName
public static String getSetter(String name) {
return "set" + capitalize(name);
}
// 首字母大寫. userName -> UserName
private static String capitalize(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
複製代碼
對於insert,update,delete三種操做來講,最終在JDBC中的執行方式一致.
/**
* 更新數據
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 成功被更新的數據條數
*/
public int update(String sqlId, Object args) {
MappedStatement ms = config.getMappedStatement(sqlId);
Connection conn = null;
PreparedStatement ps = null;
try {
conn = getConnection();
ps = getPreparedStatement(conn, ms);
setParameters(ps, ms, args);
return ps.executeUpdate();
} catch (Exception e) {
throw new RuntimeException("", e);
} finally {
close(ps, conn);
}
}
複製代碼
對於select操做來講, 須要將查詢結果封裝至指定對象中.
/**
* 查詢多條數據
*
* @param sqlId SQL節點ID
* @param args 執行SQL所需參數
* @return 查詢結果對應的MODEL(resultType指定的類)集合
*/
public <T> List<T> selectList(String sqlId, Object args) {
MappedStatement ms = config.getMappedStatement(sqlId);
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = getConnection();
ps = getPreparedStatement(conn, ms);
setParameters(ps, ms, args);
rs = ps.executeQuery();
// 查詢結果封裝至指定對象中
return handleResultSet(rs, ms);
} catch (Exception e) {
throw new RuntimeException("", e);
} finally {
close(rs, ps, conn);
}
}
複製代碼
查詢結果映射的類爲SQL節點中resultType屬性定義的類. 查詢結果的字段名和類中的屬性名一致才自動賦值. 若是不一致能夠在SQL中加入別名使其與類中屬性名一致. 例: select u_name uName from ...
// 將查詢結果集封裝至對象中
@SuppressWarnings("unchecked")
private <T> List<T> handleResultSet(ResultSet rs, MappedStatement ms) throws Exception {
List<T> list = new ArrayList<T>();
// ResultSetMetaData對象保存查詢到的數據庫相關信息.
ResultSetMetaData metaData = rs.getMetaData();
while (rs.next()) {
// 經過Java反射實例化對應的Javabean, 類型爲SQL配置文件中的resultType
// 若是不設置resultType,則沒法知道返回值的類型.因此resultType必需要設置.
Class<?> classObj = (Class<?>) Class.forName(ms.getResultType());
T t = (T) classObj.newInstance();
// 將每一個字段的值映射到對應的對象中
int count = metaData.getColumnCount();
for (int i = 1; i <= count; i++) {
// 取得字段對象Javabean中的屬性名稱
String ormName = metaData.getColumnLabel(i);
// 經過屬性名稱,用Java反射取得Javabean中set方法.
// Javabean的定義爲:全部屬性爲私有(private),提供共有(public)的get和set方法對其進行操做
// set方法爲設置該屬性的方法.set方法格式爲set+屬性名(首字母大寫),例屬性爲userName,set方法爲setUserName()
Class<?> filedType = classObj.getDeclaredField(ormName).getType();
Method setter = classObj.getMethod(BeanUtil.getSetter(ormName), filedType);
// 根據數據庫字段的類型執行相應的set方法便可將字段值設置到屬性中
setter.invoke(t, getColumnValue(rs, i));
}
list.add(t);
}
return list;
}
複製代碼
// 根據數據庫字段類型獲取其在Java中對應類型的值
private Object getColumnValue(ResultSet rs, int index) throws Exception {
int columnType = rs.getMetaData().getColumnType(index);
if (columnType == Types.BIGINT) {
return rs.getLong(index);
} else if (columnType == Types.INTEGER) {
return rs.getInt(index);
} else if (columnType == Types.VARCHAR) {
return rs.getString(index);
} else if (columnType == Types.DATE || columnType == Types.TIME || columnType == Types.TIMESTAMP) {
return rs.getDate(index);
} else if (columnType == Types.DOUBLE) {
return rs.getDouble(index);
}
return null;
}
複製代碼
用戶MODEL
// 用戶MODEL
public class User {
// id
private Long id;
// 用戶名
private String uname;
// 用戶年齡
private Integer uage;
// 用戶地址
private String uaddr;
// 備註
private String remark;
public User() {
}
// 經過用戶信息構造用戶
public User(Long id, String uname, Integer uage, String uaddr, String remark) {
this.id = id;
this.uname = uname;
this.uage = uage;
this.uaddr = uaddr;
this.remark = remark;
}
// Getter & Setter
// ...
}
複製代碼
用戶DAO接口
// 用戶DAO接口
public interface UserDAO {
// 建立用戶
int insertUser(User user);
// 更新用戶
int updateUser(User user);
// 查詢用戶
List<User> selectUser();
}
複製代碼
用戶DAO實現類
// 用戶DAO
public class UserDAOImpl extends BaseDAO implements UserDAO {
// SQL會話工廠在父類中
public UserDAOImpl(SqlSessionFactory sf) {
super(sf);
}
// 添加用戶
public int insertUser(User user) {
return getSqlSession().update("insertUser", user);
}
// 更新用戶
public int updateUser(User user) {
return getSqlSession().update("updateUser", user);
}
// 查詢用戶
public List<User> selectUser() {
return getSqlSession().selectList("selectUser");
}
}
複製代碼
爲便於DAO操做, 全部DAO繼承BaseDAO, 其中BaseDAO負責保存SQL會話工廠並提供獲取SQL會話的方法.
// DAO基類
public class BaseDAO {
// SQL會話工廠
private SqlSessionFactory sf;
// 經過SQL會話工廠實例化
public BaseDAO(SqlSessionFactory sf) {
this.sf = sf;
}
// 獲取SQL會話
protected SqlSession getSqlSession() {
return sf.getSession();
}
}
複製代碼
新建測試類, 設置Mapper位置並初始化ORM框架, 執行DAO的操做輸出結果.
public class TestImpl {
public static void main(String[] args) {
// 建立SQL會話工廠
SqlSessionFactory sf = new SqlSessionFactoryBean("*_mapper.xml").build();
// 實例化DAO
UserDAO userDAO = new UserDAOImpl(sf);
// 調用DAO中方法
int count = userDAO.insertUser(new User(1L, "zhangsan", 20, "sssss", "ok"));
List<User> userList = userDAO.selectUser();
// 輸出查詢結果
System.out.println(count);
for (User u : userList) {
System.out.println("| " + u.getId() + " | " + u.getUname() + " | ");
}
}
}
複製代碼