您好,登錄后才能下訂單哦!
這篇文章主要介紹“Mybatis是什么及怎么使用”,在日常操作中,相信很多人在Mybatis是什么及怎么使用問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Mybatis是什么及怎么使用”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
JDBC(Java Data Base Connection,java數據庫連接)是一種用于執行SQL語句的Java API,可以為多種關系數據庫提供統一訪問,它由一組用Java語言編寫的類和接口組成.JDBC提供了一種基準,據此可以構建更高級的工具和接口,使數據庫開發人員能夠編寫數據庫應用程序
優點:運行期:快捷、高效
缺點:編輯期:代碼量大、繁瑣異常處理、不支持數據庫跨平臺
jdbc核心api
DriverManager 連接數據庫
Connection 連接數據庫的抽象
Statment 執行SQL
ResultSet 數據結果集
DBUtils是Java編程中的數據庫操作實用工具,小巧簡單實用。
DBUtils封裝了對JDBC的操作,簡化了JDBC操作,可以少寫代碼。
DBUtils三個核心功能介紹
QueryRunner中提供對sql語句操作的API
ResultSetHandler接口,用于定義select操作后,怎樣封裝結果集
DBUtils類,它就是一個工具類,定義了關閉資源與事務處理的方法
ORM 對象關系映射
object java對象
relational 關系型數據
mapping 映射
Hibernate 是由 Gavin King 于 2001 年創建的開放源代碼的對象關系框架。它強大且高效的構建具有關系對象持久性和查詢服務的 Java 應用程序。
Hibernate 將 Java 類映射到數據庫表中,從 Java 數據類型中映射到 SQL 數據類型中,并把開發人員從 95% 的公共數據持續性編程工作中解放出來。
Hibernate 是傳統 Java 對象和數據庫服務器之間的橋梁,用來處理基于 O/R 映射機制和模式的那些對象。
Hibernate 使用 XML 文件來處理映射 Java 類別到數據庫表格中,并且不用編寫任何代碼。
為在數據庫中直接儲存和檢索 Java 對象提供簡單的 APIs。
如果在數據庫中或任何其它表格中出現變化,那么僅需要改變 XML 文件屬性。
抽象不熟悉的 SQL 類型,并為我們提供工作中所熟悉的 Java 對象。
Hibernate 不需要應用程序服務器來操作。
操控你數據庫中對象復雜的關聯。
最小化與訪問數據庫的智能提取策略。
提供簡單的數據詢問。
hibernate的完全封裝導致無法使用數據的一些功能。
Hibernate的緩存問題。
Hibernate對于代碼的耦合度太高。
Hibernate尋找bug困難。
Hibernate批量數據操作需要大量的內存空間而且執行過程中需要的對象太多
JdbcTemplate針對數據查詢提供了多個重載的模板方法,你可以根據需要選用不同的模板方法.如果你的查詢很簡單,僅僅是傳入相應SQL或者相關參數,然后取得一個單一的結果,那么你可以選擇如下一組便利的模板方法。
優點:運行期:高效、內嵌Spring框架中、支持基于AOP的聲明式事務
缺點:必須于Spring框架結合在一起使用、不支持數據庫跨平臺、默認沒有緩存
MyBatis 是一款優秀的持久層框架/半自動的ORM,它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設置參數和獲取結果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 對象)為數據庫中的記錄。
優點:
1、與JDBC相比,減少了50%的代碼量
2、 最簡單的持久化框架,簡單易學
3、SQL代碼從程序代碼中徹底分離出來,可以重用
4、提供XML標簽,支持編寫動態SQL
5、提供映射標簽,支持對象與數據庫的ORM字段關系映射
6、支持緩存、連接池、數據庫移植…
缺點:
1、SQL語句編寫工作量大,熟練度要高
2、數據庫移植性比較差,如果需要切換數據庫的話,SQL語句會有很大的差異
導入pom
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
添加logback配置文件
<configuration> <!--appender 追加器 日志以哪種方式進行輸出 name 取個名字 class 不同實現類會輸出到不同地方 ch.qos.logback.core.ConsoleAppender 輸出到控制臺 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!-- 格式 --> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{100} - %msg%n</pattern> </encoder> </appender> <!--cn.tulingxueyuan.mapper--> <!--控制跟細粒度的日志級別 根據包\根據類--> <logger name="cn.tulingxueyuan.mapper" level="debug"></logger> org.apache.ibatis.transaction <!--控制所有的日志級別--> <root level="error"> <!-- 將當前日志級別輸出到哪個追加器上面 --> <appender-ref ref="STDOUT" /> </root> </configuration>
Logger LOGGER= LoggerFactory.getLogger(this.getClass()); /** * 日志級別 * TRACE < DEBUG < INFO < WARN < ERROR。 * 1 2 3 4 5 */ @Test public void test02(){ LOGGER.trace("跟蹤級別"); LOGGER.debug("調試級別"); LOGGER.info("信息級別"); LOGGER.warn("警告級別"); LOGGER.error("異常級別"); }
在mybatis的項目中,我們發現了有一個mybatis-config.xml的配置文件,這個配置文件是mybatis的全局配置文件,用來進行相關的全局配置,在任何操作下都生效的配置。下面我們要針對其中的屬性做詳細的解釋,方便大家在后續使用的時候更加熟練。
官方說明:
MyBatis 的配置文件包含了會深深影響 MyBatis 行為的設置和屬性信息。 配置文檔的頂層結構如下:
configuration(配置)
environments(環境配置)
environment(環境變量)
transactionManager(事務管理器)
properties(屬性)
settings(設置)
typeAliases(類型別名)
typeHandlers(類型處理器)
objectFactory(對象工廠)
plugins(插件)
dataSource(數據源)
databaseIdProvider(數據庫廠商標識)
mappers(映射器)
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> <!--引入外部配置文件,類似于Spring中的property-placeholder resource:從類路徑引入 url:從磁盤路徑或者網絡路徑引入 --> <properties resource="db.properties"></properties> <!--用來控制mybatis運行時的行為,是mybatis中的重要配置--> <settings> <!--設置列名映射的時候是否是駝峰標識--> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!--typeAliases表示為我們引用的實體類起別名,默認情況下我們需要寫類的完全限定名 如果在此處做了配置,那么可以直接寫類的名稱,在type中配置上類的完全限定名,在使用的時候可以忽略大小寫 還可以通過alias屬性來表示類的別名 --> <typeAliases> <!-- <typeAlias type="cn.tulingxueyuan.bean.Emp" alias="Emp"></typeAlias>--> <!--如果需要引用多個類,那么給每一個類起別名肯定會很麻煩,因此可以指定對應的包名,那么默認用的是類名--> <package name="cn.tulingxueyuan.bean"/> </typeAliases> <!-- 在實際的開發過程中,我們可能分為開發環境,生產環境,測試環境等等,每個環境的配置可以是不一樣的 environment就用來表示不同環境的細節配置,每一個環境中都需要一個事務管理器以及數據源的配置 我們在后續的項目開發中幾乎都是使用spring中配置的數據源和事務管理器來配置,此處不需要研究 --> <!--default:用來選擇需要的環境--> <environments default="development"> <!--id:表示不同環境的名稱--> <environment id="development"> <transactionManager type="JDBC"/> <!--配置數據庫連接--> <dataSource type="POOLED"> <!--使用${}來引入外部變量--> <property name="driver" value="${driverClassname}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <!-- 在不同的數據庫中,可能sql語句的寫法是不一樣的,為了增強移植性,可以提供不同數據庫的操作實現 在編寫不同的sql語句的時候,可以指定databaseId屬性來標識當前sql語句可以運行在哪個數據庫中 --> <databaseIdProvider type="DB_VENDOR"> <property name="MySQL" value="mysql"/> <property name="SQL Server" value="sqlserver"/> <property name="Oracle" value="orcl"/> </databaseIdProvider> <!--將sql的映射文件適用mappers進行映射--> <mappers> <!-- 指定具體的不同的配置文件 class:直接引入接口的全類名,可以將xml文件放在dao的同級目錄下,并且設置相同的文件名稱,同時可以使用注解的方式來進行相關的配置 url:可以從磁盤或者網絡路徑查找sql映射文件 resource:在類路徑下尋找sql映射文件 --> <!-- <mapper resource="EmpDao.xml"/> <mapper resource="UserDao.xml"/> <mapper class="cn.tulingxueyuan.dao.EmpDaoAnnotation"></mapper>--> <!-- 當包含多個配置文件或者配置類的時候,可以使用批量注冊的功能,也就是引入對應的包,而不是具體的配置文件或者類 但是需要注意的是, 1、如果使用的配置文件的形式,必須要將配置文件跟dao類放在一起,這樣才能找到對應的配置文件. 如果是maven的項目的話,還需要添加以下配置,原因是maven在編譯的文件的時候只會編譯java文件 <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build> 2、將配置文件在resources資源路徑下創建跟dao相同的包名 --> <package name="cn.tulingxueyuan.dao"/> </mappers> </configuration>
MyBatis 的真正強大在于它的語句映射,這是它的魔力所在。由于它的異常強大,映射器的 XML 文件就顯得相對簡單。如果拿它跟具有相同功能的 JDBC 代碼進行對比,你會立即發現省掉了將近 95% 的代碼。MyBatis 致力于減少使用成本,讓用戶能更專注于 SQL 代碼。
SQL 映射文件只有很少的幾個頂級元素(按照應被定義的順序列出):
cache – 該命名空間的緩存配置。
cache-ref – 引用其它命名空間的緩存配置。
resultMap – 描述如何從數據庫結果集中加載對象,是最復雜也是最強大的元素。
parameterMap – 老式風格的參數映射。此元素已被廢棄,并可能在將來被移除!請使用行內參數映射。文檔中不會介紹此元素。
sql – 可被其它語句引用的可重用語句塊。
insert – 映射插入語句。
update – 映射更新語句。
delete – 映射刪除語句。
select – 映射查詢語句。
在每個頂級元素標簽中可以添加很多個屬性,下面我們開始詳細了解下具體的配置。
insert、update、delete元素
屬性 | 描述 |
id | 在命名空間中唯一的標識符,可以被用來引用這條語句。 |
parameterType | 將會傳入這條語句的參數的類全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過類型處理器(TypeHandler)推斷出具體傳入語句的參數,默認值為未設置(unset)。 |
parameterMap | 用于引用外部 parameterMap 的屬性,目前已被廢棄。請使用行內參數映射和 parameterType 屬性。 |
flushCache | 將其設置為 true 后,只要語句被調用,都會導致本地緩存和二級緩存被清空,默認值:(對 insert、update 和 delete 語句)true。 |
timeout | 這個設置是在拋出異常之前,驅動程序等待數據庫返回請求結果的秒數。默認值為未設置(unset)(依賴數據庫驅動)。 |
statementType | 可選 STATEMENT,PREPARED 或 CALLABLE。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED。 |
useGeneratedKeys | (僅適用于 insert 和 update)這會令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法來取出由數據庫內部生成的主鍵(比如:像 MySQL 和 SQL Server 這樣的關系型數據庫管理系統的自動遞增字段),默認值:false。 |
keyProperty | (僅適用于 insert 和 update)指定能夠唯一識別對象的屬性,MyBatis 會使用 getGeneratedKeys 的返回值或 insert 語句的 selectKey 子元素設置它的值,默認值:未設置(unset)。如果生成列不止一個,可以用逗號分隔多個屬性名稱。 |
keyColumn | (僅適用于 insert 和 update)設置生成鍵值在表中的列名,在某些數據庫(像 PostgreSQL)中,當主鍵列不是表中的第一列的時候,是必須設置的。如果生成列不止一個,可以用逗號分隔多個屬性名稱。 |
databaseId | 如果配置了數據庫廠商標識(databaseIdProvider),MyBatis 會加載所有不帶 databaseId 或匹配當前 databaseId 的語句;如果帶和不帶的語句都有,則不帶的會被忽略。 |
<!--如果數據庫支持自增可以使用這樣的方式--> <insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> insert into user(user_name) values(#{userName}) </insert> <!--如果數據庫不支持自增的話,那么可以使用如下的方式進行賦值查詢--> <insert id="insertUser2" > <selectKey order="BEFORE" keyProperty="id" resultType="integer"> select max(id)+1 from user </selectKey> insert into user(id,user_name) values(#{id},#{userName}) </insert>
更多詳細內容見下邊的第三章節
在xml文件中編寫sql語句的時候有兩種取值的方式,分別是#{}和${},下面來看一下他們之間的區別:
<!--獲取參數的方式: 1.#{} ==> jdbc String sql=" SELECT id,user_name FROM EMP WHERE id=?" 1.會經過JDBC當中PreparedStatement的預編譯,會根據不同的數據類型來編譯成對應數據庫所對應的數據。 2.能夠有效的防止SQL注入。 推薦使用!! 特殊用法: 自帶很多內置參數的屬性:通常不會使用。了解 javaType、jdbcType、mode、numericScale、resultMap、typeHandler. 比如 需要改變默認的NULL===>OTHER:#{id,javaType=NULL} 想保留小數點后兩位:#{id,numericScale=2} 2.${} ==> jdbc String sql=" SELECT id,user_name FROM EMP WHERE id="+id 1.不會進行預編譯,會直接將輸入進來的數據拼接在SQL中。 2.存在SQL注入的風險。不推薦使用。 特殊用法: 1.調試情況下可以臨時使用。 2.實現一些特殊功能:前提一定要保證數據的安全性。 比如:動態表、動態列. 動態SQL. --> <select id="SelectEmp" resultType="Emp" resultMap="emp_map" > SELECT id,user_name,create_date FROM EMP where id=#{id} </select>
<!-- 參數傳遞的處理: 1.單個參數:SelectEmp(Integer id); mybatis 不會做任何特殊要求 獲取方式: #:{輸入任何字符獲取參數} 2.多個參數:Emp SelectEmp(Integer id,String username); mybatis 會進行封裝 會將傳進來的參數封裝成map: 1個值就會對應2個map項 : id===> {key:arg0 ,value:id的值},{key:param1 ,value:id的值} username===> {key:arg1 ,value:id的值},{key:param2 ,value:id的值} 獲取方式: 沒使用了@Param: id=====> #{arg0} 或者 #{param1} username=====> #{arg1} 或者 #{param2} 除了使用這種方式還有別的方式,因為這種方式參數名沒有意義: 設置參數的別名:@Param(""):SelectEmp(@Param("id") Integer id,@Param("username") String username); 當使用了@Param: id=====> #{id} 或者 #{param1} username=====> #{username} 或者 #{param2} 3. javaBean的參數: 單個參數:Emp SelectEmp(Emp emp); 獲取方式:可以直接使用屬性名 emp.id=====>#{id} emp.username=====>#{username} 多個參數:Emp SelectEmp(Integer num,Emp emp); num===> #{param1} 或者 @Param emp===> 必須加上對象別名: emp.id===> #{param2.id} 或者 @Param("emp")Emp emp ====>#{emp.id} emp.username===> #{param2.username} 或者 @Param("emp")Emp emp ====>#{emp.username} 4.集合或者數組參數: Emp SelectEmp(List<String> usernames); 如果是list,MyBatis會自動封裝為map: {key:"list":value:usernames} 沒用@Param("")要獲得:usernames.get(0) =====> #{list[0]} :usernames.get(0) =====> #{agr0[0]} 有@Param("usernames")要獲得:usernames.get(0) =====> #{usernames[0]} :usernames.get(0) =====> #{param1[0]} 如果是數組,MyBatis會自動封裝為map: {key:"array":value:usernames} 沒用@Param("")要獲得:usernames.get(0) =====> #{array[0]} :usernames.get(0) =====> #{agr0[0]} 有@Param("usernames")要獲得:usernames.get(0) =====> #{usernames[0]} :usernames.get(0) =====> #{param1[0]} 5.map參數 和javaBean的參數傳遞是一樣。 一般情況下: 請求進來的參數 和pojo對應,就用pojo 請求進來的參數 沒有和pojo對應,就用map 請求進來的參數 沒有和pojo對應上,但是使用頻率很高,就用TO、DTO(就是單獨為這些參數創建一個對應的javaBean出來,使參數傳遞更規范、更重用) --> <!-- 接口:SelectEmp(String username,@Param("id") Integer id); username====> #{arg0} #{param1} id====> #{id} #{param2} 接口:SelectEmp(@Param("beginDate") String beginDate, String endDate, Emp emp); beginDate====> #{beginDate} #{param1} endDate====> #{arg1} #{param2} emp.id====>#{arg2.id} #{param2.id} 接口:SelectEmp(List<Integer> ids, String[] usernames, @Param("beginDate") String beginDate, String endDate,); ids.get(0)=====> #{list[0]} #{param1[0]} usernames[0]=====> #{array[0]} #{param2[0]} beginDate====> #{beginDate} #{param3} end====> #{arg3} #{param4} -->
EmpDao.xml
<!--當返回值的結果是集合的時候,返回值的類型依然寫的是集合中具體的類型--> <select id="selectAllEmp" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp </select> <!--在查詢的時候可以設置返回值的類型為map,當mybatis查詢完成之后會把列的名稱作為key 列的值作為value,轉換到map中 --> <select id="selectEmpByEmpReturnMap" resultType="map"> select * from emp where empno = #{empno} </select> <!--注意,當返回的結果是一個集合對象的時候,返回值的類型一定要寫集合具體value的類型, 同時在dao的方法上要添加@MapKey的注解,來設置key是什么結果 @MapKey("empno") Map<Integer,Emp> getAllEmpReturnMap();--> <select id="getAllEmpReturnMap" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp </select>
<!--1.聲明resultMap自定義結果集 resultType 和 resultMap 只能使用一個。 id 唯一標識, 需要和<select 上的resultMap 進行對應 type 需要映射的pojo對象, 可以設置別名 autoMapping 自動映射,(默認=true) 只要字段名和屬性名遵循映射規則就可以自動映射,但是不建議,哪怕屬性名和字段名一一對應上了也要顯示的配置映射 extends 如果多個resultMap有重復映射,可以聲明父resultMap,將公共的映射提取出來, 可以減少子resultMap的映射冗余 --> <resultMap id="emp_map" type="emp" autoMapping="false" extends="common_map"> <result column="create_date" property="cjsj"></result> </resultMap> <resultMap id="common_map" type="emp" autoMapping="false" > <!-- <id> 主鍵必須使用 對底層存儲有性能作用 column 需要映射的數據庫字段名 property 需要映射的pojo屬性名 --> <id column="id" property="id"></id> <result column="user_name" property="username"></result> </resultMap> <!--2.使用resultMap 關聯 自定義結果集的id--> <select id="SelectEmp" resultType="Emp" resultMap="emp_map" > SELECT id,user_name,create_date FROM EMP where id=#{id} </select>
emp.java
import java.time.LocalDate; public class Emp { private Integer id; private String username; private LocalDate createDate; private deptId deptId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public LocalDate getCreateDate() { return createDate; } public void setCreateDate(LocalDate createDate) { this.createDate = createDate; } public Integer getDeptId() { return dept; } public void setDeptId(Integer deptId) { this.dept = dept; } @Override public String toString() { return "Emp{" + "id=" + id + ", username='" + username + '\'' + ", createDate=" + createDate + ", deptId=" + deptId+ '}'; } }
EmpMapper.xml
<!-- 實現表聯結查詢的方式: 可以映射: DTO --> <resultMap id="QueryEmp_Map" type="QueryEmpDTO"> <id column="e_id" property="id"></id> <result column="user_name" property="username"></result> <result column="d_id" property="deptId"></result> <result column="dept_name" property="deptName"></result> </resultMap> <select id="QueryEmp" resultMap="QueryEmp_Map"> select t1.id as e_id,t1.user_name,t2.id as d_id,t2.dept_name from emp t1 INNER JOIN dept t2 on t1.dept_id=t2.id where t1.id=#{id} </select> <!-- 實現表聯結查詢的方式: 可以映射map --> <resultMap id="QueryEmp_Map" type="map"> <id column="e_id" property="id"></id> <result column="user_name" property="username"></result> <result column="d_id" property="deptId"></result> <result column="dept_name" property="deptName"></result> </resultMap> <select id="QueryEmp" resultMap="QueryEmp_Map"> select t1.id as e_id,t1.user_name,t2.id as d_id,t2.dept_name from emp t1 INNER JOIN dept t2 on t1.dept_id=t2.id where t1.id=#{id} </select>
QueryEmpDTO
public class QueryEmpDTO { private String deptName; private Integer deptId; private Integer id; private String username; public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } public Integer getDeptId() { return deptId; } public void setDeptId(Integer deptId) { this.deptId = deptId; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String toString() { return "QueryEmpDTO{" + "deptName='" + deptName + '\'' + ", deptId=" + deptId + ", id=" + id + ", username='" + username + '\'' + '}'; } }
Test
@Test public void test01() { try(SqlSession sqlSession = sqlSessionFactory.openSession()){ // Mybatis在getMapper就會給我們創建jdk動態代理 EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); QueryEmpDTO dto = mapper.QueryEmp(4); System.out.println(dto); } }
EmpMapper.xml
<!--嵌套結果 多 對 一 --> <resultMap id="QueryEmp_Map2" type="Emp"> <id column="e_id" property="id"></id> <result column="user_name" property="username"></result> <!-- association 實現多對一中的 “一” property 指定對象中的嵌套對象屬性 --> <association property="dept"> <id column="d_id" property="id"></id> <id column="dept_name" property="deptName"></id> </association> </resultMap> <select id="QueryEmp2" resultMap="QueryEmp_Map2"> select t1.id as e_id,t1.user_name,t2.id as d_id,t2.dept_name from emp t1 INNER JOIN dept t2 on t1.dept_id=t2.id where t1.id=#{id} </select>
<!-- 嵌套結果: 一對多 查詢部門及所有員工 --> <resultMap id="SelectDeptAndEmpsMap" type="Dept"> <id column="d_id" property="id"></id> <id column="dept_name" property="deptName"></id> <!-- <collection 映射一對多中的 “多” property 指定需要映射的“多”的屬性,一般聲明為List ofType 需要指定list的類型 --> <collection property="emps" ofType="Emp" > <id column="e_id" property="id"></id> <result column="user_name" property="username"></result> <result column="create_date" property="createDate"></result> </collection> </resultMap> <select id="SelectDeptAndEmps" resultMap="SelectDeptAndEmpsMap"> select t1.id as d_id,t1.dept_name,t2.id e_id,t2.user_name,t2.create_date from dept t1 LEFT JOIN emp t2 on t1.id=t2.dept_id where t1.id=#{id} </select>
Emp.java
import java.time.LocalDate; public class Emp { private Integer id; private String username; private LocalDate createDate; private Dept dept; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public LocalDate getCreateDate() { return createDate; } public void setCreateDate(LocalDate createDate) { this.createDate = createDate; } public Dept getDept() { return dept; } public void setDept(Dept dept) { this.dept = dept; } @Override public String toString() { return "Emp{" + "id=" + id + ", username='" + username + '\'' + ", createDate=" + createDate + ", dept=" + dept + '}'; } }
Dept.java:
public class Dept { private Integer id; private String deptName; private List<Emp> emps; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } public List<Emp> getEmps() { return emps; } public void setEmps(List<Emp> emps) { this.emps = emps; } @Override public String toString() { return "Dept{" + "id=" + id + ", deptName='" + deptName + '\'' + ", emps=" + emps + '}'; } }
EmpMapper.java:
public interface EmpMapper { /*徐庶老師實際開發中的實現方式*/ QueryEmpDTO QueryEmp(Integer id); /*實用嵌套結果實現聯合查詢 多對一 */ Emp QueryEmp2(Integer id); /*實用嵌套查詢實現聯合查詢 多對一 */ Emp QueryEmp3(Integer id); }
DeptMapper.java:
public interface DeptMapper { //嵌套查詢: 一對多 使用部門id查詢員工 Dept SelectDeptAndEmps(Integer id); // 嵌套查詢(異步查詢): 一對多 查詢部門及所有員工 Dept SelectDeptAndEmps2(Integer id); }
在上述邏輯的查詢中,是由我們自己來完成sql語句的關聯查詢的,那么,我們能讓mybatis幫我們實現自動的關聯查詢嗎?
EmpMapper.xml:
<!--嵌套查詢(分步查詢) 多 對 一 聯合查詢和分步查詢區別: 性能區別不大 分部查詢支持 懶加載(延遲加載) 需要設置懶加載,一定要使用嵌套查詢的。 要啟動懶加載可以在全局配置文件中設置 lazyLoadingEnabled=true 還可以單獨為某個分步查詢設置立即加載 <association fetchType="eager" --> <resultMap id="QueryEmp_Map3" type="Emp"> <id column="id" property="id"></id> <result column="user_name" property="username"></result> <!-- association 實現多對一中的 “一” property 指定對象中的嵌套對象屬性 column 指定將哪個字段傳到分步查詢中 select 指定分步查詢的 命名空間+ID 以上3個屬性是實現分步查詢必須的屬性 fetchType 可選, eager|lazy eager立即加載 lazy跟隨全局配置文件中的lazyLoadingEnabled --> <association property="dept" column="dept_id" select="cn.tulingxueyuan.mapper.DeptMapper.SelectDept"> </association> </resultMap> <select id="QueryEmp3" resultMap="QueryEmp_Map3"> select * from emp where id=#{id} </select>
DeptMapper.xml
<!-- 根據部門id查詢部門--> <select id="SelectDept" resultType="dept"> SELECT * FROM dept where id=#{id} </select>
DeptMapper.xml
<!-- 嵌套查詢(異步查詢): 一對多 查詢部門及所有員工 --> <resultMap id="SelectDeptAndEmpsMap2" type="Dept"> <id column="d_id" property="id"></id> <id column="dept_name" property="deptName"></id> <!-- <collection 映射一對多中的 “多” property 指定需要映射的“多”的屬性,一般聲明為List ofType 需要指定list的類型 column 需要將當前查詢的字段傳遞到異步查詢的參數 select 指定異步查詢 --> <collection property="emps" ofType="Emp" column="id" select="cn.tulingxueyuan.mapper.EmpMapper.SelectEmpByDeptId" > </collection> </resultMap> <select id="SelectDeptAndEmps2" resultMap="SelectDeptAndEmpsMap2"> SELECT * FROM dept where id=#{id} </select>
EmpMapper.xml
<!-- 根據部門id所有員工 --> <select id="SelectEmpByDeptId" resultType="emp"> select * from emp where dept_id=#{id} </select>
Emp.java
public class Emp { private Integer id; private String username; private LocalDate createDate; private Dept dept; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public LocalDate getCreateDate() { return createDate; } public void setCreateDate(LocalDate createDate) { this.createDate = createDate; } public Dept getDept() { return dept; } public void setDept(Dept dept) { this.dept = dept; } @Override public String toString() { return "Emp{" + "id=" + id + ", username='" + username + '\'' + ", createDate=" + createDate + ", dept=" + dept + '}'; } }
Dept.java:
public class Dept { private Integer id; private String deptName; private List<Emp> emps; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } public List<Emp> getEmps() { return emps; } public void setEmps(List<Emp> emps) { this.emps = emps; } @Override public String toString() { return "Dept{" + "id=" + id + ", deptName='" + deptName + '\'' + ", emps=" + emps + '}'; } }
EmpMapper.java:
public interface EmpMapper { /*徐庶老師實際開發中的實現方式*/ QueryEmpDTO QueryEmp(Integer id); /*實用嵌套結果實現聯合查詢 多對一 */ Emp QueryEmp2(Integer id); /*實用嵌套查詢實現聯合查詢 多對一 */ Emp QueryEmp3(Integer id); }
DeptMapper.java:
public interface DeptMapper { //嵌套查詢: 一對多 使用部門id查詢員工 Dept SelectDeptAndEmps(Integer id); // 嵌套查詢(異步查詢): 一對多 查詢部門及所有員工 Dept SelectDeptAndEmps2(Integer id); }
當我們在進行表關聯的時候,有可能在查詢結果的時候不需要關聯對象的屬性值(select count(1)…),那么此時可以通過延遲加載來實現功能。在全局配置文件中添加如下屬性
mybatis-config.xml
<!-- 開啟延遲加載,所有分步查詢都是懶加載 (默認是立即加載)--> <setting name="lazyLoadingEnabled" value="true"/> <!--當開啟式, 使用pojo中任意屬性都會加載延遲查詢 ,默認是false <setting name="aggressiveLazyLoading" value="false"/>--> <!--設置對象的哪些方法調用會加載延遲查詢 默認:equals,clone,hashCode,toString--> <setting name="lazyLoadTriggerMethods" value=""/>
如果設置了全局加載,但是希望在某一個sql語句查詢的時候不使用延時策略,可以添加fetchType下屬性:
<association property="dept" fetchType="eager" column="dept_id" select="cn.tulingxueyuan.mapper.DeptMapper.SelectDept"> </association>
三種關聯關系都有兩種關聯查詢的方式: 嵌套查詢,嵌套結果
Mybatis的延遲加載配置, 在全局配置文件中加入下面代碼
<settings> <setting name=”lazyLoadingEnabled” value=”true” /> <setting name=”aggressiveLazyLoading” value=”false”/> </settings>
在映射文件中,元素和元素中都已默認配置了延遲加載屬性,即默認屬性fetchType=”lazy”(屬性fetchType=”eager”表示立即加載),所以在配置文件中開啟延遲加載后,無需在映射文件中再做配置
使用元素進行一對一關聯映射非常簡單,只需要參考如下兩種示例配置即可
<resultMap>
元素中,包含了一個<collection>
子元素,MyBatis就是通過該元素來處理一對多關聯關系的<collection>
子元素的屬性大部分與<association>
元素相同,但其還包含一個特殊屬性–ofType
ofType屬性與javaType屬性對應,它用于指定實體對象中集合類屬性所包含的元素類型。<collection >
元素的使用也非常簡單,同樣可以參考如下兩種示例進行配置,具體代碼如下:
多對多
多對多的關聯關系查詢,同樣可以使用前面介紹的元素進行處理(其用法和一對多關聯關系查詢語句用法基本相同)
動態 SQL 是 MyBatis 的強大特性之一。如果你使用過 JDBC 或其它類似的框架,你應該能理解根據不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號。利用動態 SQL,可以徹底擺脫這種痛苦。
使用動態 SQL 并非一件易事,但借助可用于任何 SQL 映射語句中的強大的動態 SQL 語言,MyBatis 顯著地提升了這一特性的易用性。
如果你之前用過 JSTL 或任何基于類 XML 語言的文本處理器,你對動態 SQL 元素可能會感覺似曾相識。在 MyBatis 之前的版本中,需要花時間了解大量的元素。借助功能強大的基于 OGNL 的表達式,MyBatis 3 替換了之前的大部分元素,大大精簡了元素種類,現在要學習的元素種類比原來的一半還要少。
if
choose (when, otherwise)
trim (where, set)
foreach
bind
sql片段
EmpDao.xml
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp where <if test="empno!=null"> empno = #{empno} and </if> <if test="ename!=null"> ename like #{ename} and </if> <if test="sal!=null"> sal > #{sal} </if> </select>
上邊代碼看起來是比較正常的,但是大家需要注意的是如果我們傳入的參數值有缺失會怎么呢?這個時候拼接的sql語句就會變得有問題,例如不傳參數或者丟失最后一個參數,那么語句中就會多一個where或者and的關鍵字,因此在mybatis中也給出了具體的解決方案:
where
where 元素只會在子元素返回任何內容的情況下才插入 “WHERE” 子句。而且,若子句的開頭為 “AND” 或 “OR”,where 元素也會將它們去除。
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp <where> <if test="empno!=null"> empno = #{empno} </if> <if test="ename!=null"> and ename like #{ename} </if> <if test="sal!=null"> and sal > #{sal} </if> </where> </select>
現在看起來沒有什么問題了,但是我們的條件添加到了拼接sql語句的前后,那么我們該如何處理呢?
trim
<!-- trim截取字符串: prefix:前綴,為sql整體添加一個前綴 prefixOverrides:去除整體字符串前面多余的字符 suffixOverrides:去除后面多余的字符串 --> <select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp <trim prefix="where" prefixOverrides="and" suffixOverrides="and"> <if test="empno!=null"> empno = #{empno} and </if> <if test="ename!=null"> ename like #{ename} and </if> <if test="sal!=null"> sal > #{sal} and </if> </trim> </select>
動態 SQL 的另一個常見使用場景是對集合進行遍歷(尤其是在構建 IN 條件語句的時候)。
<!--foreach是對集合進行遍歷 collection="deptnos" 指定要遍歷的集合 close="" 表示以什么結束 index="" 給定一個索引值 item="" 遍歷的每一個元素的值 open="" 表示以什么開始 separator="" 表示多個元素的分隔符 --> <select id="getEmpByDeptnos" resultType="Emp"> select * from emp where deptno in <foreach collection="deptnos" close=")" index="idx" item="deptno" open="(" separator=","> #{deptno} </foreach> </select>
有時候,我們不想使用所有的條件,而只是想從多個條件中選擇一個使用。針對這種情況,MyBatis 提供了 choose 元素,它有點像 Java 中的 switch 語句。
<select id="getEmpByConditionChoose" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp <where> <choose> <when test="empno!=null"> empno > #{empno} </when> <when test="ename!=null"> ename like #{ename} </when> <when test="sal!=null"> sal > #{sal} </when> <otherwise> 1=1 </otherwise> </choose> </where> </select>
用于動態更新語句的類似解決方案叫做 set。set 元素可以用于動態包含需要更新的列,忽略其它不更新的列。
<update id="updateEmpByEmpno"> update emp <set> <if test="empno!=null"> empno=#{empno}, </if> <if test="ename!=null"> ename = #{ename}, </if> <if test="sal!=null"> sal = #{sal} </if> </set> <where> empno = #{empno} </where> </update>
bind 元素允許你在 OGNL 表達式以外創建一個變量,并將其綁定到當前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog"> <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" /> SELECT * FROM BLOG WHERE title LIKE #{pattern} </select>
這個元素可以用來定義可重用的 SQL 代碼片段,以便在其它語句中使用。 參數可以靜態地(在加載的時候)確定下來,并且可以在不同的 include 元素中定義不同的參數值。比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
這個 SQL 片段可以在其它語句中使用,例如:
<select id="selectUsers" resultType="map"> select <include refid="userColumns"><property name="alias" value="t1"/></include>, <include refid="userColumns"><property name="alias" value="t2"/></include> from some_table t1 cross join some_table t2 </select>
MyBatis 內置了一個強大的事務性查詢緩存機制,它可以非常方便地配置和定制。 為了使它更加強大而且易于配置,我們對 MyBatis 3 中的緩存實現進行了許多改進。
默認情況下,只啟用了本地的會話緩存,它僅僅對一個會話中的數據進行緩存。 要啟用全局的二級緩存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
當添加上該標簽之后,會有如下效果:
映射語句文件中的所有 select 語句的結果將會被緩存。
映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。
緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
緩存不會定時進行刷新(也就是說,沒有刷新間隔)。
緩存會保存列表或對象(無論查詢方法返回哪種)的 1024 個引用。
緩存會被視為讀/寫緩存,這意味著獲取到的對象并不是共享的,可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。
在進行配置的時候還會分為一級緩存和二級緩存:
一級緩存:線程級別的緩存,是本地緩存,sqlSession級別的緩存
二級緩存:全局范圍的緩存,不止局限于當前會話
一級緩存是sqlsession級別的緩存,默認是存在的。在下面的案例中,大家發現我發送了兩個相同的請求,但是sql語句僅僅執行了一次,那么就意味著第一次查詢的時候已經將結果進行了緩存。
@Test public void test01() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { EmpDao mapper = sqlSession.getMapper(EmpDao.class); List<Emp> list = mapper.selectAllEmp(); for (Emp emp : list) { System.out.println(emp); } System.out.println("--------------------------------"); List<Emp> list2 = mapper.selectAllEmp(); for (Emp emp : list2) { System.out.println(emp); } } catch (Exception e) { e.printStackTrace(); } finally { sqlSession.close(); } }
在大部分的情況下一級緩存是可以的,但是有幾種特殊的情況會造成一級緩存失效:
1、一級緩存是sqlSession級別的緩存,如果在應用程序中只有開啟了多個sqlsession,那么會造成緩存失效
@Test public void test02(){ SqlSession sqlSession = sqlSessionFactory.openSession(); EmpDao mapper = sqlSession.getMapper(EmpDao.class); List<Emp> list = mapper.selectAllEmp(); for (Emp emp : list) { System.out.println(emp); } System.out.println("================================"); SqlSession sqlSession2 = sqlSessionFactory.openSession(); EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class); List<Emp> list2 = mapper2.selectAllEmp(); for (Emp emp : list2) { System.out.println(emp); } sqlSession.close(); sqlSession2.close(); }
2、在編寫查詢的sql語句的時候,一定要注意傳遞的參數,如果參數不一致,那么也不會緩存結果
3、如果在發送過程中發生了數據的修改,那么結果就不會緩存
@Test public void test03(){ SqlSession sqlSession = sqlSessionFactory.openSession(); EmpDao mapper = sqlSession.getMapper(EmpDao.class); Emp empByEmpno = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno); System.out.println("================================"); empByEmpno.setEname("zhangsan"); int i = mapper.updateEmp(empByEmpno); System.out.println(i); System.out.println("================================"); Emp empByEmpno1 = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno1); sqlSession.close(); }
4、在兩次查詢期間,手動去清空緩存,也會讓緩存失效
@Test public void test03(){ SqlSession sqlSession = sqlSessionFactory.openSession(); EmpDao mapper = sqlSession.getMapper(EmpDao.class); Emp empByEmpno = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno); System.out.println("================================"); System.out.println("手動清空緩存"); sqlSession.clearCache(); System.out.println("================================"); Emp empByEmpno1 = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno1); sqlSession.close(); }
特性
一級緩存特性:
默認就開啟了,也可以關閉一級緩存 localCacheScope=STATEMENT
作用域:是基于sqlSession(默認),一次數據庫操作會話。
緩存默認實現類PerpetualCache ,使用map進行存儲的
查詢完就會進行存儲
先從二級緩存中獲取,再從一級緩存中獲取 key==> sqlid+sql
一級緩存失效情況:
不同的sqlSession會使一級緩存失效
同一個SqlSession,但是查詢語句不一樣
同一個SqlSession,查詢語句一樣,期間執行增刪改操作
同一個SqlSession,查詢語句一樣,執行手動清除緩存
二級緩存是全局作用域緩存,默認是不開啟的,需要手動進行配置。
Mybatis提供二級緩存的接口以及實現,緩存實現的時候要求實體類實現Serializable接口,二級緩存在sqlSession關閉或提交之后才會生效。
二級緩存的使用
步驟:
1、全局配置文件中添加如下配置:
<setting name="cacheEnabled" value="true"/>
2、需要在使用二級緩存的映射文件處使用標簽標注
3、實體類必須要實現Serializable接口
@Test public void test04(){ SqlSession sqlSession = sqlSessionFactory.openSession(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); EmpDao mapper = sqlSession.getMapper(EmpDao.class); EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class); Emp empByEmpno = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno); sqlSession.close(); Emp empByEmpno1 = mapper2.findEmpByEmpno(1111); System.out.println(empByEmpno1); sqlSession2.close(); }
緩存的屬性
eviction:表示緩存回收策略,默認是
LRU LRU:最近最少使用的,移除最長時間不被使用的對象
FIFO:先進先出,按照對象進入緩存的順序來移除
SOFT:軟引用,移除基于垃圾回收器狀態和軟引用規則的對象
WEAK:弱引用,更積極地移除基于垃圾收集器狀態和弱引用規則的對象
flushInternal:刷新間隔,單位毫秒
默認情況是不設置,也就是沒有刷新間隔,緩存僅僅調用語句時刷新
size:引用數目,正整數
代表緩存最多可以存儲多少個對象,太大容易導致內存溢出
readonly:只讀,true/false
true:只讀緩存,會給所有調用這返回緩存對象的相同實例,因此這些對象不能被修改。
false:讀寫緩存,會返回緩存對象的拷貝(序列化實現),這種方式比較安全,默認值
//可以看到會去二級緩存中查找數據,而且二級緩存跟一級緩存中不會同時存在數據,因為二級緩存中的數據是在sqlsession 關閉之后才生效的 @Test public void test05(){ SqlSession sqlSession = sqlSessionFactory.openSession(); EmpDao mapper = sqlSession.getMapper(EmpDao.class); Emp empByEmpno = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno); sqlSession.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class); Emp empByEmpno2 = mapper2.findEmpByEmpno(1111); System.out.println(empByEmpno2); Emp empByEmpno3 = mapper2.findEmpByEmpno(1111); System.out.println(empByEmpno3); sqlSession2.close(); }
緩存查詢的順序是先查詢二級緩存再查詢一級緩存
@Test public void test05(){ SqlSession sqlSession = sqlSessionFactory.openSession(); EmpDao mapper = sqlSession.getMapper(EmpDao.class); Emp empByEmpno = mapper.findEmpByEmpno(1111); System.out.println(empByEmpno); sqlSession.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class); Emp empByEmpno2 = mapper2.findEmpByEmpno(1111); System.out.println(empByEmpno2); Emp empByEmpno3 = mapper2.findEmpByEmpno(1111); System.out.println(empByEmpno3); Emp empByEmpno4 = mapper2.findEmpByEmpno(7369); System.out.println(empByEmpno4); Emp empByEmpno5 = mapper2.findEmpByEmpno(7369); System.out.println(empByEmpno5); sqlSession2.close(); }
二級緩存的作用范圍
如果設置了全局的二級緩存配置,那么在使用的時候需要注意,在每一個單獨的select語句中,可以設置將查詢緩存關閉,以完成特殊的設置
1、在setting中設置,是配置二級緩存開啟,一級緩存默認一直開啟
<setting name="cacheEnabled" value="true"/>
2、select標簽的useCache屬性:
在每一個select的查詢中可以設置當前查詢是否要使用二級緩存,只對二級緩存有效
3、sql標簽的flushCache屬性
增刪改操作默認值為true,sql執行之后會清空一級緩存和二級緩存,而查詢操作默認是false
4、sqlSession.clearCache()
只是用來清除一級緩存
二級緩存特性
默認開啟了,沒有實現
作用域:基于全局范圍,應用級別。
緩存默認實現類PerpetualCache ,使用map進行存儲的但是二級緩存根據不同的mapper命名空間多包了一層map
: org.apache.ibatis.session.Configuration#caches key:mapper命名空間 value:erpetualCache.map key==> sqlid+sql
事務提交的時候(sqlSession關閉)
先從二級緩存中獲取,再從一級緩存中獲取
實現:
開啟二級緩存<setting name="cacheEnabled" value="true"/>
在需要使用到二級緩存的映射文件中加入,基于Mapper映射文件來實現緩存的,基于Mapper映射文件的命名空間來存儲的
在需要使用到二級緩存的javaBean中實現序列化接口implements Serializable 配置成功就會出現緩存命中率 同一個sqlId: 從緩存中拿出的次數/查詢總次數
失效:
同一個命名空間進行了增刪改的操作,會導致二級緩存失效 但是如果不想失效:可以將SQL的flushCache 這是為false,但是要慎重設置,因為會造成數據臟讀問題,除非你能保證查詢的數據永遠不會執行增刪改
讓查詢不緩存數據到二級緩存中useCache=“false”
如果希望其他mapper映射文件的命名空間執行了增刪改清空另外的命名空間就可以設置:
<cache-ref namespace="cn.tulingxueyuan.mapper.DeptMapper"/>
整合redis
添加redis-mybatis 緩存適配器 依賴
<dependencies> <!--添加依賴--> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency> </dependencies>
添加redis.properties在resources根目錄
host=localhost port=6379 connectionTimeout=5000 soTimeout=5000 password=無密碼可不填 database=0 clientName=
設置mybatis二級緩存實現類
<cache ... type="org.mybatis.caches.redis.RedisCache" ....../>
整合ehcache
導入對應的maven依賴
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache --> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.8.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.0</version> </dependency>
導入ehcache配置文件
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 磁盤保存路徑 --> <diskStore path="D:\ehcache" /> <defaultCache maxElementsInMemory="1" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache> <!-- 屬性說明: l diskStore:指定數據在磁盤中的存儲位置。 l defaultCache:當借助CacheManager.add("demoCache")創建Cache時,EhCache便會采用<defalutCache/>指定的的管理策略 以下屬性是必須的: l maxElementsInMemory - 在內存中緩存的element的最大數目 l maxElementsOnDisk - 在磁盤上緩存的element的最大數目,若是0表示無窮大 l eternal - 設定緩存的elements是否永遠不過期。如果為true,則緩存的數據始終有效,如果為false那么還要根據timeToIdleSeconds,timeToLiveSeconds判斷 l overflowToDisk - 設定當內存緩存溢出的時候是否將過期的element緩存到磁盤上 以下屬性是可選的: l timeToIdleSeconds - 當緩存在EhCache中的數據前后兩次訪問的時間超過timeToIdleSeconds的屬性取值時,這些數據便會刪除,默認值是0,也就是可閑置時間無窮大 l timeToLiveSeconds - 緩存element的有效生命期,默認是0.,也就是element存活時間無窮大 diskSpoolBufferSizeMB 這個參數設置DiskStore(磁盤緩存)的緩存區大小.默認是30MB.每個Cache都應該有自己的一個緩沖區. l diskPersistent - 在VM重啟的時候是否啟用磁盤保存EhCache中的數據,默認是false。 l diskExpiryThreadIntervalSeconds - 磁盤緩存的清理線程運行間隔,默認是120秒。每個120s,相應的線程會進行一次EhCache中數據的清理工作 l memoryStoreEvictionPolicy - 當內存緩存達到最大,有新的element加入的時候, 移除緩存中element的策略。默認是LRU(最近最少使用),可選的有LFU(最不常使用)和FIFO(先進先出) -->
在mapper文件中添加自定義緩存
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
MyBatis 通過提供插件機制,讓我們可以根據自己的需要去增強MyBatis 的功能。需要注意的是,如果沒有完全理解MyBatis 的運行原理和插件的工作方式,最好不要使用插件,因為它會改變系底層的工作邏輯,給系統帶來很大的影響。
MyBatis 的插件可以在不修改原來的代碼的情況下,通過攔截的方式,改變四大核心對象的行為,比如處理參數,處理SQL,處理結果。
Mybatis插件典型適用場景
分頁功能
mybatis的分頁默認是基于內存分頁的(查出所有,再截取),數據量大的情況下效率較低,不過使用mybatis插件可以改變該行為,只需要攔截StatementHandler類的prepare方法,改變要執行的SQL語句為分頁語句即可;
公共字段統一賦值
一般業務系統都會有創建者,創建時間,修改者,修改時間四個字段,對于這四個字段的賦值,實際上可以在DAO層統一攔截處理,可以用mybatis插件攔截Executor類的update方法,對相關參數進行統一賦值即可;
性能監控
對于SQL語句執行的性能監控,可以通過攔截Executor類的update, query等方法,用日志記錄每個方法執行的時間;
其它
其實mybatis擴展性還是很強的,基于插件機制,基本上可以控制SQL執行的各個階段,如執行階段,參數處理階段,語法構建階段,結果集處理階段,具體可以根據項目業務來實現對應業務邏輯。
實現思考:
第一個問題:
不修改對象的代碼,怎么對對象的行為進行修改,比如說在原來的方法前面做一點事情,在原來的方法后面做一點事情?
答案:大家很容易能想到用代理模式,這個也確實是MyBatis 插件的原理。
第二個問題:
我們可以定義很多的插件,那么這種所有的插件會形成一個鏈路,比如我們提交一個休假申請,先是項目經理審批,然后是部門經理審批,再是HR 審批,再到總經理審批,怎么實現層層的攔截?
答案:插件是層層攔截的,我們又需要用到另一種設計模式——責任鏈模式。
在之前的源碼中我們也發現了,mybatis內部對于插件的處理確實使用的代理模式,既然是代理模式,我們應該了解MyBatis 允許哪些對象的哪些方法允許被攔截,并不是每一個運行的節點都是可以被修改的。只有清楚了這些對象的方法的作用,當我們自己編寫插件的時候才知道從哪里去攔截。在MyBatis 官網有答案,我們來看一下:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
Executor 會攔截到CachingExcecutor 或者BaseExecutor。因為創建Executor 時是先創建CachingExcecutor,再包裝攔截。從代碼順序上能看到。我們可以通過mybatis的分頁插件來看看整個插件從包裝攔截器鏈到執行攔截器鏈的過程。
在查看插件原理的前提上,我們需要來看看官網對于自定義插件是怎么來做的,官網上有介紹:通過 MyBatis 提供的強大機制,使用插件是非常簡單的,只需實現 Interceptor 接口,并指定想要攔截的方法簽名即可。這里本人踩了一個坑,在Springboot中集成,同時引入了pagehelper-spring-boot-starter 導致RowBounds參數的值被刷掉了,也就是走到了我的攔截其中沒有被設置值,這里需要注意,攔截器出了問題,可以Debug看一下Configuration配置類中攔截器鏈的包裝情況。
自定義分頁插件
@Intercepts({ @Signature(type = Executor.class,method = "query" ,args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), // 需要代理的對象和方法 @Signature(type = Executor.class,method = "query" ,args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} ) // 需要代理的對象和方法 }) public class MyPageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("簡易版的分頁插件:邏輯分頁改成物理分頁"); // 修改sql 拼接Limit 0,10 Object[] args = invocation.getArgs(); // MappedStatement 對mapper映射文件里面元素的封裝 MappedStatement ms= (MappedStatement) args[0]; // BoundSql 對sql和參數的封裝 Object parameterObject=args[1]; BoundSql boundSql = ms.getBoundSql(parameterObject); // RowBounds 封裝了邏輯分頁的參數 :當前頁offset,一頁數limit RowBounds rowBounds= (RowBounds) args[2]; // 拿到原來的sql語句 String sql = boundSql.getSql(); String limitSql=sql+ " limit "+rowBounds.getOffset()+","+ rowBounds.getLimit(); //將分頁sql重新封裝一個BoundSql 進行后續執行 BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), limitSql, boundSql.getParameterMappings(), parameterObject); // 被代理的對象 Executor executor= (Executor) invocation.getTarget(); CacheKey cacheKey = executor.createCacheKey(ms, parameterObject, rowBounds, pageBoundSql); // 調用修改過后的sql繼續執行查詢 return executor.query(ms,parameterObject,rowBounds, (ResultHandler) args[3],cacheKey,pageBoundSql); } }
攔截簽名跟參數的順序有嚴格要求,如果按照順序找不到對應方法會拋出異常:
org.apache.ibatis.exceptions.PersistenceException: ### Error opening session. Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named queryv
MyBatis 啟動時掃描 標簽, 注冊到Configuration 對象的 InterceptorChain 中。property 里面的參數,會調用setProperties()方法處理。
分頁插件使用
添加pom依賴:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>1.2.15</version> </dependency>
插件注冊,在mybatis-config.xml 中注冊插件:
<configuration> <plugins> <!-- com.github.pagehelper為PageHelper類所在包名 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <property name="helperDialect" value="mysql" /> <!-- 該參數默認為false --> <!-- 設置為true時,會將RowBounds第一個參數offset當成pageNum頁碼使用 --> <!-- 和startPage中的pageNum效果一樣 --> <property name="offsetAsPageNum" value="true" /> <!-- 該參數默認為false --> <!-- 設置為true時,使用RowBounds分頁會進行count查詢 --> <property name="rowBoundsWithCount" value="true" /> <!-- 設置為true時,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果 --> <!-- (相當于沒有執行分頁查詢,但是返回結果仍然是Page類型) --> <property name="pageSizeZero" value="true" /> <!-- 3.3.0版本可用 - 分頁參數合理化,默認false禁用 --> <!-- 啟用合理化時,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最后一頁 --> <!-- 禁用合理化時,如果pageNum<1或pageNum>pages會返回空數據 --> <property name="reasonable" value="true" /> <!-- 3.5.0版本可用 - 為了支持startPage(Object params)方法 --> <!-- 增加了一個`params`參數來配置參數映射,用于從Map或ServletRequest中取值 --> <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默認值 --> <!-- 不理解該含義的前提下,不要隨便復制該配置 --> <property name="params" value="pageNum=start;pageSize=limit;" /> </plugin> </plugins> </configuration>
調用
// 獲取配置文件 InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml"); // 通過加載配置文件獲取SqlSessionFactory對象 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession sqlSession = sqlSessionFactory.openSession()) { // Mybatis在getMapper就會給我們創建jdk動態代理 EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); PageHelper.startPage(1, 5); List<Emp> list=mapper.selectAll(); PageInfo<ServiceStation> info = new PageInfo<ServiceStation>(list, 3); System.out.println("當前頁碼:"+info.getPageNum()); System.out.println("每頁的記錄數:"+info.getPageSize()); System.out.println("總記錄數:"+info.getTotal()); System.out.println("總頁碼:"+info.getPages()); System.out.println("是否第一頁:"+info.isIsFirstPage()); System.out.println("連續顯示的頁碼:"); int[] nums = info.getNavigatepageNums(); for (int i = 0; i < nums.length; i++) { System.out.println(nums[i]); } }
代理和攔截是怎么實現的?
上面提到的可以被代理的四大對象都是什么時候被代理的呢?Executor 是openSession() 的時候創建的; StatementHandler 是SimpleExecutor.doQuery()創建的;里面包含了處理參數的ParameterHandler 和處理結果集的ResultSetHandler 的創建,創建之后即調用InterceptorChain.pluginAll(),返回層層代理后的對象。代理是由Plugin 類創建。在我們重寫的 plugin() 方法里面可以直接調用returnPlugin.wrap(target, this);返回代理對象。
單個插件的情況下,代理能不能被代理?代理順序和調用順序的關系? 可以被代理。
因為代理類是Plugin,所以最后調用的是Plugin 的invoke()方法。它先調用了定義的攔截器的intercept()方法。可以通過invocation.proceed()調用到被代理對象被攔截的方法。
調用流程時序圖:
PageHelper 原理
先來看一下分頁插件的簡單用法:
PageHelper.startPage(1, 3); List<Blog> blogs = blogMapper.selectBlogById2(blog); PageInfo page = new PageInfo(blogs, 3);
對于插件機制我們上面已經介紹過了,在這里我們自然的會想到其所涉及的核心類 :PageInterceptor。攔截的是Executor 的兩個query()方法,要實現分頁插件的功能,肯定是要對我們寫的sql進行改寫,那么一定是在 intercept 方法中進行操作的,我們會發現這么一行代碼:
String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
調用到 AbstractHelperDialect 中的 getPageSql 方法:
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) { // 獲取sql String sql = boundSql.getSql(); //獲取分頁參數對象 Page page = this.getLocalPage(); return this.getPageSql(sql, page, pageKey); }
這里可以看到會去調用 this.getLocalPage(),我們來看看這個方法:
public <T> Page<T> getLocalPage() { return PageHelper.getLocalPage(); } //線程獨享 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal(); public static <T> Page<T> getLocalPage() { return (Page)LOCAL_PAGE.get(); }
可以發現這里是調用的是PageHelper的一個本地線程變量中的一個 Page對象,從其中獲取我們所設置的 PageSize 與 PageNum,那么他是怎么設置值的呢?請看:
PageHelper.startPage(1, 3); public static <E> Page<E> startPage(int pageNum, int pageSize) { return startPage(pageNum, pageSize, true); } public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } //設置頁數,行數信息 setLocalPage(page); return page; } protected static void setLocalPage(Page page) { //設置值 LOCAL_PAGE.set(page); }
在我們調用 PageHelper.startPage(1, 3); 的時候,系統會調用 LOCAL_PAGE.set(page) 進行設置,從而在分頁插件中可以獲取到這個本地變量對象中的參數進行 SQL 的改寫,由于改寫有很多實現,我們這里用的Mysql的實現:
在這里我們會發現分頁插件改寫SQL的核心代碼,這個代碼就很清晰了,不必過多贅述:
public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT "); sqlBuilder.append(page.getPageSize()); } else { sqlBuilder.append(" LIMIT "); sqlBuilder.append(page.getStartRow()); sqlBuilder.append(","); sqlBuilder.append(page.getPageSize()); pageKey.update(page.getStartRow()); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
PageHelper 就是這么一步一步的改寫了我們的SQL 從而達到一個分頁的效果。
關鍵類總結:
引入pom依賴
<dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.4.0</version> </dependency>
編寫配置文件:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- 指定數據庫驅動 用java代碼的方式生成可以不指定(只需要吧mybatis-generator和數據庫驅動依賴到項目) <classPathEntry location ="F:\java\jar\mysql-connector-java-5.1.22-bin.jar" /> --> <!-- targetRuntime MyBatis3 可以生成通用查詢,可以指定動態where條件 MyBatis3Simple 只生成CURD --> <context id="DB2Tables" targetRuntime="MyBatis3"> <!-- 關于注釋的生成規則 --> <commentGenerator> <!-- 設置不生成注釋 --> <property name="suppressAllComments" value="true"/> </commentGenerator> <!-- 數據庫的連接信息 --> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/bookstore" userId="root" password="root"> </jdbcConnection> <!-- java類型生成方式 --> <javaTypeResolver > <!-- forceBigDecimals true 當數據庫類型為decimal 或number 生成對應的java的BigDecimal false 會根據可數據的數值長度生成不同的對應java類型 useJSR310Types false 所有數據庫的日期類型都會生成java的 Date類型 true 會將數據庫的日期類型生成對應的JSR310的日期類型 比如 mysql的數據庫類型是date===>LocalDate 必須jdk是1.8以上 --> <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <!-- pojo的生成規則 --> <javaModelGenerator targetPackage="cn.tuling.pojo" targetProject="./src/main/java"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- sql映射文件的生成規則 --> <sqlMapGenerator targetPackage="cn.tuling.mapper" targetProject="./src/main/resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <!-- dao的接口生成規則 UserMapper--> <javaClientGenerator type="XMLMAPPER" targetPackage="cn.tuling.mapper" targetProject="./src/main/java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> <table tableName="emp" domainObjectName="Emp" mapperName="EmpMapper" ></table> <table tableName="dept" domainObjectName="Dept" mapperName="DeptMapper" ></table> </context> </generatorConfiguration>
編寫測試類
public class MBGTest { @Test public void test01() throws Exception { List<String> warnings = new ArrayList<String>(); boolean overwrite = true; File configFile = new File("generatorConfig.xml"); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null); } }
調用
@Test public void test01() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { // Mybatis在getMapper就會給我們創建jdk動態代理 EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); Emp emp = mapper.selectByPrimaryKey(4); System.out.println(emp); } } /** * Mybatis3生成調用方式 */ @Test public void test02() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { // Mybatis在getMapper就會給我們創建jdk動態代理 EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); // 使用Example實現動態條件語句 EmpExample empExample=new EmpExample(); EmpExample.Criteria criteria = empExample.createCriteria(); criteria.andUserNameLike("%帥%") .andIdEqualTo(4); List<Emp> emps = mapper.selectByExample(empExample); System.out.println(emps); } }
到此,關于“Mybatis是什么及怎么使用”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。