MyBatis详解八股文

MyBatis 是支持定制化 SQL以及高级映射(ORM)的优秀的持久层框架。

一、Mybatis项目创建

1、创建myBatis工程

选择maven 输入组织名称、项目名称、版本号和工程名

核对信息并点击Finish按钮

项目结构图:

2、引入jar包依赖、创建配置文件

  • 引入依赖在pom.xml文件中
xml 复制代码
<dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
  • 创建mybatis.xml配置文件
    resources右击-new-File
  • 注意 myBatis配置顺序如下,不能颠倒,也就是如果有properties属性配置,一定放在最前面,settings放到第二个位置,以此类推
  • mybatis.xml配置文件(部分)
xml 复制代码
<!--头部-->
<?xml version="1.0" encoding="UTF-8"?>  <!--xml文件-->
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">  <!--文件约束dtd,该文件的约束规范-->
<configuration>
    <settings>
        <!--setting配置; logImpl:指定MyBatis所用日志的具体实现,STDOUT-LOGGING表示控制台输出日志信息-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <!-- 配置环境(可以配置多个环境)-->
    <environments default="dev">
    <!-- 开发环境-->
        <environment id="dev">
        	<!--事务管理器-->
            <!-- 使用JDBC事务管理-->
            <transactionManager type="JDBC"></transactionManager>
            <!--数据源-->
            <!-- 使用连接池技术-->
            <dataSource type="POOLED">
                <!-- 数据库驱动-->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <!-- 连接字符串-->
                <property name="url" value="jdbc://mysql://localhost:3306/test?userUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai"/>
                <!--数据库用户名-->
                <property name="username" value="root"/>
                <!--数据库密码-->
                <property name="password" value="123456"/>
            </dataSource>
          </environment>
    </environments>
    <!--映射文件-->
    <!--使用相对于类路径的资源引用-->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"></mapper>
    </mappers>
</configuration>
  • 写跟数据库表对应的java实体类
java 复制代码
/**
 * @Alias( string name):注解:起别名
 */
/*@Alias("us")*/
public class User {
    private Long id;
    private String username;
    private String password;
	
	public User() {
    }

    public User(Long id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public Long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
  • 映射文件UserMapper.xml
    配置dao连接数据库的sql语句(增删改查

    映射文件位置为:src/main/resources/mapper/UserMapper.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8" ?>
        <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--mapper为根标签,namespace表示命名空间,暂时写为test-->

<!--UserMapper接口全限定接口名称(包名+接口名称)-->
<mapper namespace="com.tjetc.dao.UserMapper">
<!--select是查询标签,id表示唯一标识查询,resultType是查询结果返回的对象-->
<!--select标签内写sql语句(查询sql语句)-->
<!-- id的值与UserMapper的方法名称保持一致-->
<select id="selectList" resultType="User">
    select `id`,`username`,`password` from `user`
</select>
<!--传入参数 #{}:占位。大括号id占位参数,JDBC会把#{id}代入sql-->
<!--resultType和parameterType都可以使用别名,别名的大小写不区分-->
<!--mybatis把java常用的类型都使用了别名,所以java的常用类型不用特写配置别名-->,
<select id="selectById" parameterType="_long" resultType="user">
    select `id`,`username`,`password` from `user` where id=#{id}
</select>
<select id="selectPage" parameterType="int" resultType="user">
    SELECT  `id`,`username`,`password` FROM  `user` ORDER BY  id ASC  limit #{index},#{pageSize}
</select>
<!--concat是mysql的函数,作用是拼接字符串,可以把占位符和%连在一起-->
<select id="selectLike" parameterType="String" resultType="user">
    SELECT `id`,`username`,`password` from `user` WHERE  username LIKE concat('%',#{username},'%')
</select>
<!--  增加  -->
<!-- parameterType值是对象,#{}占位参数是对象名称,防止sql注入;${}原位替换,-->
<!-- userGeneratedKeys = true使用自增主键,keyProperty="id" 把自增主键赋值给User对象的id值;目的:不会发生并发问题-->
<insert id="insert" parameterType="user" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO USER (username,password) VALUES (#{username},#{password})
</insert>
<!-- 更新-->
<update id="update" parameterType="user">
    UPDATE USER SET username=#{username},`password`=#{password} WHERE id=#{id}
</update>
<!-- 删除-->
<delete id="deleteById" parameterType="Long">
    DELETE FROM USER WHERE id=#{id}
</delete>
</mapper>
  • 测试程序
java 复制代码
public class TestUser {
//查询全部用户
@Test
public void testQueryList() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//调用sqlSession.selectList()查询集合,参数为UserMapper.xml的namespace的值 点select标签对应 id值
List<User> users = sqlSession.selectList("test.queryList");
//打印结果
System.out.println(users);
//关闭session
sqlSession.close();
} 
//根据id查询用户
@Test
public void testQueryById() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//调用sqlSession.selectOne()查询一条数据,第一个 参数为UserMapper.xml的namespace的值 点select标签对应 id值 第二个参数为参数值
User user = sqlSession.selectOne("test.queryById",1L);
//打印结果
System.out.println(user);
//关闭session
sqlSession.close();
}
}
  • 使用别名
    在mybatis.xml文件中配置别名
xml 复制代码
<typeAliases>
<!--使用别名-->
<typeAlias type="com.tjetc.entity.User" alias="User"></typeAlias>
</typeAliases>

UserMapper.xml resultType="user",这里的user大小没有影响

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 暂时命名为test-->
<mapper namespace="test">
<select id="queryList" resultType="user">
select * from user
</select>
<select id="queryById" parameterType="int" resultType="User">
select * from user where id=#{id}
</select>
</mapper>

3、dao层创建Mapper接口映射,进行对数据库的操作

一、mapper接口映射

1、写接口

注意

1.接口名与 UserMapper.xml 除去扩展名一样(推荐)

2.UserMapper.xml 的 namespace 必须是 UserMapper 接口的全路径名

写mapper映射接口的方法遵循的原则

1、接口名与UserMapper.xml除去扩展名一样(推荐)

2、接口的方法名与UserMapper.xml的id一样

3、接口的方法的参数类型与UserMapper.xml的parameterType一样

4、接口的方法的返回值类型与 UserMapper.xml 的 resultType 一样(返回 list 接口方法的返回值类型是 List 类型)

java 复制代码
public interface UserMapper {
    /**
     * 查询所有
     * @return
     */
    List<User> selectList();

    /**
     * 根据id查询单个
     * @param id
     * @return
     */
    User selectById(long id);
    /**
     * 分页
     * @param i 查询第几条数
     * @param p 查询有多少条数
     * @return
     */
    List<User> selectPage(@Param("index") int i, @Param("pageSize") int p);

    /**
     * 模糊匹配
     * @param username
     * @return
     */
    List<User> selectLike(String username);

    /**
     * 新增
     * @param user
     */
    void insert(User user);

    /**
     * 更新数据
     * @param user
     * @return 返回影响的行数
     */
    int update(User user);

    /**
     * 根据id删除数据
     * @return 返回影响的行数
     */
    int deleteById(Long id);
}

修改UserMapper.xml配置

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 为Dao接口的全限定名:包名+接口名-->
<mapper namespace="com.tjetc.dao.UserMapper">
<select id="queryList" resultType="com.tjetc.entity.User">
select * from user
</select>
<select id="queryById" parameterType="int"
resultType="com.tjetc.entity.User">
select * from user where id=#{id}
</select>
</mapper>

测试类

java 复制代码
@Test
public void testQueryList1() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取UserDao对象(代理对象)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//userDao.queryList()方法
List<User> users = userMapper.queryList();
//打印结果
System.out.println(users);
//关闭session
sqlSession.close();
}
@Test
public void testQueryById1() throws IOException {
//实例化SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new
SqlSessionFactoryBuilder();
//创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis.xml"));
//创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取UserDao对象(代理对象)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//userDao.queryById()方法
User user = userMapper.queryById(1L);
//打印结果
System.out.println(user);
//关闭session
sqlSession.close();
}

4、mybatis.xml配置优化

  • properties配置文件(连接数据库的信息)
    在resources目录下创建db.properties
xml 复制代码
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
  • 修改mybatis.xml配置
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="db.properties"></properties>
<settings>
<!--指定 MyBatis 所用日志的具体实现,STDOUT_LOGGING表示控制台输出日志信息-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<!--使用别名-->
<typeAlias type="com.tjetc.entity.User" alias="User"></typeAlias>
</typeAliases>
<!--配置环境(可以配置多个环境)-->
<environments default="dev">
<!--开发环境-->
<environment id="dev">
<!--使用JDBC事务管理-->
<transactionManager type="JDBC"></transactionManager>
<!--使用连接池技术-->
<dataSource type="POOLED">
<!--数据库驱动-->
<property name="driver" value="${driverName}"/>
<!--连接字符串-->
<property name="url" value="${url}"/>
<!--数据库用户名-->
<property name="username" value="${username}"/>
<!--数据库密码-->
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--映射文件-->
<mappers>
<mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
  • typeAliases配置
    类型别名是java类型的简写。它仅在XML配置文件中,用于简化合格的class名字
    也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean
xml 复制代码
<typeAliases>
<package name="com.tjetc.entity"/>
</typeAliases>

每一个在包 com.tjetc.entity 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 com.tjetc.entit.User 的别名为 user ;若有注解,则别名为其注解值 ,例如

java 复制代码
@Alias("adm")
public class Admin{
//....
}

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

5、配置参数解析:

1、typeHandlers

无论是mybatis设置一个参数给PreparedStatement,还是从PreparedStatement中返回ResultSet,都要面临一个问题:Java类型与JDBC类型之间的转换。

Java类型面向的是内存,JDBC类型面向的是各种关系型数据库 ,因此两者有很大的不同。

数据类型映射,是ORM框架中最关键的问题。

xml 复制代码
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

自定义类型映射器

你可以重写类型映射器,也可以创建自己的非标准类型映射器。

需要实现org.apache.ibatis.type.TypeHandler接口,或继承类

org.apache.ibatis.type.BaseTypeHandler

java 复制代码
import com.tjetc.enums.SexEnums;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class SexEnumsTypeHandler extends BaseTypeHandler<SexEnums>{
    //jdbc类型转换java类型调用
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, SexEnums sexEnums, JdbcType jdbcType) throws SQLException {
        preparedStatement.setObject(i,sexEnums);
    }
    //jdbc类型转换java类型调用
    @Override
    public SexEnums getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return (SexEnums) resultSet.getObject(s);
    }
    //jdbc类型转换java类型调用
    @Override
    public SexEnums getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return (SexEnums) resultSet.getObject(i);
    }
    //调用存储过程用
    @Override
    public SexEnums getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return null;
    }
}

objectFactory配置

当mybatis每次创建一个新的结果集对象时,都需要使用objectFactory实例。

如果希望重写默认ObjectFactory的行为,可以自定义对象工厂

  • environments配置

1、mybatis允许你同时配置多个environments。如开发、测试、生产,可以同时配置三个环境。还可以同时对多个不同类型的数据库操作。

注意:虽然你可以同时配置多个环境,但是对于一个SqlSessionFactory实例,只能选择一个环境使用。

因此,如果你想同时连接两个数据库,必须要创建两个SqlSessionFactory。

2、transactionManager配置

这里有两种事务管理类型可选,type=JDBC \| MANAGED

JDBC:依赖于从dataSource返回的Connection对象,直接使用JDBC管理事务

MANAGED:由JavaEE容器管理整个事务

注意:如果使用Spring整合mybatis,无需配置TransactionManager,因为Spring会用自己的事务环境

重写mybatis的配置

3、dataSource配置

使用标准数据源接口javax.sql.DataSource配置数据库的JDBC连接

这里有三种数据源类型可选:type="UNPOOLED \| POOLED \| JNDI")

  • UNPOOLED数据源
    这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的
    简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连
    接池并不重要,这个配置就很适合这种情形
  • POOLED
    采用数据库连接池配置,可以优化性能,是当前web项目配置首选。
  • JNDI
    这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用

4、databaseIdProvider配置

DatabaseIdProvider元素主要是为了支持不同厂商的数据库,即同时支持多个数据库

这个配置非常有用,项目如何同时支持多种数据库?

传统做法是生成多套mapper文件,在mybatis.xml中配置使用那套映射文件。

这个做法的很大缺陷是:多套映射文件中,会有很多接口的实现是相同的,如果代码修改,需要同时修改多套文件。这给开发额

外增加了很大的工作量。

二、mybatis的工作原理

底层原理这里不细说,之后单独介绍;大致原理如下图所示:

三、mybatis中的动态SQL

1、if判断条件是否成立

xml 复制代码
<!-- 判断传入的参数是否符合条件,如果符合条件,把sql语句进行拼接-->
    <select id="selectAll" parameterType="user" resultType="user">
        SELECT * FROM USER WHERE 1=1
        <if test="username!=null">
            and username=#{username}
        </if>
        <if test="password!=null">
            and password=#{password}
        </if>
    </select>

2、choose

相当于 if elseif else 只有一个 when 或者 otherwise

被执行

xml 复制代码
 <!-- choose when otherwise  相当于java if else if else-->
    <select id="selectAll1" parameterType="user" resultType="user">
        SELECT * FROM USER where 1=1
        <choose>
            <when test="username!=null">
                AND username=#{username}
            </when>
            <when test="password!=null">
                AND password=#{password}
            </when>
            <otherwise>
                AND 2=2
            </otherwise>
        </choose>
    </select>

3、where

增加 where 条件,如果没有条件成立,where 不会出现在

sql 语句中,反之出现 sql 语句中,并且忽略 where 后面的第一个 and 或者 or

xml 复制代码
<!-- 如果where标签内有条件成立,就会给sql语句添加where关键字,并且where后面的sql语句第一个and或or删除掉-->
    <select id="selectAll2" parameterType="user" resultType="user">
        SELECT  * FROM USER
        <where>
            <if test="username!=null">
                AND username=#{username}
            </if>
            <if test="password!=null">
                AND password=#{password}
            </if>
        </where>
    </select>

4、trim

例如 < trim prefix="where" prefixOverrides="and | or" > 忽略 where 后

面的第一个 and 或者 or

xml 复制代码
<select id="findAll" parameterType="User" resultType="User">
SELECT * FROM user
<trim prefix="where" prefixOverrides="and | or">
<if test="username!=null">
and username=#{username}
</if>
<if test="password!=null">
and password=#{password}
</if>
</trim>
</select>

5、set 后面必须满足于一个条件,不然不满足 sql 语法

xml 复制代码
<update id="update" parameterType="user">
update user
<set>
<if test="username!=null">
username=#{username},
</if>
<if test="password!=null">
password=#{password},
</if>
</set>
where id=#{id}
</update>

6、foreach

  • 单参list
xml 复制代码
<select id="findAll2" resultType="User">
SELECT * FROM user where id in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>

UserMapper接口的方法

java 复制代码
List<User> findAll2(List<Integer> list);

Test类

java 复制代码
@Test
    public void selectAll2(){
        //获取sqlSess对象
        SqlSession sqlSession = SqlSessionUtils.getSqlSession();
        //获取mapper映射对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        //调用方法
        //创建User对象
        User user = new User();
        //设置Username的值
        user.setUsername("安其拉");
        user.setPassword("123123");
        List<User> users = mapper.selectAll2(user);
        //打印结果
        System.out.println(users);
        //关闭sqlsession
        sqlSession.close();
    }

查询结果

java 复制代码
==> Preparing: SELECT * FROM user where id in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 6(Integer)
<== Columns: id, password, username
<== Row: 1, 123456, admin
<== Row: 3, 1111, admin1
<== Row: 6, 2333322, 3222
<== Total: 3
[User{id=1, username='admin', password='123456'}, User{id=3, username='admin1', password='1111'}, User{id=6,
username='3222', password='2333322'}]
  • 单参数组
xml 复制代码
<select id="findAll2" resultType="User">
SELECT * FROM user where id in
<foreach collection="array" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>

Usermapper

java 复制代码
List<User> findAll2(Integer[] array);

@Test

java 复制代码
@Test
public void testFindAll22() {
try {
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis.xml"))
;
// 得到 session 对象
SqlSession session = sqlSessionFactory.openSession();
//得到 mapper 接口的实现对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 通过 session 对象数据库
Integer[] array=new Integer[]{1,3,6};
List<User> list = mapper.findAll2(array);
// 控制台打印 list 对象
System.out.println(list);
// 关闭 session
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}

结果

java 复制代码
==> Preparing: SELECT * FROM user where id in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 6(Integer)
<== Columns: id, password, username
<== Row: 1, 123456, admin
<== Row: 3, 1111, admin1
<== Row: 6, 2333322, 3222
<== Total: 3
[User{id=1, username='admin', password='123456'}, User{id=3, username='admin1', password='1111'}, User{id=6,
username='3222', password='2333322'}]
  • 多个参数使用map
xml 复制代码
<select id="findAll2" resultType="User">
SELECT * FROM user where username like concat('%',#{username},'%') and id in
<foreach collection="ids" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>

UserMapper接口的方法

java 复制代码
List<User> findAll2(Map<String,Object> map);

@Test

java 复制代码
@Test
public void testFindAll22() {
try {
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis.xml"))
;
// 得到 session 对象
SqlSession session = sqlSessionFactory.openSession();
//得到 mapper 接口的实现对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 通过 session 对象数据库
Map<String,Object> map=new HashMap<>();
map.put("username", "d");
map.put("ids", Arrays.asList(1,3,6));
List<User> list = mapper.findAll2(map);
// 控制台打印 list 对象
System.out.println(list);
// 关闭 session
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}

结果

java 复制代码
==> Preparing: SELECT * FROM user where username like '%d%' and id in ( ? , ? , ? )
==> Parameters: 1(Integer), 3(Integer), 6(Integer)
<== Columns: id, password, username
<== Row: 1, 123456, admin
<== Row: 3, 1111, admin1
<== Total: 2
[User{id=1, username='admin', password='123456'}, User{id=3, username='admin1', password='1111'}]

四、MyBatis多表查询

在实际业务中,数据往往分散在多个表中,需要通过关联查询来获取完整信息。MyBatis 提供了强大的结果映射(ResultMap)功能,可以优雅地将复杂的 SQL 查询结果映射到 Java 对象中,支持一对一、一对多、多对多等关联关系。

1. 关联关系概述

数据库表之间的关联主要有三种类型:

  • 一对一(One-to-One):例如一个用户(User)对应一个身份证(IDCard)。
  • 一对多(One-to-Many):例如一个部门(Department)对应多个员工(Employee)。
  • 多对多(Many-to-Many):例如一个学生(Student)可以选修多门课程(Course),一门课程也可以被多个学生选修。多对多关系通常通过中间表实现。

MyBatis 通过 <association>(对应一对一)和 <collection>(对应一对多/多对多)标签在 ResultMap 中定义这些关联映射。

2. 准备工作:示例表结构

假设我们有以下两张表,用于演示一对多查询:

部门表(department)

字段名 类型 说明
id BIGINT 主键
name VARCHAR(50) 部门名称

员工表(employee)

字段名 类型 说明
id BIGINT 主键
name VARCHAR(50) 员工姓名
dept_id BIGINT 外键,关联部门表id

对应的 Java 实体类:

java 复制代码
// Department.java
public class Department {
    private Long id;
    private String name;
    // 一对多:一个部门有多个员工
    private List<Employee> employees;

    // 构造方法、getter/setter、toString 省略
}

// Employee.java
public class Employee {
    private Long id;
    private String name;
    private Long deptId;
    // 多对一:一个员工属于一个部门
    private Department department;

    // 构造方法、getter/setter、toString 省略
}

3. 一对一关联查询(多对一)

场景:查询员工信息,并同时获取其所属的部门信息。

3.1 使用 ResultMap 的 <association>

在 EmployeeMapper.xml 中定义 ResultMap:

xml 复制代码
<!-- 定义 resultMap,映射 Employee 对象及其关联的 Department 对象 -->
<resultMap id="employeeWithDeptResultMap" type="Employee">
    <!-- 映射 Employee 的基本字段 -->
    <id property="id" column="e_id"/>
    <result property="name" column="e_name"/>
    <result property="deptId" column="dept_id"/>
    <!-- association 映射一对一关联对象 -->
    <association property="department" javaType="Department">
        <id property="id" column="d_id"/>
        <result property="name" column="d_name"/>
    </association>
</resultMap>

<!-- 使用 resultMap 进行查询 -->
<select id="selectEmployeeWithDepartment" resultMap="employeeWithDeptResultMap">
    SELECT 
        e.id AS e_id,
        e.name AS e_name,
        e.dept_id,
        d.id AS d_id,
        d.name AS d_name
    FROM employee e
    LEFT JOIN department d ON e.dept_id = d.id
    WHERE e.id = #{id}
</select>

3.2 接口方法

java 复制代码
public interface EmployeeMapper {
    Employee selectEmployeeWithDepartment(Long id);
}

4. 一对多关联查询

场景:查询部门信息,并同时获取该部门下的所有员工列表。

4.1 使用 ResultMap 的 <collection>

在 DepartmentMapper.xml 中定义 ResultMap:

xml 复制代码
<!-- 定义 resultMap,映射 Department 对象及其关联的 Employee 集合 -->
<resultMap id="departmentWithEmployeesResultMap" type="Department">
    <!-- 映射 Department 的基本字段 -->
    <id property="id" column="d_id"/>
    <result property="name" column="d_name"/>
    <!-- collection 映射一对多关联集合 -->
    <collection property="employees" ofType="Employee">
        <id property="id" column="e_id"/>
        <result property="name" column="e_name"/>
        <result property="deptId" column="dept_id"/>
    </collection>
</resultMap>

<!-- 使用 resultMap 进行查询 -->
<select id="selectDepartmentWithEmployees" resultMap="departmentWithEmployeesResultMap">
    SELECT 
        d.id AS d_id,
        d.name AS d_name,
        e.id AS e_id,
        e.name AS e_name,
        e.dept_id
    FROM department d
    LEFT JOIN employee e ON d.id = e.dept_id
    WHERE d.id = #{id}
</select>

4.2 接口方法

java 复制代码
public interface DepartmentMapper {
    Department selectDepartmentWithEmployees(Long id);
}

5. 多对多关联查询

假设新增课程表(course)和学生选课中间表(student_course):

课程表(course)

字段名 类型 说明
id BIGINT 主键
course_name VARCHAR(50) 课程名称

学生选课中间表(student_course)

字段名 类型 说明
student_id BIGINT 外键,关联学生表id
course_id BIGINT 外键,关联课程表id

实体类:

java 复制代码
// Student.java
public class Student {
    private Long id;
    private String name;
    // 多对多:学生选修多门课程
    private List<Course> courses;
    // getter/setter 省略
}

// Course.java
public class Course {
    private Long id;
    private String courseName;
    // 多对多:课程被多个学生选修
    private List<Student> students;
    // getter/setter 省略
}

5.1 查询学生及其选修课程

StudentMapper.xml:

xml 复制代码
<resultMap id="studentWithCoursesResultMap" type="Student">
    <id property="id" column="s_id"/>
    <result property="name" column="s_name"/>
    <!-- 多对多通过 collection 映射 -->
    <collection property="courses" ofType="Course">
        <id property="id" column="c_id"/>
        <result property="courseName" column="course_name"/>
    </collection>
</resultMap>

<select id="selectStudentWithCourses" resultMap="studentWithCoursesResultMap">
    SELECT 
        s.id AS s_id,
        s.name AS s_name,
        c.id AS c_id,
        c.course_name
    FROM student s
    LEFT JOIN student_course sc ON s.id = sc.student_id
    LEFT JOIN course c ON sc.course_id = c.id
    WHERE s.id = #{id}
</select>

6. 嵌套查询(Nested Select)与延迟加载

除了使用 JOIN 一次性查询所有数据,MyBatis 还支持嵌套查询(分步查询),并可以配置延迟加载(懒加载),即用到关联对象时才执行子查询。

6.1 配置延迟加载

在 mybatis.xml 中开启延迟加载:

xml 复制代码
<settings>
    <!-- 其他配置... -->
    <!-- 开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

6.2 嵌套查询示例

在 EmployeeMapper.xml 中,使用 <association>select 属性指定另一个查询语句:

xml 复制代码
<!-- 第一步:查询员工基本信息 -->
<resultMap id="employeeWithDeptNestedResultMap" type="Employee">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="deptId" column="dept_id"/>
    <!-- 通过 select 属性引用另一个查询,column 传递参数 -->
    <association property="department" column="dept_id" 
                 select="com.tjetc.dao.DepartmentMapper.selectById"/>
</resultMap>

<select id="selectEmployeeWithDepartmentNested" resultMap="employeeWithDeptNestedResultMap">
    SELECT id, name, dept_id FROM employee WHERE id = #{id}
</select>

DepartmentMapper.xml 中定义被引用的查询:

xml 复制代码
<select id="selectById" resultType="Department">
    SELECT id, name FROM department WHERE id = #{id}
</select>

这样,当调用 selectEmployeeWithDepartmentNested 时,只会先执行员工查询;只有当访问 employee.getDepartment() 时,MyBatis 才会执行 selectById 查询部门信息。

7. <resultMap><resultType> 的区别详解

在 MyBatis 的映射文件(Mapper XML)中,<select><insert><update><delete> 等语句标签的 resultTyperesultMap 属性用于指定 SQL 查询结果的映射方式。理解两者的区别是掌握 MyBatis 结果映射的关键。

7.1 <resultType>:简单类型映射

resultType 用于指定查询结果直接映射到的 Java 类型(全限定类名或别名)。它适用于以下场景:

  1. 基本类型或简单对象 :当查询返回的列名与 Java 对象的属性名完全一致(或遵循驼峰命名自动映射)时,MyBatis 会自动创建该类型的实例,并将查询结果集的列值注入到对应属性中。
  2. 返回单个字段 :例如查询记录数 (resultType="int"resultType="java.lang.Integer")。
  3. 返回 MapresultType="map" 会将结果集的每一行映射为一个 Map<String, Object>,键为列名,值为列值。

示例:使用 resultType 映射简单对象

xml 复制代码
<!-- 假设数据库 user 表的列名是 id, username, password -->
<!-- Java 类 User 的属性名也是 id, username, password -->
<select id="selectUser" resultType="com.tjetc.entity.User">
    SELECT id, username, password FROM user WHERE id = #{id}
</select>

在这个例子中,MyBatis 会自动将 idusernamepassword 列的值映射到 User 对象的同名属性上。

优点

  • 配置简单:无需额外定义映射关系,适合属性与列名一一对应的简单场景。
  • 代码简洁:直接指定类名即可。

局限性

  • 无法处理复杂映射 :当列名与属性名不一致、需要关联查询(嵌套对象或集合)、或者需要自定义类型处理器时,resultType 无能为力。
  • 依赖命名约定:严重依赖于数据库列名与 Java 属性名的命名一致性(或开启驼峰映射配置)。

7.2 <resultMap>:高级自定义映射

resultMap 是一个显式定义的、可重用的映射规则 。它通过 <resultMap> 标签在 XML 中声明,并通过其 id 被其他语句引用。它适用于所有 resultType 无法处理的复杂场景。

核心能力

  1. 解决列名与属性名不一致 :使用 <result> 子标签明确指定 column(数据库列名)到 property(Java 属性名)的映射。
  2. 实现关联映射(一对一、一对多、多对多) :通过 <association>(关联单个对象)和 <collection>(关联对象集合)标签,将复杂的 JOIN 查询结果映射到嵌套的对象或集合中。这正是上一节"多表查询"的核心。
  3. 支持继承 :一个 <resultMap> 可以通过 extends 属性继承另一个,实现映射规则的复用。
  4. 自动映射控制 :通过 autoMapping 属性可以控制是否自动映射未明确指定的列。
  5. 构造函数映射 :使用 <constructor> 标签映射到构造方法的参数。
  6. 鉴别器 :使用 <discriminator> 标签实现根据某列值选择不同的结果映射(类似 switch-case)。

示例:使用 resultMap 处理列名不一致和关联查询

xml 复制代码
<!-- 1. 定义基础的 resultMap,处理列名不一致 -->
<resultMap id="baseUserMap" type="User">
    <id property="userId" column="id"/> <!-- 主键映射 -->
    <result property="userName" column="username"/> <!-- 普通字段映射 -->
    <result property="userPassword" column="password"/>
</resultMap>

<!-- 2. 定义扩展的 resultMap,包含部门关联(一对一) -->
<resultMap id="userWithDeptMap" type="User" extends="baseUserMap">
    <!-- 使用 extends 继承基础映射 -->
    <!-- association 处理一对一关联 -->
    <association property="department" javaType="Department">
        <id property="id" column="dept_id"/>
        <result property="name" column="dept_name"/>
    </association>
</resultMap>

<!-- 3. 在查询语句中引用自定义的 resultMap -->
<select id="selectUserWithDepartment" resultMap="userWithDeptMap">
    SELECT 
        u.id, 
        u.username, 
        u.password,
        d.id AS dept_id,
        d.name AS dept_name
    FROM user u
    LEFT JOIN department d ON u.dept_id = d.id
    WHERE u.id = #{id}
</select>

优点

  • 功能强大且灵活:可以处理任何复杂的数据结构映射。
  • 解耦 SQL 与对象模型:数据库列设计可以独立于 Java 对象属性命名。
  • 提升可维护性 :复杂的映射关系被集中定义和管理,<resultMap> 可以复用。

7.3 核心区别对比表

特性 <resultType> <resultMap>
定义方式 直接指定一个 Java 类(或别名)。 需要先使用 <resultMap> 标签显式定义映射规则,再通过其 id 引用。
映射机制 自动映射。基于列名与属性名一致(或驼峰规则)的约定。 显式映射 。通过 <id>, <result>, <association>, <collection> 等子标签精确控制。
处理列名不一致 不支持。必须列名与属性名一致,或开启全局驼峰映射。 支持 。通过 columnproperty 属性指定对应关系。
关联查询(嵌套对象/集合) 不支持。只能映射到扁平的单层对象。 核心功能 。通过 <association><collection> 完美支持。
继承与复用 不支持 支持 。通过 extends 属性实现映射规则的继承和复用。
适用场景 简单的单表查询,且对象属性与表列名完全对应。 所有复杂场景,包括:多表关联查询、列名与属性名不一致、自定义类型处理、构造函数映射等。
配置复杂度 (几乎零配置)。 (需要详细定义映射关系)。

7.4 如何选择?

  • 优先使用 resultType :当你的查询是简单的单表查询,并且数据库列名与 JavaBean 属性名完全一致(或已配置驼峰命名自动映射)时,使用 resultType 可以让配置最简洁。
  • 必须使用 resultMap :在以下任何情况下,都必须使用 resultMap
    1. 查询涉及多表 JOIN,需要将结果映射到包含嵌套对象的复杂 Java 对象中。
    2. 数据库列名与 JavaBean 属性名不一致,且你不想或不能修改其中一方。
    3. 你需要对映射过程进行更精细的控制,例如使用特定的类型处理器(TypeHandler) 、映射到构造方法 ,或者使用鉴别器
    4. 你需要复用映射规则。

7.5 总结

可以将 resultType 看作是 MyBatis 提供的一种"约定优于配置 "的快捷方式,它在简单场景下非常高效。而 resultMap 则是 MyBatis 映射能力的"完全体",它提供了完整的、声明式的配置能力,以应对实际开发中各种复杂的对象-关系映射需求。

五、Mybatis的缓存机制

MyBatis 内置了强大的缓存机制,可以有效减少数据库的访问次数,提升应用性能。MyBatis 的缓存分为两级:一级缓存二级缓存

5.1 一级缓存(本地缓存/SqlSession 级别缓存)

一级缓存是 SqlSession 级别的缓存,默认开启,无法关闭。

5.1.1 工作原理

  • 在同一个 SqlSession 中,执行相同的查询 SQL 时,第一次查询会将结果存入缓存,后续相同的查询直接从缓存中获取,不再访问数据库。
  • 一级缓存的生命周期与 SqlSession 相同,当 SqlSession 关闭或执行了增删改 操作(insertupdatedelete)后,缓存会被清空。

5.1.2 示例与验证

java 复制代码
@Test
public void testFirstLevelCache() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    
    // 第一次查询,会访问数据库
    User user1 = mapper.selectById(1L);
    System.out.println("第一次查询: " + user1);
    
    // 第二次查询相同数据,直接从一级缓存获取,不访问数据库
    User user2 = mapper.selectById(1L);
    System.out.println("第二次查询: " + user2);
    
    // 验证是否为同一个对象(缓存命中)
    System.out.println("user1 == user2: " + (user1 == user2)); // 输出 true
    
    // 执行更新操作,清空一级缓存
    user1.setUsername("newName");
    mapper.update(user1);
    sqlSession.commit(); // 提交事务
    
    // 第三次查询,缓存已清空,会再次访问数据库
    User user3 = mapper.selectById(1L);
    System.out.println("更新后查询: " + user3);
    
    sqlSession.close();
}

控制台输出可能如下:

复制代码
==>  Preparing: SELECT id, username, password FROM user WHERE id = ? 
==> Parameters: 1(Long)
<==      Total: 1
第一次查询: User{id=1, username='admin', password='123456'}
第二次查询: User{id=1, username='admin', password='123456'}  // 无 SQL 日志,命中缓存
user1 == user2: true
==>  Preparing: UPDATE user SET username=?, password=? WHERE id=? 
==> Parameters: newName(String), 123456(String), 1(Long)
<==    Updates: 1
==>  Preparing: SELECT id, username, password FROM user WHERE id = ? 
==> Parameters: 1(Long)
<==      Total: 1
更新后查询: User{id=1, username='newName', password='123456'}

5.1.3 一级缓存失效场景

  1. SqlSession 不同:每个 SqlSession 有自己独立的一级缓存。
  2. SqlSession 相同,但查询条件不同:缓存基于 SQL 语句和参数,条件不同则缓存不命中。
  3. SqlSession 相同,两次查询之间执行了增删改操作:缓存会被清空。
  4. 手动清空缓存 :调用 sqlSession.clearCache() 方法。
  5. SqlSession 关闭:缓存随之销毁。

5.2 二级缓存(全局缓存/namespace 级别缓存)

二级缓存是 namespace(Mapper 接口)级别 的缓存,多个 SqlSession 可以共享。需要手动开启

5.2.1 开启二级缓存

步骤 1:在 mybatis-config.xml 中开启全局缓存(默认已开启,但建议显式配置)

xml 复制代码
<settings>
    <!-- 其他配置... -->
    <!-- 开启全局二级缓存,默认为 true,可省略 -->
    <setting name="cacheEnabled" value="true"/>
</settings>

步骤 2:在需要使用二级缓存的 Mapper.xml 文件中添加 <cache/> 标签

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tjetc.dao.UserMapper">
    <!-- 启用本 Mapper 的二级缓存 -->
    <cache/>
    
    <!-- 其他 SQL 映射语句 -->
    <select id="selectById" resultType="User">
        SELECT id, username, password FROM user WHERE id = #{id}
    </select>
</mapper>

步骤 3:实体类实现 Serializable 接口(因为二级缓存可能将数据序列化到磁盘)

java 复制代码
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ... 原有属性和方法
}

5.2.2 <cache> 标签常用属性

xml 复制代码
<cache
  eviction="LRU"              <!-- 回收策略:LRU(最近最少使用)、FIFO(先进先出)、SOFT(软引用)、WEAK(弱引用) -->
  flushInterval="60000"       <!-- 刷新间隔(毫秒),默认不清空 -->
  size="512"                  <!-- 缓存最多可以存储的对象数目 -->
  readOnly="true"             <!-- 是否只读:true(返回缓存对象的引用,性能高)、false(返回缓存对象的拷贝,安全) -->
  blocking="false"            <!-- 是否使用阻塞缓存 -->
/>

5.2.3 二级缓存工作原理与示例

java 复制代码
@Test
public void testSecondLevelCache() {
    // 第一个 SqlSession
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    User user1 = mapper1.selectById(1L);
    System.out.println("SqlSession1 第一次查询: " + user1);
    sqlSession1.close(); // 必须关闭 SqlSession,查询结果才会存入二级缓存
    
    // 第二个 SqlSession(不同 SqlSession)
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    User user2 = mapper2.selectById(1L); // 从二级缓存获取,不查询数据库
    System.out.println("SqlSession2 查询(命中二级缓存): " + user2);
    
    // 验证是否为同一个对象(取决于 readOnly 配置)
    System.out.println("user1 == user2: " + (user1 == user2)); // readOnly=true 时为 false
    
    sqlSession2.close();
}

5.2.4 二级缓存注意事项

  1. 事务提交后缓存生效 :只有 SqlSession 执行了 commit()close() 后,查询结果才会存入二级缓存。
  2. 增删改操作会清空对应 namespace 的缓存 :执行 insertupdatedelete 后,该 namespace 下的所有缓存都会被清空。
  3. 缓存共享问题:多个 SqlSession 共享缓存时,需要注意数据一致性问题。
  4. 缓存策略选择 :根据业务场景选择合适的 eviction(回收策略)和 readOnly 属性。

5.3 使用第三方缓存(如 Ehcache、Redis)

MyBatis 提供了缓存接口 org.apache.ibatis.cache.Cache,可以方便地集成第三方缓存。

5.3.1 集成 Ehcache 示例

步骤 1:添加依赖

xml 复制代码
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.1</version>
</dependency>

步骤 2:在 Mapper.xml 中指定缓存实现

xml 复制代码
<mapper namespace="com.tjetc.dao.UserMapper">
    <!-- 使用 Ehcache 作为二级缓存实现 -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    <!-- 其他配置 -->
</mapper>

步骤 3:添加 ehcache.xml 配置文件(可选,用于自定义配置)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <diskStore path="java.io.tmpdir"/>
    
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

5.3.2 集成 Redis 缓存

类似地,可以使用 mybatis-redis 等适配器,将二级缓存数据存储到 Redis 中,实现分布式缓存。

5.4 缓存使用建议与最佳实践

  1. 合理使用缓存级别

    • 对于频繁查询且很少修改的数据,使用二级缓存。
    • 对于会话级别的数据,依赖一级缓存即可。
    • 对于实时性要求高的数据,不建议使用缓存或设置较短的过期时间。
  2. 避免缓存穿透

    • 对于查询结果为 null 的情况,也可以缓存(缓存空对象),但需要设置较短的过期时间。
  3. 注意缓存范围

    • 二级缓存是 namespace 级别的,如果多个 Mapper 操作同一张表,需要考虑缓存同步问题。
    • 可以通过 <cache-ref> 标签让多个 Mapper 共享同一个缓存空间。
  4. 监控缓存命中率

    • 在生产环境中,应监控缓存的命中率,根据实际情况调整缓存策略。
  5. 结合业务场景选择缓存策略

    • 查询为主的应用:可以大量使用缓存提升性能。
    • 写多读少的应用:需要谨慎使用缓存,避免频繁的缓存失效。

5.5 总结

MyBatis 的缓存机制是其高性能的重要保障:

  • 一级缓存:SqlSession 级别,默认开启,适用于会话内的数据重复查询。
  • 二级缓存:namespace 级别,需要手动配置,适用于跨会话的共享数据查询。
  • 第三方缓存:可以通过实现 Cache 接口集成 Ehcache、Redis 等,满足分布式、高可用的缓存需求。

合理使用缓存可以显著提升应用性能,但同时也需要关注数据一致性和内存占用问题。在实际项目中,应根据具体业务场景选择合适的缓存策略和配置参数。