這一遍看Mybatis的原因是懷念一下去年的 10月24號我寫自己第一個項目時使用全配置文件版本的MyBatis,那時我們三個人剛剛大二,說實話,當時還是覺得MyBatis挺難玩的,但是今年再看最新版的Mybatis3.5.0, 還是挺有感覺的 Mybatis的官網一級棒...
Mybatis的核心組件及其生命周期
SqlSessionFactoryBuider:
作用: 構建器,根據配置信息生成SqlSessionFactory
生命周期: 這個類可以被實例化、使用和丟棄,一旦創建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 實例的最佳作用域是方法作用域(也就是局部方法變量)。 你可以重用 SqlSessionFactoryBuilder 來創建多個 SqlSessionFactory 實例,但是最好還是不要讓其一直存在,以保證所有的 XML 解析資源可以被釋放給更重要的事情。
SqlSessionFactory
作用: 生成sqlSession
生命周期: SqlSessionFactory 一旦被創建就應該在應用的運行期間一直存在,沒有任何理由丟棄它或重新創建另一個實例。 使用 SqlSessionFactory 的最佳實踐是在應用運行期間不要重復創建多次,多次重建 SqlSessionFactory 被視為一種代碼“bad smell”。因此 SqlSessionFactory 的最佳作用域是應用作用域。 有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式
SqlSession
作用: 它表示一次sql的會話,即可以去執行sql返回結果,也可以獲取為mapper生成的代理對象 ,支持事物,通過commit、rollback方法提交或者回滾事物
生命周期: 每個線程都應該有它自己的 SqlSession 實例。SqlSession 的實例不是線程安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。 絕對不能將 SqlSession 實例的引用放在一個類的靜態域,甚至一個類的實例變量也不行。 也絕不能將 SqlSession 實例的引用放在任何類型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你現在正在使用一種 Web 框架,要考慮 SqlSession 放在一個和 HTTP 請求對象相似的作用域中。 換句話說,每次收到的 HTTP 請求,就可以打開一個 SqlSession,返回一個響應,就關閉它。 這個關閉操作是很重要的,你應該把這個關閉操作放到 finally 塊中以確保每次都能執行關閉。
SqlMapper
作用: : MyBatis的映射器,現在大多數都使用java接口,早前使用配置文件來描述sql查詢結果和java對象之間的映射規則 定義參數類型, 描述緩存,描述SQL語句 ,定義查詢結果和POJO的映射關系
生命周期: 最好把映射器放在方法作用域內
基於XML版本的環境搭建測試
基於xml版本,搭建mybatis開發環境中,存在一個主配置文件,和多個子配置文件,主配置文件中配置數據庫相關的信息, 而子配置文件中配置的是單個Dao接口層中的抽象方法對應的sql語句
主配置文件如下
需要注意的地方,下面的 <mapper >
標簽中resource
屬性存放的是從配置文件的路徑,但是從配置文件的目錄信息得和src中相應的接口位於相同的目錄
<?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">
<!--mybatis的主配置文件-->
<configuration>
<!--配置環境-->
<environments default="mysql">
<!--配置mysql的環境-->
<environment id="mysql">
<!--配置事務的類型-->
<transactionManager type="JDBC"/>
<!--配置數據源-->
<!--dataSource存在三個, 其中的POOLED池化的連接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/trymybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!--指定映射配置文件的位置,也就是針對每個Dao的配置文件的位置-->
<!--下面指定的xml配置文件的路徑,需要和src下IUserDao接口的目錄保持一致-->
<mappers>
<mapper resource="com/changwu/dao/IUserDao.xml"/>
</mappers>
</configuration>
從配置文件
需要注意的地方: 命名空間是全類名,id是方法名,返回值是全類名
還有一點就是,單個mapper標簽中,namespace和id都不能少,兩者合在一起才能確定一個全局唯一的方法,至於為什么我們配置一個接口就ok,而不用添加配置文件,那是因為Mybatis會使用代理技術,為接口生成代理對象,讓程序員使用代理對象
<?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">
<!--namespace是全類名-->
<mapper namespace="com.changwu.dao.IUserDao">
<!--
id為方法名
resultType為返回值個體的封裝類
-->
<select id="findAll" resultType="com.changwu.pojo.User">
select * from user
</select>
</mapper>
其他數據源的配置方式
把如下的配置放在resourse目錄下面
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/trymybatis
jdbc.username=root
jdbc.password=root
然后將主配置文件改成下面這樣
當然也可以在<properties>
標簽中使用url
但是需要遵循url協議的規范
<?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">
<!--mybatis的主配置文件-->
<configuration>
<properties resource="jdbcConfig.properties"> </properties>
<!--配置環境-->
<environments default="mysql">
<!--配置mysql的環境-->
<environment id="mysql">
<!--配置事務的類型-->
<transactionManager type="JDBC"/>
<!--配置數據源-->
<!--dataSource存在三個, 其中的POOLED池化的連接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--指定映射配置文件的位置,也就是針對每個Dao的配置文件的位置-->
<!--下面指定的xml配置文件的路徑,需要和src下IUserDao接口的目錄保持一致-->
<mappers>
<mapper class="com.changwu.dao.IUserDao"/>
</mappers>
</configuration>
編碼測試類
@Test
public void text01() {
try {
// 1. 讀取配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 創建SqlSessionFactory工廠
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3. 創建sqlSession
SqlSession sqlSession = factory.openSession();
// 4. SqlSession 完全包含了面向數據庫執行 SQL 命令所需的所有方法
// 使用正確描述每個語句的參數和返回值的接口(比如 BlogMapper.class),
// 你現在不僅可以執行更清晰和類型安全的代碼,而且還不用擔心易錯的字符串字面值以及強制類型轉換
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 5. 執行方法
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user.getUsername());
}
// 6, 釋放資源
sqlSession.close();
resourceAsStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
基於注解版本的環境搭建測試
因為現在依然是MyBatis孤軍深入,沒有和Spring,Springboot等框架進行整合,因此上面說的那個主配置文件無論如果都不能缺失,不像SpringBoot那樣一個@MapperScan("XXX")
就完成掃描整合這么給力
基於注解的開發模式,我們可以輕易進一步去除單個dao層的接口對應的xml配置文件,取代之的是注解,三步:
- 第一步: 刪除原來的子配置文件的目錄
- 第二步: 在dao層接口使用注解開發
@Select("select * from user")
List<User> findAll();
- 第三步: 修改部分主配置文件
<mappers>
<mapper class="com.changwu.dao.IUserDao"/>
</mappers>
常用的注解
- @Results
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Results {
// 因為當前的Results注解中存在實例的描述,使用id標識當前的map,實現給 @resultMap 的復用
String id() default "";
Result[] value() default {};
}
- @Result
繼續看看這個@Result
注解,如下: 這個注解擁有xml中的resultMap
中大部分的屬性
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Result {
boolean id() default false;
// 表中的列名
String column() default "";
// java實體類中的屬性名
String property() default "";
// 實體類型
Class<?> javaType() default void.class;
JdbcType jdbcType() default JdbcType.UNDEFINED;
Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;
// 實體之間的關系為1對1
One one() default @One;
// 實體之間的關系為1對多
Many many() default @Many;
}
- @One
跟進@One
注解, 他是對select
屬性的封裝, FetchType
是一個枚舉,三種值,分別是LAZY, EAGER, DEFAULT
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface One {
String select() default "";
FetchType fetchType() default FetchType.DEFAULT;
}
- @Many
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Many {
String select() default "";
FetchType fetchType() default FetchType.DEFAULT;
}
- @ResultMap
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ResultMap {
String[] value();
}
類型別名
原來在xml版本配置mapper時,會使用parameterType
屬性指定程序員提供的類的全類名,但是這個全類名真的太長了,於是MyBatis官方提供了給全類名取別名的標簽,在Mybatis的主配置文件中添加如下的配置,如下:
<typeAliases>
<typeAlias alias="user" type="com.changwu.pojo.User"/>
</typeAliases>
添加了全類名的配置之后,我們的在mapper中就可以使用別名了,如下: 並且在windows系統下不區分大小寫
<update id="updateUser" parameterType="UsEr">
update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}"
</update>
但是像上面這樣,為每一個POJO都添加上別名的配置,確實顯得有點麻煩,於是可以像下面這樣,為一整個包下面的類名配置別名, 別名就是類名不區分大小寫的格式
<typeAliases>
<package alias="user" type="com.changwu.pojo"/>
</typeAliases>
公共sql的抽取
一處抽取,多處使用
<sql id="findUsers">
select * form user;
</sql>
<select id="findAll" resultType="com.changwu.pojo.User">
<include refid="findUsers"></include>
</select>
優先級
MyBatis支持的3種配置方式, 編碼>properties配置文件>property元素標簽, 優先級如下:
- 在properties配置文件中指定的配置文件的屬性首先被讀取
- 根據配置文件中的resources屬性讀取類路徑下的屬性文件,或者根據url屬性讀取屬性文件並會覆蓋同名屬性
- 讀取作為方法參數傳遞的屬性,並覆蓋已經讀取的同名屬性
不要混合使用xml版和注解版兩種開發模式,否則Mybatis啟動不了
MyBytais中的參數類型的封裝
在使用注解版做開發時,我們會在每個mapper中標記好入參的類型
簡單類型
MyBattis的參數傳遞是支持簡單類型的,比如下面這種
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id = #{id}
</delete>
傳遞pojo對象
看下面的代碼和配置, 在編寫sql時,我們直接指定參數的類型的Pojo對象
@Update({"update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}"})
void updateUser(User user);
還有這種配置
<update id="updateUser" parameterType="com.changwu.pojo.User">
update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}"
</update>
那么,myBatis如何解析我們傳遞的pojo對象呢? 答案是使用ojnl(Object graphic navigation Language) 對象圖導航語言, 實際是底層是通過對象的方法來獲取數據,但是在寫法上卻省略了getXXX
比如: 我們想獲取username, 按理說這樣寫user.getUserName()
但是在ojnl表達式來說表達成 user.username
, 於是我們就可以在sql中直接寫上pojo中字段的屬性名,MyBatis會自動完成從對象中,取值解析
注意點就是說,得sql中屬性的順序和pojo中屬性的生命順序保持一致,否則存進去的就是亂序的數值
傳遞pojo包裝后的對象
開發中可能會有各種各樣的查詢條件,其中,很多時候用來查詢的條件不是簡單的數據類型,而是一類對象, 舉個例子: 如下
根據另一個封裝類去查詢用戶列表,其中的QueryVo
並不是持久化在數據庫中的對象,而是某幾個字段封裝類,於是我們像下面這樣傳遞值
@Select("select * from user where username = #{user.username}")
List<User> findUserByQueryVo(QueryVo vo);
xml版本
<select id="findByQueryVo" paramterType="com.changwu.vo.QueryVo" resultType="com.changwo.pojo.User">
select * from user where username like #{user.username}
</select>
注意點: 傳遞pojo的包裝類是有限制的, 在下面取值時,強制程序員不能把名字寫錯
user == vo對象中的屬性名user
username == vo對象中的屬性user中的屬性名username
MyBytais中的結果類型的封裝
基於XML的resultMap
前面的實驗中,我們的pojo字段名和數據表中列表保持百分百一致,所以我們在resultType
標簽中使用com.changwu.pojo.User
接收返回的數據時才沒出任何差錯,但是一般在現實的開發中,同時使用數據庫的列名的命名風格和java的駝峰命名法,然而,當我們的pojo的屬性名個sql中的列表不一致時, Mybatis是不能完成兩者的賦值的
- 解決方法1: 取別名
注解版本的,默認支持駝峰命名法,意思和忽略大小寫擦不多,但是如果兩者名字忽略大小寫之后還不一樣就真的得配置取別名了
@Select("select * from user")
@Select("select id as userId from user")
<select id="findAll" resultType="com.changwu.pojo.User">
select id as userId from user;
</select>
- 解決方法2: 使用配置
resultMap
如下:- id為當前resultMap的唯一身份標識
- type表示查詢的實體類是哪個實體類
- property為java中的實體類屬性名
- column為數據庫中列名
- 在
select
標簽中去除掉原來的resultType
,取而代之的是resultMap
<resultMap id="userMap" type="com.changwu.pojo.User">
<id property="userId" column="id"></id>
<result property="userName" column="username"></result>
</resultMap>
<select id="findAll" resultMap="userMap">
select id as userId from user;
</select>
解決方法3: 開啟駝峰命名配置
<?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>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="useGeneratedKeys" value="true"/>
</settings>
</configuration>
但是如果兩個字段的差異已經不是駝峰命名法可以解決的了,就只能去配置別名了
基於注解實現resultMap
當實體類中的屬性和表中的字段命名出現嚴重不一致時,我們使用通過注解解決此類問題
同樣property
中是java對象中的屬性, column
為表中的列名
通過@Results
中的id屬性值,使其他方法可以通過@ResultMap
復用已經存在的映射關系
@Select("select * from user")
@Results(id = "userMap",value = {
@Result(id = true,property = "",column = ""),
@Result(id = true,property = "",column = ""),
@Result(id = true,property = "",column = ""),
@Result(id = true,property = "",column = ""),
})
List<User> findAll();
@Select("select * from user where id = #{id}")
@ResultMap(value = {"userMap"})
User findById(Integer id);
MyBatis的數據連接池
如何配置
在如下Mybatis主配置文件中的 <dataSource type="POOLED">
<?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">
<!--mybatis的主配置文件-->
<configuration>
<properties resource="jdbcConfig.properties"> </properties>
<!--配置環境-->
<environments default="mysql">
<!--配置mysql的環境-->
<environment id="mysql">
<!--配置事務的類型-->
<transactionManager type="JDBC"/>
<!--配置數據源-->
<!--dataSource存在三個, 其中的POOLED池化的連接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--指定映射配置文件的位置,也就是針對每個Dao的配置文件的位置-->
<!--下面指定的xml配置文件的路徑,需要和src下IUserDao接口的目錄保持一致-->
<mappers>
<mapper class="com.changwu.dao.IUserDao"/>
</mappers>
</configuration>
POOLED
采用傳統的javax.sql.DataSource規范中的連接池,這種數據源的實現利用“池”的概念將 JDBC 連接對象組織起來,避免了創建新的連接實例時所必需的初始化和認證時間。 這是一種使得並發 Web 應用快速響應請求的流行處理方式。
特點: 使用完了的連接會被回收,而不是被銷毀
其他相應的屬性
- poolMaximumActiveConnections : 任意時間正在使用的連接數量,默認為10
- poolMaximumIdleConnections : 任意事件可能存在的空閑連接數
- poolMaximumCheckoutTime : 在被強制返回之前,池中連接被檢出(checked out)時間,默認值:20000 毫秒(即 20 秒)
- poolTimeToWait: 默認是20秒, 如果花費了20秒還沒有獲取到連接,就打印日志然后重新嘗試獲取
- poolMaximumLocalBadConnectionTolerance : 就是如果當前的線程從連接池中獲取到了一個壞的連接,數據源會允許他重新獲取一次,但是重新嘗試的次數不能超過 poolMaximumIdleConnections 與 poolMaximumLocalBadConnectionTolerance 之和。 默認值:3 (新增於 3.4.5)
- poolPingQuery: 用來檢驗連接是否正常工作並准備接受請求。默認是“NO PING QUERY SET”,這會導致多數數據庫驅動失敗時帶有一個恰當的錯誤消息
- poolPingEnabled: 是否啟用偵測查詢。若開啟,需要設置 poolPingQuery 屬性為一個可執行的 SQL 語句(最好是一個速度非常快的 SQL 語句),默認值:false。
- poolPingConnectionsNotUsedFor : 配置 poolPingQuery 的頻率。可以被設置為和數據庫連接超時時間一樣,來避免不必要的偵測,默認值:0(即所有連接每一時刻都被偵測 — 當然僅當 poolPingEnabled 為 true 時適用)。
它的實現類是PooledDataSource
, 看下它的繼承體系圖如下,它實現javax.sql的接口規范
我們看下它的獲取連接的實現代碼如下: 可以看到,從他里面獲取新的連接,不是無腦new, 而是受到最大連接數,空閑連接數,當前活躍數,工作連接數等因素的限制
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
while (conn == null) {
// 通過同步代碼塊保證了線程的安全性,因為現實環境中,多用戶並發請求獲取連接
synchronized (state) {
// 如果空閑的連接數不為空,就使用從空閑池中往外拿連接
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// 沒有空閑
// Pool does not have available connection
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// 活動的連接池的最大數量 比 預先設置的最大連接數小, 就創建新的連接
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// 判斷最先進入 活躍池中的連接,設置新的參數然后返回出去
// Cannot create new connection
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
log.debug("Bad connection. Could not roll back");
}
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
// ping to server and check the connection is valid or not
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
return conn;
}
UNPOOLED
這個數據源的實現只是每次被請求時打開和關閉連接。雖然有點慢,但對於在數據庫連接可用性方面沒有太高要求的簡單應用程序來說,是一個很好的選擇。 不同的數據庫在性能方面的表現也是不一樣的,對於某些數據庫來說,使用連接池並不重要,這個配置就很適合這種情形。UNPOOLED 類型的數據源具有以下屬性。
它存在如下的配置信息
- driver – 這是 JDBC 驅動的 Java 類的完全限定名(並不是 JDBC 驅動中可能包含的數據源類)。
- url – 這是數據庫的 JDBC URL 地址。
- username – 登錄數據庫的用戶名。
- password – 登錄數據庫的密碼。
- defaultTransactionIsolationLevel – 默認的連接事務隔離級別。
- defaultNetworkTimeout – The default network timeout value in milliseconds to wait for the database operation to complete. See the API documentation of java.sql.Connection#setNetworkTimeout() for details.
作為可選項,你也可以傳遞屬性給數據庫驅動。只需在屬性名加上“driver.”前綴即可,例如:
driver.encoding=UTF8
這將通過 DriverManager.getConnection(url,driverProperties) 方法傳遞值為 UTF8 的 encoding 屬性給數據庫驅動。
它的實現類是UnpooledDataSource
, 看下它的繼承體系圖如下,它實現javax.sql的接口規范
我們看下它對獲取連接的實現代碼如下: 每一次獲取連接都使用jdk底層的加載驅動,創建新的連接給用戶使用
private Connection doGetConnection(Properties properties) throws SQLException {
initializeDriver();
Connection connection = DriverManager.getConnection(url, properties);
configureConnection(connection);
return connection;
}
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
if (driverClassLoader != null) {
driverType = Class.forName(driver, true, driverClassLoader);
} else {
driverType = Resources.classForName(driver);
}
// DriverManager requires the driver to be loaded via the system ClassLoader.
// http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
Driver driverInstance = (Driver)driverType.newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
}
JNDI
作為了解吧,這個數據源的實現是為了能在如 EJB 或應用服務器這類容器中使用,容器可以集中或在外部配置數據源,然后放置一個 JNDI 上下文的引用.
MyBatis中的事務管理器
MyBatis中存在兩種事務管理器如下:
JDBC
xml配置
<transactionManager type="JDBC">
<property name="closeConnection" value="false"/>
</transactionManager>
這個配置就是直接使用了 JDBC 的提交和回滾設置,它依賴於從數據源得到的連接來管理事務作用域。
相關編碼的實現: 它通過sqlSession對象的commit方法,和rollback方法實現事務的提交和回滾
設置自動提交,使用openSession()
重載的方法
// 1. 讀取配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2. 創建SqlSessionFactory工廠
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3. 創建sqlSession
SqlSession sqlSession = factory.openSession(true);
MANAGED
這個配置幾乎沒做什么。它從來不提交或回滾一個連接,而是讓容器來管理事務的整個生命周期,默認情況下它會關閉連接,然而一些容器並不希望這樣,因此需要將 closeConnection 屬性設置為 false 來阻止它默認的關閉行為
<transactionManager type="MANAGED">
<property name="closeConnection" value="false"/>
</transactionManager>
如果你正在使用 Spring + MyBatis,則沒有必要配置事務管理器, 因為 Spring 模塊會使用自帶的管理器來覆蓋前面的配置
動態SQL
MyBatis的動態sql為我們提供了什么功能呢? 舉一個相似的場景,就是用戶提交了username password 兩個字段的信息到后端, 后端進行下一步校驗,然后后端的程序員可能就的通過自己拼接sql來完成這個功能 類似這樣select * from user where username = + username + and password = +password
,一個兩個沒事, 這是一個痛苦的事, 例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號
動態sql解決了這個問題
雖然在以前使用動態 SQL 並非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射語句中的強大的動態 SQL 語言得以改進這種情形。
if
最常用的一種是,將對象中滿足if條件的字段當成sql中where的條件
舉個例子,當用戶名不為空時,按照用戶名查找
<select id="findUserByCondition" resultType="com.changwu.pojo.User" parameterType="com.changwu.pojo.User">
select * from user where 1=1
<if test="userName != null">
and username = #{userName}
</if>
</select>
choose (when, otherwise)
choose 類似java中的switch case 語句,像下面這樣, 命中了某一種情況后不再匹配其他的情況,都沒有命中的話執行默認的代碼塊
Integer i =1;
switch (i) {
case 1:
//do
break;
case 2:
//do
break;
default:
//do
}
示例: 從user表中檢索,當userName不為空時,僅僅使用userName當成條件去匹配, 如果userName為空,則檢查第二個條件是否滿足,如果第二個條件滿足了,則用第二個條件當成結果拼接到sql中,所有條件都沒有就拼接 <otherwise>
標簽中的語句
<select id="findUserByConditionLike"
resultType="com.changwu.pojo.User"
parameterType="com.changwu.vo.QueryVo">
select * from user
<where>
<choose>
<when test="userName != null">
and username like #{userName}
</when>
<when test="user != null and user.sex != null">
and sex = #{user.sex}
</when>
<otherwise>
and count = 1
</otherwise>
</choose>
</where>
</select>
trim (where, set)
神奇的 AND title like #{title} , 看下面的例子
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果所有的if條件全都不成立,那么最終拼接的sql是這樣的
SELECT * FROM BLOG WHERE
如果第一個條件不成立,而第二個條件成立,拼接成的sql是這樣的
SELECT * FROM BLOG WHERE AND title like #{title}
以上的兩個結果都將導致java程序運行失敗,Mybatis推出<where>
標簽解決了這個問題,像下面這樣
被
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
foreach
forEach的功能非常強大!它允許程序員指定一個集合,然后通過foreach標簽遍歷這個集合從而完成in語句的拼接
注意點1: collection 代表將要遍歷的集合,下面我給他取名ids, 這個名字不是亂取的,對應着我的"com.changwu.vo.QueryVo" 這個vo中的一個屬性名
注意點2:#{id}里面的名和item保持一致
<select id="selectUserInIds" resultType="com.changwu.pojo.User" parameterType="com.changwu.vo.QueryVo">
select * from user where id in
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</select>
玩轉MyBatis多表關系
xml版: 一對一級聯
實驗場景: 比如說學生和學號是百分百的一對一的關系, 但是我直接用一個不太恰當的例子, 就是強制規定 用戶和賬戶的是一對一的關系,即一個用戶只能存在一個賬戶(這個例子很牽強...)
這時,我們想實現這樣的級聯效果: 在查詢賬戶的同時,級聯查詢出這個賬戶所屬的用戶信息
第一點: 就是數據庫中的表怎么設計,在賬戶表中添加一列當成外鍵,關聯用戶表中的用戶id
第二點: 我們想在查詢賬戶的同時級聯查詢出用戶的信息,說白了就是讓Mybatis幫我們將屬性封裝進我們使用resultType
標簽指定的返回值類型的對象中,於是我們就得像下面這樣寫 賬戶類,在賬戶類中添加用戶信息引用
public class Account {
private Integer id;
private Integer uid;
private Integer money;
private User user;
}
第三點: sql語句怎么寫?
關注點就是下面的resultMap
標簽中的<association>
標簽, 通過這個標簽告訴Mybatis如何將查詢出來的結果封裝進Account
中的User
字段
可以看到我在association
標簽中將user所有的屬性全都配置進去了,其實這是沒必要的,因為我的sql語句並沒有返回全部的結果
association
中存在一個 column
屬性,這個屬性存放就是在account
表中的外鍵的列名 , javaType
表示是告訴MyBatis,這個封裝類的類型
<resultMap id="accountUserMap" type="com.changwu.pojo.Account">
<id property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
<association property="user" column="uid" `javaType`="com.changwu.pojo.User">
<id property="id" column="id"/>
<result property="userName" column="userName"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
</association>
</resultMap>
<!--查詢所有的賬戶,同時包含用戶名和地址信息-->
<select id="findAllAccount" resultMap="accountUserMap" >
select a.*,u.username,u.address from account a,user u where a.uid=u.id
</select>
注解版: 一對一級聯
配置查詢賬戶時,級聯查詢出賬戶所屬的用戶,如果說,賬戶實體和數據庫中表的字段命名不同,需要用到下面的 @Result()
注解進行糾正,當然雖然我下面寫了四個@Result
,除了第一個配置id,中間兩個的property
和column
值是一樣的,所以根本沒有寫的必要
有必要的是一對一的關系需要使用 @Result()
配置,同樣column
為Account表中的關聯user表中的外鍵列名,強制不能寫錯,具體是一對一,但是一對多的關系通過one 和 manay控制, 通過fetchType控制是及早求值還是懶加載
@Select("select * from account")
@Results(id = "account",value = {
//先描述自己的信息,然后描述一對一的信息
@Result(id = true, property = "id",column = "id"),
@Result(property = "uid",column = "uid"),
@Result(property = "money",column = "money"),
@Result(property = "user",column = "uid",
one=@One(select = "com.changwu.dao.IUserDao.findById",fetchType = FetchType.EAGER))
})
List<Account> findAll();
XML版: 一對多級聯
實驗場景: 查詢用戶的信息的同時級聯查詢出用戶所有的賬戶的信息
在java中上面的描述轉換成代碼的意思是, User類中要添加一個集合,存放查詢出來的賬戶的信息, 我們進一步通過配置告訴Mybatis將查詢出的屬性封裝進這個list中
public class User {
private Integer id;
private String userName;
private Date birthday;
private String sex;
private String address;
// 在主表中唯一從表的集合
private List<Account> accounts;
像下面這樣配置
注意點就是在一對多的配置中我們使用collection
標簽,接着使用屬性ofType
標識 一的一方維護的集合中元素的類型
像這種property
類型的屬性全都是java類中的屬性名,寫錯了MyBatis會報錯
column
屬性: 按理說是數據庫中列名,如果不一樣的話,不至於報錯,但是數據一定封裝不上,但是有時候 也可能是在sql語句中為原列名取的別名的名稱
<!-- todo 一對多的配置 -->
<resultMap id="findAllUserAndUserAccount" type="com.changwu.pojo.User">
<id property="id" column="id"/>
<result property="userName" column="userName"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!--一對多的配置-->
<!--ofType是一的一方維護的集合中元素的類型-->
<collection property="accounts" ofType="com.changwu.pojo.Account" >
<id property="id" column="aid"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
</collection>
</resultMap>
<select id="findAllUserAndUserAccount" resultMap="findAllUserAndUserAccount">
select * from user u left outer join account a on u.id=a.uid
</select>
注解版: 一對多級聯
和1對1的配置很像
@Select("select * from user")
@Results(id="UserAccount",value = {
@Result(id = true ,property = "id",column = "id"),
@Result(property = "accounts",column = "id",
many = @Many(select = "com.changwu.dao.IAccountDao.findAccountByUid",fetchType = FetchType.LAZY))
})
List<User> findAll();
多對多級聯
實驗場景: 典型的用戶和角色之間的關系
多對多的配置其實和一對多一樣, 比如想在查詢出用戶信息時級聯查詢出這個用戶擁有的角色的信息
於是第一步:
我們在User類中添加屬性 private List<Role> roles;
第二步: 寫xml中的sql mapper配置
下面這個column
屬性配置的rid
其實就是在使用我們sql中為數據庫中的某列取的別名
如果查詢的結果中出現了兩個相同的列名,但是值不同,代表的意義也不同,最好就給其中一個取別名
<!--todo 多對多 role user-->
<resultMap id="roleUserMap" type="com.changwu.pojo.Role">
<!--todo 這里的colum就是不原生的數據庫的列名,而是取的別名-->
<id property="id" column="rid"/>
<result property="roleName" column="role_name"/>
<result property="roleDesc" column="role_desc"/>
<collection property="users" ofType="com.changwu.pojo.User">
<id property="id" column="id"/>
<result property="userName" column="userName"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
</collection>
</resultMap>
<select id="findAll" resultMap="roleUserMap">
select u.*,r.ID as rid,r.ROLE_NAME,r.ROLE_DESC from role r
left join user_role ur on r.ID=ur.RID
left join user u on ur.UID = u.id
</select>
MyBatis的延遲加載
association
一對一的延遲加載
即用戶和賬戶的關系是一對一的關系,我們希望這樣,當用戶僅僅查詢賬戶信息時,Mybatis僅僅執行查詢賬戶信息的語句,但是當用戶使用這個賬戶關聯的對象時,再讓MyBatis將賬戶對象中的用戶對象的引用時,觸發懶加載,讓mybatis再去查詢數據庫
像下面這樣配置xml文件, 它其實是對原生的一對一的級聯查詢的升級,將association
標簽內部的通過result
的屬性描述全部去掉了,因為目標是懶加載,加上這些描述也用不到了
取而代之的是一個新星select
, 它指向了IUserDao
中的根據id查詢用戶的方法findUserById
還有一個注意點就是,association
中的column
屬性不能去掉,而且必須寫成數據庫中Account表中存放關聯User的外鍵的那個列名,通過它指定當觸發延遲加載時,使用哪個字段給findById()
方法使用
<!--todo 延遲加載-->
<resultMap id="findAllAccountLazy" type="com.changwu.pojo.Account">
<id property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
<!-- select指定的內容, 可以查詢出單個用戶的唯一方法標識 -->
<!-- 這里的column屬性,指定的是select中指定的fingById 所需要的id值-->
<association property="user" column="uid" javaType="com.changwu.pojo.User" select="com.changwu.dao.IUserDao.findById">
</association>
</resultMap>
<select id="findAll" resultType="int" resultMap="findAllAccountLazy">
select * from account
</select>
下面是User中findById的配置, sql中的#{}
中的內容是可以隨便寫的
<!--todo 疑問下面這id可不可以亂寫 -->
<select id="findById" parameterType="int" resultMap="UserMap">
select * from user where id = #{123id}
</select>
實驗成功的結果就是,當我們使用Account的fingAll方法時,如果不繼續getUser()
,結果控制台打印單條sql,一旦使用getUser()
,控制台會繼續打印多條新的sql
collection
實現一對多的延遲加載
一個用戶存在多個賬戶,我們希望如果僅僅是查詢用戶信息則延遲加載用戶賬戶的信息,使用用戶信息時,才再次執行新的sql加載用戶的信息
實現的思路和上面的相似, 注意collection
標簽中的column
的值,已經select
標簽中findAccountByUid
的實現
<!-- todo 一對多的配置 延遲加載 -->
<resultMap id="findAllUserAndUserAccount" type="com.changwu.pojo.User">
<id property="id" column="id"/>
<result property="userName" column="userName"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!--一對多的配置-->
<!--ofType是一的一方維護的集合中元素的類型-->
<collection property="accounts" ofType="com.changwu.pojo.Account"
column="id" select="com.changwu.dao.IAccountDao.findAccountByUid">
</collection>
</resultMap>
<select id="findAllUserAndUserAccount" resultMap="findAllUserAndUserAccount">
select * from user
</select>
緩存
什么是緩存?
緩存是指將查詢的數據暫存在內存中,當下次再次查詢時,直接從內存中獲取,實現減少和數據庫交互的次數,提高執行效率
適用於緩存的數據: 經常被查詢, 不經常被修改, 而且對此類數據的一致性沒有很嚴格的要求, 與之相反的數據比如,銀行的匯率,商品的庫存中數據一致性要求極其嚴格的數據就不適合使用緩存機制
Mybatis中的一級緩存
一級緩存是存在於MyBatis的SqlSession中的數據,當用戶執行一次查詢,查詢的結果就會被緩存在SqlSession中一份,當用戶再次查詢時,先檢查sqlSession中是否存在相應的數據,如果存在的話不再重新查詢數據庫,而是取出緩存中的數據給用戶
所以,當sqlSession對象消失時,一級緩存就不復存在
一級緩存是默認存在的
像下面這個測試,全程使用同一個sqlSession,那么獲取出來的user也會是同一個, 控制台僅僅輸入一條sql,打印結果也為true
@Test
public void testFirstCache(){
IUserDao userDao = this.sqlSession.getMapper(IUserDao.class);
User user1 = userDao.findUserByIdFirstCache(42);
User user2 = userDao.findUserByIdFirstCache(42);
System.out.println(user1==user2);
}
但是像下面這樣,一級緩存將會消失
public void testFirstCache(){
IUserDao userDao = this.sqlSession.getMapper(IUserDao.class);
User user1 = userDao.findUserByIdFirstCache(42);
this.sqlSession.close();
this.sqlSession = this.factory.openSession();
userDao = this.sqlSession.getMapper(IUserDao.class);
User user2 = userDao.findUserByIdFirstCache(42);
System.out.println(user1==user2);
}
同樣適用sqlSession的clearCache()
也可以實現緩存的清空
為了安全,MyBatis的一級緩存在sqlSession出現 修改,添加,刪除,commit(),close()等方法時,就會被清空
MyBatis中的二級緩存
二級緩存指的是MyBatis中的SqlSessionFactory對象的緩存,由同一個SqlSessionFactory對象創建的SqlSession會共享這塊二級緩存
使用: 在MyBatis主配置文件中開始支持緩存的配置,默認是開啟的狀態
<setting name="cacheEnabled" value="true"></setting>
在從配置文件中開啟緩存的配置
<!--為當前的mapper開啟二級緩存的支持-->
<cache/>
第三步: 在select
標簽中添加userCache
屬性
<!--測試一級緩存-->
<select id="findUserByIdFirstCache" parameterType="int" resultMap="UserMap" useCache="true">
select * from user where id = #{id}
</select>
測試:
按照上面的配置后,編寫下面的測試方法,測試二級緩存的存在就得關閉一級緩存,在下面的測試用例中同時開啟兩個sqlSession,第一個sqlSession查詢完結果后隨即關閉,接着開啟第二個sqlSession,獲取mapper繼續查詢,但是整個流程查詢的sql僅僅會執行一次,原因就是存在二級緩存, 為什么最后的輸出結果user!=user2呢? 因為屬於SqlSessionFactory的二級緩存,存放的並不是對象,而是鍵值對形式存在的數據,使用這部分緩存時,MyBatis會自動為我們創新的對象,然后將這部分數據封裝進去,返回這個新對象
@Test
public void testFirstCache(){
SqlSession sqlSession1 = this.factory.openSession();
IUserDao userDao = sqlSession1.getMapper(IUserDao.class);
User user1 = userDao.findUserByIdFirstCache(42);
System.out.println(user1);
sqlSession1.close();
SqlSession sqlSession2 = this.factory.openSession();
userDao = sqlSession2.getMapper(IUserDao.class);
User user2 = userDao.findUserByIdFirstCache(42);
System.out.println(user2);
sqlSession2.close();
System.out.println(user1==user2);
}
注解版開啟二級緩存
在我們的dao層上添加如下注解即可
@CacheNamespace(blocking = true)