Loading

Mybatis全笔记

一、MyBatis简介

1.MyBatis历史

MyBatis最初是Apache的一个开源项目iBatis,2010年6月这个项目由Apache迁移到了Google。随着开发团队转投到了Google旗下,iBatis3.X正式更名为MyBatis。(jar包中还有iBatis的身影)

iBatis一词来源于“Internet”和“abatis”的组合,是一个基于Java的持久层框架

2.MyBatis特性

  1. MyBatis是支持定制化SQL、存储过程以及高级映射的优秀的持久层框架。
  2. MyBatis避免了几乎所有JDBC代码和手动设置参数以及获取结果集。
  3. MyBatis可以使用简单的XML或者注解用于配置和原始映射,将接口和Java的pojo映射成数据库中的记录。
  4. MyBatis是一个半自动的ORM(Object Relation Mapping)框架。

3.MyBatis和Hibernate

Hibernate也是一个持久化框架,但是在国内并没有MyBatis来得火爆。

简单比较两者:

Hibernate比较复杂,庞大,学习周期比较长,开发难度大于MyBatis,很难上手。但是Hibernate有良好的映射机制,能够自动生成sql。但是映射太多,导致数据库性能下降。

MyBatis则相对简单,对JDBC进行了封装,屏蔽了jdbc的缺点,让程序员只需要关注sql本身,容易上手。但是需要手动编写sql。

但是Hibernate自动生成的sql比较复杂,而且对sql语句的优化、修改比较困难,系统不喜欢。MyBatis需要人手动书写sql语句,更灵活,性能更好。

二、快速入门

1.开发环境

IDE:idea 2020.3

构建工具:Maven 3.8.4

MySQL版本:MySQL 8.0.26

2.创建maven工程

a>打包方式

<groupId>org.example</groupId>
<artifactId>MyBatis</artifactId>
<version>1.0-SNAPSHOT</version>
<!--打包方式-->
<packaging>jar</packaging>

b>引入依赖

<!--mysql驱动包-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.26</version>
</dependency>
<!-- mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.9</version>
</dependency>
<!-- junit测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- 简化实体类编写 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>

3.MyBatis的核心配置文件

核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息。

习惯上命名为 mybatis-config.xml ,这个文件名仅仅是建议,并非强制要求。

resources 目录下新建文件 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>
    <!--配置连接数据库的环境-->
    <environments default="development">
        <environment id="development">
            <!--事务管理:JDBC-->
            <transactionManager type="JDBC"/>
            <!--数据源-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8"/>
                <property name="username" value="root"/>
                <property name="password" value="admin"/>
            </dataSource>
        </environment>
    </environments>
    <!--引入映射文件-->
    <mappers>
        <mapper resource="org/mybatis/example/BlogMapper.xml"/>
    </mappers>
</configuration>

4.数据表和对应实体类

a>tbl_user表

create table tbl_user(
    id int primary key auto_increment,
    username varchar(20),
    password varchar(20),
    age int(11),
    sex char(1),
    email varchar(20)
);

b>User实体类

package com.example.mybatis.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// getter、setter、toString、hashcode...
@Data
// 全参构造
@AllArgsConstructor
// 无参构造
@NoArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String sex;
    private String email;
}

5.创建Mapper接口

MyBatis中的Mapper接口相当于以前的dao。但是区别在于,Mapper仅仅是接口,我们不需要提供实现类。

一张表对应一个Mapper和一个Mapper映射文件。

package com.example.mybatis.mapper;

public interface UserMapper {
    /**
     * 添加用户
     */
    int insertUser();
    
}

6.创建MyBatis的映射文件

MyBatis面向接口编程的两个一致:

  1. 映射文件的namespace要和mapper接口的全类名保持一致
  2. 映射文件中SQL语句的id要和mapper接口中的方法名一致

resource 下新建目录 mappers ,Mapper映射文件就全部放在里面。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--注意这里是Mapper.java文件的位置-->
<mapper namespace="com.example.mybatis.mapper.UserMapper">
	<!--id的值要和方法名一致-->
    <insert id="insertUser">
        insert into tbl_user(username,password,age,sex,email) values('admin','123456',23,'男','12345@qq.com')
    </insert>

</mapper>

还需要修改核心配置文件中,映射文件的位置:

<!--引入映射文件-->
<mappers>
    <mapper resource="mappers/UserMapper.xml"/>
</mappers>

此时的目录结构:

image-20220419231305007

7.通过junit测试功能

SqlSession:代表Java程序和数据库之间的对话。

SqlSessionFactory:是“生成SqlSession的工厂”

工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来生产我们需要的对象。

test 下新建目录 com.example.mybatis.test,创建 MyBatisTest.java

package com.example.mybatis.test;

import com.example.mybatis.mapper.UserMapper;
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 org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

public class MyBatisTest {

    @Test
    public void testMyBatis() throws IOException {
        // 加载静态方法
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 获取SqlSessionFactoryBuilder
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 获取SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // 获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 获取mapper接口对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        int user = userMapper.insertUser();
        System.out.println(user);

        // 执行后需要提交事务。jdbc中的executeQuery等执行命令自动会commit。
        sqlSession.commit();
    }
}

image-20220419233411643

  • SqlSession:代表Java程序和数据库之间的会话。
  • SqlSessionFactory:是“生产”SqlSession的工厂。
  • 工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象!

三、优化功能

自动提交事务

由于SqlSession默认不自动提交事务,每次执行完DML语句后,需要手动提交事务。

可以设置为自动提交事务。

image-20220502000155670

SqlSession sqlSession = sqlSessionFactory.openSession(true);

log4j记录日志

在 resource 目录下创建文件 log4j.properties

### 配置根 ###
log4j.rootLogger = debug,console,file

### 配置输出到控制台 ###
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold = DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = [%c]-%m%n

### 配置输出到文件 ###
log4j.appender.file = org.apache.log4j.FileAppender
log4j.appender.file.File = logs/lu.log
log4j.appender.file.Threshold = DEBUG
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = [%p][%d{yy-MM--dd}][%c]%m%n

### 日志输出级别 ###
log4j.logger.org.mybatis=dubug
log4j.logger.java.sql=dubug
log4j.logger.java.sql.Statement=dubug
log4j.logger.java.sql.PreparedStatement=dubug
log4j.logger.java.sql.ResultSet=dubug

日志的级别?

FATAL(致命)->ERROR(警告)->INFO(信息)->DEBUG(调试)

从左到右打印的内容越来越详细。

配置完log4j.properties之后,执行程序之后能够显示mybatis的具体情况和sql语句:

image-20220502214620667

四、测试删改功能

删除

  1. 首先在mapper中写一个删除用户的接口。
public interface UserMapper {
    /**
     * 删除用户
     *
     * @return 删除成功的条数
     */
    int deleteUser();
}
  1. 在接口对应映射文件中书写该接口执行的sql语句。
<!--匹配接口(UserMapper类)-->
<mapper namespace="com.example.mapper.UserMapper">
	<!--id的值要和接口方法名相同-->
    <delete id="deleteUser">
        delete from tbl_user where id = 13;
    </delete>
</mapper>
  1. 编写测试方法
@Test
public void testDelete() throws IOException {
    // 加载核心配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 获取SqlSessionFactoryBuilder
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 获取SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
    // 获取SqlSession,并设置自动commit;
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    // 调用删除用户的接口
    int deleteUser = userMapper.deleteUser();
    System.out.println(deleteUser);

}

更新

  1. 首先在mapper中写一个更新用户信息的接口。
public interface UserMapper {
    /**
     * 修改用户信息
     *
     * @return 修改成功的条数
     */
    int updateUser();
}
  1. 在接口对应映射文件中书写该接口执行的sql语句。
<!--匹配接口(UserMapper类)-->
<mapper namespace="com.example.mapper.UserMapper">
	<!--id的值要和接口方法名相同-->
    <update id="updateUser">
        update tbl_user set username = '法外狂徒' where id = 3;
    </update>
</mapper>
  1. 编写测试方法
@Test
public void testUpdate() throws IOException {
    // 加载核心配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 获取SqlSessionFactoryBuilder
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 获取SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
    // 获取SqlSession,并设置自动commit;
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    // 调用更新用户信息的接口
    int updateUser = userMapper.updateUser();
    System.out.println(updateUser);

}

五、测试查询功能

查询单条

  1. 首先在mapper中写一个查询用户的接口。
public interface UserMapper {
    /**
     * 根据id查询用户
     *
     * @return 查询到的用户
     */
    User getUserById();
}
  1. 在接口对应映射文件中书写该接口执行的sql语句。
<!--匹配接口(UserMapper类)-->
<mapper namespace="com.example.mapper.UserMapper">
	<!--id的值要和接口方法名相同-->
    <select id="getUserById">
        select id,username,password,age,sex,email from tbl_user where id = 3;
    </select>
</mapper>
  1. 编写测试方法
@Test
public void testGetUserById() throws IOException {
    // 加载核心配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 获取SqlSessionFactoryBuilder
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 获取SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
    // 获取SqlSession,并设置自动commit;
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    // 调用查询用户的接口
    User user = userMapper.getUserById();
    System.out.println(user);

}
  1. 执行程序

程序报错了:

image-20220502220931162

A query was run and no Result Maps were found for the Mapped Statement 'com.example.mapper.UserMapper.getUserById'. It's likely that neither a Result Type nor a Result Map was specified.

它没有一个ResultType或ResultMap被定义。

Result Type:结果类型

Result Map:结果映射

Mybatis的特性:它避免了大部分JDBC的代码,以及参数的手工设置,还有结果集的处理。但它仅仅是帮助我们处理了结果集,但是它在处理结果集的时候,我们需要给它指定当前查询出来的数据所对应的实体类对象。这次报错就是因为我们没有给它设置结果类型。Mybatis不知道执行完sql之后,要把结果集转换为什么实体类对象。

所以我们需要去设置一个ResultType,告诉它结果类型。然后它就能把查询出来的结果转换为我们指定的结果类型,并返回。

因为增删改最终返回的都是成功更新的条数,所以接口可以定义为 int 或者不返回用 void

但是查询操作不一样,无论是查询一条还是查询全部,都需要获取数据,而不是简单的一个更新条数。

  1. 修改接口对应映射文件
<!--匹配接口(UserMapper类)-->
<mapper namespace="com.example.mapper.UserMapper">
    <!--
        查询功能的标签必须设置resultType或resultMap
        resultType:设置默认的映射关系(实体类属性名和数据表的字段名相同)
        resultMap:设置自定义的映射关系(实体类属性名和数据表的字段名不相同,需要手动指定)
    -->
    <select id="getUserById" resultType="com.example.pojo.User">
        select id,username,password,age,sex,email from tbl_user where id = 3;
    </select>
</mapper>
  1. 再次执行程序

image-20220502222252634

查询多条

  1. 首先在mapper中写一个查询所有用户的接口。
public interface UserMapper {
    /**
     * 获取所有用户
     *
     * @return 所有用户的列表
     */
    List<User> getAllUser();
}
  1. 在接口对应映射文件中书写该接口执行的sql语句。
<!--匹配接口(UserMapper类)-->
<mapper namespace="com.example.mapper.UserMapper">
	<!--id的值要和接口方法名相同-->
    <select id="getAllUser" resultType="com.example.pojo.User">
        select id,username,password,age,sex,email from tbl_user;
    </select>
</mapper>
  1. 编写测试方法
@Test
public void testGetAllUser() throws IOException {
    // 加载核心配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 获取SqlSessionFactoryBuilder
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 获取SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
    // 获取SqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<User> allUser = userMapper.getAllUser();
    allUser.forEach(System.out::println);

}
  1. 执行程序

image-20220502222929075

六、核心配置文件详解

environment

<?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:配置多个连接数据库的环境(一个environments中可以有多个environment)
        属性:
            default:设置默认使用的数据库环境id(由于一次只能使用一个数据库)
    -->
    <environments default="development">
        <!--
            environment:配置某个具体的数据库环境
            属性:
                id:表示数据库环境的唯一标识,不能重复
        -->
        <environment id="development">
            <!--
                transactionManager:设置事务管理方式
                属性:
                    type:"JDBC/MANAGED"
                    JDBC:表时当前环境中,执行SQL时,使用的是JDBC中原生的事务管理方式,事务的提交和回滚需要手动处理。
                    MANAGED:被管理,例如Spring
            -->
            <transactionManager type="JDBC"/>
            <!--
                dataSource:配置数据源
                属性:
                    type:设置数据源的类型
                    type:"POOLED/UNPOOLED/JNDI"
                    POOLED:表示使用数据库连接池缓存数据库连接
                    UNPOOLED:表示不使用数据库连接池。
                    JNDI:表示使用上下文中的数据源。
            -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8"/>
                <property name="username" value="root"/>
                <property name="password" value="admin"/>
            </dataSource>
        </environment>
    </environments>
    <!--引入映射文件-->
    <mappers>
        <mapper resource="mappers/UserMapper.xml"/>
    </mappers>
</configuration> 

properties

在设置连接数据库的信息时,我们在一般情况下都会把这些信息放在一个properties文件中。而不是把连接信息写死在xml文件中。

  1. 先在 resources文件夹 下新建一个文件 jdbc.properties

    image-20220506155632115

    # 为了防止和别的properties混乱,为数据库连接信息加个前缀:jdbc
    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8
    jdbc.username=root
    jdbc.password=admin
    
  2. properties 文件引入到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>
        <!--引入数据库连接信息的properties文件-->
        <properties resource="jdbc.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>
        <!--引入映射文件-->
        <mappers>
            <mapper resource="mappers/UserMapper.xml"/>
        </mappers>
    </configuration>
    
  3. 再次测试mybatis的查询,看看是否配置成功了

    image-20220506160236396

typeAliases

在mybatis,每次写一条查询语句都需要写 resultType 结果类型,值必须写实体类的全类名。

如果有非常多的查询语句或者非常长的包命,每次写查询功能的时候都需要写结果类型,会很累。

所以mybatis提供了一种类型别名的功能:typeAliases

  1. 引入<typeAliases>标签

    image-20220506161015333

    报错原因是:

    image-20220506161205300

    在Mybatis的核心配置文件中的众多标签是要按照顺序去配置的。不能随便配置。

    Mybatis核心配置文件中,标签的顺序:

    properties?,settings?,typeAliases?,

    typeHandlers?,objectFactory?,objectWrapperFactory,

    reflectorFactory?,plugins?,environments?,

    databaseIdProvider?,mappers?

    不需要的标签可以不写(当前例子中咱们不需要settings标签,所以typeAliases标签直接写到properties标签后面)

    image-20220506161613324

  2. 为实体类设置类型别名

    <!--设置类型别名-->
    <typeAliases>
        <!--这时,映射文件中的User的全类名 com.example.pojo.User 就可以使用 User 来代替了-->
        <typeAlias type="com.example.pojo.User" alias="User"/>
    </typeAliases>
    
  3. 修改映射文件中的User的全类名为User

    <select id="getAllUser" resultType="User">
        select id,username,password,age,sex,email from tbl_user;
    </select>
    
  4. 测试,成功查询到所有数据。

    image-20220506162104824

注意:

类型别名不区分大小写。也就是说 resultType的值写成小写的 user,也可以。

<select id="getAllUser" resultType="user">
 select id,username,password,age,sex,email from tbl_user;
</select>

<typeAlias>标签中的alias属性可以不写,这样类型别名会 默认是实体类名(User),也不区分大小写。

<typeAliases>
 <typeAlias type="com.example.pojo.User"/>
</typeAliases>

package

若之后有很多的实体类需要起类型别名的话,还需要写很多的<typeAlias>标签,也很累。

所以还有一个标签:<package>

<package>标签是以包为单位,将包下所有的类型设置为默认的类型别名

<!--设置类型别名-->
<typeAliases>
    <!--这时,映射文件中的User的全类名就可以使用User来代替了-->
    <!--<typeAlias type="com.example.pojo.User" alias="User"/>-->
    <!--以包为单位,将包下所有的类型设置为默认的类型别名-->
    <package name="com.example.pojo"/>
</typeAliases>

mappers

<!--引入映射文件-->
<mappers>
    <mapper resource="mappers/UserMapper.xml"/>
</mappers>

如果以后表多了,我们的mapper接口就多了,mapper接口多了以后,映射文件也会很多,映射文件多了以后,我们就要写很多的<mapper>标签。

所以mapper里也是可以以包为单位引入映射文件的。

<!--引入映射文件-->
<mappers>
    <!--<mapper resource="mappers/UserMapper.xml"/>-->
    <!--
            以包为单位引入映射文件,要求:
                1. mapper接口所在的包要求和映射文件所在的包一致
                2. mapper接口要和映射文件的名字一致
        -->
    <package name="com.example.mapper"/>
</mappers>

image-20220506164454388

resources目录下只能新建Directory,建多级包时不能用 . 分隔,要用 / 分隔。

七、在idea中设置核心配置文件的模板

由于idea中不能直接新建Mybatis的配置模板文件,需要手动新建xml文件,并粘贴内容。但是配置内容又要去寻找,特别麻烦。

所以我们需要自己创建这个配置文件的模板,方便Mybatis的开发。

核心配置文件

设置位置:File -> Settings -> File and Code Templates

image-20220506191916564

<?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文件-->
    <properties resource=""/>
    <typeAliases>
        <!--以包的方式设置类型别名(实体类所在的包)-->
        <package name=""/>
    </typeAliases>
    <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>
    <mappers>
        <!--以包的方式引入映射文件(mapper.xml所在的包)-->
        <package name=""/>
    </mappers>
</configuration>

映射文件

<?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接口-->
<mapper namespace="">

</mapper>

八、封装SqlSessionUtils工具类

每次要执行Mybatis的功能,我们都必须先获得SqlSession对象。

而我们获得SqlSession对象的过程是一个重复的过程。所以我们可以把这个过程封装起来。

首先创建一个 utils 包,存放各种静态工具类。

package com.example.utils;

public class SqlSessionUtils {
    public static SqlSession getSqlSession(){
        SqlSession sqlSession = null;
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 事务自动commit
            sqlSession = sqlSessionFactory.openSession(true);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sqlSession;
    }
}

测试工具类

public class ParameterMapperTest {
    @Test
    public void getAllUserTest(){
        SqlSession sqlSession = SqlSessionUtils.getSqlSession();
        ParameterMapper parameterMapper = sqlSession.getMapper(ParameterMapper.class);
        List<User> allUser = parameterMapper.getAllUser();
        allUser.forEach(System.out::println);
    }
}

image-20220506194054126

九、Mybatis获取参数值的两种方式(⭐⭐⭐⭐⭐)

Mybatis中获取参数值的两种方式:${}#{}

${}的本质就是字符串拼接,#{}的本质就是占位符赋值。

能用#{}就用#{},字符串拼接容易被SQL注入。

mapper接口参数为单个

测试1 使用#{}

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 用户信息
     */
    User getUserByUsername(String username);
}
  1. 编写mapper接口对应的方法
<select id="getUserByUsername" resultType="User">
    select id, username, password, age, sex, email from tbl_user where username = #{username};
</select>
  1. 测试
@Test
public void getUserByUsernameTest(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = sqlSessionMapper.getUserByUsername("法外狂徒");
    System.out.println(user);
}
  1. 结果

image-20220506195807868

测试2 使用${}

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 用户信息
     */
    User getUserByUsername(String username);
}
  1. 编写mapper接口对应的方法
<select id="getUserByUsername" resultType="User">
    select id, username, password, age, sex, email from tbl_user where username = ${username};
</select>
  1. 测试
@Test
public void getUserByUsernameTest(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = sqlSessionMapper.getUserByUsername("法外狂徒");
    System.out.println(user);
}
  1. 结果

image-20220506200207966

${}的本质的字符串拼接,需要手动添加单引号

<select id="getUserByUsername" resultType="User">
    select id, username, password, age, sex, email from tbl_user where username = '${username}';
</select>

总结:

当mapper接口方法的参数为单个的字面量类型,可以通过${}和#{}以任意的名称获取参数值,但是需要注意${}的单引号问题。

mapper接口参数为多个

Mybatis帮忙存储参数

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 判断登录
     *
     * @param username 用户名
     * @param password 密码
     * @return 用户信息
     */
    User checkUserLogin(String username, String password);
}
  1. 编写mapper接口对应的方法
<select id="checkUserLogin" resultType="User">
    select * from tbl_user where username = #{username} and password = #{password};
</select>
  1. 测试
@Test
public void checkUserLogin() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = sqlSessionMapper.checkUserLogin("法外狂徒", "123456");
    System.out.println(user);
}
  1. 报错了,甚至连sql语句都没打印出来。说明sql语句都解析失败了。

image-20220506201808943

  1. 分析原因

Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]

未找到参数“username”。可用参数为[arg1, arg0, param1, param2]

  1. 尝试解决
<select id="checkUserLogin" resultType="User">
    <!--可用参数arg0,arg1,那我们就用一下试试-->
    select * from tbl_user where username = #{arg0} and password = #{arg1};
</select>

image-20220506202204432

<select id="checkUserLogin" resultType="User">
    select * from tbl_user where username = #{param1} and password = #{param2};
</select>

image-20220506202248197

<select id="checkUserLogin" resultType="User">
    select * from tbl_user where username = #{arg0} and password = #{param2};
</select>

image-20220506202338632

由三组数据可得:

arg0:第一个参数,arg1:第二个参数;param1:第一个参数,param2:第二个参数

而且:arg0 = param1 ,arg1 = param2

当mapper接口方法的参数为多个时,Mybatis会将这些参数放在一个map集合中,以两种方式存储

a>以 arg0,arg1,... 以键,以参数为值

b>以 param1,param2,... 以键,以参数为值

手动用map传入参数

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 验证登录(用map传入参数)
     * @param map
     * @return
     */
    User checkUserLoginByMap(Map<String, String> map);
}
  1. 编写mapper接口对应的方法
<select id="checkUserLoginByMap" resultType="User">
    select * from tbl_user where username = #{username} and password = #{password}
</select>
  1. 测试
@Test
public void checkUserLoginByMap() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    Map<String, String> map = new HashMap<>();
    map.put("username", "法外狂徒");
    map.put("password", "123456");
    User user = sqlSessionMapper.checkUserLoginByMap(map);
    System.out.println(user);
}
  1. 成功

image-20220506203755223

总结:若mapper接口方法的参数有多个时,可以手动将这些参数放在一个Map集合中存储,sql语句中以键名为参数。

mapper接口参数是一个完整表单

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 新增用户
     *
     * @param user 用户信息
     * @return 更新条数
     */
    int insertUser(User user);
}
  1. 编写mapper接口对应的方法
<insert id="insertUser" >
    <!--可以直接通过user对象中的属性名来获取属性值-->
    insert into tbl_user values(null,#{username},#{password},#{age},#{sex},#{email})
</insert>
  1. 测试
@Test
public void insertUserTest() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = new User(null, "张三", "123455", 23, "男", "123@qq.com");
    int insertUser = sqlSessionMapper.insertUser(user);
    System.out.println(insertUser);
}
  1. 成功

image-20220506205307369

总结:当mapper接口方法的参数是实体类类型的参数,只需要通过#{}以属性的方式访问属性值即可。

Java中,属性通常可以理解为get和set方法。

使用@Param注解命名参数

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 验证登录
     *
     * @param username 用户名
     * @param password 密码
     * @return 用户信息
     */
    User checkUserLoginByParam(@Param("username") String username, @Param("password") String password);
}
  1. 编写mapper接口对应的方法
<select id="checkUserLoginByParam" resultType="User">
    select * from tbl_user where username = #{username} and password = #{password}
</select>
  1. 测试
@Test
public void checkUserLoginByParam() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = sqlSessionMapper.checkUserLoginByParam("法外狂徒", "123456");
    System.out.println(user);
}
  1. 成功

image-20220506210545005

总结:当使用@Param注解时,Mybatis会将@Param注解的值为键,参数为值存储。sql中可以直接只用@Param注解的值来查询。

Mybatis会将这些参数放在一个map集合中,以两种方式存储:

a>以@Param注解的值为键,以参数为值

b>以param1,param2,...为键,参数为值

获取参数值大总结

上方获取参数的5种情况,我们可以整合为2种情况(非常建议)

  1. 实体类对象的情况
  2. 其他情况,全部都用@Param注解

十、Mybatis各种查询功能

select的返回类型

上面的例子中,我们查询了用户信息,用户列表...所以我们会为其设置返回类型——User、List<User>

但是如果我们要查询表中的总记录数该怎么实现呢?

  1. 不指定返回类型
public interface ParameterMapper {
	/**
     * 获取数据总数
     * @return 记录数
     */
	Integer getCount();
}
<select id="getCount">
    select count(*) from tbl_user
</select>

image-20220506213250413

报错了:已运行查询,但未找到映射语句“com.example.mapper.ParameterMapper.getCount”的结果映射。很可能既没有指定结果类型,也没有指定结果映射。

所以,返回类型是必须要指定的。

  1. 指定返回类型Integer
<select id="getCount" resultType="Integer">
    select count(*) from tbl_user
</select>

image-20220506213538588

  1. 指定返回类型int
<select id="getCount" resultType="int">
    select count(*) from tbl_user
</select>

image-20220506213618568

mapper接口方法的类型是Integer,Mybatis查询的返回类型定义为Integer和int时都成功了。

Mybatis的resultType对应的Java类型 - KledKled - 博客园 (cnblogs.com)

查询结果存入map集合

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 根据id查询记录并存入map集合中
     *
     * @param id id
     * @return map集合
     */
    Map<String, Object> getUserByIdToMap(@Param("id") Integer id);
}
  1. 编写mapper接口对应的方法
<select id="getUserByIdToMap" resultType="map">
    select * from tbl_user where id = #{id}
</select>
  1. 测试
@Test
public void getUserByIdToMap() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    Map<String, Object> map = sqlSessionMapper.getUserByIdToMap(3);
    System.out.println(map);
}
  1. 成功

image-20220506222626239

结果存入map集合,会以字段名为键,数据为值,存入map中.

map的使用场景:

若我们需要的数据不是从一张表中查询出来的,而是多个表的数据混合而成的,而这时,我们并没有与之相对应的实体类对象,那怎么办呢?

我们就可以使用一个Map集合存起来。

十一、特殊sql的执行

mybatis中有些情况的sql语句必须用${}而不能用#{}

但是#{}方式能够很大程度防止sql注入,${}方式无法防止Sql注入:

如果我们order by语句后用了${},那么不做任何处理的时候是存在SQL注入危险的。你说怎么防止,那我只能悲惨的告诉你,你得手动处理过滤一下输入的内容。如判断一下输入的参数的长度是否正常(注入语句一般很长),更精确的过滤则可以查询一下输入的参数是否在预期的参数集合中。

参考文章:https://www.cnblogs.com/mmzs/p/8398405.html

处理模糊查询

错误写法

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 模糊查询用户名
     *
     * @param keywords 关键词
     * @return 用户信息
     */
    User getUserByLike(@Param("keywords") String keywords);
}
  1. 编写mapper接口对应的方法
<select id="getUserByLike" resultType="User">
    select *
    from tbl_user
    where username like '%#{keywords}%'
</select>
  1. 测试
@Test
public void getUserByLike() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = sqlSessionMapper.getUserByLike("张");
    System.out.println(user);
}
  1. 失败

image-20220506225450222

正确写法

<!--=========================第一种写法(不建议)==========================-->
<select id="getUserByLike" resultType="User">
    select *
    from tbl_user
    where username like '%${keywords}%'
</select>

<!--=========================第二种写法(建议)==========================-->
<select id="getUserByLike" resultType="User">
    select *
    from tbl_user
    <!--字符串拼接-->
    where username like concat('%',#{keywords},'%')
</select>

建议使用第二种方法。第一种害怕SQL注入。

处理批量删除

错误写法

一个场景,比如在邮箱中,我们想批量删除邮件,就要把每一个要删除的邮件前面的复选框选中,然后点击删除。要想实现这个批量删除> 的话,sql语句应该怎么写?

delete from tbl_email where id=1 or id=2 or id=3 or ...
-- =============================================================
delete from tbl_email where id in (1,2,3,...)

Q:这一般都是数组或者List<Integer>来存储id嘛,然后foreach删除,谁会用这种方式String呀

A:如果用List<Integer>的话,每次循环执行一次sql语句,若list非常大的话,那效率也太差了。

所以在Mybatis中要实现批量删除的话,肯定是使用 in 关键字来放要删除的id。

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 批量删除用户
     *
     * @param ids in里面要存放的用户id
     * @return 删除条数
     */
    Integer deleteUserMore(@Param("ids") String ids);
}
  1. 编写mapper接口对应的方法
<delete id="deleteUserMore">
    delete from tbl_user where id in (#{ids})
</delete>
  1. 测试
@Test
public void deleteUserMore() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    List<Integer> list = Arrays.asList(12,15,16);
    StringJoiner stringJoiner = new StringJoiner(",");
    for (Integer integer : list) {
        stringJoiner.add(Integer.toString(integer));
    }
    Integer delete = sqlSessionMapper.deleteUserMore(stringJoiner.toString());
    System.out.println(delete);
}
  1. 失败

image-20220507102725304

为什么失败?因为我们使用了 #{},它会自动帮我们的参数两边加上单引号,所以其实最终执行的sql是:

delete from tbl_user where id in ('12,15,16')

这就导致了 '12,15,16' 成为了一个值。

正确写法

<delete id="deleteUserMore">
    delete from tbl_user where id in (${ids})
</delete>

image-20220507103158121

当我们要使用 where id in 这种方式来进行批量删除时,只能使用 ${} 的方式,不能使用 #{} 的方式。

动态设置表名

比如MySQL中,一张表中有很多很多的数据,如果数据太多的话,它是不利于MySQL的性能的。为了提高性能,我们可以讲这一张表水平切分,将它拆分为多张表共同存储这一张表中的数据。所以这时,我们在查询数据或者操作数据时,我们操作的表可能是多张。

既然操作的表不一样,那我们操作的表名就需要动态设置

错误写法

<select id="getAllUserByMoreTable" resultType="User">
    select * from #{tablename}
</select>

由于 #{} 会自动在两边加上单引号,所以会导致最终sql语句变为:

select * from 'tablename';

这样就错误了

正确写法

所以这种情况也只能使用 ${}

<select id="getAllUserByMoreTable" resultType="User">
    select * from ${tablename}
</select>

再提一次,使用${}的时候要手动防止sql注入,过滤传入参数的内容。

添加时获取自增的主键

有时,由于数据表中设置了主键自增策略或者框架中设置了自动生成主键的策略,所以我们在我们要插入一条数据时,可以不指定主键,让它自动生成。

但是,在有的新增数据功能中,我们在插入数据之后还需要获取自动生成的主键。

如果再使用一条查询语句去获取的话,就太影响效率了,所以我们需要在插入数据成功之后并得到这个主键。

  1. 创建一个mapper接口
public interface ParameterMapper {
    /**
     * 插入数据并获取自动生成的主键
     *
     * @param user 用户信息
     * @return 更新条数
     */
    Integer insertUserGetId(User user);
}
  1. 编写mapper接口对应的方法
<!--
	useGeneratedKeys:当前标签中使用了自动生成的主键
	keyProperty:将自动生成的主键的值赋值给传输到映射文件中参数的某个属性。
-->
<insert id="insertUserGetId" useGeneratedKeys="true" keyProperty="id">
    insert into tbl_user
    values (null, #{username}, #{password}, #{age}, #{sex}, #{email});
</insert>
<!--===========================================================================-->
<!--
	useGeneratedKeys:当前标签中使用了自动生成的主键
	keyProperty:将自动生成的主键的值赋值给传输到映射文件中参数的某个属性。
-->
<insert id="insertUserGetId" useGeneratedKeys="true" keyProperty="id">
    insert into tbl_user
    values (123456, #{username}, #{password}, #{age}, #{sex}, #{email});
</insert>
  1. 测试
@Test
public void insertUserGetId() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    ParameterMapper sqlSessionMapper = sqlSession.getMapper(ParameterMapper.class);
    User user = new User(null,"王五","123456",22,"男","123@qq.com");
    Integer insert = sqlSessionMapper.insertUserGetId(user);
    // 直接查询上面传入的user。原来是null,没有值,要看执行完sql语句后,有没有值。
    System.out.println(user);
}
  1. 成功

image-20220507110238015

image-20220507110059787

useGeneratedKeys:当前标签中使用了自动生成的主键
keyProperty:将自动生成的主键的值赋值给传输到映射文件中参数的某个属性。

十二、自定义映射resultMap

引入问题

  1. 新建一个实体类 User2,但是要做一些小操作。把 username --> user ,password --> passwd
package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author: 王忠舟
 * @Date: 2022/5/1
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User2 {
    private Integer id;
    // username字段对应user
    private String user;
    // password字段对应passwd
    private String passwd;
    private Integer age;
    private String sex;
    private String email;
}
  1. 重新搭建一个 User2Mapper接口User2Mapper.xmlUser2MapperTest.java
<select id="getAllUser2" resultType="User2">
    select * from tbl_user
</select>

3.测试查询所有用户信息

image-20220507112317855

为什么?

image-20220507112345444

需要实体类中的变量名和数据表中的字段名一样,Mybatis才能自动匹配。否则匹配不上。

那要怎么解决呢?

方法1:为字段起别名

为查询的字段添加别名,别名和实体类中的属性名相同。

<select id="getAllUser2" resultType="User2">
    select id,username user,password passwd,age,sex,email from tbl_user
</select>

image-20220507112812493

方法2:全局配置

设置全局配置,能够将下划线 _ 自动映射为驼峰(该例子不符合,自行实验)

即字段 user_name ,能够映射到属性 userName

  1. 在核心配置文件中配置
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

方法3:resultMap(常用)

通过resultMap设置自定义的映射关系 (用得最多)

<?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接口-->
<mapper namespace="com.example.mapper.User2Mapper">
    <!--
        id:用来匹配resultMap
        type:用来处理哪个类型的映射关系
        用了resultMap,要把所有字段的映射关系都设置出来
    -->
    <resultMap id="User2ResultMap" type="User2">
        <!--id:用来表示主键的映射关系-->
        <!--property:实体类属性名,column:sql语句查询出的字段名-->
        <id property="id" column="id"/>
        <!--result:用来表示普通字段的映射关系-->
        <result property="user" column="username"/>
        <result property="passwd" column="password"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <result property="email" column="email"/>
    </resultMap>
	<!--resultMap:用id匹配resultMap-->
    <select id="getAllUser2ResultMap" resultMap="User2ResultMap">
        select *
        from tbl_user
    </select>
</mapper>

image-20220507114310275

注意:

用了resultMap,一定要把所有字段的映射关系都设置出来

这种方式使用最多

多对一映射问题

问题描述:

查询员工信息和对应的部门信息

通过级联赋值

比如有的实体类中的某个变量类型是另一个实体类。

@Data
public class Emp {
    private Integer empno;
    private String ename;
    private String job;
    private Integer mgr;
    private Date hiredate;
    private Double sal;
    private Double comm;
    private Integer deptno;
    // Dept对象
    private Dept dept;
}

这样是话,我们要怎么查询呢?

  1. 设置一个 resultMap
<resultMap id="getEmpAndDept" type="Emp">
    <id property="empno" column="empno"/>
    <result property="ename" column="ename"/>
    <result property="job" column="job"/>
    <result property="mgr" column="mgr"/>
    <result property="hiredate" column="hiredate"/>
    <result property="sal" column="sal"/>
    <result property="comm" column="comm"/>
    <!--使用级联赋值,将deptno,dname,loc赋值给dept对象下的三个属性-->
    <result property="dept.deptno" column="deptno"/>
    <result property="dept.dname" column="dname"/>
    <result property="dept.loc" column="loc"/>
</resultMap>
  1. 使用该resultMap来接收最终返回体。
<select id="getEmpAndDept" resultMap="getEmpAndDept">
    select *
    from emp
    left join dept d
    on emp.deptno = d.deptno
    where emp.empno = #{eid}
</select>

image-20220507140855379

image-20220507141204104

通过association标签

association:处理多对一的映射关系

property:需要处理多对的映射关系的属性名

javaType:该属性的类型

  1. 设置 resultMap 中的标签
<resultMap id="getEmpAndDept" type="Emp">
    <id property="empno" column="empno"/>
    <result property="ename" column="ename"/>
    <result property="job" column="job"/>
    <result property="mgr" column="mgr"/>
    <result property="hiredate" column="hiredate"/>
    <result property="sal" column="sal"/>
    <result property="comm" column="comm"/>
    <result property="deptno" column="deptno"/>
    <!--
		property:属性名
		javaType:该属性对应的Java类型
	-->
    <association property="dept" javaType="Dept">
        <id property="deptno" column="deptno"/>
        <result property="dname" column="dname"/>
        <result property="loc" column="loc"/>
    </association>
</resultMap>
  1. 测试

image-20220507141621059

分步查询(常用)

  1. 先写resultMap
<resultMap id="getEmpAndDept" type="Emp">
    <!--上面都是基本写法-->
    <id property="empno" column="empno"/>
    <result property="ename" column="ename"/>
    <result property="job" column="job"/>
    <result property="mgr" column="mgr"/>
    <result property="hiredate" column="hiredate"/>
    <result property="sal" column="sal"/>
    <result property="comm" column="comm"/>
    <result property="deptno" column="deptno"/>
    <!--
    	依然是使用association标签括起来
		property:属性名
		select:分步查询所要使用的sql的唯一标识(namespace.SQLid或mapper接口的全类名.方法名)
		column:设置分步查询的条件(所需要的第一步查询到的值的字段名)
    -->
    <association property="dept" select="com.example.mapper.DeptMapper.getEmpAndDeptByStepTwo" column="deptno">
    </association>
</resultMap>
  1. 编写sql语句
<!--返回体依然使用resultMap,而且是刚才编写的resultMap-->
<select id="getDeptByEno" resultMap="getEmpAndDept">
    select *
    from emp
    where empno = #{empno}
</select>
  1. 写另一个mapper接口 DeptMapper
public interface DeptMapper {
    /**
     * 通过分步查询查询员工信息和对应的部门信息
     * 分步查询第二步:通过deptno查询员工所对应的信息
     */
    Dept getEmpAndDeptByStepTwo(@Param("deptno") Integer deptno);
}
  1. 再写 DeptMapper.xml
<mapper namespace="com.example.mapper.DeptMapper">
    <select id="getEmpAndDeptByStepTwo" resultType="Dept">
        select *
        from dept where deptno = #{deptno};
    </select>
</mapper>
  1. 测试
@Test
public void getDeptByEno(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
    Emp empAndDept = empMapper.getDeptByEno(7369);
    System.out.println(empAndDept);
}

说明一下这个是如何执行的?

  1. 首先是执行了 EmpMapper 接口中的 getDeptByEno(7369) 方法
  2. 根据方法,Mybatis执行了 select * from emp where empno = 7369
  3. 查询出来的结果放到了 resultMap 中,基本值都放了进去,随即处理 association标签 中的数据问题
  4. 判断出了是 分步查询 ,所以 select 属性,去执行了 DeptMapper 中的 getEmpAndDeptByStepTwo() 方法,参数是第一步中 column属性 字段所对应的值。
  5. 然后将 getEmpAndDeptByStepTwo() 方法的结果赋值给了 association" 里的 dept 属性
  6. 最终组合完成了 resultMap

延迟加载

明明一句sql就能完成的事,为什么要用分步查询,用两步、两句sql来实现呢?

只写为一句sql,那它可以完成一个完整的功能。若用分步查询,写成两句sql,那这两句sql各自就是一个功能。

分步查询就可以实现一个 “延迟加载” 的功能,什么是 “延迟加载”?

如上面的分步查询例子:当我们直接执行 getEmpAndDeptByStepTwo() 这个方法,他会根据部门号返回部门信息。但是如果我们执行 getDeptByEno() 方法的话,他先根据员工号查询员工,然后分步查询,再根据员工号去查询对应的部门信息。

现在有一个问题,如果我们只想利用 getDeptByEno() 这个方法获取员工姓名,可以,但是第二步就完全没必要了。

而 “延迟加载” 就是 按需执行。调用分步查询方法,如果你只想获取员工姓名,那么它只会执行第一步;如果你想要获取该员工对应的部门信息,它就会执行两步,获取部门信息。

  1. 延迟加载在 Mybatis 中默认是不开启的。所以需要我们手动开启。在核心配置文件中设置。
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <!--全局设置延迟加载,又叫懒加载,设置为true,即可开启-->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

全局设置延迟加载,会使所有的分步查询都是延迟加载。

若只想设置某个分步查询实现延迟加载,可以:

image-20220507153120291

  1. 比如我们只想要获取用户姓名,那肯定是使用通过员工号获取员工信息的方法。
<select id="getDeptByEno" resultMap="getEmpAndDept">
    select *
    from emp
    where empno = #{empno}
</select>
@Test
public void getDeptByEno(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
    Emp empAndDept = empMapper.getDeptByEno(7369);
    System.out.println(empAndDept.getEname());
}
  1. 执行,可以看到Mybatis只执行了第一步,就获取到了用户姓名,就没有再执行第二步了。

image-20220507151454723

对比未开启 “延迟加载”

<settings>
 <setting name="mapUnderscoreToCamelCase" value="true"/>
 <!--关闭延迟加载-->
 <setting name="lazyLoadingEnabled" value="false"/>
</settings>

image-20220507151639700

  1. 执行获取“员工姓名”和“部门信息”
@Test
public void getDeptByEno(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
    Emp empAndDept = empMapper.getDeptByEno(7369);
    System.out.println(empAndDept.getEname());
    System.out.println("=====================================");
    System.out.println(empAndDept.getDept().getDname());
}

未开启延迟加载

image-20220507152140125

未开启延迟加载,结果是 先执行完所有是sql语句,然后依次从结果中获取“员工姓名”和“部门信息”

开启延迟加载

image-20220507152430600

开启延迟加载,结果是 获取员工信息,则执行获取员工的sql;获取部门信息,则执行获取部门信息的sql。

延迟加载,就是把分步查询,依次执行得到结果。

image-20220507152622988

一对多映射问题

使用collection标签

@Data
public class Dept {
    private Integer deptno;
    private String dname;
    private String loc;
    // 一个部门对应多个员工
    private List<Emp> empList;
}
  1. 编写接口
public interface DeptMapper {
    /**
     * 获取部门及部门中所有员工信息
     *
     * @param deptno 部门号
     * @return 部门信息
     */
    Dept getAllEmp(@Param("deptno") Integer deptno);
}
  1. 编写sql语句,使用resultMap
<select id="getAllEmp" resultMap="deptAndEmp">
    select *
    from dept
             left join emp e on dept.deptno = e.deptno
    where e.deptno = #{deptno}
</select>
  1. 编写resultMap
<resultMap id="deptAndEmp" type="Dept">
    <id property="deptno" column="deptno"/>
    <result property="dname" column="dname"/>
    <result property="loc" column="loc"/>
    <!--collection是专门用来处理一对多关系的,定义List泛型中的属性名-->
    <collection property="empList" ofType="Emp">
        <id property="empno" column="empno"/>
        <id property="ename" column="ename"/>
        <id property="job" column="job"/>
        <id property="mgr" column="mgr"/>
        <id property="hiredate" column="hiredate"/>
        <id property="sal" column="sal"/>
        <id property="comm" column="comm"/>
        <id property="deptno" column="deptno"/>
    </collection>
</resultMap>

collection标签就是专门用来处理一对多的关系的,换句话说collection就是处理集合用的。

  1. 测试
@Test
public void getDeptAndEmpTest(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
    Dept dept = deptMapper.getAllEmp(20);
    System.out.println(dept);
}

image-20220507155616146

分步查询

步骤和多对一时的分步查询基本一致。

  1. 先写resultMap
    <resultMap id="deptAndEmpByStepResultMap" type="Dept">
        <id property="deptno" column="deptno"/>
        <result property="dname" column="dname"/>
        <result property="loc" column="loc"/>
        <collection property="empList" select="com.example.mapper.EmpMapper.getAllEmpByStepTwo" column="deptno">

        </collection>
    </resultMap>
  1. 编写sql语句
<!--返回体依然使用resultMap,而且是刚才编写的resultMap-->
<select id="getDept" resultMap="deptAndEmpByStepResultMap">
    select *
    from dept
    where deptno = #{deptno}
</select>
  1. 写另一个mapper接口 EmpMapper
public interface EmpMapper {
    /**
     * 根据部门号获取属于该部门的员工
     */
    List<Emp> getAllEmpByStepTwo(@Param("deptno") Integer deptno);
}
  1. 再写 Emp.xml
<select id="getAllEmpByStepTwo" resultType="Emp">
    select *
    from emp
    where deptno = #{deptno}
</select>
  1. 测试
@Test
public void getDeptAndEmpByStep(){
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
    Dept dept = deptMapper.getDept(20);
    System.out.println(dept);
}

image-20220507160902073

十三、动态SQL

Mybatis框架的动态SQL技术是一种根据特定条件动态拼装SQL语句的功能,它存在的意义是为了解决拼接SQL语句字符串时的痛点问题。

应用场景:多条件查询

比如在购物网站中,要筛选出你想要的物品,可能会选择多个条件,但也有可能只选择了一个,因为各种条件是可选可不选的,所以前端会把所有条件的选择情况都发送到后端,没选的就是null。但是没有选择的条件是绝对不能出现在我们的sql语句之中的。

所以我们在多条件查询的时候需要对这些条件进行筛选,条件值为null的就踢出sql语句。

if

可以使用<if test="">标签来写条件,只有当test里面的条件满足时,才会将 if 标签内的sql语句拼接上去。

test里面的条件中的 ,直接用 and 表示

<select id="getEmpByCondition" resultType="Emp">
    select * from emp where
    <if test="ename != null and ename != ''">
        ename = #{ename}
    </if>
    <if test="deptno != null and deptno != ''">
         and deptno = #{deptno}
    </if>
</select>
@Test
public void getEmpByCondition() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DynamicSQLMapper dynamicSQLMapper = sqlSession.getMapper(DynamicSQLMapper.class);
    Emp emp = new Emp(null, "SMITH", null, null, null, null, null, 20, null);
    List<Emp> empList = dynamicSQLMapper.getEmpByCondition(emp);
    empList.forEach(System.out::println);
}

image-20220507201712031

根据标签中test属性所对应的表达式决定标签中的内容是否需要拼接到SQL中

where

上面的例子中有一个明显的问题,如果后面的if都不满足的话,最终的sql语句就是:

select * from emp where

显然,这是错的!!

所以,我们需要让 where 也动态生成。

还有一个问题,如果第一个if不满足,而第二个if满足,那最终的sql语句就是:

select * from emp where and deptno = #{deptno}

显然,这也是错误的!

Mybatis还有一个where标签,用来动态生成where的

<select id="getEmpByCondition" resultType="Emp">
    select * from emp
    <where>
        <if test="ename != null and ename != ''">
            ename = #{ename}
        </if>
        <if test="deptno != null and deptno != ''">
            and deptno = #{deptno}
        </if>
    </where>
</select>
@Test
public void getEmpByCondition() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DynamicSQLMapper dynamicSQLMapper = sqlSession.getMapper(DynamicSQLMapper.class);
    Emp emp = new Emp(null, null, null, null, null, null, null, 20, null);
    List<Emp> empList = dynamicSQLMapper.getEmpByCondition(emp);
    empList.forEach(System.out::println);
}

image-20220507203354038

这次我们是让第一个条件不满足,第二个条件满足。不仅where自动生成了,而且还把上面第二个错误情况中多余的and也自动删掉了。

可以推理一下,既然内容前and都能自动去掉,那如果有多余的or肯定也能自动去掉。

注意:内容后多余的and或or不会被去掉

尝试一个新的情况:把所有条件全部删掉,如果是原来的方式,会出现上述错误中的多余where:

@Test
public void getEmpByCondition() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DynamicSQLMapper dynamicSQLMapper = sqlSession.getMapper(DynamicSQLMapper.class);
    Emp emp = new Emp(null, null, null, null, null, null, null, null, null);
    List<Emp> empList = dynamicSQLMapper.getEmpByCondition(emp);
    empList.forEach(System.out::println);
}

image-20220507203801446

若没有成立的条件,where也不会在自动生成了!!

当where标签中有内容时,会自动生成where关键字,并且将内容前多余的and或or去掉。当where标签中没有内容时,此时where关键字没有效果。(不会自动生成where关键字)。注意:内容后多余的and或or不会被去掉

trim

image-20220507204501595

四个属性说明:

  • prefix:将trim标签中内容前面添加指定内容。
  • suffix:将trim标签中内容后面添加指定内容。
  • prefixOverrides:将trim标签中内容前面去掉指定内容。
  • suffixOverrides:将trim标签中内容后面去掉指定内容。
<select id="getEmpByCondition" resultType="Emp">
    select * from emp
    <trim prefix="where" suffixOverrides="or|and">
        <if test="ename != null and ename != ''">
            ename = #{ename} and
        </if>
        <if test="deptno != null and deptno != ''">
            deptno = #{deptno}
        </if>
    </trim>
</select>
@Test
public void getEmpByCondition() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DynamicSQLMapper dynamicSQLMapper = sqlSession.getMapper(DynamicSQLMapper.class);
    Emp emp = new Emp(null, "SMITH", null, null, null, null, null, null, null);
    List<Emp> empList = dynamicSQLMapper.getEmpByCondition(emp);
    empList.forEach(System.out::println);
}

image-20220507205301870

当使用 trim 标签后,并设置去掉后面的and或or,那么会自动将最终sql语句中的 and 去掉。

尝试一个新的情况:把所有条件全部删掉,看看会不会自动去掉where,因为我们设置了前缀where

@Test
public void getEmpByCondition() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DynamicSQLMapper dynamicSQLMapper = sqlSession.getMapper(DynamicSQLMapper.class);
    Emp emp = new Emp(null, null, null, null, null, null, null, null, null);
    List<Emp> empList = dynamicSQLMapper.getEmpByCondition(emp);
    empList.forEach(System.out::println);
}

image-20220507205642059

若trim中没有条件,也不会自动生成where。

若标签中有内容时,会按照配置向内容前后添加内容或去掉内容。当标签中没有内容时,trim标签也不会有任何效果。

choose、when、otherwise

choose、when、otherwise

when相当于if ... ,else if ...

otherwise相当于else

choose表示一个完整的if、else if、else结构

<select id="getEmpByCondition" resultType="Emp">
    select * from emp
    <where>
        <choose>
            <!--因为是if、else if、else,所以肯定只有一个能满足,故不需要写and-->
            <when test="ename != null and ename != ''">
                ename = #{ename}
            </when>
            <when test="deptno != null and deptno != ''">
                deptno = #{deptno}
            </when>
            <otherwise>
                empno=1
            </otherwise>
        </choose>
    </where>
</select>

foreach

处理数组

问题:删除数组中的所有empno所对应的数据

<!--@Param("empnos") Integer[] empnos-->
<delete id="deleteMoreByArray">
    delete
    from emp
    where empno id in
    <!--
            collection:要循环的集合
            item:每一项
            separator:每一项之间的分隔符
            open:开头符号
            close:结尾符号
        -->
    <foreach collection="empnos" item="empno" separator="," open="(" close=")">
        #{empno}
    </foreach>
</delete>

要循环的集合是通过接口传递进来的:@Param("empnos") Integer[] empnos

每一项之间需要分隔符是因为在in关键字里面,每一项之间都需要用“,”分隔,如果手动把“,”写在#{}旁边的话,会导致最前面或最后面多了一个“,”。注意:分隔符前面和后面默认添加一个空格。

开头符号和结尾符号是因为in关键字中的值,需要用()括起来。

问题:删除数组中的所有empno所对应的数据(使用or来分隔)

<!--@Param("empnos") Integer[] empnos-->
<delete id="deleteMoreByArray">
    delete
    from emp
    where 
    <foreach collection="empnos" item="empno" separator="or">
        empno = #{empno}
    </foreach>
</delete>

处理集合

问题:将集合内每一个对象插入到数据表中

<!--@Param("empList") List<Emp> empList-->
<insert id="insertMoreByList">
    insert into emp values
    <foreach collection="empList" item="emp" separator=",">
        (null,#{emp.ename},#{emp.job},#{emp.mgr},#{emp.hiredate},#{emp.sal},#{emp.comm},#{emp.deptno})
    </foreach>
</insert>

sql

在以后真实开发中,我们在写各种查询语句时,不能使用 * ,都要写具体的字段。但是有很多查询全部字段的sql,我们每次都要完整地写出来,就会很麻烦。例如:

select empno, ename, job, mgr, hiredate, sal, comm, deptno from emp;

每条查询所有字段的sql中都要写一遍,不得累死!!所以:我们需要把经常用到的字段用一个片段记录下来

例题:

<sql id="empAllColumns">deptno,dname,loc</sql>

<select id="demo" resultType="Dept">
    select <include refid="empAllColumns"/> from dept;
</select>

image-20220507215034386

Mybatis自动将 <include refid="empAllColumns"/> 转换为 deptno,dname,loc

设置SQL片段:<sql id="empAllColumns">deptno,dname,loc</sql>

引用SQL片段:<include refid="empAllColumns"/>

如果以后开发一个mapper.xml,里面有上百条查询语句,字段又多到爆炸,这个方法就很好!!!

十四、Mybatis的缓存

Mybatis中的缓存会将我们查询的数据进行缓存,然后这个时候当我们再次去查询的时候,如果缓存中有,就从缓存中拿;如果缓存中没有,再到数据库中去取。注:只对查询功能有效

一级缓存

一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问。

@Test
public void getDeptAndEmpTest() {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession();
    DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
    Dept dept = deptMapper.getAllEmp(20);
    System.out.println(dept);
    Dept dept2 = deptMapper.getAllEmp(20);
    System.out.println(dept2);
}

image-20220507220852479

只要是通过同一个SqlSession查询出来的数据是会被缓存的。(两个不同mapper,但是同一个SqlSession获取的,也是可以缓存的)

在Mybatis中,一级缓存是自动开启的。

使一级缓存失效的四种情况:

  • SqlSession不是同一个
  • 同一个SqlSession但是查询条件不同
  • 同一个SqlSession两次查询期间执行了任何一次增删改操作
  • 同一个SqlSession两次查询期间手动清空了缓存(SqlSession.clearCache())

二级缓存

二级缓存是SqlSessionFactory级别的,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存,此后若再次执行相同的查询语句,结果就会从缓存中获取。

二级缓存开启的条件:

  1. 在核心配置文件中,设置全局配置属性 cacheEnabled="true"(默认为true,不需要设置)
  2. 在映射文件中设置标签<cache/>
  3. 二级缓存必须在SqlSession关闭或提交之后有效
  4. 查询的数据所转换的实体类类型必须实现序列化的接口

使二级缓存失效的情况:

  • 两次查询之间执行的了任意的增删改,会使一级和二级缓存同时失效。

十五、Mybatis的逆向工程

  • 正向工程:先创建Java实体类,由框架负责根据实体类生成数据表
  • 逆向工程:先创建数据库表,由框架负责根据数据库表(代码生成器),反向生成如下资源:
    • Java实体类
    • Mapper接口
    • Mapper映射文件

可参考文章:https://blog.csdn.net/qq_52797170/article/details/123969559

十六、Mybatis的分页插件

在实际的项目开发中,常常需要使用到分页,分页方式分为两种:前端分页和后端分页。

前端分页

一次ajax请求数据的所有记录,然后在前端缓存并且计算count和分页逻辑,一般前端组件(例如dataTable)会提供分页动作。
特点是:简单,很适合小规模的web平台;当数据量大的时候会产生性能问题,在查询和网络传输的时间会很长。

后端分页

在ajax请求中指定页码pageNum和每页的大小pageSize,后端查询出当页的数据返回,前端只负责渲染。
特点是:复杂一些;性能瓶颈在MySQL的查询性能,这个当然可以调优解决。一般来说,开发使用的是这种方式。

可参考文章:https://blog.csdn.net/vcj1009784814/article/details/106391982

参考资源:

【尚硅谷】2022版MyBatis教程

posted @ 2022-05-07 23:41  KledKled  阅读(12)  评论(0编辑  收藏  举报