Java - Mybatis基本使用
1. Mybatis介绍
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary JavaObject,普通的 Java对象)映射成数据库中的记录。且有缓存机制,可以提高査询效率,
Mybatis是一个 半ORM框架,可以消除了JDBC的代码和步骤,让开发者只关注SQL本身。
ORM 对象-关系-映射
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
2.1 开发步骤
- 确定表数据和结构
确认数据库中表的结构,字段名,字段类型等。 - 确定实体类
创建pojo层(entity层),将数据库中的表结构映射到pojo类中。 - 创建项目,加入依赖
导入mybatis的依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
-
创建mybatis核心配置文件
mybatis-config.xml
mybatis的核心配置文件mybatis-config.xml,位于src/main/resources目录下。 -
写mybatis核心配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<!-- 使用jdbc进行事务管理 -->
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- 加载mysql数据驱动 -->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!-- 数据库url地址 -->
<property name="url" value="jdbc:mysql://localhost:3306/book?useSSL=false&useUnicode=true&ucharacterEncoding=utf8"/>
<!-- 数据库用户名 -->
<property name="username" value="root"/>
<!-- 数据库密码 -->
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<!-- 加载映射文件-->
<mappers>
</mappers>
</configuration>
- 写Mapper接口
在src/main/java目录下创建接口包,如org.example.mapper,并创建接口类BookInfoMapper.java
package org.example.mapper;
import org.example.model.BookInfo;
public interface BookInfoMapper {
BookInfo findBookInfoById(Integer id);
}
- 写Mapper接口对应的xml文件
有两种方式,一种是直接在mapper目录下,和接口文件同名,另一种是在resources目录下创建同名xml文件。两种方法都可以,只不过mybatis配置文件中需要加载的映射文件路径不同。这里使用第二种。
例: 在resources目录下创建BookInfoMapper.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">
<!-- namespace 命名空间 用来关联映射文件xml和接口文件,即为接口的类路径-->
<mapper namespace="org.example.mapper.BookInfoMapper">
<!-- 定义查询方法,id为方法名,resultType为返回值类型-->
<!-- #{id} 占位符,用于接收参数相当于之前预处理的?-->
<select id="findBookInfoById" resultType="org.example.entity.BookInfo">
select * from bookinfo where id = #{id}
</select>
</mapper>
- 修改核心配置文件中,加载mapper映射文件
因为上面配置了mapper文件,所以需要在mybatis核心配置文件中加载映射文件。
<mappers>
<!-- 加载映射文件 因为就在resources'目录下和核心配置文件同级,所以直接写文件名即可-->
<mapper resource="BookInfoMapper.xml"/>
</mappers>
-
通过核心配置文件,获取SqlSessionFactoryBuilder
-
通过获得SqlSessionFactoryBuilder获得SqlSessionFactory
-
通过SqlSessionFactory获得SqlSession
简化直接将上述步骤封装为工具类,方便调用。如下:src/main/java/org/example/util/MyBatisUtil.java
package org.example.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;
public class MyBatisUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
String resource = "mybatis-config.xml";
try {
InputStream is = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
} catch (IOException e) {
throw new RuntimeException("Error initializing MyBatisUtil", e);
}
}
/**
* 获取SqlSession对象
* @return SqlSession
*/
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
/**
* 关闭SqlSession对象
* @param sqlSession 需要关闭的SqlSession对象
*/
public static void closeSqlSession(SqlSession sqlSession) {
if (sqlSession != null) {
sqlSession.close();
}
}
}
- 最后一步,执行SQL
最后测试一下是否配置成功,在src/test/java目录下创建测试类,如TestMybatis.java
package org.example.test;
import org.apache.ibatis.session.SqlSession;
import org.example.mapper.BookInfoMapper;
import org.example.model.BookInfo;
import org.example.util.MyBatisUtil;
import org.junit.Test;
import java.io.IOException;
public class TestMybatis {
public static void main(String[] args) {
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
// 利用动态代理技术,获得BookInfoMapper接口的实现类对象
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
// 调用BookInfoMapper中的findBookInfoById方法,查询ID为1的书籍信息
BookInfo bookInfo = mapper.findBookInfoById(1);
System.out.println(bookInfo);
// // 直接通过SqlSession调用selectOne方法,查询ID为1的书籍信息
// BookInfo bookInfo1 = (BookInfo) session.selectOne("org.example.mapper.BookInfoMapper.findBookInfoById", 1);
// System.out.println(bookInfo1);
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
输出
BookInfo(id=1, name=道听途说, author=何金银, desc=都市异闻、悬疑灵异、神秘文化、中式恐怖……15个让你忘记呼吸的精彩故事。惊喜收录神秘篇章《爱你,林西》。亲眼所见,未必真实。道听途说,未必虚假。, price=29.7)
3. 单元测试
单元测试,是用来测试一个模块的各个功能是否正确的测试工作。直接在src/test/java目录下创建测试类即可。
创建单元测试前,需要现在pom文件中引入junit的依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
直接创建测试类,由于上面测试mybatis,已经了创建TestMybatis.java,所以不再创建,直接使用这个类。
创建测试方法,并在方法加上注解@Test,内容同样是测试mybatis。
@Test
public void testMybatis(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
// 利用动态代理技术,获得BookInfoMapper接口的实现类对象
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
// 调用BookInfoMapper中的findBookInfoById方法,查询ID为1的书籍信息
BookInfo bookInfo = mapper.findBookInfoById(1);
System.out.println(bookInfo);
// // 直接通过SqlSession调用selectOne方法,查询ID为1的书籍信息
// BookInfo bookInfo1 = (BookInfo) session.selectOne("org.example.mapper.BookInfoMapper.findBookInfoById", 1);
// System.out.println(bookInfo1);
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
4. CRUD
4.1 查询
4.1.1 单条件查询
单条件查询,起始创建对应的mapper映射文件时就已经演示过了,所以这里不再赘叙。
<select id="findBookInfoById" resultType="org.example.entity.BookInfo">
select * from bookinfo where id = #{id}
</select>
但是需要注意的是,在映射文件中,参数的名字必须和pojo类中的属性名一致,否则会封装失败,无法对应字段。
例如:
select name bname,author bauthor,price bprice from bookinfo where id = #{id}
@Data
public class BookInfo {
private Integer id;
private String name;
private String author;
private String desc;
private Double price;
}
ORM自动封装时会发现,此时对应的实体类中并没有对应的字段名,就会取类型默认值
4.1.2 查询全部
- 查询全部,设计接口方法 (BookInfoMapper.java)
public interface BookInfoMapper {
//根据编号查找对应图书信息
BookInfo findBookInfoById(Integer id);
//查询全部图书信息
List<BookInfo> findAllBookInfo();
}
- 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
<!-- 虽然方法查询返回的是List集合,但是返回类型resutType还是要写集合中存储的类型 -->
<select id="findAllBookInfo" resultType="org.example.model.BookInfo">
select * from bookinfo
</select>
- 在测试类中编写测试方法 (TestMybatis.java)
@Test
public void testFindAllBookInfo(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
// 调用BookInfoMapper中的findAllBookInfo方法,查询全部图书信息
List<BookInfo> bookInfoList = mapper.findAllBookInfo();
System.out.println(bookInfoList);
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
输出
[BookInfo(id=1, name=道听途说, author=何金银, desc=都市异闻、悬疑灵异、神秘文化、中式恐怖……15个让你忘记呼吸的精彩故事。惊喜收录神秘篇章《爱你,林西》。亲眼所见,未必真实。道听途说,未必虚假。, price=29.7),
BookInfo(id=2, name=第十三位陪审员, author=史蒂夫·卡瓦纳, desc=金BI首奖得主史蒂夫卡瓦纳法庭推理神作。如果无法击败主宰者,那就成为他。连环谋杀,一场与时间博弈的竞赛,杀人只是游戏的开端。, price=21.6),
BookInfo(id=3, name=一本关于我们的书, author=李晔, desc=精装全彩,有趣、有用、有心!感情升温神器,炙手可热的DIY创意礼物。世界那么大,很幸运我和你成为“我们”。手残党的福音,手艺人的乐园,跟着提示,一笔一画写成专属于你和TA的书。, price=32.8),
BookInfo(id=4, name=自成人间, author=季羡林, desc=感动中国获奖人物,东方世界文学巨擘。白岩松、金庸、贾平凹、林青霞极力推崇。收录多篇入选语文教材、中高考阅读佳作,央视《朗读者》一读再读。恰似人间惊鸿客,只等秋风过耳边。, price=22.5)]
由输出可以看出,测试成功
4.1.3 多参数查询
多参数查询同上面一样的步骤
- 设计接口方法 (BookInfoMapper.java)
List<BookInfo> findBookInfo(String name,String author);
- 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
<select id="findBookInfo" resultType="org.example.model.BookInfo">
select * from bookinfo where name like concat('%',#{name},'%')
and author like concat('%',#{author},'%')
</select>
- 在测试类中编写测试方法 (TestMybatis.java)
@Test
public void testFindBookInfo(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
// 调用BookInfoMapper中的findAllBookInfo方法,查询全部图书信息
List<BookInfo> bookInfoList = mapper.findBookInfo("途说","何");
System.out.println(bookInfoList);
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
输出
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]
...
...
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]
at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:210)
at org.apache.ibatis.reflection.wrapper.MapWrapper.get(MapWrapper.java:45)
at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:116)
at org.apache.ibatis.executor.BaseExecutor.createCacheKey(BaseExecutor.java:225)
at org.apache.ibatis.executor.CachingExecutor.createCacheKey(CachingExecutor.java:149)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:154)
... 35 more
这时候我们发现,测试程序发生了报错,具体原因是Cause: org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]
这是因为,默认是不支持多参数查询的,为了实现多参数查询,有两种办法,
第一种,将参数按照顺序填写,param1,param2,param3...
<select id="findBookInfo" resultType="org.example.model.BookInfo">
select * from bookinfo where name like concat('%',#{param1},'%')
and author like concat('%',#{param2},'%')
</select>
第二种,使用注解,在mapper接口方法上,添加@Param注解,指定参数名
List<BookInfo> findBookInfo(@Param("name") String name,@Param("author") String author);
对应xml文件中,字段名要和注解中的参数名一致
<select id="findBookInfo" resultType="org.example.model.BookInfo">
select * from bookinfo where name like concat('%',#{name},'%')
and author like concat('%',#{author},'%')
</select>
两种方法选一种,这时候再进行测试
[BookInfo(id=1, name=道听途说, author=何金银, desc=都市异闻、悬疑灵异、神秘文化、中式恐怖……15个让你忘记呼吸的精彩故事。惊喜收录神秘篇章《爱你,林西》。亲眼所见,未必真实。道听途说,未必虚假。, price=29.7)]
此时观察到正常输出
4.1.4 Map参数查询
Map参数查询同上面一样的步骤
- 设计接口方法 (BookInfoMapper.java)
List<BookInfo> findBookInfoByMap(Map<String,Object> map);
- 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
<select id="findBookInfoByMap" resultType="org.example.model.BookInfo">
select * from bookinfo where name like concat('%',#{nameKey},'%')
and author like concat('%',#{authorKey},'%')
</select>
- 在测试类中编写测试方法 (TestMybatis.java)
@Test
public void testFindBookInfoByMap(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
// 调用BookInfoMapper中的findAllBookInfo方法,查询全部图书信息
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("nameKey", "途说");
map.put("authorKey", "");
List<BookInfo> bookInfoList = mapper.findBookInfoByMap(map);
System.out.println(bookInfoList);
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
输出
[BookInfo(id=1, name=道听途说, author=何金银, desc=都市异闻、悬疑灵异、神秘文化、中式恐怖……15个让你忘记呼吸的精彩故事。惊喜收录神秘篇章《爱你,林西》。亲眼所见,未必真实。道听途说,未必虚假。, price=29.7)]
可以看出,通过传入Map对象,也可以实现多参数的查询,但是需要注意的是,在xml文件中,字段名要和map中存放的key值相同,案例中存放的key值分别是nameKey和authorKey,所以xml文件中的字段名也要相应的修改,也要使用#{nameKey}和#{authorKey}来引用。
4.1.5 传入对象查询
传入对象查询同上面一样的步骤
- 设计接口方法 (BookInfoMapper.java)
//条件查询全部图书信息,传入对象参数
List<BookInfo> findBookInfoByObject(BookInfo bookInfo);
- 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
<select id="findBookInfoByObject" resultType="org.example.model.BookInfo">
select * from bookinfo where name like concat('%',#{name},'%')
and author like concat('%',#{author},'%')
</select>
- 在测试类中编写测试方法 (TestMybatis.java)
@Test
public void testFindBookInfoByObject(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
BookInfo bookInfo = new BookInfo();
bookInfo.setName("途说");
bookInfo.setAuthor("");
List<BookInfo> bookInfoList = mapper.findBookInfoByObject(bookInfo);
System.out.println(bookInfoList);
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
同样可以正常输出,需要注意的是,实体类中需要有相应的构造方法
5. 新增
新增传入参数的方法同查询一致,不再给出,返回值默认返回受影响的行数,这里只演示传入对象的方法
- 设计接口方法 (BookInfoMapper.java)
//添加图书信息
int addBookInfo(BookInfo bookInfo);
- 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
<!-- parameterType 是指传入的参数类型 可以省略不写 -->
<insert id="addBookInfo" parameterType="org.example.model.BookInfo">
insert into bookinfo values
(null,#{name},#{author},#{desc},#{price})
<!-- 这里传入null是因为在数据库中已经设置了id是自增,会自动生成,所以需要插入null -->
</insert>
- 在测试类中编写测试方法 (TestMybatis.java)
@Test
public void testAddBookInfo(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
System.out.println(mapper.addBookInfo(new BookInfo("我与地坛", "史铁生", "2024年百班千人寒假书单 九年级推荐阅读", 17.5)) > 0);
// 需要注意的是,这里执行成功后,数据库中也没有数据显示,是因为我们没有提交事务,所以数据并没有真正的插入到数据库中,需要手动提交事务
session.commit();
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
输出
true
tip:mysql默认的事务,是自动提交的,但是在mybatis的封装下,是默认没有自动提交的,所以需要我们在增删改操作后,手动提交事务,才会真正的插入到数据库中。也可以在创建SqlSession对象时,通过openSesson()的重载方法,设置openSession(boolean autoCommit),设置为true来实现自动提交事务。
6. 更新(修改)
更新传入参数的方法同新增一致,不再给出,返回值默认返回受影响的行数,这里只演示传入对象的方法
- 设计接口方法 (BookInfoMapper.java)
//修改图书信息
int updateBookInfo(BookInfo bookInfo);
- 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
<update id="updateBookInfo" parameterType="org.example.model.BookInfo">
<!-- 更新语句的ID,用于在Mapper接口中调用 -->
<!-- parameterType 指定了传入参数的类型,这里是 org.example.model.BookInfo -->
update bookinfo
<!-- 更新的表名 -->
<set>
<!-- <set> 标签用于动态生成 SQL 的 SET 子句,会自动去除多余的逗号 -->
<if test="name != null">
<!-- <if> 标签用于条件判断,test 属性中的表达式为 true 时,才会将里面的 SQL 片段包含到最终生成的 SQL 语句中 -->
name = #{name},
<!-- 如果 name 不为空,则生成 name = #{name}, 这里的 #{name} 是 MyBatis 的占位符,表示从传入的 BookInfo 对象中获取 name 属性的值 -->
</if>
<if test="author != null">
author = #{author},
<!-- 如果 author 不为空,则生成 author = #{author}, 这里的 #{author} 是 MyBatis 的占位符,表示从传入的 BookInfo 对象中获取 author 属性的值 -->
</if>
<if test="desc != null">
`desc` = #{desc},
<!-- 如果 desc 不为空,则生成 `desc` = #{desc}, 这里的 `desc` 用反引号包围,因为 desc 是 MySQL 的保留关键字,需要特殊处理 -->
</if>
<if test="price != null">
price = #{price},
<!-- 如果 price 不为空,则生成 price = #{price}, 这里的 #{price} 是 MyBatis 的占位符,表示从传入的 BookInfo 对象中获取 price 属性的值 -->
</if>
</set>
where id = #{id}
<!-- where 子句,用于指定更新的条件,这里的 #{id} 是 MyBatis 的占位符,表示从传入的 BookInfo 对象中获取 id 属性的值 -->
</update>
- 在测试类中编写测试方法 (TestMybatis.java)
@Test
public void testUpdateBookInfo(){
SqlSession session = null;
try {
// 获取SqlSession对象
session = MyBatisUtil.getSqlSession();
BookInfoMapper mapper = session.getMapper(BookInfoMapper.class);
BookInfo bookInfo = new BookInfo();
bookInfo.setId(9);
bookInfo.setDesc("祭拜星空,生者和死者都将在那里汇聚,浩然而成万古消息");
System.out.println(mapper.updateBookInfo(bookInfo) > 0);
// session.commit();
} finally {
// 关闭SqlSession对象,释放资源
MyBatisUtil.closeSqlSession(session);
}
}
输出
true
tip:更新操作,需要注意的是,更新操作的条件一定要有,否则会导致全部数据被更新,所以一定要传入更新条件id,因为id是唯一的,否则容易发生大批量修改错误。