## 初步瞭解 ### 總體架構設計 Mybatis 整體框架如下: ![img](https://zhangjiahao-blog.oss-cn-beijing.aliyuncs.com/picgo/202305161021323.png) ##### 介面層 MyBatis 和資料庫的交互有兩種 ...
初步瞭解
總體架構設計
Mybatis 整體框架如下:
介面層
MyBatis 和資料庫的交互有兩種方式:
- 使用傳統的 MyBatis 提供的 API;
- 使用 Mapper 介面;
使用傳統的 MyBatis 提供的 API
這是傳統的傳遞 Statement Id 和查詢參數給 SqlSession 對象,使用 SqlSession 對象完成和資料庫的交互;MyBatis 提供了非常方便和簡單的 API,供用戶實現對資料庫的增刪改查數據操作,以及對資料庫連接信息和 MyBatis 自身配置信息的維護操作。
使用 Mapper 介面
MyBatis 將配置文件中的每一個<mapper>
節點抽象為一個 Mapper 介面,而這個介面中聲明的方法和跟<mapper>
節點中的<select|update|delete|insert>
節點項對應,即<select|update|delete|insert>
節點的 id 值為 Mapper 介面中的方法名稱,parameterType 值表示 Mapper 對應方法的入參類型,而 resultMap 值則對應了 Mapper 介面表示的返回值類型或者返回結果集的元素類型。
根據 MyBatis 的配置規範配置好後,通過 SqlSession.getMapper(XXXMapper.class)方法,MyBatis 會根據相應的介面聲明的方法信息,通過動態代理機制生成一個 Mapper 實例,我們使用 Mapper 介面的某一個方法時,MyBatis 會根據這個方法的方法名和參數類型,確定 Statement Id,底層還是通過 SqlSession.select("statementId",parameterObject);或者 SqlSession.update("statementId",parameterObject); 等等來實現對資料庫的操作, MyBatis 引用 Mapper 介面這種調用方式,純粹是為了滿足面向介面編程的需要。(其實還有一個原因是在於,面向介面的編程,使得用戶在介面上可以使用註解來配置 SQL 語句,這樣就可以脫離 XML 配置文件,實現“0 配置”)。
數據處理層
數據處理層可以說是 MyBatis 的核心,從大的方面上講,它要完成兩個功能:
- 通過傳入參數構建動態 SQL 語句;
- SQL 語句的執行以及封裝查詢結果集成
List<E>
通過傳入參數構建動態 SQL 語句;
動態語句生成可以說是 MyBatis 框架非常優雅的一個設計,MyBatis 通過傳入的參數值,使用 Ognl 來動態地構造 SQL 語句,使得 MyBatis 有很強的靈活性和擴展性。
參數映射指的是對於 java 數據類型和 jdbc 數據類型之間的轉換:這裡有包括兩個過程:查詢階段,我們要將 java 類型的數據,轉換成 jdbc 類型的數據,通過 preparedStatement.setXXX() 來設值;另一個就是對 resultset 查詢結果集的 jdbcType 數據轉換成 java 數據類型。
SQL 語句的執行以及封裝查詢結果集成List<E>
動態 SQL 語句生成之後,MyBatis 將執行 SQL 語句,並將可能返回的結果集轉換成List<E>
列表。MyBatis 在對結果集的處理中,支持結果集關係一對多和多對一的轉換,並且有兩種支持方式,一種為嵌套查詢語句的查詢,還有一種是嵌套結果集的查詢。
框架支撐層
- 事務管理機制
事務管理機制對於 ORM 框架而言是不可缺少的一部分,事務管理機制的質量也是考量一個 ORM 框架是否優秀的一個標準。
- 連接池管理機制
由於創建一個資料庫連接所占用的資源比較大, 對於數據吞吐量大和訪問量非常大的應用而言,連接池的設計就顯得非常重要。
- 緩存機制
為了提高數據利用率和減小伺服器和資料庫的壓力,MyBatis 會對於一些查詢提供會話級別的數據緩存,會將對某一次查詢,放置到 SqlSession 中,在允許的時間間隔內,對於完全相同的查詢,MyBatis 會直接將緩存結果返回給用戶,而不用再到資料庫中查找。
- SQL 語句的配置方式
傳統的 MyBatis 配置 SQL 語句方式就是使用 XML 文件進行配置的,但是這種方式不能很好地支持面向介面編程的理念,為了支持面向介面的編程,MyBatis 引入了 Mapper 介面的概念,面向介面的引入,對使用註解來配置 SQL 語句成為可能,用戶只需要在介面上添加必要的註解即可,不用再去配置 XML 文件了,但是,目前的 MyBatis 只是對註解配置 SQL 語句提供了有限的支持,某些高級功能還是要依賴 XML 配置文件配置 SQL 語句。
引導層
引導層是配置和啟動 MyBatis 配置信息的方式。MyBatis 提供兩種方式來引導 MyBatis :基於 XML 配置文件的方式和基於 Java API 的方式。
主要構件及其相互關係
主要的核心部件解釋如下:
SqlSession
作為 MyBatis 工作的主要頂層 API,表示和資料庫交互的會話,完成必要資料庫增刪改查功能Executor
MyBatis 執行器,是 MyBatis 調度的核心,負責 SQL 語句的生成和查詢緩存的維護StatementHandler
封裝了 JDBC Statement 操作,負責對 JDBC statement 的操作,如設置參數、將 Statement 結果集轉換成 List 集合。ParameterHandler
負責對用戶傳遞的參數轉換成 JDBC Statement 所需要的參數,ResultSetHandler
負責將 JDBC 返回的 ResultSet 結果集對象轉換成 List 類型的集合;TypeHandler
負責 java 數據類型和 jdbc 數據類型之間的映射和轉換MappedStatement
MappedStatement 維護了一條<select|update|delete|insert>
節點的封裝,SqlSource
負責根據用戶傳遞的 parameterObject,動態地生成 SQL 語句,將信息封裝到 BoundSql 對象中,並返回BoundSql
表示動態生成的 SQL 語句以及相應的參數信息Configuration
MyBatis 所有的配置信息都維持在 Configuration 對象之中。
流程簡解
準備
/src/main/resources/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 根標簽 -->
<configuration>
<properties>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&useSSL=false&autoReconnect=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</properties>
<!-- 環境,可以配置多個,default:指定採用哪個環境 -->
<environments default="test">
<environment id="test">
<!-- 事務管理器,JDBC類型的事務管理器 -->
<transactionManager type="JDBC"/>
<!-- 數據源,池類型的數據源 -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/> <!-- 配置了properties,所以可以直接引用 -->
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="TeacherMapper.xml"/>
</mappers>
</configuration>
-
/src/main/resources/TeacherMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- mapper:根標簽,namespace:命名空間,隨便寫,一般保證命名空間唯一 --> <mapper namespace="TeacherMapper"> <!-- statement,內容:sql語句。id:唯一標識,隨便寫,在同一個命名空間下保持唯一 resultType:sql語句查詢結果集的封裝類型,tb_user即為資料庫中的表 --> <select id="selectTest" resultType="org.apache.ibatis.test.Teacher"> select * from teacher where id = #{id} </select> </mapper>
-
/src/main/resources/log4j.properties
log4j.rootLogger=DEBUG, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
-
/src/main/java/org/apache/ibatis/test/Teacher.java
package org.apache.ibatis.test; public class Teacher { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Teacher{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
-
/src/main/java/org/apache/ibatis/test/Test.java
package org.apache.ibatis.test; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class Test { public static void main(String[] args) throws IOException { // 指定全局配置文件 String resource = "mybatis-config.xml"; // 讀取配置文件 InputStream inputStream = Resources.getResourceAsStream(resource); // 構建sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 獲取sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 操作CRUD,第一個參數:指定statement,規則:命名空間+“.”+statementId // 第二個參數:指定傳入sql的參數:這裡是用戶id Teacher test = sqlSession.selectOne("TeacherMapper.selectTest", 1); System.out.println(test.getName()); } }
整體流程
- 通過
mybatis-config.xml
進行初始化創建出SqlSessionFactory
。其內部是通過創建XMLConfigBuilder
對象,然後自己進行解析XML
文件,把文件內容解析封裝為Configuration
對象,最後由SqlSessionFactory
進行封裝為SqlSessionFactory
對象來完成SqlSessionFactory
的創建。 - 通過
SqlSessionFactory
對象進行開啟一個SqlSession
。其內部是通過獲取Configuration
中Environment
數據進行構建TransactionFactory
–>Executor
,最後封裝為SqlSession
返回進行使用 - 通過
SqlSession
執行XML
中對應的方法。其中是先通過statement
即XML 指定的類#方法名稱
獲取MappedStatement
對象,然後提交給Executor
進行執行。
目錄詳解
exception
包
下圖是Mybatis
中異常的關係圖:
背景知識
- 工廠模式
- 異常的封裝
講解
工廠模式
public class ExceptionFactory {
private ExceptionFactory() {
// Prevent Instantiation
}
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
}
- 私有構造函數:導致該工廠無法創建出對應的對象
- 靜態
wrapException
方法,用於通過異常信息和異常類型進行封裝異常為Mybatis
中的異常類型。全局通過ExceptionFactory.wrapException()
進行生產出對應的異常對象
異常類型
-
IbatisException:
Mybatis
中最高的異常,但是直接繼承該類的子類只有PersistenceException
,而且該類也添加了@Deprecated
說明以後可能去除。 -
PersistenceException: 譯為持久化異常。
Mybatis
對應是持久化框架,後期可能該異常類型為Mybatis
所有異常的父類。 -
TooManyResultsException:譯為多條返回結果異常。用處為
selectOne
卻返回多條記錄時所拋出的異常。 -
TypeException: 譯為類型異常。當
Mybatis
中需要類型轉化時,若轉換失敗則會拋出該異常。 -
CacheException: 譯為緩存異常。當
Mybatis
讀取緩存中數據出現問題時則會拋出該異常。 -
ParsingException: 譯為解析異常。當前代碼未看到使用。
-
ScriptingException: 譯為腳本異常。
-
ResultMapException: 譯為結果映射異常。在結果轉換為對應類型的對象時,若轉換失敗則會拋出異常。
-
DataSourceException: 譯為數據源異常。在初始化數據源時若出現錯誤則會拋出該異常。
-
TransactionException: 譯為事務異常。在給
connection
開啟事務時若失敗則會拋出該異常。 -
BuilderException: 譯為建造異常。在建造對象失敗時會拋出該異常。
-
SqlSessionException: 譯為
SqlSession
的異常。基本只會在SqlSessionManager
中使用,主要是SqlSession
使用過程中的異常。 -
ReflectionException: 譯為反射異常。基本只會在反射使用時會拋出該異常。
-
ExecutorException: 譯為執行器異常。會線上程操作資料庫的時候拋出該異常。
-
BatchExecutorException:譯為批量執行器異常。會線上程批量操作資料庫的時候拋出該異常。
-
BindingException: 譯為綁定異常。主要是
mapper
映射的時候會拋出該異常。 -
LogException: 譯為日誌異常。目前只在
LogFactory
構建日誌相關的時候才會拋出該異常。 -
PluginException: 譯為插件異常。目前只在
Plugin
中使用,在獲取插件信息時候會拋出該異常。
Mybatis
類型主要是根據業務相關包放在一起,所以命名絕大多數都能夠直觀的看到原因所在。
jdbc
包
背景知識
- 模版模式
- 易用性
講解
模版模式
某些類通用的一些處理方法一致,但是處理對象可能存在不同,此時可以使用模版方法,抽取父類編寫通用處理方法,子類只需實現獲取對象的方法即可。
public class SQL extends AbstractSQL<SQL> {
@Override
public SQL getSelf() {
return this;
}
}
public abstract class AbstractSQL<T> {
private static final String AND = ") \nAND (";
private static final String OR = ") \nOR (";
private final SQLStatement sql = new SQLStatement();
public abstract T getSelf();
public T UPDATE(String table) {
sql().statementType = SQLStatement.StatementType.UPDATE;
sql().tables.add(table);
return getSelf();
}
public T SET(String sets) {
sql().sets.add(sets);
return getSelf();
}
......
}
以上代碼可知:
- 若用戶需要自定
SQL
如ExplainSQL
,從而進行性能調優,此時只需要繼承AbstractSQL
即可,而無需編寫原方法。
易用性
為了用戶使用方便和構建 SQL
的直觀性,AbstractSQL
命名採用了全大寫的模式,以此讓用戶更加易用。
SqlRunner
類
public int insert(String sql, Object... args) throws SQLException {
PreparedStatement ps;
if (useGeneratedKeySupport) {
ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
} else {
ps = connection.prepareStatement(sql);
}
try {
setParameters(ps, args);
ps.executeUpdate();
if (useGeneratedKeySupport) {
List<Map<String, Object>> keys = getResults(ps.getGeneratedKeys());
if (keys.size() == 1) {
Map<String, Object> key = keys.get(0);
Iterator<Object> i = key.values().iterator();
if (i.hasNext()) {
Object genkey = i.next();
if (genkey != null) {
try {
return Integer.parseInt(genkey.toString());
} catch (NumberFormatException e) {
//ignore, no numeric key support
}
}
}
}
}
return NO_GENERATED_KEY;
} finally {
try {
ps.close();
} catch (SQLException e) {
//ignore
}
}
}
public int update(String sql, Object... args) throws SQLException {
PreparedStatement ps = connection.prepareStatement(sql);
try {
setParameters(ps, args);
return ps.executeUpdate();
} finally {
try {
ps.close();
} catch (SQLException e) {
//ignore
}
}
}
此類在Mybatis
中沒有任何的使用,此類應該只是為了提供給用戶,讓用戶可以自定義執行相關 SQL
,分析其方法本質為原始JDBC
相關操作流程。
ScriptRunner
類
private void executeFullScript(Reader reader) {
StringBuilder script = new StringBuilder();
try {
BufferedReader lineReader = new BufferedReader(reader);
String line;
while ((line = lineReader.readLine()) != null) {
script.append(line);
script.append(LINE_SEPARATOR);
}
String command = script.toString();
println(command);
executeStatement(command);
commitConnection();
} catch (Exception e) {
String message = "Error executing: " + script + ". Cause: " + e;
printlnError(message);
throw new RuntimeSqlException(message, e);
}
}
private void executeLineByLine(Reader reader) {
StringBuilder command = new StringBuilder();
try {
BufferedReader lineReader = new BufferedReader(reader);
String line;
while ((line = lineReader.readLine()) != null) {
handleLine(command, line);
}
commitConnection();
checkForMissingLineTerminator(command);
} catch (Exception e) {
String message = "Error executing: " + command + ". Cause: " + e;
printlnError(message);
throw new RuntimeSqlException(message, e);
}
}
private void handleLine(StringBuilder command, String line) throws SQLException {
String trimmedLine = line.trim();
if (lineIsComment(trimmedLine)) {
Matcher matcher = DELIMITER_PATTERN.matcher(trimmedLine);
if (matcher.find()) {
delimiter = matcher.group(5);
}
println(trimmedLine);
} else if (commandReadyToExecute(trimmedLine)) {
command.append(line.substring(0, line.lastIndexOf(delimiter)));
command.append(LINE_SEPARATOR);
println(command);
executeStatement(command.toString());
command.setLength(0);
} else if (trimmedLine.length() > 0) {
command.append(line);
command.append(LINE_SEPARATOR);
}
}
此類的核心方法如上,可看出其本質和SqlRunner
類一致。