前言
之前針對MyBatis的使用進行過總結,但是現在在看spring源碼的過程中,爲了看懂spring第三方框架集成的原理,就重新開始研究mybatis發現之前總結的有些不太具體,這裏重新梳理一下,順便看看MyBatis源碼
準備工作
需要建立一個項目,pom依賴如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.learn</groupId>
<artifactId>mybatis-learn</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<spring.version>5.1.3.RELEASE</spring.version>
<slf4j.version>1.7.12</slf4j.version>
<mysql.version>5.1.47</mysql.version>
<mybatis.version>3.5.1</mybatis.version>
<mybatis-spring.version>2.0.0</mybatis-spring.version>
<druid.version>1.1.14</druid.version>
<lombok.version>1.18.6</lombok.version>
<jackson.version>2.9.7</jackson.version>
<pagehelper.version>5.0.0</pagehelper.version>
<junit.version>4.12</junit.version>
<mbg.version>1.3.5</mbg.version>
<c3p0.version>0.9.1</c3p0.version>
<cglib.version>3.2.5</cglib.version>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!--mybatis 和Spring整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<!--
<version>3.3.0-SNAPSHOT</version>
-->
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.29</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.29</version>
</dependency>
</dependencies>
</project>
需要執行的SQL
create database mybatis_source_learn;
CREATE TABLE `blog` (
`bid` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`author_id` int(11) DEFAULT NULL,
PRIMARY KEY (`bid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `author` (
`author_id` int(16) NOT NULL AUTO_INCREMENT,
`author_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`author_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1002 DEFAULT CHARSET=utf8;
CREATE TABLE `comment` (
`comment_id` int(16) NOT NULL AUTO_INCREMENT,
`content` varchar(255) DEFAULT NULL,
`bid` int(16) DEFAULT NULL,
PRIMARY KEY (`comment_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `blog` (`bid`, `name`, `author_id`) VALUES (1, '這裏是博客one', 1001);
INSERT INTO `blog` (`bid`, `name`, `author_id`) VALUES (2, '這裏是博客two', 1008);
INSERT INTO `author` (`author_id`, `author_name`) VALUES (1001, '江戶川柯南');
INSERT INTO `comment` (`comment_id`, `content`, `bid`) VALUES (1, '寫得真好,學習了', 1);
INSERT INTO `comment` (`comment_id`, `content`, `bid`) VALUES (2, '剛好碰到這個問題,謝謝', 1);
其他的數據庫訪問框架
JDBC的時代
JDBC是最原始的時代,我們直接固定寫好連接,然後根據表的字段類型和字段名稱需要手動獲取數據並處理相關類型轉換
具體的一個實例如下:
/**
* autor:liman
* createtime:2020/4/26
* comment:jdbc的簡單實例
*/
@Slf4j
public class JdbcMain {
private static final String className = "com.mysql.jdbc.Driver";
private static final String connectionString = "jdbc:mysql://localhost:3306/mybatis_source_learn?useSSL=true";
private static final String username = "root";
private static final String password = "root";
public static void main(String[] args) {
Connection conn = null;
Statement statement = null;
Blog blog = new Blog();
try {
//1、加載指定的類對象(SPI)
Class.forName(className);
//2、獲取連接對象
conn = DriverManager.getConnection(connectionString,username,password);
//3、獲取statement對象
statement = conn.createStatement();
String sql= "select bid,name,author_id from blog";
//4、通過statement對象執行SQL語句獲得最後的結果集
ResultSet resultSet= statement.executeQuery(sql);
//處理結果集
while(resultSet.next()){
Integer bid = resultSet.getInt("bid");
String name = resultSet.getString("name");
Integer authorId = resultSet.getInt("author_id");
blog.setAuthorId(authorId);
blog.setBid(bid);
blog.setName(name);
log.info("blog對象:{}",blog);
}
//關閉並釋放一些資源
resultSet.close();
statement.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
運行結果如下:
我們可以看到JDBC操作數據源存在的一些問題:
1、需要手動獲取和關閉資源,2、代碼重複,3、業務邏輯與數據操作的代碼耦合在一起,4、結果集需要手動處理。
數據源和DBUtils
這裏以hikari爲例
先看HikariUtils
package com.learn.dbutils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.SQLException;
public class HikariUtil {
private static final String PROPERTY_PATH = "/hikari.properties";
private static final Logger LOGGER = LoggerFactory.getLogger(HikariUtil.class);
//dataSource
private static HikariDataSource dataSource;
//用於處理結果集的QueryRunner
private static QueryRunner queryRunner;
//初始化的時候,初始化數據源,並通過數據源初始化QueryRunner
public static void init() {
HikariConfig config = new HikariConfig(PROPERTY_PATH);
dataSource = new HikariDataSource(config);
queryRunner = new QueryRunner(dataSource);
}
public static QueryRunner getQueryRunner() {
check();
return queryRunner;
}
public static Connection getConnection() {
check();
try {
Connection connection = dataSource.getConnection();
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static void close(Connection connection) {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private static void check() {
if (dataSource == null || queryRunner == null) {
throw new RuntimeException("DataSource has not been init");
}
}
}
模擬一個數據訪問層對象
/**
這裏的QueryRunner作用似乎和我們接觸的SqlSession一樣
*/
@Slf4j
public class BlogDao {
private static QueryRunner queryRunner;
static{
queryRunner = HikariUtil.getQueryRunner();
}
public static void selectBlog(Integer bid) throws SQLException {
String sql = "select * from blog where bid = ?";
Object[] params = new Object[]{bid};
Blog blog = queryRunner.query(sql,new BeanHandler<>(Blog.class),params);
log.info("blog:{}",blog);
}
public static void selectList() throws SQLException {
String sql = "select * from blog";
List<Blog> list = queryRunner.query(sql,new BeanListHandler<>(Blog.class));
log.info("blog list:{}",list);
}
}
測試的類
public class DBUtilMain {
public static void main(String[] args) throws SQLException {
HikariUtil.init();
BlogDao.selectBlog(1);
BlogDao.selectList();
}
}
運行結果
可以看到其詳細的初始化的過程。
用腳指頭應該能想到,這個操作最後的數據結果集映射是通過反射來處理的。
JdbcTemplate
spring中也爲我們封裝了一套訪問數據庫的操作——spring jdbc,通常我們打交道的就是其中的JdbcTemplate這個對象
配置完數據源,並將數據源注入給JdbcTemplate
<bean id="jdbc" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath*:jdbc.properties"/>
</bean>
<!-- Druid連接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 基本屬性 url、user、password -->
<property name="driverClassName" value="${druid.driverClassName}" />
<property name="url" value="${druid.url}" />
<property name="username" value="${druid.username}" />
<property name="password" value="${druid.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="maxActive" value="${druid.maxActive}" />
<property name="initialSize" value="${druid.initialSize}" />
<property name="minIdle" value="${druid.minIdle}" />
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="${druid.maxWait}" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />
<property name="testWhileIdle" value="${druid.testWhileIdle}" />
<property name="testOnBorrow" value="${druid.testOnBorrow}" />
<property name="testOnReturn" value="${druid.testOnReturn}" />
<!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" />
<property name="maxOpenPreparedStatements" value="${druid.maxOpenPreparedStatements}" />
<!-- 配置監控統計攔截的filters -->
<property name="filters" value="${druid.filters}" />
<!--<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${druid.public.key}" />-->
</bean>
<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
<property name="dataSource" ref="dataSource"></property>
</bean>
這樣,我們就可以直接通過注入的JdbcTemplate操作數據源了,並且通過RowMapper完成相關的數據映射
@Slf4j
public class JdbcTemplateTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
JdbcTemplate jdbcTemplate = (JdbcTemplate) applicationContext.getBean("jdbcTemplate");
String sql = "select * from tbl_emp";
List<Employee> employeeList = jdbcTemplate.query(sql,new EmployeeRowMapper());
log.info("employeeList:{}",employeeList);
List queryList = jdbcTemplate.query(sql, new BaseRowMapper(Employee.class));
log.info("queryList:{}",queryList);
}
}
其中我們定義了兩種RowMapper的數據結果映射方式,一種是EmploreeRowMapper另一種是BaseRowMapper,BaseRowMapper相當於是針對EmployeeRowMapper的一種抽象。底層也是利用方式完成數據集的映射和處理,這裏簡單貼出EmployeeRowMapper
/**
* @Author: qingshan
* @Date: 2019/3/31 15:50
* 實現RowMapper接口
*/
public class EmployeeRowMapper implements RowMapper {
@Override
public Object mapRow(ResultSet resultSet, int i) throws SQLException {
Employee employee = new Employee();
employee.setEmpId(resultSet.getInt("emp_id"));
employee.setEmpName(resultSet.getString("emp_name"));
employee.setGender(resultSet.getString("gender"));
employee.setEmail(resultSet.getString("email"));
return employ ee;
}
}
上述都是我們比較熟悉的操作訪問數據庫的方式,這些工具演變的過程中,解決了數據結果集的封裝問題,同時也具備了對數據源的支持。但是走到JdbcTemplate的時候,並沒有解決SQL語句的硬編碼問題,也沒有實現實體類到數據庫記錄的映射的功能,同時也沒有提供緩存。
直到ORM框架的出現,ORM框架使得我們操作數據庫可以通過對象去操作。ORM幫我們解決的問題就是:程序對象與關係型數據庫相互映射的問題。
早期我們都接觸過Hibernate,Hibernate本身也是一款比較優秀的框架,但是其也存在一些問題:
1、get()、save()、update()操作,實際操作的是所有的字段,沒有辦法指定部分字段。不夠靈活
2、SQL都是自動生成的,如果要做SQL級別的優化是很麻煩的。性能不夠優秀
3、不支持動態SQL
半自動的ORM框架MyBatis就很好的解決了上面的問題,在MyBatis裏面SQL和代碼是分離的。
mybatis使用實例
編程式使用
之前我們在將mybatis集成到spring中的時候,我們很多配置化都是自動完成的,這樣有些本質我們是看不到的,這裏我們通過先編程式的使用方式來總結mybatis的使用
沒有Mapper接口的方式
在沒有Mapper接口的時代,我們是通過自己手動指定查詢語句的描述符來進行操作的,如以下實例所示:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.mybatis.mapper.BlogMapper">
<!-- 聲明這個namespace使用二級緩存 -->
<cache/>
<resultMap id="BaseResultMap" type="blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" useCache="false">
select * from blog where bid = #{bid}
</select>
<!-- $只能用在自定義類型和map上 -->
<select id="selectBlogByBean" parameterType="blog" resultType="blog" >
select bid, name, author_id authorId from blog where name = '${name}'
</select>
<select id="selectBlogList" resultMap="BaseResultMap" >
select bid, name, author_id authorId from blog
</select>
<update id="updateByPrimaryKey" parameterType="blog">
update blog
set name = #{name,jdbcType=VARCHAR}
where bid = #{bid,jdbcType=INTEGER}
</update>
<insert id="insertBlog" parameterType="blog">
insert into blog (bid, name, author_id)
values (#{bid,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{authorId,jdbcType=INTEGER})
</insert>
</mapper>
定義一個Mapper.xml,指定namespace ,然後在測試的代碼中我們通過指定查詢語句的id來進行數據查詢
同時我們還需要將這個Mapper.xml交給MyBatis的全局配置文件管理,MyBatis的全局配置文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<!-- 打印查詢語句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局緩存(二級緩存)-->
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.learn.entity.Blog" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 單獨使用時配置成MANAGED沒有事務 -->
<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>
<mappers>
<!-- 將Mapper的配置文件交給MyBatis管理 -->
<mapper resource="BlogMapper.xml"/>
</mappers>
</configuration>
測試實例
@Slf4j
public class MyBatisApiTest {
@Test
public void testStatement() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
Blog blog = sqlSession.selectOne("com.learn.mybatis.mapper.BlogMapper.selectBlogById",1);
log.info("api 獲取的blog:{}",blog);
}catch (Exception e){
log.error("異常信息:{}",e);
}finally {
sqlSession.close();
}
}
}
這種方式需要將針對查詢語句的statement固定寫在代碼中。
有Mapper接口的時代
在準備一個Mapper之後,我們可以直接通過獲取Mapper中的方式來實現數據的讀取
1、準備一個mapper接口
public interface BlogMapper {
/**
* 根據主鍵查詢文章
* @param bid
* @return
*/
public Blog selectBlogById(Integer bid);
}
2、測試實例
@Test
public void testMapperSelectInterface() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
log.info("mapper get blog entity:{}",blog);
} finally {
session.close();
}
}
總結到這裏,我們需要留意一下MyBatis中常見的幾個對象的生命週期了,官網上也有描述 myBatist 官網。
1、SqlSessionFactoryBuilder只是用來構建SqlSessionFactory,而SqlSessionFactroy在應用程序中只會有一個,SqlSessionFactoryBuilder構建完成了SqlSessionFactory就完成了使命。
2、SqlSessionFactory這個是用來創建SqlSession的,每次程序訪問數據庫,都需要創建一個會話,因此SqlSessionFactory的生命週期應該是整個應用的生命週期。
3、SqlSession就是一個會話,不是線程安全的,不能線程間共享,因此需要在會話結束之後關閉sqlSession。
4、Mapper其實是從SqlSession中獲取的一個代理對象(畢竟接口沒有具體的實現,通過代理實現這個Mapper接口的邏輯)Mapper的應用週期應該就存在於我們的方法級別,與數據交互的方法結束之後Mapper生命週期結束。
與spring和springboot等的集成使用,這裏不再贅述,之前進行過總結。
動態SQL
按照官網的分類,MyBatis的動態標籤主要有四類:if,choose(when,otherwise),trim(where,set),foreach。
if標籤
<select id="selectById" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tbl_emp e
<if test="empId!=null"><!--if標籤判斷值是否爲空-->
e.emp_id = #{empId,jdbcType=INTEGER}
</if>
</select>
where標籤
上述if標籤可以用where標籤改寫。
<select id="selectById" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tbl_emp e
<where>
e.emp_id = #{empId,jdbcType=INTEGER}
</where>
</select>
choose標籤
其實choose的作用就是如果存在多個條件,如果某些條件有值,則將有條件的SQL語句拼裝在where之後
<select id="getEmpListChoose" resultMap="empResultMap" parameterType="com.learn.entity.Employee">
SELECT * FROM tbl_emp e
<where>
<choose>
<when test="empId !=null">
e.emp_id = #{empId, jdbcType=INTEGER}
</when>
<when test="empName != null and empName != ''">
AND e.emp_name LIKE CONCAT(CONCAT('%', #{empName, jdbcType=VARCHAR}),'%')
</when>
<when test="email != null ">
AND e.email = #{email, jdbcType=VARCHAR}
</when>
<otherwise>
</otherwise>
</choose>
</where>
</select>
trim標籤
在需要去掉where,and,逗號之類的符號的時候,trim標籤就顯的很有用
<insert id="insertSelective" parameterType="com.learn.entity.Employee">
insert into tbl_emp
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="empId != null">
emp_id,
</if>
<if test="empName != null">
emp_name,
</if>
<if test="gender != null">
gender,
</if>
<if test="email != null">
email,
</if>
<if test="dId != null">
d_id,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="empId != null">
#{empId,jdbcType=INTEGER},
</if>
<if test="empName != null">
#{empName,jdbcType=VARCHAR},
</if>
<if test="gender != null">
#{gender,jdbcType=CHAR},
</if>
<if test="email != null">
#{email,jdbcType=VARCHAR},
</if>
<if test="dId != null">
#{dId,jdbcType=INTEGER},
</if>
</trim>
</insert>
在SQL語句中,insert語句中最後一個屬性結尾是不能帶有逗號的。trim就通過一些屬性解決了手動拼寫這些SQL的麻煩之處
foreach標籤
遍歷我們自己的集合,然後將每一個數值放入
<select id="selectByIds" parameterType="java.util.List" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tbl_emp e
<where>
e.emp_id in
<foreach collection="list" item="empId" open="(" separator="," close=")">
#{empId,jdbcType=INTEGER}
</foreach>
</where>
</select>
最終執行的SQL語句爲
select emp_id, emp_name, gender, email, d_id from tbl_emp e WHERE e.emp_id in ( 1 , 2 )
foreach標籤也常用於生成批量操作的語句。
批量操作
MyBatis裏面是支持批量操作的,包括批量插入,更新,刪除等。我們可直接傳入一個list,set,map或者數組,配合動態SQL的標籤,MyBatis會自動幫助我們生成語法正確的SQL語句。
批量插入
<insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true">
insert into tbl_emp (emp_id, emp_name, gender,email, d_id)
values
<foreach collection="list" item="emps" index="index" separator=",">
( #{emps.empId},#{emps.empName},#{emps.gender},#{emps.email},#{emps.dId} )
</foreach>
</insert>
批量更新
批量更新的SQL高效操作可以用到case when語句,一個實例如下:
-- 實例:更新categories表的display_order字段和title字段,只是針對滿足id爲1,2,3的記錄進行更新。如果id爲1則dispaly_order置爲1
UPDATE categories SET
display_order = CASE id
WHEN 1 THEN 3
WHEN 2 THEN 4
WHEN 3 THEN 5
END,
title = CASE id
WHEN 1 THEN 'New Title 1'
WHEN 2 THEN 'New Title 2'
WHEN 3 THEN 'New Title 3'
END
WHERE id IN (1,2,3)
這個語句在mybatis中可以這樣操作
<!-- 注意separator 和 open emps爲傳入進來的參數-->
<update id="updateBatch">
update tbl_emp set
emp_name =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
when #{emps.empId} then #{emps.empName}
</foreach>
,gender =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
when #{emps.empId} then #{emps.gender}
</foreach>
,email =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
when #{emps.empId} then #{emps.email}
</foreach>
where emp_id in
<foreach collection="list" item="emps" index="index" separator="," open="(" close=")">
#{emps.empId}
</foreach>
</update>
Batch Executor
MyBatis的動態標籤固然非常牛皮,但是如果數據量非常大,拼接的SQL語句大於4M,mybatis是會拋出一個異常的,畢竟MySQL服務端對接收的SQL語句的到小是有限制的。這個時候就需要用到MyBatis爲我們提供的Batch Executor了
可以在全局配置中指定Batch Executor
<setting name="defaultExecutorType" value="BATCH" />
也可以在創建SqlSession的時候指定批處理器
SqlSession sqlSession = SqlSessionFactory.openSession(ExecutorType.BATCH);
其實Batch Executor就是對JDBC中的PreparedStatement中的addBatch()方法的封裝,原理都是先攢夠一批SQL以後再發送給數據庫執行。
MyBatis中常見的三種執行器可以參見這篇博客介紹——MyBatis Executor。
resultMap和resultType的區別
在mybatis中,我們映射結果有兩個標籤,一個是resultMap一個是resultType。**resultType是select標籤下的一個屬性,適用於返回JDK類型和實體,這種情況下結果集的列和實體類的屬性可以直接映射。如果返回的字段無法直接映射,就需要用到resultMap了。**上面的關聯查詢實例中我們就不能用resultType來完成,因爲我們用到了級聯查詢,在沒有級聯對象的情況下resultType就無法滿足我們的需求了。
關聯查詢
我們還是以實例來總結關聯查詢的一些內容,這裏我們打算關聯Blog和Author兩個對象。
準備工作
建立一個BlogAndAuthor對象
public class BlogAndAuthor implements Serializable {
Integer bid; // 文章ID
String name; // 文章標題
Author author; // 作者
@Override
public String toString() {
return "BlogAndAuthor{" +
"bid=" + bid +
", name='" + name + '\'' +
", author=" + author +
'}';
}
}
畢竟mybatis查詢出來的結果需要有關聯屬性的實體去接收,因此會定義一個新的實體(不建議在原來的實體上增加屬性)
這裏可以理解爲一對一的關聯查詢行爲,一對一的關聯查詢有兩種方式
嵌套結果的方式
這種操作方式不存在N+1問題。
<resultMap id="BlogWithAuthorResultMap" type="com.learn.entity.associate.BlogAndAuthor">
<id column="bid" property = "bid"></id>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.learn.entity.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
這種比較簡單,只是利用標籤配置一個關聯的屬性,然後該標籤內部配置映射關係即可。
嵌套查詢的方式
<!-- 另一種聯合查詢(一對一)的實現,但是這種方式有“N+1”的問題 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.learn.entity.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.learn.entity.Author"
column="author_id" select="selectAuthor"></association>
</resultMap>
<!-- 嵌套查詢 -->
<select id="selectAuthor" parameterType="int" resultType="com.learn.entity.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
<!-- 根據文章查詢作者,一對一,嵌套查詢,存在N+1問題,可通過開啓延遲加載解決 -->
<select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
這種方式存在一個N+1問題,還是舉例說明一下N+1問題吧,這裏Blog對象和Author對象關聯,如果一個Blog由N個作者完成,那麼我們在查詢完指定Blog的時候,會再次發送N個查詢語句去查詢這N個作者的信息。這樣會白白浪費我們的數據庫性能,這個問題可以通過延遲加載的開關來解決這個問題。
在mybatis的全局配置文件中settings標籤下可以開啓這個配置
<!-- 延遲加載的全局開關。當開啓時,所有關聯對象都會延遲加載。默認 false -->
<setting name="lazyLoadingEnabled" value="true"/>
可以用如下代碼進行測試
/**
* autor:liman
* createtime:2020/5/3
* comment:
*/
public class TestAssocaite {
@Test
public void testAssociate() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
BlogAndAuthor blogAndAuthorWithQuery = blogMapper.selectBlogWithAuthorQuery(1);
blogAndAuthorWithQuery.getName();//沒用到Author會發出一條SQL查詢語句
//blogAndAuthorWithQuery.getAuthor();//用到了Author會發出兩條SQL查詢語句
}catch (Exception e){
log.info("異常信息:{}",e);
}finally {
sqlSession.close();
}
}
}
同時準帶提一下另一個屬性
<!-- 當開啓時,任何方法的調用都會加載該對象的所有屬性。默認 false,可通過associate標籤的 fetchType來覆蓋-->
<setting name="aggressiveLazyLoading" value="false"/>
<!--同時還有一個lazyLoadTriggerMethods屬性,這個屬性指定執行equals,toString,clone,hashCode等方法同樣能觸發對數據庫的查詢-->
延遲加載的底部依舊是通過代理的方式完成,代理的方式可以由我們自己去配置
通過如下參數改變動態代理的方式
<setting name="proxyFactory" value="CGLIB" />
總結
本篇博客簡單梳理了一下MyBatis的優勢,以及我們爲啥要採用mybatis,後面順便總結了一下簡單的配置方式,mybatis的一些簡單用法