mybatis是目前非常流行的ORM框架,在其基礎上還衍生了mybatis-plus)等優秀的框架。mybatis簡單,易用,易擴展。 接下來我們用一個簡單的例子(幾乎包含所有配置)來看一下mybatis相關的配置項 下面是一個包含用戶、商品、訂單的DAO例子,關係圖如下:
mybatis的配置
MyBatis 的配置文件包含了會深深影響 MyBatis 行爲的設置和屬性信息。 各屬性介紹可以參考[官網](mybatis – MyBatis 3 | 配置) 此示例項目的mybatis-config.xml
內容如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 定義屬性值 -->
<properties>
<property name="username" value="root"/>
<property name="id" value="development"/>
</properties>
<!-- 全局配置信息 -->
<!-- 一個配置完整的 settings 元素的示例如下:-->
<settings>
<!-- 全局性地開啓或關閉所有映射器配置文件中已配置的任何緩存。-->
<setting name="cacheEnabled" value="true"/>
<!-- 延遲加載的全局開關。當開啓時,所有關聯對象都會延遲加載。 特定關聯關係中可通過設置 fetchType 屬性來覆蓋該項的開關狀態。。-->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 開啓時,任一方法的調用都會加載該對象的所有延遲加載屬性。 否則,每個延遲加載屬性會按需加載(參考 lazyLoadTriggerMethods)。(在 3.4.1 及之前的版本中默認爲 true) -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允許單個語句返回多結果集(需要數據庫驅動支持)。 -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列標籤代替列名。實際表現依賴於數據庫驅動,具體可參考數據庫驅動的相關文檔,或通過對比測試來觀察。 -->
<setting name="useColumnLabel" value="true"/>
<!-- 允許 JDBC 支持自動生成主鍵,需要數據庫驅動支持。如果設置爲 true,將強制使用自動生成主鍵。儘管一些數據庫驅動不支持此特性,但仍可正常工作(如 Derby)。 -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 應如何自動映射列到字段或屬性。 NONE 表示關閉自動映射;PARTIAL 只會自動映射沒有定義嵌套結果映射的字段。 FULL 會自動映射任何複雜的結果集(無論是否嵌套)。 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 指定發現自動映射目標未知列(或未知屬性類型)的行爲。
NONE: 不做任何反應
WARNING: 輸出警告日誌('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日誌等級必須設置爲 WARN)
FAILING: 映射失敗 (拋出 SqlSessionException) --> <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<!-- 配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(PreparedStatement); BATCH 執行器不僅重用語句還會執行批量更新。 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 設置超時時間,它決定數據庫驅動等待數據庫響應的秒數。 -->
<setting name="defaultStatementTimeout" value="60"/>
<!-- 爲驅動的結果集獲取數量(fetchSize)設置一個建議值。此參數只可以在查詢設置中被覆蓋。 -->
<setting name="defaultFetchSize" value="100"/>
<!-- 指定語句默認的滾動策略。
FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同於未設置)-->
<setting name="defaultResultSetType" value="DEFAULT"/>
<!-- 是否允許在嵌套語句中使用分頁(RowBounds)。如果允許使用則設置爲 false。 -->
<setting name="safeRowBoundsEnabled" value="false"/>
<!-- 是否允許在嵌套語句中使用結果處理器(ResultHandler)。如果允許使用則設置爲 false。 -->
<setting name="safeResultHandlerEnabled" value="true"/>
<!-- 是否開啓駝峯命名自動映射,即從經典數據庫列名 A_COLUMN 映射到經典 Java 屬性名 aColumn。 -->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!-- MyBatis 利用本地緩存機制(Local Cache)防止循環引用和加速重複的嵌套查詢。 默認值爲 SESSION,會緩存一個會話中執行的所有查詢。
若設置值爲 STATEMENT,本地緩存將僅用於執行語句,對相同 SqlSession 的不同查詢將不會進行緩存。 -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 當沒有爲參數指定特定的 JDBC 類型時,空值的默認 JDBC 類型。 某些數據庫驅動需要指定列的 JDBC 類型,多數情況直接用一般類型即可,比如 NULL、VARCHAR 或 OTHER。 -->
<setting name="jdbcTypeForNull" value="OTHER"/>
<!-- 指定對象的哪些方法觸發一次延遲加載。 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
<!-- 指定動態 SQL 生成使用的默認腳本語言。 -->
<setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
<!-- 指定 Enum 使用的默認 TypeHandler 。 -->
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
<!-- 指定當結果集中值爲 null 的時候是否調用映射對象的 setter(map 對象時爲 put)方法,這在依賴於 Map.keySet() 或 null 值進行初始化時比較有用。注意基本類型(int、boolean 等)是不能設置成 null 的。 -->
<setting name="callSettersOnNulls" value="false"/>
<!-- 當返回行的所有列都是空時,MyBatis默認返回 null。 當開啓這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集(如集合或關聯)。 -->
<setting name="returnInstanceForEmptyRow" value="false"/>
<!-- 指定 MyBatis 增加到日誌名稱的前綴。 -->
<setting name="logPrefix" value="learn_mybatis_log_"/>
<!-- 指定 MyBatis 所用日誌的具體實現,未指定時將自動查找。 -->
<!-- <setting name="logImpl"--> <!-- value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>-->
<!-- 指定 Mybatis 創建可延遲加載對象所用到的代理工具。 CGLIB | JAVASSIST--> <setting name="proxyFactory" value="JAVASSIST"/>
<!-- 指定 VFS 的實現 -->
<!-- <setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/> --> <!-- 允許使用方法簽名中的名稱作爲語句參數名稱。 爲了使用該特性,你的項目必須採用 Java 8 編譯,並且加上 -parameters 選項。(新增於 3.4.1) -->
<setting name="useActualParamName" value="true"/>
<!-- 指定一個提供 Configuration 實例的類。 這個被返回的 Configuration 實例用來加載被反序列化對象的延遲加載屬性值。 這個類必須包含一個簽名爲static Configuration getConfiguration() 的方法。(新增於 3.2.3) -->
<!-- <setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/> --> <!-- 從SQL中刪除多餘的空格字符。請注意,這也會影響SQL中的文字字符串。 -->
<setting name="shrinkWhitespacesInSql" value="true"/>
<!-- 指定一個擁有 provider 方法的 sql provider 類 (新增於 3.5.6). 這個類適用於指定 sql provider 註解上的type(或 value) 屬性(當這些屬性在註解中被忽略時)。 (e.g. @SelectProvider) --> <!-- <setting name="defaultSqlProviderType" value="true"/> -->
<!-- 爲 'foreach' 標籤的 'nullable' 屬性指定默認值。 -->
<setting name="nullableOnForEach" value="false"/>
<!-- 當應用構造器自動映射時,參數名稱被用來搜索要映射的列,而不再依賴列的順序。-->
<setting name="argNameBasedConstructorAutoMapping" value="false"/>
</settings>
<typeAliases>
<!-- 配置別名信息 -->
<typeAlias alias="CustomerEntity" type="com.mingshashan.mybatis.learn.entity.CustomerEntity"/>
<typeAlias alias="OrderEntity" type="com.mingshashan.mybatis.learn.entity.OrderEntity"/>
<typeAlias alias="OrderItemEntity" type="com.mingshashan.mybatis.learn.entity.OrderItemEntity"/>
<typeAlias alias="ProductEntity" type="com.mingshashan.mybatis.learn.entity.ProductEntity"/>
<typeAlias alias="TagEntity" type="com.mingshashan.mybatis.learn.entity.TagEntity"/>
<typeAlias alias="CustomCache" type="com.mingshashan.mybatis.learn.mybatis.CustomCache"/>
</typeAliases>
<typeHandlers>
<typeHandler handler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"/>
</typeHandlers>
<!-- 每次 MyBatis 創建結果對象的新實例時,它都會使用一個對象工廠(ObjectFactory)實例來完成實例化工作。
默認的對象工廠需要做的僅僅是實例化目標類,要麼通過默認無參構造方法, 要麼通過存在的參數映射來調用帶有參數的構造方法。 如果想覆蓋對象工廠的默認行爲,可以通過創建自己的對象工廠來實現。 -->
<objectFactory type="com.mingshashan.mybatis.learn.mybatis.CustomObjectFactory"/>
<!-- MyBatis 提供在構造對象的時候,對於指定的對象進行特殊的加工,其配置方式如下 -->
<objectWrapperFactory type="com.mingshashan.mybatis.learn.mybatis.CustomMapWrapperFactory"/>
<reflectorFactory type="com.mingshashan.mybatis.learn.mybatis.CustomReflectorFactory"/>
<environments default="dev">
<environment id="dev">
<!-- 配置事務管理器的類型 -->
<transactionManager type="JDBC"/>
<!-- 配置數據源的類型,以及數據庫連接的相關信息 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.75.138:3306/dev?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="000000"/>
</dataSource>
</environment>
<environment id="test">
<!-- 配置事務管理器的類型 -->
<transactionManager type="JDBC"/>
<!-- 如果environment id爲test則使用JNDI -->
<dataSource type="JNDI">
<property name="data_source" value="jdbc/mybatis-jndi"/>
<property name="initial_context" value="java:/comp/env"/>
</dataSource>
</environment>
</environments>
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle"/>
<property name="Mysql" value="mysql"/>
</databaseIdProvider>
<!-- 配置映射配置文件的位置 -->
<mappers>
<mapper resource="mapper/CustomerMapper.xml"/>
<mapper resource="mapper/OrderItemMapper.xml"/>
<mapper resource="mapper/OrderMapper.xml"/>
<mapper resource="mapper/ProductMapper.xml"/>
<mapper resource="mapper/TagMapper.xml"/>
</mappers>
</configuration>
下面是mapper配置,即xml映射文件的配置,我們只看其中的CustomerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mingshashan.mybatis.learn.dao.mapper.CustomerMapper">
<!--
cache- 配置本定命名空間的緩存。
type- cache實現類,默認爲PERPETUAL,可以使用自定義的cache實現類(別名或完整類名皆可)
eviction- 回收算法,默認爲LRU,可選的算法有:
LRU– 最近最少使用的:移除最長時間不被使用的對象。
FIFO– 先進先出:按對象進入緩存的順序來移除它們。
SOFT– 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。
WEAK– 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。
flushInterval- 刷新間隔,默認爲1個小時,單位毫秒
size- 緩存大小,默認大小1024,單位爲引用數
readOnly- 只讀
-->
<cache type="com.mingshashan.mybatis.learn.mybatis.CustomCache"
size="2048" flushInterval="60000">
<property name="cacheFile" value="./cache.tmp"/>
</cache>
<!--
cache-ref–從其他命名空間引用緩存配置。
如果你不想定義自己的cache,可以使用cache-ref引用別的cache。因爲每個cache都以namespace爲id,所以cache-ref只需要配置一個namespace屬性就可以了。需要注意的是,如果cache-ref和cache都配置了,以cache爲準。
-->
<!-- <cache-ref namespace="com.someone.application.data.SomeMapper"/> -->
<resultMap id="CustomerMap" type="com.mingshashan.mybatis.learn.entity.CustomerEntity">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
<result property="gender" column="gender"></result>
<result property="phone" column="phone"></result>
<result property="addressList" column="address_info"
typeHandler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"
javaType="java.util.List"
jdbcType="VARCHAR"
></result>
<result property="gmtCreated" column="gmt_created"></result>
<result property="gmtModified" column="gmt_modified"></result>
<result property="isValid" column="is_valid"></result>
<collection property="tagEntityList" ofType="TagEntity">
<id property="id" column="id"/>
<result property="name" column="name"/>
</collection>
</resultMap>
<sql id="customer_column">
id, name, gender, phone, address_info, gmt_created, gmt_modified
</sql>
<insert id="saveCustomer" parameterType="CustomerEntity">
INSERT INTO T_ARG_CUSTOMER(
<include refid="customer_column"></include>
)
values(#{id}, #{name}, #{gender}, #{phone}, #{addressList}, #{gmtCreated}, #{gmtModified})
</insert>
<update id="updateCustomer" parameterType="CustomerEntity">
update T_ARG_CUSTOMER set
name = #{name},
gender = #{gender},
phone = #{phone},
address_info = #{addressList},
gmt_modified = #{gmtModified}
from t_arg_customer
<where>
ID = #{id}
</where>
</update>
<update id="deleteCustomerById">
update T_ARG_CUSTOMER set is_valid = '0'
<where>
id = #{id}
</where>
</update>
<select id="findById" resultType="CustomerEntity">
select
<include refid="customer_column"></include>
from T_ARG_CUSTOMER
<where>
id = #{id}
</where>
</select>
</mapper>
Mybatis的啓動加載
整個過程的時序圖大概如下 先是XMLConfigBuilder 再是XMLMapperBuilder 然後結合着代碼來分析
1.SqlSessionFactoryBuilder
接下來看一下mybatis的啓動加載過程,從mybatis的官方文檔看mybatis的啓動加載入口代碼示例:
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
可以知道,其入口就是new SqlSessionFactoryBuilder().build(inputStream)
。接下來我們來分析一下其啓動加載過程。 代碼如下:
public class SqlSessionFactoryBuilder {
// 方法入口
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 構造XMLConfigBuilder,將解析邏輯交給XMLConfigBuilder去處理
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 執行解析邏輯。解析完後構造DefaultSqlSessionFactory
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
}
2.XMLConfigBuilder
而parse
方法會調用parseConfiguration
方法解析mybatis的configuration配置
public class XMLConfigBuilder extends BaseBuilder {
// 解析配置,即將mybatis-config.xml的xml配置轉換爲對應的mybatis模型配置
private void parseConfiguration(XNode root) {
try {
// properties標籤
propertiesElement(root.evalNode("properties"));
// settings標籤
Properties settings = settingsAsProperties(root.evalNode("settings"));
// vfs配置
loadCustomVfs(settings);
// 日誌配置
loadCustomLogImpl(settings);
// 別名配置
typeAliasesElement(root.evalNode("typeAliases"));
// 插件配置
pluginElement(root.evalNode("plugins"));
// 自定義的objectFactory配置
objectFactoryElement(root.evalNode("objectFactory"));
// 自定義的objectWrapperFactory配置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 自定義的reflectorFactory配置
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 其他的settings配置
settingsElement(settings);
// 在objectFactory and objectWrapperFactory初始化後,配置environments
environmentsElement(root.evalNode("environments"));
// databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// typeHandlers
typeHandlerElement(root.evalNode("typeHandlers"));
// 所有的mappers
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
}
3.XmlMapperBuilder
可以看到mapperElement會調用XmlMapperBuilder的parse方法去解析處理,其主要代碼如下:
public class XMLMapperBuilder extends BaseBuilder {
public void parse() {
//先從loadedResources判斷是不是已經加載過
if (!configuration.isResourceLoaded(resource)) {
// 如果沒有加載過,則去處理mapper標籤下面的元素
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps(); // 解析待定ResultMaps
parsePendingCacheRefs(); // 解析待定ChacheRefs
parsePendingStatements(); // 解析待定Statements
}
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 用到了MapperBuilderAssistant
builderAssistant.setCurrentNamespace(namespace);
// 處理cache-ref
cacheRefElement(context.evalNode("cache-ref"));
// 處理cache
cacheElement(context.evalNode("cache"));
// 處理parameterMap
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 處理resultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 處理sql
sqlElement(context.evalNodes("/mapper/sql"));
// "select|insert|update|delete" 這些statement的處理
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
關於上面的
parsePendingResultMaps(); // 解析待定ResultMaps
parsePendingCacheRefs(); // 解析待定ChacheRefs
parsePendingStatements(); // 解析待定Statements
可以參考:could not find a parent resultmap with id XMLMapperBuilder.parse() -> 存在resultMap繼承,父resultMap還未加載的,拋出Could not find a parent resultmap with id xx, 被上層XMLMapperBuilder.resultMapElement()方法捕獲, 存儲到 this.configuration.addIncompleteResultMap()即 configuration.IncompleteResultMap 列表內,屬於不完全加載, 每一次parse循環會執行 this.parsePendingResultMaps() 檢驗IncompleteResultMap的父(extends) resultMap是否被加載進來,如果上一次parse加載了,這次就可以處理IncompleteResultMap進行完全加載. 由於每次都要循環檢驗,畢竟有消耗,因此爲避免這種消耗,是否可以讓父resultMap優先加載呢,這樣後面就不會存在IncompleteResultMap , 需要把父mapper.xml放目錄結構父層級 或者 以AxxMapper.xml開頭排同層級之前(注意:這並不能解決後面checkDaoConfig報錯問題)
4.代碼調試過程
SqlSessionFactory的build方法爲mybatis配置加載的入口 然後XMLConfigBuilder的parseConfiguration解析mybatis-config.xml的配置 接着調用XMLMapperBuilder的parse方法解析所有的mapper.xml文件 其parse方法執行如下: 之後調用XMLStatementBuilder的parseStatementNode方法 XMLStatementBuilder會交給MapperBuilderAssistant的addMappedStatement方法由MappedStatement的builder模式構造出MappedStatement 再將構造出的MappedStatement放到Configuration的mappedStatements內部map容器中
5.Mybatis配置的實例化對象Configuration
整個mybatis的配置會保存在org.apache.ibatis.session.Configuration
對象(一個全局的實例) 而可以從SqlSessionFactory接口獲取到Configuration,其關係圖如下:
DefaultSqlSessionFactory會持有Configuration對象
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
}
Configuration的結構如下圖所示:
通過上面的配置信息,代碼調試及代碼執行流程,可以看到mybatis整個啓動加載過程會解析mybatis配置裏面的所有信息。
項目工程地址:learn-mybatis