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&amp;useUnicode=true&amp;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 查询全部

  1. 查询全部,设计接口方法 (BookInfoMapper.java)
public interface BookInfoMapper {
    //根据编号查找对应图书信息
    BookInfo findBookInfoById(Integer id);
    //查询全部图书信息
    List<BookInfo> findAllBookInfo();
}
  1. 在xml映射文件编写对应SQL语句 (BookInfoMapper.xml)
    <!-- 虽然方法查询返回的是List集合,但是返回类型resutType还是要写集合中存储的类型 -->
    <select id="findAllBookInfo" resultType="org.example.model.BookInfo">
            select * from bookinfo
    </select>
  1. 在测试类中编写测试方法 (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 多参数查询

多参数查询同上面一样的步骤

  1. 设计接口方法 (BookInfoMapper.java)
    List<BookInfo> findBookInfo(String name,String author);
  1. 在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>
  1. 在测试类中编写测试方法 (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参数查询同上面一样的步骤

  1. 设计接口方法 (BookInfoMapper.java)
    List<BookInfo> findBookInfoByMap(Map<String,Object> map);
  1. 在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>
  1. 在测试类中编写测试方法 (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值分别是nameKeyauthorKey,所以xml文件中的字段名也要相应的修改,也要使用#{nameKey}#{authorKey}来引用。

4.1.5 传入对象查询

传入对象查询同上面一样的步骤

  1. 设计接口方法 (BookInfoMapper.java)
    //条件查询全部图书信息,传入对象参数
    List<BookInfo> findBookInfoByObject(BookInfo bookInfo);
  1. 在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>
  1. 在测试类中编写测试方法 (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. 新增

新增传入参数的方法同查询一致,不再给出,返回值默认返回受影响的行数,这里只演示传入对象的方法

  1. 设计接口方法 (BookInfoMapper.java)
    //添加图书信息
    int addBookInfo(BookInfo bookInfo);
  1. 在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>
  1. 在测试类中编写测试方法 (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. 更新(修改)

更新传入参数的方法同新增一致,不再给出,返回值默认返回受影响的行数,这里只演示传入对象的方法

  1. 设计接口方法 (BookInfoMapper.java)
    //修改图书信息
    int updateBookInfo(BookInfo bookInfo);
  1. 在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>

  1. 在测试类中编写测试方法 (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是唯一的,否则容易发生大批量修改错误。