Mybaties笔记

1. 概述

MyBatis 是一款持久层框架, 免除了几乎所有的 JDBC 代码工作。MyBatis通过 XML 或注解来配置和映射原始类型为数据库中的记录。

(以前叫ibatis, 2010年6月16号被谷歌托管,改名为MyBatis)

官方文档地址:https://mybatis.org/mybatis-3/zh/index.html

maven依赖

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>


2. 环境准备及测试样例

2.1 数据库准备

mysql,新建mybatis数据库,然后创建student表,创建三行数据。

CREATE SCHEMA `mybatis` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin ;

CREATE TABLE `student` (
`id` int(11) NOT NULL COMMENT '学号',
`name` varchar(45) COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
`gender` varchar(45) COLLATE utf8_bin DEFAULT NULL COMMENT '性别',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

insert into student(`id`,`name`,`gender`) values
(1,'虞姬','女'),
(2,'鲁班七号','男'),
(3,'孙悟空','男');


2.2 idea新建maven工程,引入依赖

<?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.nesc</groupId>
<artifactId>mybaties01</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!--junit测试依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
</project>


2.3 创建mybatis-config.xml

官方文档地址上有模板,copy一个过来,官方推荐名叫mybatis-config.xml,不叫这个也行,就别费事自己瞎起名了。把自己数据配置写上,最下面那个标签先注释掉,文件默认放在resource根目录下

<?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>
<!--可以存在多个数据库配置,default 只能选择其中一个id进行启用-->
<environments default="development">
<!--id 让上面选择启用谁的唯一标志-->
<environment id="development">
<!--Mybatis事务管理类型[JDBC|MANAGED]二选一,默认用的jdbc 那个没啥用 -->
<transactionManager type="JDBC"/>
<!--[UNPOOLED|POOLED|JNDI] 选POOLED 那俩没啥用-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--下面这句先注释-->
<!-- <mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>-->
</configuration>


2.4 编写mybatis工具类

package com.nesc.util;

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;

/**
* @author wanghl
* @date 2020/11/30 9:12
**/
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static{
try {
//加载文件
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 获取SqlSession 公共方法
* @return
*/
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
}


2.5 编写数据库对应java实体类

package com.nesc.domain;

/**
* @author wanghl
* @date 2020/11/30 9:49
**/
public class Student {
/**
* 学号
*/
private int id;
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private String gender;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
'}';
}
}


2.6 编写StudentDao

package com.nesc.dao;

import com.nesc.domain.Student;

import java.util.List;

/**
* @author wanghl
* @date 2020/11/30 10:03
**/
public interface StudentDao {
public List<Student> getAll();
}


2.7 编写mapper.xml

从官网copy个 mapper的xml文件 如下

<?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="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>


改成自己的,resource文件目录下新建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">
<!--唯一命名空间,同时绑定dao接口 等同于实现接口-->
<mapper namespace="com.nesc.dao.StudentDao">

<!--id 代表要实现的接口方法 resultType 返回值类型-->
<select id="getAll" resultType="com.nesc.domain.Student">
select * from student
</select>
</mapper>


2.8 注册mapper

在mybatis-config.xml中注册自己的mapper,就是之前注释的那段,改成自己的

<!--写完代码 将mapper文件注册到 管理器中-->
<mappers>
<mapper resource="mapper/StudentMapper.xml"/>
</mappers>


2.9 编写测试类

package com.nesc.dao;

import com.nesc.domain.Student;
import com.nesc.util.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

/**
* @author wanghl
* @date 2020/11/30 10:12
**/
public class StudentDaoTest {

/**
* 测试 StudentDao的 getAll方法
*/
@Test
public void getAllTest(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
//调用 接口中 getAll 方法
List<Student> all = mapper.getAll();
//遍历结果
for (int i = 0; i < all.size(); i++) {
System.out.println(all.get(i).toString());
}
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}
}


2.10 运行结果

Student{id=1, name='虞姬', gender='女'}
Student{id=2, name='鲁班七号', gender='男'}
Student{id=3, name='孙悟空', gender='男'}


3. 增删改查实现

3.1 insert

StudentDao类新增接口定义

/**
* 新增学生数据
* @param student
* @return
*/
int insert(Student student);


StudentMapper.xml新增方法实现,参数用#{}接收方法定义中的参数名称值

<!--id 代表要实现的接口方法 parameterType 类型为对象的时候 下面参数会自动拆箱-->
<insert id="insert" parameterType="com.nesc.domain.Student">
insert into student(id ,name ,gender) values (#{id},#{name},#{gender})
</insert>


StudentDaoTest新增对insert的测试方法

/**
* 测试 StudentDao的 insert
*/
@Test
public void insert(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
Student student = new Student();
student.setId(4);
student.setName("蔡文姬");
student.setGender("女");
//调用 接口方法
int result = mapper.insert(student);
System.out.println(result);
//提交事务 否则执行不生效
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}


第一次执行结果

1


第二次执行结果

Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '4' for key 'PRIMARY'


3.2 delete

StudentDao类新增接口定义

/**
* 根据id删除数据
* @param id
* @return
*/
int deleteById(int id);


StudentMapper.xml新增方法实现,参数用#{}接收方法定义中的参数名称值

<!--id 代表要实现的接口方法-->
<delete id="deleteById" parameterType="int">
delete from student where id = #{id}
</delete>


StudentDaoTest新增对deleteById的测试方法

/**
* 测试 StudentDao的 deleteById
*/
@Test
public void deleteByIdTest(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
//调用 接口方法
int result = mapper.deleteById(1);
System.out.println(result);
//提交事务 否则执行不生效
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}


第一次执行结果:成功

1


第二次执行结果:失败

0


3.3 update

StudentDao类新增接口定义

/**
* 根据id修改数据
* @param student
* @return
*/
int updateById(Student student);


StudentMapper.xml新增方法实现,参数用#{}接收方法定义中的参数名称值

<!--id 代表要实现的接口方法 parameterType 类型为对象的时候 下面参数会自动拆箱-->
<update id="updateById" parameterType="com.nesc.domain.Student">
update student set name = #{name}, gender = #{gender} where id = #{id}
</update>


StudentDaoTest新增对updateById的测试方法

/**
* 测试 StudentDao的 updateById
*/
@Test
public void updateByIdTest(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
Student student = new Student();
student.setId(2);
student.setName("李信");
student.setGender("男");
//调用 接口方法
int result = mapper.updateById(student);
System.out.println(result);
Student s = mapper.getById(2);
System.out.println(s);
//提交事务 否则执行不生效
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}


执行结果:成功

1
Student{id=2, name='李信', gender='男'}


3.4 select

StudentDao类新增查询接口定义

/**
* 根据id查询数据
* @param id
* @return
*/
Student getById(int id);


StudentMapper.xml新增方法实现,参数用#{}接收方法定义中的参数名称值

<!--id 代表要实现的接口方法  resultType 返回值类型 parameterType 参数类型-->
<select id="getById" parameterType="int" resultType="com.nesc.domain.Student">
select * from student where id = #{id}
</select>


StudentDaoTest新增对getById的测试方法

/**
* 测试 StudentDao的 getById
*/
@Test
public void getByIdTest(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
//调用 接口方法
Student student = mapper.getById(1);
System.out.println(student.toString());
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}


执行结果

Student{id=1, name='虞姬', gender='女'}


4. Mybatis 配置文件(mybatis-config.xml)

从xml提示中可以看见,标签中一共11个字标签,且有先后顺序,顺序写错,提示器会给错误提示,并给出实际顺序提示。

(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)


从官方文档看介绍了9个。

  • configuration(配置)
  • environment(环境变量)
  • transactionManager(事务管理器)
  • dataSource(数据源)

4.1 属性(propertie)

实战常用的是将数据库相关信息存到db.properties文件中,然后将文件引入到mybatis-config.xml配置文件中,在文件中通过${}对变量进行绑定,

4.1.1 常见例子

新建db.properties文件,放在resource目录下

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
username=root
password=123456


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>
<!--引入同目录下的db.properties文件-->
<properties resource="db.properties"></properties>
<!--可以存在多个数据库配置,default 只能选择其中一个id进行启用-->
<environments default="development">
<!--id 让上面选择启用谁的唯一标志-->
<environment id="development">
<!--Mybatis事务管理类型[JDBC|MANAGED]二选一,默认用的jdbc 那个没啥用 -->
<transactionManager type="JDBC"/>
<!--[UNPOOLED|POOLED|JNDI] 选POOLED 那俩没啥用-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

<!--写完代码 将mapper文件注册到 管理器中-->
<mappers>
<mapper resource="mapper/StudentMapper.xml"/>
</mappers>
</configuration>


4.1.2 扩展

如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:

  • 首先读取在 properties 元素体内指定的属性。
  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件。
  • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性。

例子如下

<!--db.properties文件中的内容会覆盖 此处变量-->
<properties resource="db.properties">
<property name="username" value="wanghl12W"></property>
</properties>
//初始化传入参数 会覆盖db.properties的变量值
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);
// ... 或者 ...
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);


4.2 类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。

换句话说就是非必要,有他代码看起来简短一些,没他代码长一些,没有也行。

4.2.1 常规例子

在mybatis-config.xml中配置

<typeAliases>
<!--给com.nesc.domain.Student 起个 唯一的小名-->
<typeAlias type="com.nesc.domain.Student" alias="student"></typeAlias>
</typeAliases>


然后在StudentMapper.xml中就可以这样写,把里面所有的com.nesc.domain.Student 都替换成student,这里主要是resultType使用了

<?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">
<!--唯一命名空间,同时绑定dao接口 等同于实现接口-->
<mapper namespace="com.nesc.dao.StudentDao">

<!--id 代表要实现的接口方法 resultType 返回值类型-->
<select id="getAll" resultType="student">
select * from student
</select>

<!--id 代表要实现的接口方法 resultType 返回值类型 parameterType 参数类型-->
<select id="getById" parameterType="int" resultType="student">
select * from student where id = #{id}
</select>

<!--id 代表要实现的接口方法-->
<delete id="deleteById" parameterType="student">
delete from student where id = #{id}
</delete>

<!--id 代表要实现的接口方法 parameterType 类型为对象的时候 下面参数会自动拆箱-->
<insert id="insert" parameterType="student">
insert into student(id ,name ,gender) values (#{id},#{name},#{gender})
</insert>

<!--id 代表要实现的接口方法 parameterType 类型为对象的时候 下面参数会自动拆箱-->
<update id="updateById" parameterType="student">
update student set name = #{name}, gender = #{gender} where id = #{id}
</update>
</mapper>


4.2.2 实战常用方式

为了防止配置文件中写过多的标签,显着配置文件长,可以配置包名+@Alias标签实现,同时也减少对配置文件的频繁更改

在mybatis-config.xml中配置

<!--扫描com.nesc.domain包下,所有类名头上有@Alias("xxx")标签的自动注册到这里  没有写Alias的 默认将类名首字母小写 拿过来进行注册-->
<typeAliases>
<package name="com.nesc.domain"/>
</typeAliases>


在com.nesc.domain.Student类名头上配置

package com.nesc.domain;

import org.apache.ibatis.type.Alias;
@Alias("student")
public class Student {
//...
}


4.2.3 扩展(了解)

下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。

别名

映射的类型

_byte

byte

_long

long

_short

short

_int

int

_integer

int

_double

double

_float

float

_boolean

boolean

string

String

byte

Byte

long

Long

short

Short

int

Integer

integer

Integer

double

Double

float

Float

boolean

Boolean

date

Date

decimal

BigDecimal

bigdecimal

BigDecimal

object

Object

map

Map

hashmap

HashMap

list

List

arraylist

ArrayList

collection

Collection

iterator

Iterator

4.3 设置(setting)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。全部参数看官方文档,此处记录常用的

<settings>
<!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存-->
<setting name="cacheEnabled" value="true"/>
<!--允许 JDBC 支持自动生成主键,需要数据库驱动支持-->
<setting name="useGeneratedKeys" value="false"/>
<!--是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn-->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!--开启mybatis日志,不同日志可能需要引用第三方日志依赖-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>


4.4 映射器(mappers)

四种方式绑定注册*Mapper.xml,

<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器,此方法xml文件与java文件必须同目录,必须同名 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>


放在类中打包时候src/main/java下的xml默认不会打包编译,需要在pom中增加编译配置

<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>


4.5 数据库厂商标识(databaseIdProvider)

兼容多数据库,有时候开发的时候不确定数据库版本,写sql语句的时候不同版本数据的情况就都写出来

mybatis-config.xml中增加如下配置

<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mySQL"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>


然后在*Mapper.xml中用法 用databaseId变量来判断,优先找到匹配的

<select id="selectTime" resultType="String" databaseId="mySQL">
SELECT NOW()
</select>
<select id="selectTime" resultType="String" databaseId="oracle">
SELECT 'oralce'||to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') FROM dual
</select>


或是if中用_databaseId变量判断

<select id="selectTime" resultType="String">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'mySQL'">
select now()
</if>
</select>


如果不知道自己数据库名称,用下面代码执行一下看看

/**
* 获取数据库类型
*/
@Test
public void getDbTypeTest() {
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//java.sql.Connection
Connection connection = null;
try {
connection = sqlSession.getConnection();
String dbName = connection.getMetaData().getDatabaseProductName();
String dbVersion = connection.getMetaData().getDatabaseProductVersion();
System.out.println("数据库名称是:" + dbName + ";版本是:" + dbVersion);
} catch (SQLException e) {
e.printStackTrace();
}
}


5. 结果映射(resultMap)

​resultMap​​ 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ​​ResultSets​​ 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。

这里提一下resultType,实际上resultType是后台默认创建了一个resultMap去将查询结果的column和java bean的properties关联起来,如果这俩名称不一致的时候,这时候就需要显式的将resultMap写出来,比如数据库字段叫sex,而java bean中类属性叫gender,这就不匹配,有查询结果,但是接收不到。

5.1 简单映射

解决一些字段属性不一致问题,这里我们把数据库表student中gender字段改名为sex

ALTER TABLE `mybatis`.`student` 
CHANGE COLUMN `gender` `sex` VARCHAR(45) NULL DEFAULT NULL COMMENT '性别' ;


StudentMapper.xml

<?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">
<!--唯一命名空间,同时绑定dao接口 等同于实现接口-->
<mapper namespace="com.nesc.dao.StudentDao">

<!--这里自定义结果映射,不一致的属性在下面进行配置,一致的默认不用动-->
<resultMap id="studentResult" type="student">
<result column="sex" property="gender"></result>
</resultMap>

<!--这里用自定义resultMap 代替 resultType -->
<select id="getAll" resultMap="studentResult">
select id,name,sex from student
</select>
</mapper>


5.2 复杂映射

5.2.1 一对一(association)

一对一,代码表现形式就是java类中包含另一个类对象

新建国家系列代码

  1. 新建表、插入数据
CREATE TABLE `mybatis`.`country` (
`id` INT NOT NULL,
`name` VARCHAR(45) NULL,
`child_id` INT NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_bin;

INSERT INTO `mybatis`.`country` (`id`, `name`, `child_id`) VALUES ('1', '大秦', '34', '2');
INSERT INTO `mybatis`.`country` (`id`, `name`, `child_id`) VALUES ('2', '东胜神洲', '25', '3');
INSERT INTO `mybatis`.`country` (`id`, `name`, `child_id`) VALUES ('3', '东汉', '28', '4');


  1. 新增Country.java
package com.nesc.domain;

import org.apache.ibatis.type.Alias;

/**
* @author wanghl
* @date 2020/11/30 9:49
**/
@Alias("country")
public class Country {
/**
* 国家id
*/
private int id;
/**
* 姓名
*/
private String name;

/**
* 子民ID
*/
private int childId;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getChildId() {
return childId;
}

public void setChildId(int childId) {
this.childId = childId;
}

@Override
public String toString() {
return "Country{" +
"id=" + id +
", name='" + name + '\'' +
", childId=" + childId +
'}';
}
}


  1. Student类中 新增Country属性
package com.nesc.domain;

import org.apache.ibatis.type.Alias;

/**
* @author wanghl
* @date 2020/11/30 9:49
**/
@Alias("student")
public class Student {
/**
* 学号
*/
private int id;
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private String gender;

/**
* 国家
*/
private Country country;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}


public Country getCountry() {
return country;
}

public void setCountry(Country country) {
this.country = country;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
", country=" + country +
'}';
}
}


  1. StudentMapper.xml 修改getAll方法及对应resultMap
<!--id 代表要实现的接口方法  resultType 返回值类型-->
<select id="getAll" resultMap="studentResult">
select
s.id,
s.name,
s.sex,
c.id country_id,
c.name country_name,
c.child_id
from student s ,country c
where s.id = c.child_id
</select>

<!--这里自定义结果映射,复杂关系映射全部属性都要写-->
<resultMap id="studentResult" type="student">
<!--这里写id标签会快一些,result标签也不是不行-->
<id property="id" column="id"></id>
<result property="name" column="name" ></result>
<result property="gender" column="sex"></result>
<!--一对一对象就用association标签,property表示在student类中Country类的变量名,
javaType就是完整的Country类型 可以用别名country,这里这么写直观一些-->
<association property="country" javaType="com.nesc.domain.Country" >
<id property="id" column="country_id"></id>
<result property="name" column="country_name"></result>
<result property="childId" column="child_id"></result>
</association>
</resultMap>


  1. 运行StudentDaoTest中getAllTest后运行结果
Student{id=2, name='李信', gender='男', country=Country{id=1, name='大秦', childId=2}}
Student{id=3, name='孙悟空', gender='男', country=Country{id=2, name='东胜神洲', childId=3}}
Student{id=4, name='蔡文姬', gender='女', country=Country{id=3, name='东汉', childId=4}}


5.2.2 一对多(collection)

新建装备库一系列代码

  1. 新建表及数据
CREATE TABLE `mybatis`.`armory` (
`id` INT NOT NULL,
`name` VARCHAR(45) NULL,
`student_id` INT NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_bin;

INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('1', '暗影战斧', '2');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('2', '名刀', '2');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('3', '破军', '2');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('4', '追猎刀锋', '3');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('5', '无尽战刃', '3');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('6', '宗师之力', '3');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('7', '圣杯', '4');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('8', '极影', '4');
INSERT INTO `mybatis`.`armory` (`id`, `name`, `student_id`) VALUES ('9', '冷静之靴', '4');


  1. 新建Armory类
package com.nesc.domain;

import org.apache.ibatis.type.Alias;

/**
* @author wanghl
* @date 2020/12/2 9:57
**/
@Alias("armory")
public class Armory {
/**
* 装备id
*/
private int id;
/**
* 装备名称
*/
private String name;

/**
* 所属学生
*/
private int studentId;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getStudentId() {
return studentId;
}

public void setStudentId(int studentId) {
this.studentId = studentId;
}

@Override
public String toString() {
return "Armory{" +
"id=" + id +
", name='" + name + '\'' +
", studentId=" + studentId +
'}';
}
}


  1. Student类新增List属性
package com.nesc.domain;

import org.apache.ibatis.type.Alias;

import java.util.List;

/**
* @author wanghl
* @date 2020/11/30 9:49
**/
@Alias("student")
public class Student {
/**
* 学号
*/
private int id;
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private String gender;

/**
* 国家
*/
private Country country;

/**
* 装备
*/
private List<Armory> list;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}


public Country getCountry() {
return country;
}

public void setCountry(Country country) {
this.country = country;
}

public List<Armory> getList() {
return list;
}

public void setList(List<Armory> list) {
this.list = list;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
", country=" + country +
", list=" + list +
'}';
}
}


  1. StudentMapper.xml 修改getAll方法及对应resultMap
<!--这里自定义结果映射,复杂关系映射全部属性都要写-->
<resultMap id="studentResult" type="student">
<!--这里写id标签会快一些,result标签也不是不行-->
<id property="id" column="id"></id>
<result property="name" column="name"></result>
<result property="gender" column="sex"></result>
<!--一对一对象就用association标签,property表示在student类中Country类的变量名,
javaType就是完整的Country类型 可以用别名country,这里这么写直观一些-->
<association property="country" javaType="com.nesc.domain.Country" >
<id property="id" column="country_id"></id>
<result property="name" column="country_name"></result>
<result property="childId" column="child_id"></result>
</association>
<!--一对一对象就用collection标签,property表示在student类中Armory类的变量名,
ofType就是完整的Country类型 可以用别名country,这里这么写直观一些-->
<collection property="list" ofType="com.nesc.domain.Armory">
<id property="id" column="arms_id"></id>
<result property="name" column="arms_name"></result>
<result property="studentId" column="student_id"></result>
</collection>
</resultMap>

<!--id 代表要实现的接口方法 resultType 返回值类型-->
<select id="getAll" resultMap="studentResult">
select
s.id,
s.name,
s.sex,
c.id country_id,
c.name country_name,
c.child_id,
a.id arms_id,
a.name arms_name,
a.student_id
from student s ,country c,armory a
where s.id = c.child_id and s.id = a.student_id
</select>


  1. 运行StudentDaoTest中getAllTest后运行结果
Student{id=2, name='李信', gender='男', country=Country{id=1, name='大秦', childId=2}, list=[Armory{id=1, name='暗影战斧', studentId=2}, Armory{id=2, name='名刀', studentId=2}, Armory{id=3, name='破军', studentId=2}]}

Student{id=3, name='孙悟空', gender='男', country=Country{id=2, name='东胜神洲', childId=3}, list=[Armory{id=4, name='追猎刀锋', studentId=3}, Armory{id=5, name='无尽战刃', studentId=3}, Armory{id=6, name='宗师之力', studentId=3}]}

Student{id=4, name='蔡文姬', gender='女', country=Country{id=3, name='东汉', childId=4}, list=[Armory{id=7, name='圣杯', studentId=4}, Armory{id=8, name='极影', studentId=4}, Armory{id=9, name='冷静之靴', studentId=4}]}


5.2.3 多对一

就是一对多主体换一下,sql都差不多,就是接收的resultMap不一样,主体对换了

  1. Armory类中增加Student属性
package com.nesc.domain;

import org.apache.ibatis.type.Alias;

/**
* @author wanghl
* @date 2020/12/2 9:57
**/
@Alias("armory")
public class Armory {
/**
* 装备id
*/
private int id;
/**
* 装备名称
*/
private String name;

/**
* 所属学生
*/
private int studentId;

private Student student;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getStudentId() {
return studentId;
}

public void setStudentId(int studentId) {
this.studentId = studentId;
}

public Student getStudent() {
return student;
}

public void setStudent(Student student) {
this.student = student;
}

@Override
public String toString() {
return "Armory{" +
"id=" + id +
", name='" + name + '\'' +
", studentId=" + studentId +
", student=" + student +
'}';
}
}


  1. 新增ArmoryDao.java及ArmoryMapper.xml
package com.nesc.dao;

import com.nesc.domain.Armory;
import com.nesc.domain.Student;

import java.util.List;

/**
* @author wanghl
* @date 2020/12/2 10:03
**/
public interface ArmoryDao {
/**
* 查询所有数据
* @return
*/
List<Armory> getAll();

}


ArmoryMapper.xml

<?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">
<!--唯一命名空间,同时绑定dao接口 等同于实现接口-->
<mapper namespace="com.nesc.dao.ArmoryDao">

<!--这里自定义结果映射,复杂关系映射全部属性都要写-->
<resultMap id="armoryResult" type="armory">
<!--这里写id标签会快一些,result标签也不是不行-->
<id property="id" column="arms_id"></id>
<result property="name" column="arms_name"></result>
<result property="studentId" column="student_id" ></result>

<collection property="student" ofType="com.nesc.domain.Student">
<id property="id" column="arms_id"></id>
<result property="name" column="arms_name"></result>
<result property="gender" column="sex"></result>
</collection>
</resultMap>

<!--id 代表要实现的接口方法 resultType 返回值类型-->
<select id="getAll" resultMap="armoryResult">
select
s.id,
s.name,
s.sex,
a.id arms_id,
a.name arms_name,
a.student_id
from student s ,armory a
where s.id = a.student_id
</select>

</mapper>


  1. mybatis-config.xml注册mapper
<mappers>
<mapper resource="mapper/StudentMapper.xml"/>
<mapper resource="mapper/ArmoryMapper.xml"/>
</mappers>


  1. 新建ArmoryDaoTest测试类
package com.nesc.dao;

import com.nesc.domain.Armory;
import com.nesc.domain.Student;
import com.nesc.util.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

/**
* @author wanghl
* @date 2020/12/2 10:12
**/
public class ArmoryDaoTest {

/**
* 测试 ArmoryDao getAll方法
*/
@Test
public void getAllTest(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
ArmoryDao mapper = sqlSession.getMapper(ArmoryDao.class);
//调用 接口中 getAll 方法
List<Armory> all = mapper.getAll();
//遍历结果
for (int i = 0; i < all.size(); i++) {
System.out.println(all.get(i).toString());
}
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}
}


  1. 运行getAll测试方法
Armory{id=1, name='暗影战斧', studentId=2, student=Student{id=1, name='暗影战斧', gender='男', country=null, list=null}}
Armory{id=2, name='名刀', studentId=2, student=Student{id=2, name='名刀', gender='男', country=null, list=null}}
Armory{id=3, name='破军', studentId=2, student=Student{id=3, name='破军', gender='男', country=null, list=null}}
Armory{id=4, name='追猎刀锋', studentId=3, student=Student{id=4, name='追猎刀锋', gender='男', country=null, list=null}}
Armory{id=5, name='无尽战刃', studentId=3, student=Student{id=5, name='无尽战刃', gender='男', country=null, list=null}}
Armory{id=6, name='宗师之力', studentId=3, student=Student{id=6, name='宗师之力', gender='男', country=null, list=null}}
Armory{id=7, name='圣杯', studentId=4, student=Student{id=7, name='圣杯', gender='女', country=null, list=null}}
Armory{id=8, name='极影', studentId=4, student=Student{id=8, name='极影', gender='女', country=null, list=null}}
Armory{id=9, name='冷静之靴', studentId=4, student=Student{id=9, name='冷静之靴', gender='女', country=null, list=null}}


5.2.4 多对多

同一对多,只不过是双方都是一对多关系而已,写法同一对多没区别

6. 动态SQL

官方:动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

总结:没有也能对付用,用了能省不少事。

6.1 if

常用在表单查询页面多个查询条件,N选几的情况,动态拼接SQL

好处:

  1. 如果id不为空,不会出现 SELECT * FROM student WHERE AND id = #{id} 的情况,and会被过滤
  2. 前端页面多条件N选2查询会动态拼接sql
<select id="findStudent" resultType="student">
SELECT * FROM student WHERE sex = '男'
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
</select>


6.2 choose、when、otherwise

mybatis版本的switch case

<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>


6.3 trim、where、set

先说where,标签 就是代替where的,好处是 当where后续条件不满足条件没拼接的时候不会出现 select 1 from dual where 这种半截sql语句,而且如果where后面是个 and 也会给过滤掉

<select id="findStudent" resultType="student">
SELECT * FROM student
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
</where>
</select>


set标签,就是动态的update,也会过滤set属性后多余的逗号

<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>


再说trim标签,上面where会过滤and,set会过滤逗号,都是trim实现的,这个可以对其他标签自定义

where过滤原理

<trim prefix="WHERE" prefixOverrides="AND">
...
</trim>


set过滤原理

<trim prefix="SET" suffixOverrides=",">
...
</trim>


6.4 foreach

主要用在in语法,遍历某些集合的时候用

<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>/select>


7. 缓存

减少和数据库交互次数,减少系统开销,提升效率。mybatis默认分一级缓存和二级缓存

7.1 一级缓存

也叫本地缓存,SqlSession级别的,也就是SqlSession开关一次 就失效了。当sqlSession执行增删改的时候,就会清空一级缓存

一级缓存特点

  • 和二级缓存不同它不需要配置.默认开启
  • 不得不用,无法剔除,mybatis-config.xml中 setting配置的 cacheEnabled=false 无法关闭一级缓存
  • 无法管理一级缓存

为了演示效果,需要配置mybatis-config.xml的log来查看调用数据库情况,这里用STDOUT_LOGGING,这个不用额外引入依赖

<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>


代码示例

/**
* 测试一级缓存(本地缓存)
* 通过下面示例的执行日志可知,第一次调用数据库中访问,有sql打印,第二次从内存直接读取,执行更新/删除语句后,缓存清空,第三次再次调用数据库,打印sql语句
*/
@Test
public void localSessionCacheTest(){
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
try {
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
System.out.println("=============第一次调用==============");
mapper.getById(2);
System.out.println("=============第二次调用==============");
mapper.getById(2);
System.out.println("=============任意更新/删除语句==============");
//我删除id=3的数据 任何删除更新语句都可以 本次缓存清空
mapper.deleteById(3);
sqlSession.commit();
System.out.println("=============第三次调用==============");
mapper.getById(2);
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}
}


执行日志

Created connection 1691875296.
Returned connection 1691875296 to pool.
=============第一次调用==============
Opening JDBC Connection
Checked out connection 1691875296 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
==> Preparing: select * from student where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, sex
<== Row: 2, 李信, 男
<== Total: 1
=============第二次调用==============
=============任意更新/删除语句==============
==> Preparing: delete from student where id = ?
==> Parameters: 3(Integer)
<== Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
=============第三次调用==============
==> Preparing: select * from student where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, sex
<== Row: 2, 李信, 男
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]


7.2 二级缓存

也叫本地全局缓存,namespace级别的(*Mapper.xml里那个),随应用重启后失效

二级缓存开启

  • mybatis-config.xml中 setting配置的 cacheEnabled=true ,虽说默认不用配置,但是不能给改成false,这个可以关闭二级缓存
  • *Mapper.xml中增加标签
<!--
eviction 缓存策略
flushInterval 缓存刷新时间 ms单位
size 最大缓存引用个数 达到上限根据缓存策略进行处理
readOnly 返回的对象被认为是只读的 修改可能会在不同线程中的调用者产生冲突
-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>


可用的清除策略有:

  • ​LRU​​ – 最近最少使用:移除最长时间不被使用的对象。
  • ​FIFO​​ – 先进先出:按对象进入缓存的顺序来移除它们。
  • ​SOFT​​ – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • ​WEAK​​ – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

代码示例

StudentMapper.xml中增加cache

<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>


java测试用例

/**
* 测试二级缓存(全局缓存)
* 通过下面示例和执行结果可知,第一次读数据库,第二次缓存,第三次数据库,第四次数据库
*/
@Test
public void SecondLevelSessionCacheTest() {
try {
System.out.println("=============第一次调用==============");
//从工具类种获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//获取mapper 接口对象
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
//调用 接口中 getAll 方法
mapper.getById(2);
sqlSession.close();
System.out.println("=============第二次调用==============");
sqlSession = MybatisUtils.getSqlSession();
//获取mapper 接口对象
mapper = sqlSession.getMapper(StudentDao.class);
//调用 接口中 getAll 方法
mapper.getById(2);
sqlSession.close();
System.out.println("=============任意更新/删除语句==============");
sqlSession = MybatisUtils.getSqlSession();
//获取mapper 接口对象
mapper = sqlSession.getMapper(StudentDao.class);
//我删除id= 3的数据
mapper.deleteById(3);
sqlSession.commit();
sqlSession.close();
System.out.println("=============第三次调用==============");
sqlSession = MybatisUtils.getSqlSession();
//获取mapper 接口对象
mapper = sqlSession.getMapper(StudentDao.class);
mapper.getById(2);
sqlSession.close();
} catch (Exception e) {
e.printStackTrace();
}
}


执行结果

Created connection 1691875296.
Returned connection 1691875296 to pool.
Cache Hit Ratio [com.nesc.dao.StudentDao]: 0.0
Opening JDBC Connection
Checked out connection 1691875296 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
==> Preparing: select * from student where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, sex
<== Row: 2, 李信, 男
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Returned connection 1691875296 to pool.
=============第二次调用==============
Cache Hit Ratio [com.nesc.dao.StudentDao]: 0.5
=============任意更新/删除语句==============
Opening JDBC Connection
Checked out connection 1691875296 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
==> Preparing: delete from student where id = ?
==> Parameters: 3(Integer)
<== Updates: 0
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Returned connection 1691875296 to pool.
=============第三次调用==============
Cache Hit Ratio [com.nesc.dao.StudentDao]: 0.3333333333333333
Opening JDBC Connection
Checked out connection 1691875296 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
==> Preparing: select * from student where id = ?
==> Parameters: 2(Integer)
<== Columns: id, name, sex
<== Row: 2, 李信, 男
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@64d7f7e0]
Returned connection 1691875296 to pool.