MyBatis 基本使用总结 && 手动实现 MyBatis
传统的DB
操作图 && 它的问题:
MyBatis 使用方法
看到上面传统操作DB的问题后, 我们看一下MyBatis带给了什么便利:
MyBatis 官方文档: https://mybatis.org/mybatis-3/zh_CN/index.html
MyBatis 基本使用 (使用插入做演示)
创建所需库 && 表
首先我们创建MyBatis
库以及MyBatis
表, 如下:
CREATE DATABASE `mybatis`;
USE `mybatis`;
CREATE TABLE `monster`(
`id` INT NOT NULL AUTO_INCREMENT,
`age` INT NOT NULL,
`birthday` DATE DEFAULT NULL,
`email` VARCHAR(255) NOT NULL,
`gender` TINYINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`salary` DOUBLE NOT NULL,
PRIMARY KEY(`id`)
)CHARSET=utf8;
定义 父 && 子 模块
随后我们创建一个父项目, 例如:创建起来父项目后, 配置
pom.xml
, 那么它的所有子模块就会使用父项目的环境, 定义如下 pom.xml, 让其所有子模块应用该模块.
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
接下来按照如下步骤创建子模块:当我们创建成功后, 我们的父模块的
pom.xml
文件中会增加如下定义:
<modules>
<module>MyBatis01</module>
</modules>
而子模块中的pom.xml
文件中会增加如下配置:
<parent>
<groupId>org.example</groupId>
<artifactId>My</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
明显是一个父模块 指向 子模块
以及子模块 确认 父模块
的关系.
配置 mybatis-config.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/> <!-- 配置事务管理器 -->
<dataSource type="POOLED"> <!-- 配置数据源 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/> <!-- 配置数据库驱动 -->
<property name="url"
value="jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>
<!-- 配置链接的 URL
jdbc:mysql: 表明 JDBC 链接 MYSQL
127.0.0.1:3306 指明 IP 以及 MYSQL 服务器端口号
mybatis 选择指定数据库
& 表示 & 的转义符
useUnicode 使用 UNICODE 编码, characterEncoding 指定字符集放置乱码
-->
<property name="username" value="root"/>
<property name="password" value="toor"/>
</dataSource>
</environment>
</environments>
<!-- <mappers>-->
<!-- <mapper resource="org/mybatis/example/BlogMapper.xml"/>--> <!-- 暂时注销 -->
<!-- </mappers>-->
</configuration>
这里要注意的是, 要放到类路径下, 如图:
根据对应表创建 JavaBean
public class Monster {
/*
* mysql> desc monster;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| age | int(11) | NO | | NULL | |
| birthday | date | YES | | NULL | |
| email | varchar(255) | NO | | NULL | |
| gender | tinyint(4) | NO | | NULL | |
| name | varchar(255) | NO | | NULL | |
| salary | double | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
* */
private Integer id;
private Integer age;
private LocalDateTime birthday; // 如果Mysql驱动是8.0以下, 那么使用 Date
private String email;
private Integer gender;
private String name;
private Double salary;
// setter && getter && 有参构造 && 无参构造 && toString方法
}
创建 MonsterMapper 接口 && 创建 MonsterMapper SQL 文件
定义MonsterMapper
接口如下:
public interface MonsterMapper {
public void addMonster(Monster monster); // 定义一个增加 Monster 对象的功能
}
定义MonsterMapper.xml
文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.Mapper.MonsterMapper"> <!-- namespace 用于指定 接口 -->
<insert id="addMonster" parameterType="com.heihu577.Beans.Monster">
<!--
id 用于指定接口中所声明的方法, 这里对应上了 public void addMonster(Monster monster);
parameterType 用于指定参数中所指定的 Bean
-->
INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`)
VALUES(#{age}, #{birthday}, #{email}, #{gender}, #{name}, #{salary})
<!--
SQL 语句中的 #{age} 是用于指定 传递进来的 Monster 对象的 age 属性的具体值
-->
</insert>
</mapper>
定义完毕之后, 此时我们需要打开mybatis-config.xml
文件中的mappers
标签并进行配置如下:
<mappers>
<mapper resource="com/heihu577/Mapper/MonsterMapper.xml"/> <!-- 指明所定义的XML-SQL文件 -->
</mappers>
创建 MyBatis 工具类
定义工具类如下:
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
// 初始化 SqlSessionFactory
static {
// 指定资源文件, 配置文件
String resource = "mybatis-config.xml";
try {
InputStream resourceAsStream = Resources.getResourceAsStream(resource); // 注意是 org.apache.ibatis.io 包下的 Resources 类
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
最终测试结果
public class MonsterMapperTest {
private SqlSession sqlSession;
private MonsterMapper monsterMapper;
@Before // 加入 junit 单元测试中的 Before 注解, 在 Test 方法测试前会进行执行
public void init() {
sqlSession = MyBatisUtils.getSqlSession();
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
System.out.println(mapper);
}
@Test
public void t1() {
System.out.println("T1");
/*
最终报错:
### The error may exist in com/heihu577/Mapper/MonsterMapper.xml
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.io.IOException: Could not find resource com/heihu577/Mapper/MonsterMapper.xml
*/
}
}
* 报错问题解决
其报错终极原因则是, 我们使用Resources.getResourceAsStream
读取的目录是class
类目录, 而默认Maven
不会将我们.java
目录下的文件输出到.class
目录中, 所以在这里我们需要增加一个Maven
配置项:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
注意放入到properties
标签之后, 声明完毕后, 我们进行Maven-clean-install
即可运行成功, 最终运行结果:
org.apache.ibatis.binding.MapperProxy@cd2dae5
T1
测试插入方法
public class MonsterMapperTest {
private SqlSession sqlSession;
private MonsterMapper monsterMapper;
@Before // 加入 junit 单元测试中的 Before 注解, 在 Test 方法测试前会进行执行
public void init() {
sqlSession = MyBatisUtils.getSqlSession();
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
}
@Test
public void t1() {
monsterMapper.addMonster(new Monster(1, 1, LocalDateTime.now(), "[email protected]", 1, "张三", 120.));
/*
mysql> SELECT * FROM monster;
Empty set (0.00 sec)
mysql>
mysql> SELECT * FROM monster;
+----+-----+------------+-----------------+--------+--------+--------+
| id | age | birthday | email | gender | name | salary |
+----+-----+------------+-----------------+--------+--------+--------+
| 1 | 1 | 2024-04-13 | [email protected] | 1 | 张三 | 120 |
+----+-----+------------+-----------------+--------+--------+--------+
1 row in set (0.10 sec)
*/
}
}
MyBatis 使用说明图
Insert, Update, Delete
我们可以根据官网https://mybatis.org/mybatis-3/zh_CN/sqlmap-xml.html#insert_update_and_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 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
插入操作 - 获得自增长 ID
其中useGeneratedKeys
以及keyProperty
可以进行设置得到自增长ID, 如下:
<insert id="addMonster" parameterType="com.heihu577.Beans.Monster" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`)
VALUES(#{age}, #{birthday}, #{email}, #{gender}, #{name}, #{salary})
</insert>
增加两个属性之后, 我们可以看到结果如下:
@Test
public void t3() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster = new Monster();
monster.setAge(99);
monster.setEmail("[email protected]");
monster.setBirthday(LocalDateTime.now());
monster.setGender(20);
monster.setName("gogogo");
monster.setSalary(120.);
mapper.addMonster(monster);
System.out.println("增加的对象: " + monster);
System.out.println("自增长ID: " + monster.getId());
/*
增加的对象: Monster{id=4, age=99, birthday=2024-04-14T14:50:23.474, email='[email protected]', gender=20, name='gogogo', salary=120.0}
自增长ID: 4
*/
}
删除操作 (事务管理)
在MonsterMapper
接口中定义如下方法声明:
public void deleteMonster(Integer id); // 根据 ID 号进行删除
随后在MonsterMapper.xml
文件中进行实现:
<delete id="deleteMonster" parameterType="java.lang.Integer">
DELETE FROM `monster` WHERE `id` = #{id}
</delete>
随后我们将我们的表修改为Innodb
, 这样MyBatis
需要进行提交事务才可以成功生效, 创建测试代码:
@Test
public void t4() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
mapper.deleteMonster(1);
sqlSession.commit(); // 提交
sqlSession.close(); // 删除
System.out.println("删除成功!");
}
修改操作
在MonsterMapper
接口中定义如下方法定义:
public void updateMonsterById(Monster monster); // 通过 Monster 修改值
随后我们定义MonsterMapper.xml
中, 实现该接口:
<update id="updateMonsterById" parameterType="com.heihu577.Beans.Monster">
UPDATE `monster`
SET `age` = #{age}, `birthday` = #{birthday}, `email` = #{email},
`gender` = #{gender}, `name` = #{name}, `salary` = #{salary}
WHERE `id` = #{id}
</update>
定义测试方法:
@Test
public void t5() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
/*
+----+-----+------------+------------------+--------+-----------+--------+
| id | age | birthday | email | gender | name | salary |
+----+-----+------------+------------------+--------+-----------+--------+
| 2 | 0 | 2024-04-14 | [email protected] | 2 | 张三: 0 | 120 |
+----+-----+------------+------------------+--------+-----------+--------+
* */
Monster monster = new Monster();
monster.setId(2);
monster.setName("李四");
monster.setEmail("[email protected]");
monster.setBirthday(LocalDateTime.now());
monster.setAge(30);
monster.setSalary(200.);
monster.setGender(3);
mapper.updateMonsterById(monster);
sqlSession.commit();
sqlSession.close();
/*
*
* +----+-----+------------+------------------+--------+-----------+--------+
| id | age | birthday | email | gender | name | salary |
+----+-----+------------+------------------+--------+-----------+--------+
| 2 | 30 | 2024-04-14 | [email protected] | 3 | 李四 | 200 |
+----+-----+------------+------------------+--------+-----------+--------+
* */
}
设置 MyBatis 别名
在我们前面定义mapper
中, 属性名称type
都是写的类全路径, 这样很麻烦, 这里我们可以在mybatis-config.xml
文件中进行配置一个别名, 如下:
<typeAliases>
<typeAlias type="com.heihu577.Beans.Monster" alias="Monster"/>
</typeAliases>
定义完毕之后, 我们在MonsterMapper.xml
文件中就可以进行使用, 如下:
<insert id="addMonster" parameterType="Monster" useGeneratedKeys="true" keyProperty="id">
<!-- parameterType 进行了简写 -->
INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`)
VALUES(#{age}, #{birthday}, #{email}, #{gender}, #{name}, #{salary})
</insert>
SELECT 查询操作
根据ID返回Bean
在MonsterMapper
中定义如下方法签名:
public Monster getMonsterById(Integer id); // 传入 ID, 得到 Monster 对象
随后在MonsterMapper.xml
文件中进行实现:
<select id="getMonsterById" resultType="Monster" parameterType="java.lang.Integer">
SELECT * FROM `Monster` WHERE `ID` = #{id}
</select>
测试结果:
@Test
public void t6() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster = mapper.getMonsterById(2);
System.out.println(monster);
// Monster{id=2, age=30, birthday=2024-04-14T00:00, email='[email protected]', gender=3, name='李四', salary=200.0}
}
查询出所有Bean
在MonsterMapper
中定义如下方法声明:
public List<Monster> getAllMonsters();
在MonsterMapper.xml
文件中进行实现该方法:
<select id="getAllMonsters" resultType="Monster">
SELECT * FROM `Monster`
</select>
最终测试结果:
@Test
public void t7() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
List<Monster> allMonsters = mapper.getAllMonsters();
System.out.println(allMonsters);
/*
* [Monster{id=2, age=30, birthday=2024-04-14T00:00, email='[email protected]', gender=3, name='李四', salary=200.0}, Monster{id=3, age=1, birthday=2024-04-14T00:00, email='[email protected]', gender=2, name='张三: 1', salary=120.0}, Monster{id=4, age=99, birthday=2024-04-14T00:00, email='[email protected]', gender=20, name='gogogo', salary=120.0}, Monster{id=6, age=12, birthday=2024-04-14T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}]
* */
}
配置 MyBatis 日志输出
用于查看MyBatis
向数据库发送了什么内容信息, 在mybatis-config.xml
文件中进行配置:
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
我们可以看到如下结果:
@Test
public void t8() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
List<Monster> allMonsters = mapper.getAllMonsters();
/*
* ==> Preparing: SELECT * FROM `Monster`
==> Parameters:
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 2, 30, 2024-04-14, [email protected], 3, 李四, 200.0
<== Row: 3, 1, 2024-04-14, [email protected], 2, 张三: 1, 120.0
<== Row: 4, 99, 2024-04-14, [email protected], 20, gogogo, 120.0
<== Row: 6, 12, 2024-04-14, [email protected], 1, 张三, 120.0
<== Total: 4
* */
System.out.println(allMonsters);
}
手动实现 MyBatis 机制
MyBatis 核心框架图
具体实现步骤
我们创建一个子项目即可, 随后我们按照步骤依次操作.
搭建基础环境
导入具体依赖项
<dependencies>
<!-- 用于读取 XML 配置 -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!-- 根据 bean, 自动生成 getter && setter -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
创建对应的库 && 表
CREATE DATABASE `heihu_mybatis`;
USE `heihu_mybatis`
CREATE TABLE `monster` (
`id` INT NOT NULL AUTO_INCREMENT,
`age` INT NOT NULL,
`birthday` DATE DEFAULT NULL,
`email` VARCHAR(255) NOT NULL,
`gender` TINYINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`salary` DOUBLE NOT NULL,
PRIMARY KEY (`id`)
) CHARSET=utf8; -- 目前默认还是 MyIsAm 引擎
INSERT INTO `monster` VALUES(NULL, 200, '2000-11-11', '[email protected]', 1, '牛魔王', 8888.88);
实现思路图
读取配置文件, 建立连接
我们可以看到如下图, 接下来我们要完成读取 XML 文件 -> 建立链接
的操作.
创建 heihu_mybatis.xml 文件
在resources
目录下定义heihu_mybatis.xml
文件, 并且文件内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<!-- 配置数据库链接信息 --> <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<!-- 配置链接数据库的 URL --> <property name="url" value="jdbc:mysql://127.0.0.1:3306/heihu_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>
<!-- 用户名 --> <property name="username" value="root"/>
<!-- 密码 --> <property name="password" value="root"/>
</database>
当然了, 定义该文件是用于被读取的, 所以下面我们需要定义一个HeihuConfiguration
类, 专门用于XML文件处理
.
创建 com.heihu577.SqlSession.HeihuConfiguration
定义如下代码:
public class HeihuConfiguration {
private static ClassLoader loader = ClassLoader.getSystemClassLoader(); // 定义类加载器, 可以进行读取文件
private SAXReader saxReader = new SAXReader(); // 用于读取 XML 文件的工具
/* 传入 xml 数据库配置文件, 返回一个 Connection */
public Connection build(String xmlFile) {
System.out.println(">> HeihuConfiguration::build 进行读取 " + xmlFile + " 配置文件, 并返回一个链接");
Connection result = null;
InputStream xmlInputStream = loader.getResourceAsStream(xmlFile); // 读取外部传递进来的配置文件
try {
Document document = saxReader.read(xmlInputStream);
Element rootElement = document.getRootElement(); // 读取 <database> 标签, 结果: org.dom4j.tree.DefaultElement@73a8dfcc [Element: <database attributes: []/>]
result = getConnectionByElement(rootElement); // 得到 Connection
} catch (DocumentException e) {
throw new RuntimeException(e);
}
return result;
}
}
那么, 接下来我们定义一个getConnectionByElement
方法, 专门用于处理<database>
标签的解析, 代码变为如下:
public class HeihuConfiguration {
private static ClassLoader loader = ClassLoader.getSystemClassLoader(); // 定义类加载器, 可以进行读取文件
private SAXReader saxReader = new SAXReader(); // 用于读取 XML 文件的工具
/* 传入 xml 数据库配置文件, 返回一个 Connection */
public Connection build(String xmlFile) {
System.out.println(">> HeihuConfiguration::build 进行读取 " + xmlFile + " 配置文件, 并返回一个链接");
Connection result = null;
InputStream xmlInputStream = loader.getResourceAsStream(xmlFile); // 读取外部传递进来的配置文件
try {
Document document = saxReader.read(xmlInputStream);
Element rootElement = document.getRootElement(); // 读取 <database> 标签
result = getConnectionByElement(rootElement); // 得到 Connection
} catch (DocumentException e) {
throw new RuntimeException(e);
}
return result;
}
/* 根据XML根节点, 读取数据库配置并返回 */
public Connection getConnectionByElement(Element rootElement) {
System.out.println(">> HeihuConfiguration::getConnectionByElement 进行读取 " + rootElement.getName() + " 标签, " +
"并提取数据库配置, 返回 Connection");
Connection result = null;
String dirverClass = null, url = null, username = null, password = null;
List<Element> elements = rootElement.elements();
for (Element element : elements) {
if (!"property".equals(element.getName())) {
continue;
}
String nameType = element.attributeValue("name");
switch (nameType) {
case "driverClass":
dirverClass = element.attributeValue("value");
break;
case "url":
url = element.attributeValue("value");
break;
case "username":
username = element.attributeValue("value");
break;
case "password":
password = element.attributeValue("value");
break;
}
}
System.out.println("tdirverClass -> " + dirverClass);
System.out.println("turl -> " + url);
System.out.println("tusername -> " + username);
System.out.println("tpassword -> " + password);
try {
Class.forName(dirverClass);
result = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return result;
}
}
在当前类创建Junit
测试方法, 代码如下:
@Test
public void testBuild() {
HeihuConfiguration heihuConfiguration = new HeihuConfiguration();
Connection connection = heihuConfiguration.build("mybatis_config.xml");
System.out.println(connection);
/*
>> HeihuConfiguration::build 进行读取 mybatis_config.xml 配置文件, 并返回一个链接
>> HeihuConfiguration::getConnectionByElement 进行读取 database 标签, 并提取数据库配置, 返回 Connection
dirverClass -> com.mysql.cj.jdbc.Driver
url -> jdbc:mysql://127.0.0.1:3306/heihu_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8
username -> root
password -> root
com.mysql.cj.jdbc.ConnectionImpl@691a7f8f
*/
}
可以看到, 通过传入配置文件名称, 可以成功得到Connection
链接.
编写执行器, 发送 SQL 语句, 进行测试
当然了, 我们可以得到Connection
, 那么我们就可以发送SQL语句
, 接下来我们完成如下部分:
创建 JavaBean (使用 lombok 自动生成 setter && getter && toString && 有参构造 && 无参构造)
这里我们针对 Monster 表进行创建一个 JavaBean, 放置在com.heihu577.Beans
包包中, 代码如下:
@Getter // lombok 下的 注解
@Setter // 加入这些注解后, 会自动生成 getter && setter && toString && 无参构造 && 有参构造
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Monster {
/*
* +----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| age | int(11) | NO | | NULL | |
| birthday | date | YES | | NULL | |
| email | varchar(255) | NO | | NULL | |
| gender | tinyint(4) | NO | | NULL | |
| name | varchar(255) | NO | | NULL | |
| salary | double | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+
7 rows in set (0.01 sec)
* */
private Integer id;
private Integer age;
private LocalDate birthday;
private String email;
private Integer gender;
private String name;
private Double salary;
}
当然, 如果感觉依次加这些注解比较麻烦, 我们还可以直接声明@Data
来进行代替, 例如:
@Data // 代替上面那么多声明, 但是 Data 注解不包含有参构造器, 所以我们需要增加声明
@AllArgsConstructor // 如果只填写了 AllArgsConstructor, 那么默认的无参构造器会被覆盖, 所以在这里我们还需要定义一个无参构造器
@NoArgsConstructor
public class Monster {
/*
* +----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| age | int(11) | NO | | NULL | |
| birthday | date | YES | | NULL | |
| email | varchar(255) | NO | | NULL | |
| gender | tinyint(4) | NO | | NULL | |
| name | varchar(255) | NO | | NULL | |
| salary | double | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+
7 rows in set (0.01 sec)
* */
private Integer id;
private Integer age;
private LocalDate birthday;
private String email;
private Integer gender;
private String name;
private Double salary;
}
编写 HeihuExecutor
定义Executor
接口如下:
public interface Executor {
public String XML_FILE = "mybatis_config.xml"; // 定义配置文件路径
public <T> T query(String sql, Object parameter, Class<T> clazz); // SQL 语句, 所需参数, 注入的 BEAN 类型
}
对其进行实现:
public class HeihuExecutor implements Executor {
private HeihuConfiguration heihuConfiguration = new HeihuConfiguration();
@Override
public <T> T query(String sql, Object parameter, Class<T> clazz) { // 传入 ABC.class, 编译器就会知道返回的是 ABC 类型
System.out.println(">> HeihuExecutor::query 发送 SQL 语句");
System.out.println("tSQL: " + sql);
System.out.println("tParameter: " + parameter);
Connection connection = heihuConfiguration.build(XML_FILE); // 读取 XML 文件, 并得到 Connection 链接
Object result = null;
try {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, String.valueOf(parameter));
ResultSet resultSet = preparedStatement.executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData(); // 获得列名称
int columnCount = metaData.getColumnCount(); // 得到列的数量
result = clazz.newInstance();
resultSet.next();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i); // 获得每一列名称
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) { // 使用列名称与JavaBean属性名称进行匹配
if (columnName.equals(declaredField.getName())) { // 匹配成功后进行封装 BEAN
declaredField.setAccessible(true);
Object value = resultSet.getObject(declaredField.getName()); // 拿出查询出的结果
if(value instanceof Date){
value = ((Date) value).toLocalDate(); // Date 需要进行转换
}
declaredField.set(result, value); // 放置到 JavaBean 中
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return (T) result; // 将封装好的 BEAN 进行返回
}
}
测试结果
在本类增加如下测试方法:
@Test
public void test() {
HeihuExecutor heihuExecutor = new HeihuExecutor();
Monster monster = heihuExecutor.query("SELECT * FROM `monster` WHERE `id` = ?", 1, Monster.class);
System.out.println(monster);
/*
>> HeihuExecutor::query 发送 SQL 语句
SQL: SELECT * FROM `monster` WHERE `id` = ?
Parameter: 1
>> HeihuConfiguration::build 进行读取 mybatis_config.xml 配置文件, 并返回一个链接
>> HeihuConfiguration::getConnectionByElement 进行读取 database 标签, 并提取数据库配置, 返回 Connection
dirverClass -> com.mysql.cj.jdbc.Driver
url -> jdbc:mysql://127.0.0.1:3306/heihu_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8
username -> root
password -> root
Monster(id=1, age=200, birthday=2000-11-11, [email protected], gender=1, name=牛魔王, salary=8888.88)
*/
}
编写 SqlSession
public class HeihuSqlSession {
private HeihuExecutor executor = new HeihuExecutor(); // SqlSession 具有发送 SQL 语句的功能
public <T> T selectOne(String sql, Object parameter, Class<T> clazz) {
System.out.println(">> HeihuSqlSession::selectOne 准备发送 SQL 语句, 调用 HeihuExecutor::query 进行发送");
return executor.query(sql, parameter, clazz);
}
}
当然, 测试也及其简单, 如下:
@Test
public void test() {
Monster monster = selectOne("SELECT * FROM `monster` WHERE `id` = ?", 1, Monster.class);
System.out.println(monster);
/*
>> HeihuSqlSession::selectOne 准备发送 SQL 语句, 调用 HeihuExecutor::query 进行发送
>> HeihuExecutor::query 发送 SQL 语句
SQL: SELECT * FROM `monster` WHERE `id` = ?
Parameter: 1
>> HeihuConfiguration::build 进行读取 mybatis_config.xml 配置文件, 并返回一个链接
>> HeihuConfiguration::getConnectionByElement 进行读取 database 标签, 并提取数据库配置, 返回 Connection
dirverClass -> com.mysql.cj.jdbc.Driver
url -> jdbc:mysql://127.0.0.1:3306/heihu_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8
username -> root
password -> root
Monster(id=1, age=200, birthday=2000-11-11, [email protected], gender=1, name=牛魔王, salary=8888.88)
*/
}
当然, 目前我们的SqlSession
只完成了调用 Executor 发送 SQL 语句
的功能.
编写 Mapper
创建MonsterMapper
接口如下:
package com.heihu577.Mapper; // 注意新开一个 Mapper 包
import com.heihu577.Beans.Monster;
public interface MonsterMapper {
public Monster queryMonsterById(Integer id);
}
随后我们在resources
目录下定义一个MonsterMapper.xml
文件, 文件内容如下:
<?xml version="1.0"?>
<mapper namespace="com.heihu577.Mapper.MonsterMapper"> <!-- 指定对 MonsterMapper 进行编写 SQL 语句 -->
<select id="queryMonsterById" parameterType="java.lang.Integer" resultType="com.heihu577.Beans.Monster">
SELECT * FROM `monster` WHERE `id` = ?
</select>
<!-- ID="方法名称" parameterType="接收的参数类型" resultType="返回的BEAN类型" -->
</mapper>
开发 MapperBean
根据上面XML文件的定义, 接下来我们对其进行封装成一个MapperBean
其中关系如下:
定义 MapperBean 保存 Mapper 名称 以及 XML 中所配置的方法等
接下来我们将它们之间的关系进行保存到一个BEAN
对象中, 例如, 定义如下BEAN
:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MapperBean {
private String MapperName; // 对应具体某个 XML 文件
private List<MapperInfo> MapperInfos = new Vector<>(); // 对应某个 XML 文件中所定义的每一条标签的信息
public void MapperInfoAdd(MapperInfo mapperInfo) {
MapperInfos.add(mapperInfo); // 增加一条 Mapper 信息
}
}
定义完毕之后, 我们继续定义MapperInfo
定义 MapperInfo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MapperInfo {
private String sqlType; // sql类型: <select> <update> <delete> <insert>
// <select id="getMonsterById" resultType="com.heihu577.Beans.Monster">
private String funcName; // 上述 id 部分 (方法名称)
private Object resultType; // 上述 resultType 部分 (返回类型)
private String parameterType; // 参数类型
private String sql; // 需要执行的 SQL 语句 (<select> 这一部分 </select>)
}
这样定义之后, 我们就可以将XML文件
与接口
进行绑定在一起了.
解析 MonsterMapper
因为我们之前的HeihuConfiguration
是用于处理XML
文件信息的, 所以在这里我们需要读取Mapper
文件, 读取到接口信息, 并生成MapperBean
, 定义HeihuConfiguration::readMapper
方法, 具体实现如下:
/* 根据传递过来的 xml 路径名称, 生成其 MapperBean 并返回 */
public MapperBean readMapper(String path) {
MapperBean result = new MapperBean();
InputStream datasource = loader.getResourceAsStream(path); // 根据给过来的 XML 路径, 得到 InputStream 流
try {
Document document = saxReader.read(datasource);
Element element = document.getRootElement(); // 读取根节点
String clazzString = element.attributeValue("namespace");
/*
* 读取 <mapper namespace="com.heihu577.Mapper.MonsterMapper">
* */
result.setMapperName(clazzString);
List<Element> elements = element.elements();
if (!elements.isEmpty()) {
/*
* 开始遍历
<select id="queryMonsterById" parameterType="java.lang.Integer" resultType="com.heihu577.Beans
* .Monster">
SELECT * FROM `monster` WHERE `id` = ?
</select>
等标签, 并封装, 为了方便理解, 下面的变量名称可能不合理
* */
for (Element el : elements) {
MapperInfo mapperInfo = new MapperInfo(); // 准备存放
String select = el.getName(); // 也就是SQL类型
String id = el.attributeValue("id"); // 也就是对应接口方法名称
String parameterType = el.attributeValue("parameterType"); // 也就是参数类型
String resultType = el.attributeValue("resultType"); // 返回结果类型
String sql = el.getStringValue(); // 具体 SQL 语句
mapperInfo.setSql(sql);
mapperInfo.setFuncName(id);
mapperInfo.setSqlType(select);
mapperInfo.setResultType(Class.forName(resultType).newInstance());
mapperInfo.setParameterType(parameterType);
result.MapperInfoAdd(mapperInfo); // 加入进去
}
}
} catch (DocumentException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return result;
}
测试结果
@Test
public void testReadMapper() {
MapperBean mapperBean = readMapper("MonsterMapper.xml");
System.out.println(mapperBean);
/*
MapperBean(MapperName=com.heihu577.Mapper.MonsterMapper, MapperInfos=[MapperInfo(sqlType=select, funcName=queryMonsterById, resultType=Monster(id=null, age=null, birthday=null, email=null, gender=null, name=null, salary=null), parameterType=java.lang.Integer, sql=
SELECT * FROM `monster` WHERE `id` = ?
)])
*/
}
此时已经可以根据对应的Mapper.xml
文件进行生成对应的MapperBean
了, 接下来就是通过动态代理
, 进行返回具体接口的对象.
动态代理 Mapper 的方法
接下来完成该部分功能:
定义代理类
/* 代理对象类, 链接 XML 文件 以及 接口类, 让 HeihuSqlSession.getMapper 用 */
public class HeihuMapperProxy implements InvocationHandler {
private HeihuSqlSession sqlSession = new HeihuSqlSession(); // 调用 mapperObj.方法名 时 发送 SQL 语句用
private Class<?> mapper; // 传入 Mapper 文件名称
private HeihuConfiguration heihuConfiguration = new HeihuConfiguration(); // 因为要读取 XML 信息, 所以一定要有 HeihuConfiguration
public HeihuMapperProxy(Class<?> mapperFile) {
this.mapper = mapperFile;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() != mapper) {
return null;
}
MapperBean mapperBean = heihuConfiguration.readMapper(mapper.getSimpleName() + ".xml"); // 通过 XML 文件, 得到
// MapperBean
List<MapperInfo> mapperInfos = mapperBean.getMapperInfos();
for (MapperInfo mapperInfo : mapperInfos) {
String funcName = mapperInfo.getFuncName();
if (funcName.equals(method.getName())) { // 调用的方法在XML中有定义
String sql = mapperInfo.getSql();
return sqlSession.selectOne(sql, String.valueOf(args[0]), mapperInfo.getResultType().getClass());
}
}
return null;
}
}
该代理类的本质则是, 比较方法名称是否有对应上的MapperInfo
中所包含的方法名称, 如果存在, 那么直接调用heihuSqlSession
对象的selectOne
方法进行查询即可.
定义 getMapper 方法, 并且生成代理对象
此时我们在HeihuSqlSession
中增加一个getMapper
方法, 专门用于返回代理对象用.
public class HeihuSqlSession {
private HeihuExecutor executor = new HeihuExecutor(); // SqlSession 具有发送 SQL 语句的功能
public <T> T selectOne(String sql, Object parameter, Class<T> clazz) {
System.out.println(">> HeihuSqlSession::selectOne 准备发送 SQL 语句, 调用 HeihuExecutor::query 进行发送");
return executor.query(sql, parameter, clazz);
}
public <T> T getMapper(Class<T> clazz) { // 传入进来的是 接口名, 返回代理类
System.out.println(">> HeihuSqlSession::getMapper 进行得到 Mapper 类");
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new HeihuMapperProxy(clazz));
}
}
测试结果
在本类进行定义如下方法:
@Test
public void getMapperTest() {
MonsterMapper mapper = getMapper(MonsterMapper.class);
Monster monster = mapper.queryMonsterById(1);
System.out.println(monster);
/*
>> HeihuSqlSession::getMapper 进行得到 Mapper 类
>> HeihuSqlSession::selectOne 准备发送 SQL 语句, 调用 HeihuExecutor::query 进行发送
>> HeihuExecutor::query 发送 SQL 语句
SQL:
SELECT * FROM `monster` WHERE `id` = ?
Parameter: 1
>> HeihuConfiguration::build 进行读取 mybatis_config.xml 配置文件, 并返回一个链接
>> HeihuConfiguration::getConnectionByElement 进行读取 database 标签, 并提取数据库配置, 返回 Connection
dirverClass -> com.mysql.cj.jdbc.Driver
url -> jdbc:mysql://127.0.0.1:3306/heihu_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8
username -> root
password -> root
Monster(id=1, age=200, birthday=2000-11-11, [email protected], gender=1, name=牛魔王, salary=8888.88)
*/
}
可以看到, 已经可以通过一个getMapper
方法来得到一个Mapper
类, 并且通过动态代理的方式, 调用了sqlSession
中的selectOne
方法, 就实现了. 没有任何类
实现该接口
, 但是可以进行调用的效果.
总结图
MyBatis 两种方式
原生 API 调用
我们可以通过如下测试代码, 来查看SqlSession
的具体类型:
public class MonsterMapperTest3 {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void t1() {
System.out.println(sqlSession.getClass()); // class org.apache.ibatis.session.defaults.DefaultSqlSession
}
}
而DefaultSqlSession
是由如下方法进行定义的:而这里的
String statement
参数, 并不是SQL
语句, 而是接口方法的指定
增加一条记录 API 调用
使用方法如下:
public class MonsterMapperTest3 {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void testInsert() {
/*
* 当前数据库定义如下:
* mysql> truncate table monster;
Query OK, 0 rows affected (0.49 sec)
mysql> SELECT * FROM monster;
Empty set (0.00 sec)
* */
Monster monster = new Monster();
monster.setId(2);
monster.setGender(6);
monster.setEmail("[email protected]");
monster.setSalary(123.3);
monster.setName("heihu888");
monster.setBirthday(LocalDateTime.now());
monster.setAge(199);
int affectedRow = sqlSession.insert("com.heihu577.Mapper.MonsterMapper.addMonster", monster);
// sqlSession.insert 方法对应如下
/*
* public interface MonsterMapper {
* public void addMonster(Monster monster); // 定义一个增加 Monster 对象的功能
* }
* 因为 mybatis-config.xml 文件中进行绑定了 Mapper 文件目录, 所以在这里 Mybatis 知道执行的 SQL 语句是什么模板
* */
System.out.println("影响的行数: " + affectedRow);
sqlSession.commit();
/*
* mysql> SELECT * FROM monster;
+----+-----+------------+---------------+--------+----------+--------+
| id | age | birthday | email | gender | name | salary |
+----+-----+------------+---------------+--------+----------+--------+
| 1 | 199 | 2024-04-19 | [email protected] | 6 | heihu888 | 123.3 |
+----+-----+------------+---------------+--------+----------+--------+
1 row in set (0.00 sec)
* */
sqlSession.close();
}
}
查询功能 API 调用
public void testSelect() {
Object o = sqlSession.selectOne("com.heihu577.Mapper.MonsterMapper.getMonsterById", 1);
System.out.println(o);
/*
==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}
*/
}
当然, 还有修改记录的update
方法, 以及删除记录的delete
方法, 需要注意的是, 如果是增删改操作, 必须使用sqlSession.commit()
方法进行提交, 否则遇到事务无法更新数据库.
使用注解方式操作 MyBatis
接下来我们使用注解的方式进行配置接口方法以及SQL语句, 定义如下接口:
public interface MonsterAnnotation {
@Insert("INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`) VALUES(#{age}, #{birthday}, " +
"#{email}, #{gender}, #{name}, #{salary})")
public void addMonster(Monster monster); // 定义一个增加 Monster 对象的功能
@Delete("DELETE FROM `monster` WHERE `id` = #{id}")
public void deleteMonster(Integer id); // 根据 ID 号进行删除
@Update("UPDATE `monster` " +
"SET `age` = #{age}, `birthday` = #{birthday}, `email` = #{email}, " +
"`gender` = #{gender}, `name` = #{name}, `salary` = #{salary} " +
"WHERE `id` = #{id}")
public void updateMonsterById(Monster monster); // 通过 Monster 修改值
@Select("SELECT * FROM `Monster` WHERE `ID` = #{id}")
public Monster getMonsterById(Integer id); // 传入 ID, 得到 Monster 对象
@Select("SELECT * FROM `Monster`")
public List<Monster> getAllMonsters();
}
当然了, 我们仍需要在mybatis-config.xml
文件中进行指明该接口:
<mappers>
<mapper resource="com/heihu577/Mapper/MonsterMapper.xml"/>
<mapper resource="com/heihu577/Mapper/MonkMapper.xml"/>
<!-- 上面是之前指明的 XML 文件路径, 下面是刚刚定义的接口全路径 -->
<mapper class="com.heihu577.Mapper.MonsterAnnotation"/>
</mappers>
测试结果:
public class MonsterAnnotationTest {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
private MonsterAnnotation monsterAnnotation;
@Test
public void testMonster() {
monsterAnnotation = sqlSession.getMapper(MonsterAnnotation.class);
List<Monster> allMonsters = monsterAnnotation.getAllMonsters();
System.out.println(allMonsters); // [Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}]
}
}
返回自增长 ID
当然了, 我们在使用XML
方式进行配置Mapper
时, 可以通过属性名称进行指定, 而如果我们使用注解
方式进行指明时, 可以使用@Options
注解, 如下:
public interface MonsterAnnotation {
@Insert("INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`) VALUES(#{age}, #{birthday}, " +
"#{email}, #{gender}, #{name}, #{salary})")
@Options(useGeneratedKeys = true, keyProperty = "id")
/*
* 对应上
* <insert id="addMonster" parameterType="Monster" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`)
VALUES(#{age}, #{birthday}, #{email}, #{gender}, #{name}, #{salary})
</insert>
了
* */
public void addMonster(Monster monster); // 定义一个增加 Monster 对象的功能
}
最终运行结果如下:
@Test
public void testMonster2() {
Monster monster = new Monster(999, 188, LocalDateTime.now(), "[email protected]", 8, "heihu666", 123.3);
monsterAnnotation = sqlSession.getMapper(MonsterAnnotation.class);
monsterAnnotation.addMonster(monster);
System.out.println("增长的 ID: " + monster.getId());
/*
最终运行结果:
==> Preparing: INSERT INTO `monster`(`age`,`birthday`,`email`,`gender`,`name`,`salary`) VALUES(?, ?, ?, ?, ?, ?)
==> Parameters: 188(Integer), 2024-04-19T17:59:03.238(LocalDateTime), [email protected](String), 8(Integer), heihu666(String), 123.3(Double)
<== Updates: 1
增长的 ID: 3
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@66d18979]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@66d18979]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@66d18979]
Returned connection 1725008249 to pool.
*/
sqlSession.commit();
sqlSession.close();
}
配置文件详解
properties 标签引入 properties 文件
这里类似于Spring
中的<context:property-placeholder>
标签, 用于引入properties
文件, 然后进行引入其中的属性, 那么我们在resources
目录下创建jdbc.properties
文件, 配置信息如下:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=root
然后我们这样进行引用:
<configuration>
<properties resource="jdbc.properties"/> <!-- 可以配置 properties 信息, 也可以引入 properties 文件 -->
<!-- 其他配置项 ... -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/> <!-- 配置事务管理器 -->
<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>
<!-- 其他配置项 ... -->
</configuration>
具体可以参考: https://mybatis.net.cn/configuration.html#properties
settings 标签, 配置日志等设置
具体参考: https://mybatis.net.cn/configuration.html#settings
typeAlias 标签, 配置 Bean 类型 (一把梭哈)
在之前我们使用过该标签, 上面的案例为:
<typeAliases>
<typeAlias type="com.heihu577.Beans.Monster" alias="Monster"/>
<typeAlias type="com.heihu577.Beans.Monk" alias="Monk"/>
</typeAliases>
而因为我们的这些Bean
都在同一个包下, 我们可以直接这样配置, 就可以直接生成效果:
<typeAliases>
<package name="com.heihu577.Beans"/>
</typeAliases>
具体参考: https://mybatis.net.cn/configuration.html#typeAliases
mapper 标签一把梭哈
<mappers>
<!-- <mapper resource="com/heihu577/Mapper/MonsterMapper.xml"/> <!– 指明所定义的XML-SQL文件 –>-->
<!-- <mapper resource="com/heihu577/Mapper/MonkMapper.xml"/>-->
<!-- <mapper class="com.heihu577.Mapper.MonsterAnnotation"/>-->
<package name="com.heihu577.Mapper"/>
</mappers>
当然了, 这样我们就不用一条一条
指定了.
类型自动转换
我们可以看一下https://mybatis.net.cn/configuration.html#typeHandlers给出的表格:
XML 映射器
ParameterType
-
传入简单类型, 按照 ID 查 Monster -
传入JAVABEAN, 查询时需要有多个筛选条件 -
当有多个条件时, 传入的参数就是POJO类型和JAVA对象, 比如 Monster 对象 -
当传入参数类是 String 时, 也可以使用 ${} 方法来接收参数.
通过 ID 或 NAME 查询 (多条件查询)
在这里我们可以这样定义方法声明:
public interface MonsterMapper {
public List<Monster> findMonsterByIdOrName(Monster monster); // 通过 ID 或 NAME 查询
}
随后定义MonsterMapper.xml
文件内容如下:
<mapper namespace="com.heihu577.Mapper.MonsterMapper">
<select id="findMonsterByIdOrName" parameterType="com.heihu577.Beans.Monster" resultType="com.heihu577.Beans.Monster"> <!-- 方法名称, 参数类型, 返回结果类型 -->
SELECT * FROM `monster` WHERE `id` = #{id} OR `name` = #{name}
</select>
</mapper>
创建测试代码:
public class TestMonsterMapper {
private SqlSession sqlSession;
private MonsterMapper monsterMapper;
@Before
public void beforeFunction() {
sqlSession = MyBatisUtils.getSqlSession();
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
}
@Test
public void testFindMonsterByIdOrName() {
Monster monster = new Monster();
monster.setId(1);
monster.setName("张三");
// 这里的目的是查询出 ID 为 1, name 为 张三 的记录.
System.out.println(monsterMapper.findMonsterByIdOrName(monster));
/*
[Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}, Monster{id=4, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}, Monster{id=5, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}]
*/
sqlSession.close();
}
}
查询 NAME 包含 heihu (模糊查询)
我们可以这样定义如下方法定义:
public interface MonsterMapper {
public List<Monster> findMonsterByName(String name); // 模糊查询
}
其中XML
文件的配置如下:
<select id="findMonsterByName" parameterType="String" resultType="com.heihu577.Beans.Monster">
SELECT * FROM `monster` WHERE `name` LIKE '%${name}%'
</select>
注意, 包含的 SQL 语句中使用 ${} 进行包围
创建测试代码:
@Test
public void testFindMonsterByName2() {
List<Monster> monsters = monsterMapper.findMonsterByName("heihu");
System.out.println(monsters);
/*
* [Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}, Monster{id=3, age=188, birthday=2024-04-19T00:00, email='[email protected]', gender=8, name='heihu666', salary=123.3}]
* */
}
SQL 注入问题
${} 的使用方式会引起 SQL 注入, 例如:
@Test
public void testFindMonsterByName1() {
List<Monster> monsters = monsterMapper.findMonsterByName("heihu' AND 1=updatexml(1,concat(1,user(),1),1)-'");
System.out.println(monsters);
/*
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLException: XPATH syntax error: 'root@localhost1'
成功进行报错注入.
*/
}
传入 HashMap 类型
声明一个方法, 按照传入参数是HashMap
的方式, 查询name=heihu888
并且salary=123.3
的所有Monster
.
返回 Map 类型
定义如下方法声明:
public List<Map<String, Object>> findMonsterByIdAndSalary_ParameterHashMap_ReturningMap(HashMap<String, Object> hashMap); // 传入 HashMap, 返回 Map
在MonsterMapper.xml
文件中定义如下SQL语句:
<select id="findMonsterByIdAndSalary_ParameterHashMap_ReturningMap" parameterType="hashmap" resultType="map">
SELECT * FROM `monster` WHERE `id` = #{id} OR `id` = #{idx}
</select>
其中parameterType=hashmap
以及resultType=map
的简写原因, 是因为官网中https://mybatis.net.cn/configuration.html#typeAliases中指明了类型关系, 如下:
别名 | 映射的类型 |
---|---|
_byte | byte |
... | ... |
map | Map |
hashmap | HashMap |
最终运行结果:
@Test
public void testReturnMap() {
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("id", 1); // 与 SQL 中给出的 #{id} 对应上
objectObjectHashMap.put("idx", 3); // 与 SQL 中给出的 #{idx} 对应上
List<Map<String, Object>> resultMap =
monsterMapper.findMonsterByIdAndSalary_ParameterHashMap_ReturningMap(objectObjectHashMap);
for (Map<String, Object> stringObjectMap : resultMap) {
System.out.println(stringObjectMap);
}
/*
* ==> Preparing: SELECT * FROM `monster` WHERE `id` = ? OR `id` = ?
==> Parameters: 1(Integer), 3(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Row: 3, 188, 2024-04-19, [email protected], 8, heihu666, 123.3
<== Total: 2
{birthday=2024-04-19, gender=6, name=heihu888, id=1, salary=123.3, age=199, [email protected]}
{birthday=2024-04-19, gender=8, name=heihu666, id=3, salary=123.3, age=188, [email protected]}
* */
}
ResultType
取出 JAVA BEAN 中字段
创建一张User
表:
CREATE TABLE `user`(
`user_id` int unsigned primary key auto_increment comment '用户ID',
`user_name` varchar(32) unique comment '用户名',
`user_pass` varchar(32) not null
)engine=Innodb;
INSERT INTO `user` VALUES(NULL, 'admin', 'admin888');
创建完毕后, 我们创建对应的JavaBean
:
@Data // 引入 lombok
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
}
注意这里并没有与数据库中的字段编写一致.
随后我们定义UserMapper
接口, 如下:
public interface UserMapper {
public void addUser(User user); // 使用 bean 进行插入
}
定义UserMapper.xml
文件, 内容如下:
<mapper namespace="com.heihu577.Mapper.UserMapper">
<insert id="addUser" parameterType="com.heihu577.Beans.User">
INSERT INTO `user`(`user_name`, `user_pass`) VALUES(#{username}, #{password})
<!-- 注意 SQL 中 #{username} 与 #{password} 是取出的 `java bean` 属性 -->
</insert>
</mapper>
定义完毕之后, 我们在mybatis-config.xml
文件中, 进行加入该mapper
, 如下:
<mappers>
<mapper resource="com/heihu577/Mapper/MonsterMapper.xml"/>
<mapper class="com.heihu577.Mapper.UserMapper"/> <!-- 加入进来 UserMapper -->
</mappers>
测试结果:
public class TestUserMapper {
@Test
public void t1() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User(null, "guest", "guest888");
userMapper.addUser(user);
/*
控制台:
* Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@55fe41ea]
==> Preparing: INSERT INTO `user`(`user_name`, `user_pass`) VALUES(?, ?)
==> Parameters: guest(String), guest888(String)
<== Updates: 1
数据库:
+---------+-----------+-----------+
| user_id | user_name | user_pass |
+---------+-----------+-----------+
| 1 | admin | admin888 |
| 3 | guest | guest888 | 增加了这一条记录
+---------+-----------+-----------+
* */
sqlSession.commit();
sqlSession.close();
}
}
resultType (查询结果别名演示)
接下来演示正常的resultType
如何使用, 定义findUserById
方法定义, 如下:
public List<User> findUserById(Integer id); // 指定ID的字段
随后在UserMapper.xml
文件中进行实现该方法:
<select id="findUserById" resultType="com.heihu577.Beans.User" parameterType="integer">
<!--
数据库中的字段: user_id, user_name, user_pass
BEAN中的字段: id, username, password
将查询出来的结果, 对应BEAN中的字段即可 (使用 AS 别名)
-->
SELECT `user_id` AS `id`, `user_name` AS `username`, `user_pass` AS `password` FROM `user` WHERE `user_id` = #{id}
</select>
最终运行结果:
@Test
public void t2() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allUser = mapper.findUserById(1);
System.out.println(allUser);
/*
==> Preparing: SELECT `user_id` AS `id`, `user_name` AS `username`, `user_pass` AS `password` FROM `user` WHERE `user_id` = ?
==> Parameters: 1(Integer)
<== Columns: id, username, password
<== Row: 1, admin, admin888
<== Total: 1
[User(id=1, username=admin, password=admin888)]
*/
}
resultMap (不使用别名的方式)
因为上面的形式, 我们需要一点一点AS
进行指定别名, 才能查询到正确的值, 那么接下来我们可以使用Mybatis
提供的resultMap
进行指定数据库字段
与BEAN
的关系, 等同于一个映射... 具体实现方法如下: 定义findAllUser
方法如下:
public List<User> findAllUser();
随后在XML文件中定义resultMap, 并使用, 如下:
<resultMap id="userResultMap" type="com.heihu577.Beans.User">
<result column="user_id" property="id"/> <!-- 数据库中是 user_id, BEAN 中是 id -->
<result column="user_name" property="username"/> <!-- 数据库中是 user_name, BEAN 中是 username -->
<result column="user_pass" property="password"/> <!-- 数据库中是 user_pass, BEAN 中是 password -->
</resultMap>
<select id="findAllUser" resultMap="userResultMap">
SELECT * FROM `user`
</select>
测试代码:
@Test
public void t3() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allUser = mapper.findAllUser();
System.out.println(allUser);
/*
运行结果:
==> Preparing: SELECT * FROM `user`
==> Parameters:
<== Columns: user_id, user_name, user_pass
<== Row: 1, admin, admin888
<== Row: 3, guest, guest888
<== Total: 2
[User(id=1, username=admin, password=admin888), User(id=3, username=guest, password=guest888)]
* */
}
动态 SQL 语句
具体功能为: 如果外部传递进来的值是1
, 那么执行A-SQL
, 如果外部传递进来的值是2
, 那么执行B-SQL
.
if 标签
下面我们使用一个案例来进行演示, 查询出age > 程序员输入进来的AGE
的所有妖怪. 如果程序员输入的age
不大于0, 那么输出所有妖怪.
public interface MonsterMapper {
public List<Monster> findMonsterByAge(@Param("age") Integer age); // 使用 @Param 给 if 标签使用
}
那么我们在MonsterMapper.xml
文件中这样实现:
<select id="findMonsterByAge" resultType="Monster" parameterType="Integer">
<!-- resultMap 已在 mybatis-config.xml 文件中进行配置了别名, parameterType 官网已给出了简写形式 -->
SELECT * FROM `monster` WHERE 1=1
<if test="age > 0">
AND age > #{age}
</if>
</select>
实现完毕后进行测试:
public class TestMonster {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
List<Monster> monsters1 = monsterMapper.findMonsterByAge(2); // Preparing: SELECT * FROM `monster` WHERE 1=1 AND age > ?
List<Monster> monsters2 = monsterMapper.findMonsterByAge(-1); // Preparing: SELECT * FROM `monster`
System.out.println(monsters1);
System.out.println(monsters2);
}
}
可以看到, 通过外部参数的不同, MyBatis
发送了不同的SQL语句
.
where 标签
需求: 查询 id > 2
, 并且 name = 张三
的所有记录, 如果name | id
为空, 则不拼接WHERE
条件. 在MonsterMapper
中定义方法声明:
public List<Monster> findMonsterByIdAndName(Monster monster);
在MonsterMapper.xml
文件中进行实现:
<select id="findMonsterByIdAndName" resultType="Monster" parameterType="Monster">
SELECT * FROM `monster`
<where>
<if test="name != null and age != null"> <!-- 因为是 age 是 Integer 类型, 所以判断 null 即可 -->
`age` > #{age} AND `name` = #{name}
</if>
<if test="name == null and age != null">
`age` > #{age}
</if>
<if test="age == null and name != null">
`name` = #{name}
</if>
</where>
<!--
这里有四种情况:
当 name&&age不为空, 则查询一个 AND 条件
当 name 为空, age 不为空, 则增加查询 age 的 WHERE 条件
当 age 为空, name 不为空, 则增加查询 name 的 WHERE 条件
当 age 和 name 都为空, 则不增加条件
-->
</select>
运行结果:
@Test
public void t2() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster = new Monster();
monster.setAge(2);
monster.setName("张三");
List<Monster> monsters = mapper.findMonsterByIdAndName(monster);
System.out.println(monsters);
// 当 monster.setAge && monster.setName 不为空时, 会有 WHERE 条件的限制, 否则, 查询所有.
}
WHERE 标签特性
在WHERE标签中, 假设存在这样的SQL:
<where>
<if test="条件为真">
AND 逻辑判断1
</if>
<if test="条件也为真">
AND 逻辑判断2
</if>
</where>
在两个条件判断都成立的情况下, 看似拼接了WHERE AND 逻辑判断1 AND 逻辑判断2
, 其实MyBatis
会将最开头的AND
拿掉, 解析为WHERE 逻辑判断1 AND 逻辑判断2
, 所以在这里SQL
依然成立.
choose/when/otherwise
-> 如果 name 不为空, 就按照名字查询妖怪 -> 如果指定的 id > 0, 就按照 id 来查询妖怪 -> 如果前面两个条件都不满足, 就默认查询 salary > 100的. -> 要求 choose/when/otherwise 标签实现, 传入参数要求使用 Map
定义如下接口中的方法:
public List<Monster> findMonsterByNameAndId_Choose(Map<String, Object> map);
定义MonsterMapper.xml
文件, 进行实现它:
<select id="findMonsterByNameAndId_Choose" parameterType="map" resultType="Monster">
SELECT * FROM `monster`
<where>
<choose>
<when test="name != null and name != ''">
`name` = #{name}
</when>
<when test="id > 0">
`id` = #{id}
</when>
<otherwise>
`salary` > 100
</otherwise>
</choose>
</where>
</select>
测试结果:
@Test
public void t3() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
/*
* 存在 name 的情况:
* ==> Parameters: 张三(String)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 4, 12, 2024-04-22, [email protected], 1, 张三, 120.0
<== Row: 5, 12, 2024-04-22, [email protected], 1, 张三, 120.0
<== Total: 2
[Monster{id=4, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}, Monster{id=5, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}]
* */
map.put("id", 4);
/* 没有 name, 但有 id 的情况
* ==> Preparing: SELECT * FROM `monster` WHERE `id` = ?
==> Parameters: 4(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 4, 12, 2024-04-22, [email protected], 1, 张三, 120.0
<== Total: 1
[Monster{id=4, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}]
* */
List<Monster> monsters = monsterMapper.findMonsterByNameAndId_Choose(map);
/*
* map 是一个空的 map, 则会报错
* */
System.out.println(monsters);
}
foreach 标签
查询 ID 为 1, 3, 5
的妖怪. 准备如下接口:
public List<Monster> findMonsterByID_ForEach(Map<String, Object> map);
在MonsterMapper.xml
中进行实现:
<select id="findMonsterByID_ForEach" resultType="Monster" parameterType="map">
SELECT * FROM `monster` <!-- 正常情况应该使用 IN(1,3,5) 进行得到集合 -->
<!-- 在这里我们使用 K-V 为这种情况: ids - [1,3,5], 来进行遍历出 IN 的结果 -->
<if test="ids != null and ids != ''">
<!-- 说明 ids 不为空 -->
<where>
`id` in <!-- WHERE `id` in -->
<foreach collection="ids" separator="," item="id" open="(" close=")">
<!--
collection: 被遍历的集合
separator: 用什么分割
item: 生成出的名称
open: 最左边是什么字符包围
close: 最右边是什么字符包围
-->
#{id}
</foreach>
</where>
</if>
</select>
最终运行结果:
@Test
public void t4() {
MonsterMapper mapper = sqlSession.getMapper(MonsterMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("ids", new Integer[]{1, 3, 5}); // ids 是被遍历的集合
List<Monster> monsters = mapper.findMonsterByID_ForEach(map);
System.out.println(monsters);
/*
* ==> Preparing: SELECT * FROM `monster` WHERE `id` in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 5(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Row: 3, 188, 2024-04-19, [email protected], 8, heihu666, 123.3
<== Row: 5, 12, 2024-04-22, [email protected], 1, 张三, 120.0
<== Total: 3
[Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}, Monster{id=3, age=188, birthday=2024-04-19T00:00, email='[email protected]', gender=8, name='heihu666', salary=123.3}, Monster{id=5, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}]
* */
}
trim 标签 (应用较少)
定义如下接口中的方法:
public List<Monster> findMonsterByIdAndName_Trim(Monster monster);
定义如下XML
:
<select id="findMonsterByIdAndName_Trim" resultType="Monster" parameterType="Monster">
SELECT * FROM `monster`
<trim prefix="WHERE" prefixOverrides="AND|OR"> <!-- 如果发现开头是 AND, 那么将开头的 AND 替换为 WHERE -->
<!-- 等同于一个 where 标签 -->
<if test="age != null">
AND `age` > #{age}
</if>
<if test="name != null and name != ''">
AND `name` = #{name}
</if>
</trim>
</select>
测试结果:
@Test
public void t5() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster = new Monster();
monster.setName("张三");
monster.setAge(5);
List<Monster> monsters = monsterMapper.findMonsterByIdAndName_Trim(monster);
System.out.println(monsters);
/*
==> Preparing: SELECT * FROM `monster` WHERE `age` > ? AND `name` = ?
==> Parameters: 5(Integer), 张三(String)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 4, 12, 2024-04-22, [email protected], 1, 张三, 120.0
<== Row: 5, 12, 2024-04-22, [email protected], 1, 张三, 120.0
<== Total: 2
[Monster{id=4, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}, Monster{id=5, age=12, birthday=2024-04-22T00:00, email='[email protected]', gender=1, name='张三', salary=120.0}]
*/
}
相当于扩展的 WHERE 标签.
set 标签
对指定 ID 的妖怪进行修改, 如果没有设置新的属性, 则保持原来的值.
这里 set 标签是用来应用于 update 语句的, 我们可以通过判断外部传递过来的 map 属性中, 是否含有某一项, 如果含有, 才增加条件, 定义如下接口方法.
public void updateMonster(Monster monster);
随后在MonsterMapper.xml
文件中进行定义它:
<update id="updateMonster" parameterType="map">
UPDATE `monster`
<set> <!-- set 标签可以智能的清除多余的 逗号 -->
<if test="age != null">
`age` = #{age},
</if>
<if test="email != null and email != ''">
`email` = #{email},
</if>
<if test="name != null and name != ''">
`name` = #{name},
</if>
<if test="birthday != null">
`birthday` = #{birthday},
</if>
<if test="gender != null and gender != ''">
`gender` = #{gender},
</if>
<if test="salary != null">
`salary` = #{salary},
</if>
</set>
WHERE `id` = #{id}
</update>
运行结果:
@Test
public void t6() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("id", 5);
map.put("birthday", "2021-04-22");
map.put("gender", 7);
/*
* | 5 | 12 | 2024-04-22 | [email protected] | 1 | 张三 | 120 |
* */
monsterMapper.updateMonster(map); // Preparing: UPDATE `monster` SET `birthday` = ?, `gender` = ? WHERE `id` = ?
sqlSession.commit();
/*
*| 5 | 12 | 2021-04-22 | [email protected] | 7 | 张三 | 120 |
* */
}
综合练习
要求hero
表: id号属性, nickname(外号)属性, skill(本领)属性, rank(排行)属性, salary(薪水)属性, join_datetime(入伙时期)
完成功能 (创建新项目完成):
-
创建 hero 表 -
编写方法, 添加 hero 记录 -
编写方法, 查询 rank 大于 10 的所有 hero, 如果输入的 rank 不大于 0, 则输出所有 hero -
编写方法, 查询 rank 为 3, 6, 10 [rank 可变] 的 hero -
编写方法, 修改 hero 信息, 如果没有设置新的属性值, 则保持原来的值 -
编写方法, 可以根据 id 查询 hero, 如果没有传入 id, 就返回所有 hero
第一题 (并且搭建基础环境)
配置本地pom.xml
文件, 允许com.heihu577.Mappers
进行输出到target
目录中, 并且引入lombok
:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
<!-- 父项目还包含了 mysql-driver 等 -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
定义完毕之后, 我们进行定义jdbc.properties
文件内容如下:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis_homework?useSSL=true&useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=root
随后我们创建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>
<properties resource="jdbc.properties"/> <!-- 引入刚才的 jdbc.properties 文件 -->
<typeAliases>
<package name="com.heihu577.Beans"/> <!-- 批量添加别名 -->
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/> <!-- 引入进来properties信息 -->
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- <mappers>-->
<!-- <mapper resource="org/mybatis/example/BlogMapper.xml"/>-->
<!-- </mappers>-->
</configuration>
定义完毕之后, 我们开始创建数据库部分:
create database mybatis_homework;
use mybatis_homework;
create table `hero`(
`id` int unsigned primary key auto_increment comment 'ID号',
`nickname` varchar(64) not null comment '外号',
`skill` varchar(64) not null comment '本领',
`rank` tinyint unsigned default 0 comment '排行',
`salary` decimal(10, 2) not null default 0.0 comment '薪水',
`join_datetime` datetime not null comment '入伙日期' -- MYSQL 中用下划线
)engine=innodb;
并且配置对应的JavaBean
:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Hero {
/*
* +---------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| nickname | varchar(64) | NO | | NULL | |
| skill | varchar(64) | NO | | NULL | |
| rank | tinyint(3) unsigned | YES | | 0 | |
| salary | decimal(10,2) | NO | | 0.00 | |
| join_datetime | datetime | NO | | NULL | |
+---------------+---------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
* */
private Integer id;
private String nickname;
private String skill;
private Integer rank;
private BigDecimal salary;
private LocalDateTime joinDatetime; // JavaBean 中用驼峰
}
那么随后我们创建对应的HeroMapper
接口内容如下:
public interface HeroMapper {}
并且创建对应的HeroMapper.xml
文件内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.Mappers.HeroMapper"> <!-- namespace 用于指定 接口 -->
</mapper>
架子已搭好, 在mybatis-config.xml
中进行注册该XML
文件:
<mappers>
<mapper resource="com/heihu577/Mappers/HeroMapper.xml"/>
</mappers>
随后创建MyBatisUtils
类, 用于获取SqlSession
:
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
InputStream is = MyBatisUtils.class.getClassLoader().getResourceAsStream("mybatis-config.xml"); // 当然, 也可以使用
// Mybatis 提供的 Resources 类
sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
}
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession(); // 返回 SqlSession
}
}
编写方法, 添加 hero 记录
在HeroMapper
接口中创建如下方法声明:
public void insertHeroByJavaBean(Hero hero);
在HeroMapper.xml
文件中进行实现该接口:
<insert id="insertHeroByJavaBean" parameterType="Hero" useGeneratedKeys="true" databaseId="id"> <!-- 返回添加索引 -->
<!-- 注意这里属性不要出错, 否则可能爆出 BindingException 错误 -->
INSERT INTO `hero` VALUES(NULL, #{nickname}, #{skill}, #{rank}, #{salary}, #{joinDatetime});
</insert>
创建测试文件, 进行测试该SQL
语句:
@Test
public void t1() {
HeroMapper heroMapper = sqlSession.getMapper(HeroMapper.class);
System.out.println(heroMapper.getClass()); // class com.sun.proxy.$Proxy5 代理类型
System.out.println(heroMapper); // org.apache.ibatis.binding.MapperProxy@23faf8f2
Hero hero = new Hero();
hero.setNickname("电击小子");
hero.setSkill("电人");
hero.setRank(1);
hero.setSalary(new BigDecimal("1999.10"));
hero.setJoinDatetime(LocalDateTime.now());
heroMapper.insertHeroByJavaBean(hero);
sqlSession.commit(); // 提交事务
sqlSession.close();
System.out.println("自增的ID: " + hero.getId()); // 自增的ID: 1
}
编写后续测试环境代码
在这里我们需要编写后续测试环境, 执行 truncate table hero
后, t1方法修改为如下情况:
@Test
public void t1() {
HeroMapper heroMapper = sqlSession.getMapper(HeroMapper.class);
System.out.println(heroMapper.getClass()); // class com.sun.proxy.$Proxy5 代理类型
System.out.println(heroMapper);
for (int i = 0; i < 100; i++) { // 添加 100 条记录, 方便测试
Hero hero = new Hero();
hero.setNickname("电击小子-" + i);
hero.setSkill("电人-" + i);
hero.setRank(i);
hero.setSalary(new BigDecimal("2000.10"));
hero.setJoinDatetime(LocalDateTime.now());
heroMapper.insertHeroByJavaBean(hero);
sqlSession.commit();
System.out.println("自增的ID: " + hero.getId());
}
sqlSession.close();
}
查询 rank 大于 10 的所有 hero, 如果输入的 rank 不大于 0, 则输出所有 hero
在HeroMapper
接口中定义如下方法:
public List<Hero> findHeroByRand(@Param("rank") Integer rank); // 因为要当条件, 所以使用 @Param 注解
在HeroMapper.xml
中进行实现:
<select id="findHeroByRand" resultType="Hero" parameterType="Integer">
SELECT * FROM `hero`
<where>
<if test="rank > 0">
AND `rank` > 10
</if>
</where>
</select>
最终运行结果:
@Test
public void t2() {
HeroMapper heroMapper = sqlSession.getMapper(HeroMapper.class);
List<Hero> heroByRand = heroMapper.findHeroByRand(2); // SELECT * FROM `hero` WHERE `rank` > 10
System.out.println(heroByRand);
List<Hero> heroByRand1 = heroMapper.findHeroByRand(-1); // SELECT * FROM `hero`
System.out.println(heroByRand1);
}
编写方法, 查询 rank 为 3, 6, 10 [rank 可变] 的 hero
在HeroMapper
接口中定义如下方法:
public List<Hero> findHeroByRank(Map<String, Object> map);
在HeroMapper.xml
文件中定义如下接口实现:
<select id="findHeroByRank" resultType="Hero" parameterType="map">
SELECT * FROM `hero`
<where>
`id` in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</where>
</select>
最终测试结果:
@Test
public void t3() {
HeroMapper heroMapper = sqlSession.getMapper(HeroMapper.class);
HashMap<String, Object> map = new HashMap<>();
map.put("ids", Arrays.asList(3, 6, 10));
List<Hero> heroByRank = heroMapper.findHeroByRank(map);
System.out.println(heroByRank);
/*
==> Preparing: SELECT * FROM `hero` WHERE `id` in ( ? , ? , ? )
==> Parameters: 3(Integer), 6(Integer), 10(Integer)
<== Columns: id, nickname, skill, rank, salary, join_datetime
<== Row: 3, 电击小子-2, 电人-2, 2, 2000.10, 2024-04-23 12:02:22
<== Row: 6, 电击小子-5, 电人-5, 5, 2000.10, 2024-04-23 12:02:22
<== Row: 10, 电击小子-9, 电人-9, 9, 2000.10, 2024-04-23 12:02:22
<== Total: 3
[Hero(id=3, nickname=电击小子-2, skill=电人-2, rank=2, salary=2000.10, joinDatetime=null), Hero(id=6, nickname=电击小子-5, skill=电人-5, rank=5, salary=2000.10, joinDatetime=null), Hero(id=10, nickname=电击小子-9, skill=电人-9, rank=9, salary=2000.10, joinDatetime=null)]
*/
}
编写方法, 修改 hero 信息, 如果没有设置新的属性值, 则保持原来的值
在HeroMapper
接口中定义如下方法声明:
public void updateHero(Hero hero);
在HeroMapper.xml
文件中定义如下XML
配置:
<update id="updateHero" parameterType="Hero">
UPDATE `hero`
<set>
<if test="nickname != null and nickname != ''">
`nickname` = #{nickname},
</if>
<if test="skill != null and skill != ''">
`skill` = #{skill},
</if>
<if test="rank != null and rank != ''">
`rank` = #{rank},
</if>
<if test="salary != null and salary != ''">
`salary` = #{salary},
</if>
<if test="joinDatetime != null"> <!-- 注意, 日期不能判断 != '', 否则会报错 -->
`join_datetime` = #{joinDatetime},
</if>
</set>
WHERE `id` = #{id}
</update>
测试结果:
@Test
public void t4() {
HeroMapper heroMapper = sqlSession.getMapper(HeroMapper.class);
Hero hero = new Hero();
hero.setId(98); // 要修改的 ID
hero.setNickname("电击大王");
hero.setSkill("控制小弟");
hero.setSalary(new BigDecimal("999.99"));
hero.setJoinDatetime(LocalDateTime.now());
hero.setRank(100);
/*
* +----+--------------+--------------+------+--------+---------------------+
| id | nickname | skill | rank | salary | join_datetime |
+----+--------------+--------------+------+--------+---------------------+
| 98 | 电击小子-97 | 电人-97 | 97 | 2000.10 | 2024-04-23 12:02:28 |
+----+--------------+--------------+------+--------+---------------------+
* */
heroMapper.updateHero(hero);
sqlSession.commit();
/*
* +----+--------------+--------------+------+--------+---------------------+
| id | nickname | skill | rank | salary | join_datetime |
+----+--------------+--------------+------+--------+---------------------+
| 98 | 电击大王 | 控制小弟 | 100 | 999.99 | 2024-04-23 15:58:32 |
+----+--------------+--------------+------+--------+---------------------+
* */
sqlSession.close();
}
编写方法, 可以根据 id 查询 hero, 如果没有传入 id, 就返回所有 hero
在HeroMapper
接口中定义如下方法声明:
public List<Hero> findHeroById(@Param("id") Integer id);
在HeroMapper.xml
文件中定义如下方法实现:
<select id="findHeroById" parameterType="Integer" resultType="Hero">
SELECT * FROM `hero`
<where>
<if test="id != null and id != 0">
AND `id` = #{id}
</if>
</where>
</select>
运行结果:
@Test
public void t5() {
HeroMapper heroMapper = sqlSession.getMapper(HeroMapper.class);
heroMapper.findHeroById(0); // SELECT * FROM `hero`
heroMapper.findHeroById(8); // SELECT * FROM `hero` WHERE `id` = ?
}
映射关系
在这里我们看一下MyBatis
中处理一对一, 一对多, 多对一的关系, 是如何实现的.
一对一映射关系
一个经典的案例, 则是Person(人) <-> IdCard(身份证)
, 一个人只有一个身份证, 一个身份证对应一个人.
Idencard 编写
在这里我们创建一个子项目, 并引入lombok
, 使其Maven
运行时, XML
文件会输出到target
目录中:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
创建表格:
CREATE TABLE `idencard`(
id int primary key auto_increment comment '身份证ID号',
card_sn varchar(32) not null default '' comment '具体身份证号'
)engine=innodb charset=utf8;
CREATE TABLE `person`(
id int unsigned primary key auto_increment comment 'ID号',
name varchar(32) not null default '' comment '姓名',
card_id int comment '身份证ID号',
foreign key(card_id) references idencard(id) -- 创建外键
)engine=innodb charset=utf8;
INSERT INTO `idencard` VALUES(1, '111111111110');
INSERT INTO `person` VALUES(1, '张三', 1);
创建完毕后, 我们创建对应的JavaBean
:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Idencard {
private Integer id;
private String card_sn;
}
重点关注Person
类如何定义:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private Integer id;
private String name;
private Idencard card; // 这里 card 名字可以任意起名
// 在数据表中的关系则是 Person 对应上了 Idencard
// 虽然数据库中的保存的是 card_id, 但是在我们实体的 JavaBean 中, 一定要用类关系
}
因为IdencardMapper
的定义来说比较简单, 所以在这里我们先定义IdencardMapper
, 定义接口如下:
public interface IdencardMapper {
public Idencard getIdencardById(Integer id); // 根据 id 查询出对应的 Idencard 信息
}
定义如下IdencardMapper.xml
文件:
<mapper namespace="com.heihu577.Mappers.IdencardMapper">
<!--
特别注意, 在 mybatis-config.xml 文件中需要定义如下标签:
<mappers>
<package name="com.heihu577.Mappers"/> 将我们的 xml 文件扫描进去
</mappers>
-->
<select id="getIdencardById" parameterType="Integer" resultType="Idencard">
<!--
因为配置了别名:
<typeAliases>
<package name="com.heihu577.Beans"/>
</typeAliases>
所以在这里可以直接指定 resultType = Idencard
-->
SELECT * FROM `idencard` WHERE `id` = #{id}
</select>
</mapper>
测试结果:
@Test
public void t1() {
IdencardMapper idencardMapper = sqlSession.getMapper(IdencardMapper.class);
Idencard idencardById = idencardMapper.getIdencardById(1);
System.out.println(idencardById);
/*
==> Preparing: SELECT * FROM `idencard` WHERE `id` = ?
==> Parameters: 1(Integer)
<== Columns: id, card_sn
<== Row: 1, 111111111110
<== Total: 1
Idencard(id=1, card_sn=111111111110)
*/
}
Person 编写
按照惯例方式编写存在的问题
创建如下JavaBean
:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private Integer id;
private String name;
private Idencard card; // 在数据表中的关系则是 Person 对应上了 Idencard
// 虽然数据库中的保存的是 card_id, 但是在我们实体的 JavaBean 中, 一定要用类关系
}
并且创建PersonMapper
接口如下:
public interface PersonMapper {
public Person getPersonById(Integer id);
// 通过 Person 的 id 获得对应的 Person, 并且 Idencard 是存在关系的 [级联查询]
}
随后我们在PersonMapper.xml
文件中进行实现:
<mapper namespace="com.heihu577.Mappers.PersonMapper">
<select id="getPersonById" parameterType="Integer" resultType="Person">
SELECT * FROM `person` WHERE `id` = #{id}
</select>
</mapper>
测试一下我们可以发现给我们带来的问题:
@Test
public void t2() {
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
Person personById = personMapper.getPersonById(1);
/*
* ==> Preparing: SELECT * FROM `person` WHERE `id` = ?
==> Parameters: 1(Integer)
<== Columns: id, name, card_id
<== Row: 1, 张三, 1
<== Total: 1
Person(id=1, name=张三, card=null) 这里 card 并没有生成记录, 所以是存在问题的, 因为结果集中并没有找到叫 card 的结果
* */
System.out.println(personById);
}
第一种解决方式
在这里我们可以使用resultMap
进行解决该问题.
<resultMap id="getPersonByIdMap" type="Person"> <!-- id: resultMap名称 type: getPersonById 方法返回类型 -->
<result property="id" column="id"/> <!-- BEAN 中叫 id, 数据库中也叫 id -->
<result property="name" column="name"/> <!-- BEAN 中叫 name, 数据库中也叫 name -->
<!--
参考:
public class Person {
private Integer id;
private String name;
private Idencard card;
}
-->
<association property="card" javaType="Idencard"> <!-- association 用于处理复杂类型, BEAN 中叫 card, 并且参数类型是 Idencard -->
<result property="id" column="id"/> <!-- 因为 MyBatis 映射时, 是从结果集按照顺序映射的, 所以这里不会报错 -->
<result property="card_sn" column="card_sn"/>
<!--
参考:
public class Idencard {
private Integer id;
private String card_sn;
}
-->
</association>
</resultMap>
<select id="getPersonById" parameterType="Integer" resultMap="getPersonByIdMap"> <!-- 在这里我们使用 resultMap -->
SELECT * FROM `person`, `idencard` WHERE `person`.`card_id` = `idencard`.`id` AND `person`.`id` = #{id}
</select>
最终运行结果:
==> Preparing: SELECT * FROM `person`, `idencard` WHERE `person`.`card_id` = `idencard`.`id` AND `person`.`id` = ?
==> Parameters: 1(Integer)
<== Columns: id, name, card_id, id, card_sn
<== Row: 1, 张三, 1, 1, 111111111110
<== Total: 1
Person(id=1, name=张三, card=Idencard(id=1, card_sn=111111111110))
可以从中看到, 成功映射到card
属性.
当然了, 我们平时都使用的result
标签进行操作的, 在这里我们可以对主键使用id
标签, 以优化查询速度, 如下:
<resultMap id="getPersonByIdMap" type="Person">
<id property="id" column="id"/> <!-- 在这里可以使用 id 标签, 因为是 primary key -->
<result property="name" column="name"/>
<association property="card" javaType="Idencard">
<id property="id" column="id"/> <!-- 这里也可以使用 id 标签, 因为是 primary key -->
<result property="card_sn" column="card_sn"/>
</association>
</resultMap>
第二种解决方式 (表多使用)
在PersonMapper
接口中定义如下方法定义:
public Person getPersonById2(Integer id);
在PersonMapper.xml
文件中进行实现:
<resultMap id="getPersonByIdMap2" type="Person">
<id property="id" column="id"/>
<result property="name" column="name"/>
<association property="card" column="card_id" select="com.heihu577.Mappers.IdencardMapper.getIdencardById"/>
<!-- 将查过来的 card_id, 进行调用 getIdencardById 方法 -->
<!--
这个时候又执行了 SELECT * FROM `Idencard` WHERE `id` = 刚刚查询过来的 ID, 随后封装成了 Idencard 对象
-->
</resultMap>
<select id="getPersonById2" parameterType="Integer" resultMap="getPersonByIdMap2">
SELECT * FROM `person` WHERE `id` = #{id}
<!--
目前查找出单个信息, 例如:
mysql> SELECT * FROM `person` WHERE `id` = 1;
| 1 | 张三 | 1 |
1 row in set (0.00 sec)
-->
</select>
运行结果:
@Test
public void t3() {
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
Person personById2 = personMapper.getPersonById2(1);
System.out.println(personById2);
/*
==> Preparing: SELECT * FROM `person` WHERE `id` = ?
==> Parameters: 1(Integer)
<== Columns: id, name, card_id
<== Row: 1, 张三, 1
核心思想: 查询出 card_id, 然后取出 card_id 去 Idencard 表中找, 等于是将单条 SQL 语句, 使用了多条进行解决了
====> Preparing: SELECT * FROM `idencard` WHERE `id` = ?
====> Parameters: 1(Integer)
<==== Columns: id, card_sn
<==== Row: 1, 111111111110
<==== Total: 1
<== Total: 1
Person(id=1, name=张三, card=Idencard(id=1, card_sn=111111111110))
*/
}
注解方式实现 (不推荐)
定义IdencardMapperAnnotation文件内容如下:
public interface IdenCardMapperAnnotation {
@Select("SELECT * FROM `idencard` WHERE `id` = #{id}")
public Idencard getIdenCardById(Integer id);
}
随后我们定义PersonMapperAnnotation
, 内容如下:
public interface PersonMapperAnnotation {
@Select("SELECT * FROM `person` WHERE `id` = #{id}")
@Results({
@Result(id = true, property = "id", column = "id"), // 相当于 <id property="id" column="id"/>
@Result(property = "name", column = "name"), // 相当于 <result property="name" column="name"/>
@Result(property = "card", column = "card_id",one = @One(
select = "com.heihu577.Mappers.IdenCardMapperAnnotation.getIdenCardById")
)
/*
* 相当于 <association property="card" column="card_id" select="com.heihu577.Mappers.IdencardMapper.getIdencardById"/>
* 只不过需要指定 one, 来表明是 一对一的关系, 而 one 中 select 则是上面 xml 中的 select
* */
})
public Person getPersonById(Integer id);
}
最终运行结果:
@Test
public void t2() {
PersonMapperAnnotation mapper = sqlSession.getMapper(PersonMapperAnnotation.class);
Person personById = mapper.getPersonById(1);
System.out.println(personById);
/*
==> Preparing: SELECT * FROM `person` WHERE `id` = ?
==> Parameters: 1(Integer)
<== Columns: id, name, card_id
<== Row: 1, 张三, 1
====> Preparing: SELECT * FROM `idencard` WHERE `id` = ?
====> Parameters: 1(Integer)
<==== Columns: id, card_sn
<==== Row: 1, 111111111110
<==== Total: 1
<== Total: 1
Person(id=1, name=张三, card=Idencard(id=1, card_sn=111111111110))
*/
}
一对多映射关系
一对多的案例, 我们就使用User(用户) - Pet(宠物)
来进行说明关系吧. 需求说明: 实现级联查询, 通过 user
的 id 可以查询到用户信息, 并且可以查询到关联的 pet
信息, 反过来, 通过Pet
的id
可以查询到Pet
的信息, 并且可以级联查询到它主人User
对象的信息.
表创建
CREATE TABLE `user`(
id int primary key auto_increment,
name varchar(32) not null default ''
)charset=utf8 engine=innodb;
CREATE TABLE `pet`(
id int primary key auto_increment,
nickname varchar(32) not null default '',
user_id int,
foreign key(user_id) references `user`(id)
);
INSERT INTO `user` VALUES(NULL, '宋江'),(NULL,'张飞'); -- 两个主人
INSERT INTO `pet` VALUES(1, '黑背', 1),(2, '小哈', 1); -- 宋江的两个宠物
INSERT INTO `pet` VALUES(3, '波斯猫', 2),(4, '贵妃猫', 2); -- 张飞的两个宠物
JavaBean 创建
创建User
类如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
/*
* +-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(32) | NO | | | |
+-------+-------------+------+-----+---------+----------------+
* */
private Integer id;
private String name;
private List<Pet> pets; // 一个 User(主人) 可以有多个 Pet(宠物)
}
并且创建Pet
类, 如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Pet {
/*
* +----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| nickname | varchar(32) | NO | | | |
| user_id | int(11) | YES | MUL | NULL | |
+----------+-------------+------+-----+---------+----------------+
* */
private Integer id;
private String nickname;
private User user; // 一个 Pet(宠物) 只能有一个 User(主人)
}
Mapper 创建
创建PetMapper
接口, 内容如下:
public interface PetMapper {
// 通过 User 的 id 来获取 pet 对象, 可能有多个, 因此用 List 进行接收
public List<Pet> getPetByUserId(Integer userId);
// 通过 Pet 的 id 获取 Pet 对象
public Pet getPetById(Integer id);
}
并且定义UserMapper
接口, 内容如下:
public interface UserMapper {
// 通过 id 获取 User 对象
public User getUserById(Integer id);
}
创建 Mapper 对应的 xml 文件
创建UserMapper.xml
文件内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.Mappers.UserMapper">
<resultMap id="getUserByIdMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- List 集合使用 collection 标签, 如果是 普通 Bean, 使用 association -->
<collection property="pets" column="id" ofType="Pet"
select="com.heihu577.Mappers.PetMapper.getPetByUserId"/>
<!-- collection标签, association标签, column + select, column 是 select 语句的参数 -->
<!--
property: bean属性名称
column: 作为方法参数
ofType: 指定方法返回的类型
select: 调用目标 Mapper 方法
-->
</resultMap>
<!-- 因为 User Bean 中, 存在 pets 属性, 而该属性在数据库中是没有定义的, 所以需要 resultMap 集合 -->
<select id="getUserById" resultMap="getUserByIdMap" parameterType="Integer">
SELECT * FROM `user` WHERE `id` = #{id}
</select>
</mapper>
随后我们实现PetMapper.xml
文件, 文件内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.Mappers.PetMapper">
<resultMap id="PetResultMap" type="Pet">
<id property="id" column="id"/>
<result property="nickname" column="nickname"/>
<association property="user" column="user_id" select="com.heihu577.Mappers.UserMapper.getUserById"/>
<!-- association 用于指定单个 JavaBean -->
</resultMap>
<select id="getPetByUserId" parameterType="Integer" resultMap="PetResultMap"> <!-- 定义 resultMap -->
SELECT * FROM `pet` WHERE `user_id` = #{userId}
</select>
</mapper>
测试问题 (堆栈溢出问题)
在这里我们定义如下测试类:
public class T4 {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void t1() {
PetMapper petMapper = sqlSession.getMapper(PetMapper.class);
List<Pet> petByUserId = petMapper.getPetByUserId(1);
System.out.println(petByUserId); // java.lang.StackOverflowError
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User userById = userMapper.getUserById(1);
System.out.println(userById); // java.lang.StackOverflowError
}
}
System.out.println
会抛出异常, 原因是, pet
中有了user
, 而user
中也有了pet
, 会爆出一个经典的堆栈溢出
错误.
原因则是两者的 toString 方法的定义, 输出问题. 我们只需要在
Pet && User
中不定义 toString 方法即可.
我们只需要将两个Bean
中的注解声明改为如下即可:
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
最终运行结果:
@Test
public void t1() {
PetMapper petMapper = sqlSession.getMapper(PetMapper.class);
List<Pet> petByUserId = petMapper.getPetByUserId(1);
for (Pet pet : petByUserId) {
System.out.println("宠物: " + pet.getNickname() + " 主人: " + pet.getUser().getName());
/*
宠物: 黑背 主人: 宋江
宠物: 小哈 主人: 宋江
*/
}
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User userById = userMapper.getUserById(1);
System.out.println("主人: " + userById.getName() + " 宠物: " + userById.getPets());
// 主人: 宋江 宠物: [com.heihu577.Beans.Pet@2cdd0d4b, com.heihu577.Beans.Pet@7e9131d5]
}
最后一个方法实现 (resultMap 复用)
在我们的PetMapper
接口中, 我们还剩下一个方法没有实现, 如下:
public Pet getPetById(Integer id); // 通过 Pet 的 id 获取 Pet 对象
接下来我们进行实现, 如下:
<resultMap id="PetResultMap" type="Pet"> <!-- 因为之前定义过该 resultMap, 所以在这里发现可以进行复用 -->
<id property="id" column="id"/>
<result property="nickname" column="nickname"/>
<association property="user" column="user_id" select="com.heihu577.Mappers.UserMapper.getUserById"/>
</resultMap>
我们可以发现, 这里的映射关系, 我们也可以应用, 具体实现如下:
<select id="getPetById" parameterType="Integer" resultMap="PetResultMap">
SELECT * FROM `pet` WHERE `id` = #{id}
</select>
最终运行结果:
@Test
public void t1() {
PetMapper petMapper = sqlSession.getMapper(PetMapper.class);
Pet petById = petMapper.getPetById(1);
System.out.println(petById);
/*
==> Preparing: SELECT * FROM `pet` WHERE `id` = ?
==> Parameters: 1(Integer)
<== Columns: id, nickname, user_id
<== Row: 1, 黑背, 1
====> Preparing: SELECT * FROM `user` WHERE `id` = ?
====> Parameters: 1(Integer)
<==== Columns: id, name
<==== Row: 1, 宋江
======> Preparing: SELECT * FROM `pet` WHERE `user_id` = ?
======> Parameters: 1(Integer)
<====== Columns: id, nickname, user_id
<====== Row: 1, 黑背, 1
<====== Row: 2, 小哈, 1
<====== Total: 2
<==== Total: 1
<== Total: 1
com.heihu577.Beans.Pet@2cdd0d4b
*/
}
注解实现多对一 (不推荐)
定义UserMapperAnnotation
接口如下:
public interface UserMapperAnnotation {
@Select("SELECT * FROM `user` WHERE `id` = #{id}")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "pets", column = "id", many = @Many(select = "com.heihu577.Mappers.PetMapperAnnotation" +
".getPetByUserId"))
})
public User getUserById(Integer id);
}
该定义完全参考了我们上面的UserMapper.xml
文件中的resultMap
标签的定义, 对比一下即可找到规律, 下面我们来定义PetMapperAnnotation
接口, 如下:
public interface PetMapperAnnotation {
@Select("SELECT * FROM `pet` WHERE `user_id` = #{userId}")
@Results(id = "myResultMap", value = { // 这里 id 则是指定 resultMap 的名称
@Result(id = true, property = "id", column = "id"),
@Result(property = "nickname", column = "nickname"),
@Result(property = "user", column = "user_id", one = @One(select = "com.heihu577.Mappers.UserMapperAnnotation.getUserById"))
})
public List<Pet> getPetByUserId(Integer userId);
@Select("SELECT * FROM `pet` WHERE `id` = #{id}")
@ResultMap("myResultMap") // 应用上面定义的 resultMap
public Pet getPetById(Integer id);
}
使用完毕之后我们进行测试:
@Test
public void t1() {
Integer userId = 1;
Integer petId = 2;
UserMapperAnnotation userMapperAnnotation = sqlSession.getMapper(UserMapperAnnotation.class);
User user = userMapperAnnotation.getUserById(userId);
System.out.println("查询 userId = " + user.getId() + " 的用户信息");
List<Pet> pets = user.getPets();
for (Pet pet : pets) {
System.out.println(user.getName() + " -> 宠物ID: " + pet.getId() + " 宠物名称: " + pet.getNickname());
/*
* 查询 userId = 1 的用户信息
宋江 -> 宠物ID: 1 宠物名称: 黑背
宋江 -> 宠物ID: 2 宠物名称: 小哈
* */
}
PetMapperAnnotation petMapperAnnotation = sqlSession.getMapper(PetMapperAnnotation.class);
Pet pet = petMapperAnnotation.getPetById(petId);
System.out.println("宠物名称: " + pet.getNickname() + " 主人: " + pet.getUser().getName());
// 宠物名称: 小哈 主人: 宋江
List<Pet> Pets = petMapperAnnotation.getPetByUserId(userId);
for (Pet pet1 : Pets) {
System.out.println(userId + " -> 宠物名称: " + pet1.getNickname());
/*
* 1 -> 宠物名称: 黑背
1 -> 宠物名称: 小哈
* */
}
}
缓存
一级缓存 (一个 sqlSession 一个缓存)
当我们第一次查询id = 1
的Monster
后, 再次查询id = 1
的Monster
对象, 就会直接从一级缓存获取, 不会再次发出SQL
. 接下来我们创建mybatis_cache
模块, 并引用常用依赖即可, 配置方面就不多说了, 并且我们将前面所用到的Monster
类, MonsterMapper
, MonsterMapper.xml
文件, MyBatisUtils
文件拷贝过来. 当一切运行正常时, 我们开始测试:
public class T1 {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster = monsterMapper.getMonsterById(1);
System.out.println(monster);
// Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}
}
}
快速入门
当我们测试查询SQL
语句时, 我们可以通过控制台看到, MyBatis默认开启一级缓存
, 如下:
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster1 = monsterMapper.getMonsterById(1);
System.out.println("=============================");
Monster monster2 = monsterMapper.getMonsterById(1);
/*
* ==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
=============================
* */
}
可以看到, 总共发送了一次SQL语句. 具体debug过程, 放在第四阶段 主流框架主流框架【5】- MyBatis108.一级缓存执行流程Debug.mp4
. 其中存储结构如下:在我们第二次发送相同的
SQL
语句时, 会从缓存中取出.
一级缓存失效问题
当我们两次发送相同的SQL语句时, 除非将sqlSession
关闭掉, 并且将其重新获取, 即可发送两次SQL语句.
public class T1 {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster1 = monsterMapper.getMonsterById(1);
sqlSession.close();
System.out.println("=============================");
sqlSession = MyBatisUtils.getSqlSession(); // 重新获取
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster2 = monsterMapper.getMonsterById(1);
/*
* ==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8297b3a]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8297b3a]
Returned connection 136936250 to pool.
=============================
Opening JDBC Connection
Checked out connection 136936250 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8297b3a]
==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
* */
}
}
或者我们使用sqlSession.clearCache()
即可导致一级缓存失效.
准备如下代码:
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster1 = monsterMapper.getMonsterById(1);
sqlSession.clearCache();
System.out.println("=============================");
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster2 = monsterMapper.getMonsterById(1);
/*
* ==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8297b3a]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8297b3a]
Returned connection 136936250 to pool.
=============================
Opening JDBC Connection
Checked out connection 136936250 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8297b3a]
==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
* */
}
还有一种情况, 就是当我们修改数据库内容后, 一级缓存也会失效.
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster = monsterMapper.getMonsterById(1);
System.out.println(monster);
monsterMapper.updateMonsterById(
new Monster(1, 0, LocalDateTime.now(), "[email protected]", 9, "黑客2", 123d)
); // 修改 ID 为 1 的 Monster 表
monster = monsterMapper.getMonsterById(1);
System.out.println(monster);
/*
* ==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}
==> Preparing: UPDATE `monster` SET `age` = ?, `birthday` = ?, `email` = ?, `gender` = ?, `name` = ?, `salary` = ? WHERE `id` = ?
==> Parameters: 0(Integer), 2024-04-27T14:20:27.812(LocalDateTime), [email protected](String), 9(Integer), 黑客2(String), 123.0(Double), 1(Integer)
<== Updates: 1
==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 0, 2024-04-27, [email protected], 9, 黑客2, 123.0
<== Total: 1
Monster{id=1, age=0, birthday=2024-04-27T00:00, email='[email protected]', gender=9, name='黑客2', salary=123.0}
* */
// 按道理这里需要使用 sqlSession.commit(), 这里不 commit() 也会清空缓存.
}
二级缓存 (全局缓存)
二级缓存可以参考, mybatis官方文档:https://mybatis.net.cn/sqlmap-xml.html#cache
快速入门
我们需要在mybatis-config.xml
文件中定义如下配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
随后我们需要将我们的JavaBean
类, 进行实现Serializable
接口, 因为二级缓存有可能会用到序列化技术. 如下:
public class Monster implements Serializable {
private static final long serialVersionUID = 1L;
// ... 其他内容
}
随后我们将配置标签放入到MonsterMapper.xml
文件中, 如下:
<mapper namespace="com.heihu577.Mapper.MonsterMapper">
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> <!-- 参数含义官网有说明 -->
<!-- 其他内容... -->
</mapper>
那么我们可以进行测试:
public class T2 {
private SqlSession sqlSession = MyBatisUtils.getSqlSession();
@Test
public void t1() {
MonsterMapper monsterMapper = sqlSession.getMapper(MonsterMapper.class);
Monster monster01 = monsterMapper.getMonsterById(1);
System.out.println(monster01);
System.out.println("===================================");
sqlSession.close();
// 关闭后第二次获取 sqlSession
sqlSession = MyBatisUtils.getSqlSession();
monsterMapper = sqlSession.getMapper(MonsterMapper.class);
monster01 = monsterMapper.getMonsterById(1);
System.out.println(monster01);
/*
* ==> Preparing: SELECT * FROM `Monster` WHERE `ID` = ?
==> Parameters: 1(Integer)
<== Columns: id, age, birthday, email, gender, name, salary
<== Row: 1, 199, 2024-04-19, [email protected], 6, heihu888, 123.3
<== Total: 1
Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}
===================================
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@38afe297]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@38afe297]
Returned connection 951050903 to pool.
Cache Hit Ratio [com.heihu577.Mapper.MonsterMapper]: 0.5
Monster{id=1, age=199, birthday=2024-04-19T00:00, email='[email protected]', gender=6, name='heihu888', salary=123.3}
* */
}
}
注意事项 && 使用陷阱
关闭二级缓存总开关
若在mybatis-config.xml
文件中定义如下标签:
<setting name="cacheEnabled" value="false"/>
则关闭二级缓存总开关.
二级缓存开启前提
因为二级缓存是基于Mapper
的, 如果对应Mapper
并没有定义<cache>
标签, 则该Mapper
并没有开启二级缓存.
对应方法不开启二级缓存
例如:
<select id="getAllMonsters" resultType="Monster">
SELECT * FROM `Monster`
</select>
<select id="getMonsterById" resultType="Monster" parameterType="java.lang.Integer" useCache="false">
SELECT * FROM `Monster` WHERE `ID` = #{id}
</select>
在配置好二级缓存的前提下, getMonsterById
因为配置了useCache
属性, 会导致二级缓存失效. 而getAllMonsters
则不会.
默认情况下不需要使用该属性, 除非有特殊的业务需求.
增删改语句刷新缓存设置
因为增删改语句会刷新缓存, 如果不刷新缓存, 可能会造成数据的脏读(数据不一致)
, 所以在这里有一个flushCache
属性, 默认为true
.
<delete id="deleteMonster" parameterType="java.lang.Integer" flushCache="true">
DELETE FROM `monster` WHERE `id` = #{id}
</delete>
一级缓存 二级缓存执行顺序
其中执行顺序为: 二级缓存 > 一级缓存 > 数据库
当然, 当我们定义了二级缓存
, 当一级缓存
的sqlSession
被关闭, MyBatis
会把一级缓存的数据信息, 放入到二级缓存中.
ehCache
pom.xml 文件引入依赖
我们需要引入如下依赖:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
mybatis-config.xml 设置 cacheEnabled
虽然cacheEnabled
默认为true
, 但是这里根据习惯还是显示的声明一下:
<setting name="cacheEnabled" value="true"/>
ehcache 配置文件
将如下配置文件内容放入到resources/
目录下, 命名为: ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
user.home – 用户主目录
user.dir – 用户当前工作目录
java.io.tmpdir – 默认临时文件路径
-->
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
<!--
defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
-->
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
在 Mapper 中进行定义 ehcache
在的MonsterMapper.xml
文件中, 定义如下标签:
<!-- <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> <!– 这是 MyBatis 自带的二级缓存策略 –>-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/> <!-- 引入 ehCache -->
最终debug结果 (使用正常的查询语句即可调用出来):
理解 EhCache 和 MyBatis 缓存的关系
MyBatis 提供了一个接口 Cache 只要实现了 Cache 接口, 就可以作为二级缓存产品和 MyBatis 整合使用, EhCache 就是实现了该接口. MyBatis 默认情况(一级缓存), 是使用的 PerpetualCache 类实现 Cache 接口的, 是核心类.
原文始发于微信公众号(Heihu Share):开发基础 | MyBatis 基本使用总结 && 手动实现 MyBatis
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论