mybatis看这一篇就够了,简单全面一发入魂
文章目錄
- Mybatis
-
- 概述
- 快速入門
-
- 原生開發示例
- 基于Mapper代理的示例
- 基于注解的示例
- 應用場景
-
- 主鍵返回
- 批量查詢
- 動態SQL
- 緩存
- 關聯查詢
- 延遲加載
- 逆向工程
- PageHelper分頁插件
- Mybatis Plus
Mybatis
概述
-
mybatis是什么?有什么特點?
它是一款半自動的ORM持久層框架,具有較高的SQL靈活性,支持高級映射(一對一,一對多),動態SQL,延遲加載和緩存等特性,但它的數據庫無關性較低
-
什么是ORM?
Object Relation Mapping,對象關系映射。對象指的是Java對象,關系指的是數據庫中的關系模型,對象關系映射,指的就是在Java對象和數據庫的關系模型之間建立一種對應關系,比如用一個Java的Student類,去對應數據庫中的一張student表,類中的屬性和表中的列一一對應。Student類就對應student表,一個Student對象就對應student表中的一行數據
-
為什么mybatis是半自動的ORM框架?
用mybatis進行開發,需要手動編寫SQL語句。而全自動的ORM框架,如hibernate,則不需要編寫SQL語句。用hibernate開發,只需要定義好ORM映射關系,就可以直接進行CRUD操作了。由于mybatis需要手寫SQL語句,所以它有較高的靈活性,可以根據需要,自由地對SQL進行定制,也因為要手寫SQL,當要切換數據庫時,SQL語句可能就要重寫,因為不同的數據庫有不同的方言(Dialect),所以mybatis的數據庫無關性低。雖然mybatis需要手寫SQL,但相比JDBC,它提供了輸入映射和輸出映射,可以很方便地進行SQL參數設置,以及結果集封裝。并且還提供了關聯查詢和動態SQL等功能,極大地提升了開發的效率。并且它的學習成本也比hibernate低很多
-
快速入門
只需要通過如下幾個步驟,即可用mybatis快速進行持久層的開發
- 編寫全局配置文件
- 編寫mapper映射文件
- 加載全局配置文件,生成SqlSessionFactory
- 創建SqlSession,調用mapper映射文件中的SQL語句來執行CRUD操作
原生開發示例
-
在本地虛擬機mysql上創建一個庫yogurt,并在里面創建一張student表
-
打開IDEA,創建一個maven項目
-
導入依賴的jar包
<dependencies><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.10</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.4.6</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>provided</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.10</version><scope>test</scope></dependency></dependencies> -
創建一個po類
package com.yogurt.po;import lombok.*;@Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString public class Student {private Integer id;private String name;private Integer score;private Integer age;private Integer gender;} -
編寫mapper映射文件(編寫SQL)
<!-- StudentMapper.xml --> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="test"><select id="findAll" resultType="com.yogurt.po.Student">SELECT * FROM student;</select><insert id="insert" parameterType="com.yogurt.po.Student">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});</insert><delete id="delete" parameterType="int">DELETE FROM student WHERE id = #{id};</delete> </mapper> -
編寫數據源properties文件
db.url=jdbc:mysql://192.168.183.129:3306/yogurt?characterEncoding=utf8 db.user=root db.password=root db.driver=com.mysql.jdbc.Driver -
編寫全局配置文件(主要是配置數據源信息)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration><!-- 配置文件信息 --><properties resource="properties/db.properties"></properties><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><!-- 從配置文件中加載屬性 --><property name="driver" value="${db.driver}"/><property name="url" value="${db.url}"/><property name="username" value="${db.user}"/><property name="password" value="${db.password}"/></dataSource></environment></environments><mappers><!-- 加載前面編寫的SQL語句的文件 --><mapper resource="StudentMapper.xml"/></mappers></configuration> -
編寫dao類
package com.yogurt.dao;import com.yogurt.po.Student; 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; import java.util.List;public class StudentDao {private SqlSessionFactory sqlSessionFactory;public StudentDao(String configPath) throws IOException {InputStream inputStream = Resources.getResourceAsStream(configPath);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);}public List<Student> findAll() {SqlSession sqlSession = sqlSessionFactory.openSession();List<Student> studentList = sqlSession.selectList("findAll");sqlSession.close();return studentList;}public int addStudent(Student student) {SqlSession sqlSession = sqlSessionFactory.openSession();int rowsAffected = sqlSession.insert("insert", student);sqlSession.commit();sqlSession.close();return rowsAffected;}public int deleteStudent(int id) {SqlSession sqlSession = sqlSessionFactory.openSession();int rowsAffected = sqlSession.delete("delete",id);sqlSession.commit();sqlSession.close();return rowsAffected;} } -
測試
public class SimpleTest {private StudentDao studentDao;@Beforepublic void init() throws IOException {studentDao = new StudentDao("mybatis-config.xml");}@Testpublic void insertTest() {Student student = new Student();student.setName("yogurt");student.setAge(24);student.setGender(1);student.setScore(100);studentDao.addStudent(student);}@Testpublic void findAllTest() {List<Student> all = studentDao.findAll();all.forEach(System.out::println);} }
總結:
- 編寫mapper.xml,書寫SQL,并定義好SQL的輸入參數,和輸出參數
- 編寫全局配置文件,配置數據源,以及要加載的mapper.xml文件
- 通過全局配置文件,創建SqlSessionFactory
- 每次進行CRUD時,通過SqlSessionFactory創建一個SqlSession
- 調用SqlSession上的
selectOne,selectList,insert,delete,update等方法,傳入mapper.xml中SQL標簽的id,以及輸入參數
注意要點
-
全局配置文件中,各個標簽要按照如下順序進行配置,因為mybatis加載配置文件的源碼中是按照這個順序進行解析的
<configuration><!-- 配置順序如下properties settingstypeAliasestypeHandlersobjectFactorypluginsenvironmentsenvironmenttransactionManagerdataSourcemappers--> </configuration>各個子標簽說明如下
-
<properties>一般將數據源的信息單獨放在一個properties文件中,然后用這個標簽引入,在下面environment標簽中,就可以用
${}占位符快速獲取數據源的信息 -
<settings>用來開啟或關閉mybatis的一些特性,比如可以用
<setting name="lazyLoadingEnabled" value="true"/>來開啟延遲加載,可以用<settings name="cacheEnabled" value="true"/>來開啟二級緩存 -
<typeAliases>在mapper.xml中需要使用
parameterType和resultType屬性來配置SQL語句的輸入參數類型和輸出參數類型,類必須要寫上全限定名,比如一個SQL的返回值映射為Student類,則resultType屬性要寫com.yogurt.po.Student,這太長了,所以可以用別名來簡化書寫,比如<typeAliases><typeAlias type="com.yogurt.po.Student" alias="student"/> </typeAliases>之后就可以在
resultType上直接寫student,mybatis會根據別名配置自動找到對應的類。當然,如果想要一次性給某個包下的所有類設置別名,可以用如下的方式
<typeAliases><package name="com.yogurt.po"/> </typeAliases>如此,指定包下的所有類,都會以簡單類名的小寫形式,作為它的別名
另外,對于基本的Java類型 -> 8大基本類型以及包裝類,以及String類型,mybatis提供了默認的別名,別名為其簡單類名的小寫,比如原本需要寫
java.lang.String,其實可以簡寫為string -
<typeHandlers>用于處理Java類型和Jdbc類型之間的轉換,mybatis有許多內置的TypeHandler,比如StringTypeHandler,會處理Java類型String和Jdbc類型CHAR和VARCHAR。這個標簽用的不多
-
<objectFactory>mybatis會根據
resultType或resultMap的屬性來將查詢得到的結果封裝成對應的Java類,它有一個默認的DefaultObjectFactory,用于創建對象實例,這個標簽用的也不多 -
<plugins>可以用來配置mybatis的插件,比如在開發中經常需要對查詢結果進行分頁,就需要用到pageHelper分頁插件,這些插件就是通過這個標簽進行配置的。在mybatis底層,運用了責任鏈模式+動態代理去實現插件的功能
<!-- PageHelper 分頁插件 --> <plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"><property name="helperDialect" value="mysql"/></plugin> </plugins> -
<environments>用來配置數據源
-
<mappers>用來配置mapper.xml映射文件,這些xml文件里都是SQL語句
-
-
mapper.xml的SQL語句中的占位符
${}和#{}一般會采用
#{},#{}在mybatis中,最后會被解析為?,其實就是Jdbc的PreparedStatement中的?占位符,它有預編譯的過程,會對輸入參數進行類型解析(如果入參是String類型,設置參數時會自動加上引號),可以防止SQL注入,如果parameterType屬性指定的入參類型是簡單類型的話(簡單類型指的是8種java原始類型再加一個String),#{}中的變量名可以任意,如果入參類型是pojo,比如是Student類public class Student{private String name;private Integer age;//setter/getter }那么
#{name}表示取入參對象Student中的name屬性,#{age}表示取age屬性,這個過程是通過反射來做的,這不同于${},${}取對象的屬性使用的是OGNL(Object Graph Navigation Language)表達式而
${},一般會用在模糊查詢的情景,比如SELECT * FROM student WHERE name like '%${name}%';它的處理階段在
#{}之前,它不會做參數類型解析,而僅僅是做了字符串的拼接,若入參的Student對象的name屬性為zhangsan,則上面那條SQL最終被解析為SELECT * FROM student WHERE name like '%zhangsan%';而如果此時用的是
SELECT * FROM student WHERE name like '%#{name}%';這條SQL最終就會變成SELECT * FROM student WHERE name like '%'zhangsan'%';所以模糊查詢只能用${},雖然普通的入參也可以用${},但由于${}不會做類型解析,就存在SQL注入的風險,比如SELECT * FROM user WHERE name = '${name}' AND password = '${password}'我可以讓一個user對象的password屬性為
'OR '1' = '1,最終的SQL就變成了SELECT * FROM user WHERE name = 'yogurt' AND password = ''OR '1' = '1',因為OR '1' = '1'恒成立,這樣攻擊者在不需要知道用戶名和密碼的情況下,也能夠完成登錄驗證另外,對于pojo的入參,
${}中獲取對象屬性的語法和#{}幾乎一樣,但${}在mybatis底層是通過OGNL表達式語言進行處理的,這跟#{}的反射處理有所不同對于簡單類型(8種java原始類型再加一個String)的入參,
${}中參數的名字必須是value,例子如下<select id="fuzzyCount" parameterType="string" resultType="int">SELECT count(1) FROM `user` WHERE name like '%${value}%' </select>為什么簡單類型的變量名必須為value呢?因為mybatis源碼中寫死的value,哈哈
上面其實是比較原始的開發方式,我們需要編寫dao類,針對mapper.xml中的每個SQL標簽,做一次封裝,SQL標簽的id要以字符串的形式傳遞給SqlSession的相關方法,容易出錯,非常不方便;為了簡化開發,mybatis提供了mapper接口代理的開發方式,不需要再編寫dao類,只需要編寫一個mapper接口,一個mapper的接口和一個mapper.xml相對應,只需要調用SqlSession對象上的getMapper(),傳入mapper接口的class信息,即可獲得一個mapper代理對象,直接調用mapper接口中的方法,即相當于調用mapper.xml中的各個SQL標簽,此時就不需要指定SQL標簽的id字符串了,mapper接口中的一個方法,就對應了mapper.xml中的一個SQL標簽
基于Mapper代理的示例
全局配置文件和mapper.xml文件是最基本的配置,仍然需要。不過,這次我們不編寫dao類,我們直接創建一個mapper接口
package com.yogurt.mapper;import com.yogurt.po.Student;import java.util.List;public interface StudentMapper {List<Student> findAll();int insert(Student student);int delete(Integer id);List<Student> findByName(String value);
}
而我們的mapper.xml文件如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.yogurt.mapper.StudentMapper"><select id="findAll" resultType="com.yogurt.po.Student">SELECT * FROM student;</select><insert id="insert" parameterType="com.yogurt.po.Student">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});</insert><delete id="delete" parameterType="int">DELETE FROM student WHERE id = #{id};</delete><select id="findByName" parameterType="string" resultType="student">SELECT * FROM student WHERE name like '%${value}%';</select>
</mapper>
mapper接口和mapper.xml之間需要遵循一定規則,才能成功的讓mybatis將mapper接口和mapper.xml綁定起來
- mapper接口的全限定名,要和mapper.xml的namespace屬性一致
- mapper接口中的方法名要和mapper.xml中的SQL標簽的id一致
- mapper接口中的方法入參類型,要和mapper.xml中SQL語句的入參類型一致
- mapper接口中的方法出參類型,要和mapper.xml中SQL語句的返回值類型一致
測試代碼如下
public class MapperProxyTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void init() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);}@Testpublic void test() {SqlSession sqlSession = sqlSessionFactory.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> studentList = mapper.findAll();studentList.forEach(System.out::println);}
}
結果如下
這個mapper接口,mybatis會自動找到對應的mapper.xml,然后對mapper接口使用動態代理的方式生成一個代理類
基于注解的示例
如果實在看xml配置文件不順眼,則可以考慮使用注解的開發方式,不過注解的開發方式,會將SQL語句寫到代碼文件中,后續的維護性和擴展性不是很好(如果想修改SQL語句,就得改代碼,得重新打包部署,而如果用xml方式,則只需要修改xml,用新的xml取替換舊的xml即可)
使用注解的開發方式,也還是得有一個全局配置的xml文件,不過mapper.xml就可以省掉了,具體操作只用2步,如下
-
創建一個Mapper接口
package com.yogurt.mapper; import com.yogurt.po.Student; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import java.util.List;public interface PureStudentMapper {@Select("SELECT * FROM student")List<Student> findAll();@Insert("INSERT INTO student (name,age,score,gender) VALUES (#{name},#{age},#{score},#{gender})")int insert(Student student); } -
在全局配置文件中修改
<mappers>標簽,直接指定加載這個類<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration><properties resource="properties/db.properties"></properties><typeAliases><package name="com.yogurt.po"/></typeAliases><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${db.driver}"/><property name="url" value="${db.url}"/><property name="username" value="${db.user}"/><property name="password" value="${db.password}"/></dataSource></environment></environments><mappers><mapper class="com.yogurt.mapper.PureStudentMapper"/></mappers></configuration>
測試代碼如下
public class PureMapperTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void init() throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);}@Testpublic void test() {SqlSession sqlSession = sqlSessionFactory.openSession();PureStudentMapper mapper = sqlSession.getMapper(PureStudentMapper.class);mapper.insert(new Student(10,"Tomcat",120,60,0));sqlSession.commit();List<Student> studentList = mapper.findAll();studentList.forEach(System.out::println);}
}
結果如下
注:當使用注解開發時,若需要傳入多個參數,可以結合@Param注解,示例如下
package org.mybatis.demo.mapper;import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.mybatis.demo.po.Student;import java.util.List;public interface PureStudentMapper {@Select("SELECT * FROM student WHERE name like '%${name}%' AND major like '%${major}%'")List<Student> find(@Param("name") String name, @Param("major") String major);
}
@Param標簽會被mybatis處理并封裝成一個Map對象,比如上面的示例中,實際傳入的參數是一個Map對象,@Param標簽幫忙向Map中設置了值,即它做了
Map<String,Object> map = new HashMap<>();
map.put("name", name);
map.put("major",major);
將方法形參中的name和major放到了map對象中,所以在@Select標簽中可以用${name}和${major}取出map對象中的值。
--------------------(我是分割線)
上面我們見到了在全局配置文件中,兩種配置mapper的方式,分別是
<!-- 在mapper接口中使用注解 -->
<mappers><mapper class="com.yogurt.mapper.PureStudentMapper"/>
</mappers><!-- 普通加載xml -->
<mappers><mapper resource="StudentMapper.xml"/>
</mappers>
而在實際工作中,一般我們會將一張表的SQL操作封裝在一個mapper.xml中,可能有許多張表需要操作,那么我們是不是要在<mappers>標簽下寫多個<mapper>標簽呢?其實不用,還有第三種加載mapper的方法,使用<package>標簽
<mappers><package name="com.yogurt.mapper"/>
</mappers>
這樣就會自動加載com.yogurt.mapper包下的所有mapper,這種方式需要將mapper接口文件和mapper.xml文件都放在com.yogurt.mapper包下,且接口文件和xml文件的文件名要一致。注意,在IDEA的maven開發環境下,maven中還需配置<resources>標簽,否則maven打包不會將java源碼目錄下的xml文件打包進去,見下文
三種加載mapper的方式總結
-
<mapper resource="" />加載普通的xml文件,傳入xml的相對路徑(相對于類路徑)
-
<mapper class="" />使用mapper接口的全限定名來加載,若mapper接口采用注解方式,則不需要xml;若mapper接口沒有采用注解方式,則mapper接口和xml文件的名稱要相同,且在同一個目錄
-
<package name="" />掃描指定包下的所有mapper,若mapper接口采用注解方式,則不需要xml;若mapper接口沒有采用注解方式,則mapper接口和xml文件的名稱要相同,且在同一目錄
注意:用后兩種方式加載mapper接口和mapper.xml映射文件時,可能會報錯
仔細檢查了一下,mapper接口文件和xml映射文件確實放在了同一個目錄下,而且文件名一致,xml映射文件的namespace也和mapper接口的全限定名對的上。為什么會這樣呢?
其實是因為,對于src/main/java 源碼目錄下的文件,maven打包時只會將該目錄下的java文件打包,而其他類型的文件都不會被打包進去,去工程目錄的target目錄下看看maven構建后生成的文件
我們需要在pom.xml中的<build> 標簽下 添加<resources> 標簽,指定打包時要將xml文件打包進去
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource></resources>
</build>
此時再用maven進行打包,看到對應目錄下有了xml映射文件(特別注意,這里配置了pom.xml下的resource標簽后,可能會引發一些問題,例如原本src/main/resources資源目錄下的文件沒有被打包進來,參考我的這篇文章maven打包時的資源文件問題)
此時再運行單元測試,就能正常得到結果了
應用場景
主鍵返回
通常我們會將數據庫表的主鍵id設為自增。在插入一條記錄時,我們不設置其主鍵id,而讓數據庫自動生成該條記錄的主鍵id,那么在插入一條記錄后,如何得到數據庫自動生成的這條記錄的主鍵id呢?有兩種方式
-
使用
useGeneratedKeys和keyProperty屬性<insert id="insert" parameterType="com.yogurt.po.Student" useGeneratedKeys="true" keyProperty="id">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});</insert> -
使用
<selectKey>子標簽<insert id="insert" parameterType="com.yogurt.po.Student">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});<selectKey keyProperty="id" order="AFTER" resultType="int" >SELECT LAST_INSERT_ID();</selectKey></insert>如果使用的是mysql這樣的支持自增主鍵的數據庫,可以簡單的使用第一種方式;對于不支持自增主鍵的數據庫,如oracle,則沒有主鍵返回這一概念,而需要在插入之前先生成一個主鍵。此時可以用
<selectKey>標簽,設置其order屬性為BEFORE,并在標簽體內寫上生成主鍵的SQL語句,這樣在插入之前,會先處理<selectKey>,生成主鍵,再執行真正的插入操作。<selectKey>標簽其實就是一條SQL,這條SQL的執行,可以放在主SQL執行之前或之后,并且會將其執行得到的結果封裝到入參的Java對象的指定屬性上。注意<selectKey>子標簽只能用在<insert>和<update>標簽中。上面的LAST_INSERT_ID()實際上是MySQL提供的一個函數,可以用來獲取最近插入或更新的記錄的主鍵id。
測試代碼如下
public class MapperProxyTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void init() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);}@Testpublic void test() {SqlSession sqlSession = sqlSessionFactory.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);Student student = new Student(-1, "Podman", 130, 15, 0);mapper.insert(student);sqlSession.commit();System.out.println(student.getId());}
}
結果如下
批量查詢
主要是動態SQL標簽的使用,注意如果parameterType是List的話,則在標簽體內引用這個List,只能用變量名list,如果parameterType是數組,則只能用變量名array
<select id="batchFind" resultType="student" parameterType="java.util.List">SELECT * FROM student<where><if test="list != null and list.size() > 0">AND id in<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach></if></where>
</select>
@Testpublic void testBatchQuery() {SqlSession sqlSession = sqlSessionFactory.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> students = mapper.batchFind(Arrays.asList(1, 2, 3, 7, 9));students.forEach(System.out::println);}
結果
動態SQL
可以根據具體的參數條件,來對SQL語句進行動態拼接。
比如在以前的開發中,由于不確定查詢參數是否存在,許多人會使用類似于where 1 = 1 來作為前綴,然后后面用AND 拼接要查詢的參數,這樣,就算要查詢的參數為空,也能夠正確執行查詢,如果不加1 = 1,則如果查詢參數為空,SQL語句就會變成SELECT * FROM student where ,SQL不合法。
mybatis里的動態標簽主要有
-
if<!-- 示例 --> <select id="find" resultType="student" parameterType="student">SELECT * FROM student WHERE age >= 18<if test="name != null and name != ''">AND name like '%${name}%'</if> </select>當滿足test條件時,才會將
<if>標簽內的SQL語句拼接上去 -
choose<!-- choose 和 when , otherwise 是配套標簽 類似于java中的switch,只會選中滿足條件的一個 --> <select id="findActiveBlogLike"resultType="Blog">SELECT * FROM BLOG WHERE state = ‘ACTIVE’<choose><when test="title != null">AND title like #{title}</when><when test="author != null and author.name != null">AND author_name like #{author.name}</when><otherwise>AND featured = 1</otherwise></choose> </select> -
trim-
where<where>標簽只會在至少有一個子元素返回了SQL語句時,才會向SQL語句中添加WHERE,并且如果WHERE之后是以AND或OR開頭,會自動將其刪掉<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><where>標簽可以用<trim>標簽代替<trim prefix="WHERE" prefixOverrides="AND | OR">... </trim> -
set在至少有一個子元素返回了SQL語句時,才會向SQL語句中添加SET,并且如果SET之后是以
,開頭的話,會自動將其刪掉<set>標簽相當于如下的<trim>標簽<trim prefix="SET" prefixOverrides=",">... </trim>
可以通過
<trim>標簽更加靈活地對SQL進行定制實際上在mybatis源碼,也能看到trim與set,where標簽的父子關系
-
-
foreach用來做迭代拼接的,通常會與SQL語句中的
IN查詢條件結合使用,注意,到parameterType為List(鏈表)或者Array(數組),后面在引用時,參數名必須為list或者array。如在foreach標簽中,collection屬性則為需要迭代的集合,由于入參是個List,所以參數名必須為list<select id="batchFind" resultType="student" parameterType="list">SELECT * FROM student WHERE id in<foreach collection="list" item="item" open="(" separator="," close=")">#{item}</foreach> </select> -
sql可將重復的SQL片段提取出來,然后在需要的地方,使用
<include>標簽進行引用<select id="findUser" parameterType="user" resultType="user">SELECT * FROM user<include refid="whereClause"/> </select><sql id="whereClause"><where><if test="user != null">AND username like '%${user.name}%'</if></where> </sql> -
bindmybatis的動態SQL都是用OGNL表達式進行解析的,如果需要創建OGNL表達式以外的變量,可以用bind標簽
<select id="selectBlogsLike" resultType="Blog"><bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />SELECT * FROM BLOGWHERE title LIKE #{pattern} </select>
緩存
-
一級緩存
默認開啟,同一個SqlSesion級別共享的緩存,在一個SqlSession的生命周期內,執行2次相同的SQL查詢,則第二次SQL查詢會直接取緩存的數據,而不走數據庫,當然,若第一次和第二次相同的SQL查詢之間,執行了DML(INSERT/UPDATE/DELETE),則一級緩存會被清空,第二次查詢相同SQL仍然會走數據庫
一級緩存在下面情況會被清除
- 在同一個SqlSession下執行增刪改操作時(不必提交),會清除一級緩存
- SqlSession提交或關閉時(關閉時會自動提交),會清除一級緩存
- 對mapper.xml中的某個CRUD標簽,設置屬性
flushCache=true,這樣會導致該MappedStatement的一級緩存,二級緩存都失效(一個CRUD標簽在mybatis中會被封裝成一個MappedStatement) - 在全局配置文件中設置
<setting name="localCacheScope" value="STATEMENT"/>,這樣會使一級緩存失效,二級緩存不受影響
-
二級緩存
默認關閉,可通過全局配置文件中的
<settings name="cacheEnabled" value="true"/>開啟二級緩存總開關,然后在某個具體的mapper.xml中增加<cache />,即開啟了該mapper.xml的二級緩存。二級緩存是mapper級別的緩存,粒度比一級緩存大,多個SqlSession可以共享同一個mapper的二級緩存。注意開啟二級緩存后,SqlSession需要提交,查詢的數據才會被刷新到二級緩存當中
緩存的詳細分析可以參考我之前的文章 => 極簡mybatis緩存
關聯查詢
使用<resultMap> 標簽以及<association>和<collection> 子標簽,進行關聯查詢,比較簡單,不多說
延遲加載
延遲加載是結合關聯查詢進行應用的。也就是說,只在<association>和<collection> 標簽上起作用
對于關聯查詢,若不采用延遲加載策略,而是一次性將關聯的從信息都查詢出來,則在主信息比較多的情況下,會產生N+1問題,導致性能降低。比如用戶信息和訂單信息是一對多的關系,在查詢用戶信息時,設置了關聯查詢訂單信息,如不采用延遲加載策略,假設共有100個用戶,則我們查這100個用戶的基本信息只需要一次SQL查詢
select * from user;
若開啟了關聯查詢,且不是延遲加載,則對于這100個用戶,會發出100條SQL去查用戶對應的訂單信息,這樣會造成不必要的性能開銷(其實我認為稱之為1+N問題更為合適)
select * from orders where u_id = 1;
select * from orders where u_id = 2;
....
select * from orders where u_id = 100;
當我們可能只關心id=3的用戶的訂單信息,則很多的關聯信息是無用的,于是,采用延遲加載策略,可以按需加載從信息,在需要某個主信息對應的從信息時,再發送SQL去執行查詢,而不是一次性全部查出來,這樣能很好的提升性能。
另外,針對N+1問題,除了采用延遲加載的策略按需進行關聯查詢。如果在某些場景下,確實需要查詢所有主信息關聯的從信息。在上面的例子中,就是如果確實需要把這100個用戶關聯的訂單信息全部查詢出來,那怎么辦呢?這里提供2個解決思路。
1是采用連接查詢,只使用1條SQL即可,如下
select * from user as u left join orders as o on u.id = o.u_id;
但使用連接查詢查出來的結果是兩表的笛卡爾積,還需要自行進行數據的分組處理
2是使用兩個步驟來完成,先執行一條SQL,查出全部的用戶信息,并把用戶的id放在一個集合中,然后第二條SQL采用IN關鍵字查詢即可。這種方式也可以簡化為子查詢,如下
select * from orders where u_id in (select id from user);
現在說回來,mybatis的延遲加載默認是關閉的,可以通過全局配置文件中的<setting name="lazyLoadingEnabled" value="true"/>來開啟,開啟后,所有的SELECT查詢,若有關聯對象,都會采用延遲加載的策略。當然,也可以對指定的某個CRUD標簽單獨禁用延遲加載策略,通過設置SELECT標簽中的fetchType=eager,則可以關閉該標簽的延遲加載。
(還有一個侵入式延遲加載的概念,在配置文件中通過<setting name="aggressiveLazyLoading" value="true">來開啟,大概是說,訪問主對象中的主信息時,就會觸發延遲加載,將從信息查詢上來,這其實并不是真正意義的延遲加載,真正意義上的延遲加載應該是訪問主對象中的從信息時,才觸發延遲加載,去加載從信息,侵入式延遲加載默認是關閉的,一般情況下可以不用管他)
注意,延遲加載在關聯查詢的場景下才有意義。需要配合<resultMap>標簽下的<association>和<collecction> 標簽使用
<!-- StudentMapper.xml -->
<resultMap id="studentExt" type="com.yogurt.po.StudentExt"><result property="id" column="id"/><result property="name" column="name"/><result property="score" column="score"/><result property="age" column="age"/><result property="gender" column="gender"/><!-- 當延遲加載總開關開啟時,resultMap下的association和collection標簽中,若通過select屬性指定嵌套查詢的SQL,則其fetchType默認是lazy的,當在延遲加載總開關開啟時,需要對個別的關聯查詢禁用延遲加載時,才有必要配置fetchType = eager --><!--column用于指定用于關聯查詢的列property用于指定要封裝到StudentExt中的哪個屬性javaType用于指定關聯查詢得到的對象select用于指定關聯查詢時,調用的是哪一個DQL--><association property="clazz" javaType="com.yogurt.po.Clazz" column="class_id"select="com.yogurt.mapper.ClassMapper.findById" fetchType="lazy"/></resultMap><select id="findLazy" parameterType="string" resultMap="studentExt">SELECT * FROM student WHERE name like '%${value}%';</select>
<!-- com.yogurt.mapper.ClassMapper -->
<select id="findById" parameterType="int" resultType="com.yogurt.po.Clazz">SELECT * FROM class WHERE id = #{id}
</select>
/** 用于封裝關聯查詢的對象 **/
public class StudentExt{private Integer id;private String name;private Integer score;private Integer age;private Integer gender;/** 關聯對象 **/private Clazz clazz;//getter/setter
}
逆向工程
mybatis官方提供了mapper自動生成工具mybatis-generator-core來針對單表,生成PO類,以及Mapper接口和mapper.xml映射文件。針對單表,可以不需要再手動編寫xml配置文件和mapper接口文件了,非常方便。美中不足的是它不支持生成關聯查詢。一般做關聯查詢,就自己單獨寫SQL就好了。
基于IDEA的mybatis逆向工程操作步驟如下
-
配置maven插件
<build><plugins><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.7</version><configuration><!-- 輸出日志 --><verbose>true</verbose><overwrite>true</overwrite></configuration></plugin></plugins></build> -
在resources目錄下創建名為generatorConfig.xml的配置文件
-
配置文件的模板如下
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration><!--導入屬性配置--><properties resource="properties/xx.properties"></properties><!-- 指定數據庫驅動的jdbc驅動jar包的位置 --><classPathEntry location="C:\Users\Vergi\.m2\repository\mysql\mysql-connector-java\8.0.11\mysql-connector-java-8.0.11.jar" /><!-- context 是逆向工程的主要配置信息 --><!-- id:起個名字 --><!-- targetRuntime:設置生成的文件適用于那個 mybatis 版本 --><context id="default" targetRuntime="MyBatis3"><!--optional,旨在創建class時,對注釋進行控制--><commentGenerator><property name="suppressDate" value="true" /><!-- 是否去除自動生成的注釋 true:是 : false:否 --><property name="suppressAllComments" value="true" /></commentGenerator><!--jdbc的數據庫連接--><jdbcConnection driverClass="${db.driver}"connectionURL="${db.url}"userId="${db.user}"password="${db.password}"></jdbcConnection><!--非必須,類型處理器,在數據庫類型和java類型之間的轉換控制--><javaTypeResolver><!-- 默認情況下數據庫中的 decimal,bigInt 在 Java 對應是 sql 下的 BigDecimal 類 --><!-- 不是 double 和 long 類型 --><!-- 使用常用的基本類型代替 sql 包下的引用類型 --><property name="forceBigDecimals" value="false" /></javaTypeResolver><!-- targetPackage:生成的實體類所在的包 --><!-- targetProject:生成的實體類所在的硬盤位置 --><javaModelGenerator targetPackage="mybatis.generator.model"targetProject=".\src\main\java"><!-- 是否允許子包 --><property name="enableSubPackages" value="false" /><!-- 是否清理從數據庫中查詢出的字符串左右兩邊的空白字符 --><property name="trimStrings" value="true" /></javaModelGenerator><!-- targetPackage 和 targetProject:生成的 mapper.xml 文件的包和位置 --><sqlMapGenerator targetPackage="mybatis.generator.mappers"targetProject=".\src\main\resources"><!-- 針對數據庫的一個配置,是否把 schema 作為字包名 --><property name="enableSubPackages" value="false" /></sqlMapGenerator><!-- targetPackage 和 targetProject:生成的 mapper接口文件的包和位置 --><javaClientGenerator type="XMLMAPPER"targetPackage="mybatis.generator.dao" targetProject=".\src\main\java"><!-- 針對 oracle 數據庫的一個配置,是否把 schema 作為子包名 --><property name="enableSubPackages" value="false" /></javaClientGenerator><!-- 這里指定要生成的表 --><table tableName="student"/><table tableName="product"/></context> </generatorConfiguration> -
雙擊執行mybatis-generator的maven插件
執行日志如下
生成的文件如下
能看到mybatis-generator除了給我們生成了基本的PO類(上圖的Student和Product),還額外生成了Example類。Example類是為了方便執行SQL時傳遞查詢條件的。使用的示例如下
public class GeneratorTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void init() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("mysql8-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);}@Testpublic void test() {SqlSession sqlSession = sqlSessionFactory.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);StudentExample example = new StudentExample();StudentExample.Criteria criteria = example.createCriteria();criteria.andNameLike("%o%");List<Student> students = mapper.selectByExample(example);students.forEach(System.out::println);}
}
結果如下
PageHelper分頁插件
使用該插件,快速實現查詢結果的分頁,使用步驟如下
-
pom.xml中配置依賴
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.1.6</version> </dependency> -
mybatis全局配置文件中配置
<plugin>標簽<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration><properties resource="properties/xx.properties"></properties><plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"><property name="helperDialect" value="mysql"/></plugin></plugins><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${db.driver}"/><property name="url" value="${db.url}"/><property name="username" value="${db.user}"/><property name="password" value="${db.password}"/></dataSource></environment></environments><mappers><package name="mybatis.generator.dao"/></mappers></configuration> -
在執行查詢之前,先設置分頁信息
// 查詢第一頁,每頁3條信息 PageHelper.startPage(1,3);先看一下查所有數據
@Testpublic void test() {SqlSession sqlSession = sqlSessionFactory.openSession();ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);//PageHelper.startPage(1,3);List<Product> products = mapper.selectByExample(new ProductExample());products.forEach(System.out::println);}加上PageHelper分頁
@Testpublic void test() {SqlSession sqlSession = sqlSessionFactory.openSession();ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);PageHelper.startPage(1,3);List<Product> products = mapper.selectByExample(new ProductExample());products.forEach(System.out::println);}
特別注意:在編寫mapper.xml的時候,SQL語句的結尾不要帶上;,因為PageHelper插件是在SQL末尾拼接LIMIT關鍵字來進行分頁的,若SQL語句帶上了;,就會造成SQL語法錯誤
另外,PageHelper會先查詢總數量,然后再發出分頁查詢,打開mybatis的日志時,可以看到發出了2條SQL
當開啟PageHelper時,查詢得到的List實際是PageHelper中自定義的一個類Page,這個類實現了List接口,并封裝了分頁的相關信息(總頁數,當前頁碼等)。
可以通過PageInfo來獲取分頁的相關信息,代碼如下@Test public void test() {SqlSession sqlSession = factory.openSession();PageHelper.startPage(1,3);ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);List<Product> list = mapper.findAll();list.forEach(System.out::println);PageInfo<Product> pageInfo = new PageInfo<>(list);System.out.println(pageInfo.getTotal()); // 獲得總數System.out.println(pageInfo.getPageSize()); // 獲得總頁數 }PageHelper插件的源碼分析可以查看我之前的文章 =>
極簡PageHelper源碼分析
Mybatis Plus
mybatis雖然非常方便,但也需要編寫大量的SQL語句,于是mybatis plus就應運而生了。它是一個mybatis增強工具,為了簡化開發,提高效率。搭配Spring-Boot食用簡直不要太爽。
可以參考我的這篇文章 mybatis-plus一發入魂 ,或者mybatis-plus官網,以及慕課網的入門教程和進階教程
(完)
注:該文是一篇較為全面詳細的筆記,內容篇幅很長。當對mybatis的使用較為熟練后,可以查看這篇極為簡短的 mybatis精髓總結,從整體架構和源碼層面上把握mybatis。
總結
以上是生活随笔為你收集整理的mybatis看这一篇就够了,简单全面一发入魂的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小G有一个大树
- 下一篇: 【每日一题】8月10日题目精讲—排座椅