MyBatis 学习

MyBatis 基础

MyBatis 出现的原因

JDBC 进行开发存在的问题

  • 数据库配置信息存在硬编码问题(如果要更换数据库等操作,需要修改源代码,需要重新打包部署项目),数据库连接创建、释放频繁造成系统资源浪费(数据库连接、释放等代码被放在持久层,而持久层代码会被多次调用),从而影响系统性能。
  • SQL 语句在代码中硬编码,造成代码不易维护,实际应用 SQL 变化的可能较大,SQL 变动需要改变 Java 代码。
  • 查询操作时,需要手动将结果集中的数据手动封装到实体中。

应对问题的解决方法

  • 使用数据库连接池初始化连接资源。
  • 将 SQL 语句抽取到 xml 配置文件中。
  • 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射。

1. MyBatis 简介

MyBatis 是一个优秀的基于 ORM 的半自动(需要手动编写 SQL)轻量级(程序在启动期间消耗资源较少)持久层框架,是对 JDBC 的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建 connection、创建 statement、手动设置参数、结果集检索等 jdbc 繁杂的过程代码。

半自动与全自动:区别在于是否需要手动编写 SQL。

半自动:MyBatis

全自动:Hibernate

2. ORM 思想

ORM(Object Relational Mapping)对象关系映射

  • O - 对象模型:实体对象,即在程序中根据数据库表结构建立的一个个实体 JavaBean。
  • R - 关系型数据库的数据结构:关系数据库领域的 Relational(建立的数据库表)
  • M - 映射:从 R(数据库)到 O(对象模型)的映射,可通过 XML 文件映射。

实现

  1. 让实体类和数据库表进行一一对应关系(一个实体类对应一张数据库表,一个对象对应数据库表中的一条记录)。先让实体类和数据库表对应,再让实体类属性和表里面字段对应。
  2. 建立实体和数据库表之间的映射关系。Mybatis 的作用,借助 XML 或 注解 完成映射关系的确立。
  3. 不需要直接操作数据库表,直接操作表对应的实体类关系。

ORM 是一种思想,帮助我们跟踪实体的变化,并将实体的变化翻译成 SQL 脚本,执行到数据库中去。也就是将实体的变化映射到数据库表的变化。Mybatis 采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC api 底层访问的细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。

3. Mybatis 基本步骤

  1. 创建数据库及 user 表。

    -- create
    CREATE DATABASE `mybatis_db`; 
    USE `mybatis_db`; 
    CREATE TABLE `user` ( 
    	`id` INT(11) N`jdbc_user`OT NULL AUTO_INCREMENT,
    	`username` VARCHAR(32) NOT NULL COMMENT '用户名称',
    	`birthday` DATETIME DEFAULT NULL COMMENT '生日',
    	`sex` CHAR(1) DEFAULT NULL COMMENT '性别',
    	`address` VARCHAR(256) DEFAULT NULL COMMENT '地址',
    	PRIMARY KEY (`id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8; 
    
    -- insert
    INSERT INTO `user`(`id`,`username`,`birthday`,`sex`,`address`) VALUES 
    		(1,'牛牛','1996-12-21 00:00:00','男','山西忻州'), 
     		(2,'个个','2001-03-15 00:00:00','女','陕西西安');
    
  2. 创建 maven 工程,在 pom.xml 导入相关依赖的坐标(MyBatis 坐标、MySQL 驱动坐标、junit 测试单元坐标)。

    <!-- 指定编码和版本 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.complier.encoding>UTF-8</maven.complier.encoding>
        <java.version>11</java.version>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    
    <!-- 导入相关依赖 -->
    <dependencies>
        <!-- 导入mybatis依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.8</version>
        </dependency>
    
        <!-- mysql驱动坐标 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
            <scope>runtime</scope>
        </dependency>
    
        <!-- 单元测试坐标 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  3. 编写 User 实体类。

    public class User {
        private int id;
        private String username;
        private Date birthday;
        private String sex;
        private String address;
    	
        // getter、setter和toString
    }
    
  4. 编写 UserMapper.xml 映射配置文件(ORM 思想)。

    <?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="user">
    	<!-- namespace - 命名空间,与id属性共同构成唯一标识,则定位时就使用user.findAll -->
    	<!-- resultType - 将查询到的结果与此字段对应的类进行关联,自动完成映射封装,要求是类的全路径。 -->
        <select id="findAll" resultType="com.chrisniu.domain.User">
            <!-- 要求没有封号 ; -->
            select * from user
        </select>
    </mapper>
    
  5. 编写 sqlMapConfig.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配置的是运行环境 -->
        <environments default="develop">
            <environment id="develop">
                <!-- 当前的事务管理器是JDBC -->
                <transactionManager type="JDBC"/>
                <!-- 数据源信息,即数据库配置信息 -->
    		   <!-- POOLED:使用mybatis的连接池;UNPOOLED:不使用连接池,每一次都是新的连接 -->
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql:///mybatis_db"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </environment>
        </environments>
    
        <!-- 引入自定义类和SQL查询到结果的映射配置文件,即将4编写的映射进行配置加载 -->
        <mappers>
            <mapper resource="mapper/UserMapper.xml"/>
        </mappers>
    </configuration>
    

    进行映射配置时,类似 <mapper resource="mapper/UserMapper.xml"></mapper> 标签中不存值时,可以简写为 <mapper resource="mapper/UserMapper.xml"/>

  6. 编写测试代码。

    public class MybatisTest {
        @Test
        public void mybatisQuickStartTest() throws IOException {
            // 1.加载核心配置文件
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            // 2.获取sqlSessionFactory工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            // 3.获取SqlSession对象
            SqlSession sqlSession = sqlSessionFactory.openSession();
    
            // 4.执行sql,参数就是statementId,即配置sql语句时的userspace.id
            List<User> users = sqlSession.selectList("user.findAll");
            // 5.打印查找到的结果
            for (User user : users) {
                System.out.println(user);
            }
    
            // 6.关闭连接
            sqlSession.close();
        }
    }
    

4. MyBatis 映射文件

使用 XxxMapper.xml 文件完成 MySQL 语句的映射,进而使用映射后的方式进行 CRUD。

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 映射配置文件的DTD约束头 -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- 根标签,根标签中的namespace与语句中的id共同构成数据库操作的唯一标识,即namespace.id -->
<mapper namespace="user">
	<!-- 映射的SQL操作 -->
</mapper>

Mybatis 查询

<mapper namespace="..."></mapper> 是根标签,根标签中的 namespace 属性与操作标签中的 id 共同构成数据库操作的唯一标识。查询语句使用 <select></select> 标签,在映射文件中使用 resultType 属性指定从数据库中查询到的数据绑定的数据类型(完全限定名)。查询操作使用的 API 是sqlSession.selectList("命名空间.id");

<!-- 查询所有 -->
<!-- select标签表示查询操作。添加insert,修改update,删除delete -->
<!-- namespace - 命名空间,与内部操作的id属性共同构成唯一标识,定位时使用user.findAll -->
<!-- resultType - 将查询到的结果与resultType对应的类进行关联,自动完成映射封装,要求是类的全路径 -->
<select id="findAll" resultType="com.chrisniu.domain.User">
    <!-- 要执行的SQL语句,要求没有封号 ; -->
    select * from user
</select>

// java语句
List<User> users = sqlSession.selectList("UserMapper.findAll");

Mybatis 新增

插入语句使用 <insert></insert> 标签,在映射文件中使用 parameterType 属性指定要插入到数据库中的数据类型(完全限定名)。

SQL 语句中使用 #{实体属性名} 方式引用实体中的属性值(准确说是 get 方法后的小写单词),且要与前面数据库中的属性一一对应。

插入操作使用的 API 是 sqlSession.insert("命名空间.id", 实体对象); ,且插入操作涉及数据库数据变化,所以要使用 sqlSession 对象进行显式地提交事务,即 sqlSession.commit();

<!-- 新增用户 -->
<insert id="saveUser" parameterType="com.chrisniu.domain.User">
    <!-- insert into user 是对user表进行操作,后面的属性对应user表中的属性 -->
    <!-- #{} 是mybatis中的占位符,等同于JDBC中的 ? ,内部的属性名与User类的属性一一对应,
             准确来说是与get方法后面的首字母小写的属性相同。如getUsername,则此属性名为username -->
    <!-- parameterType:指定查询到的数据要封装成的类的类型 -->
    insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address})
</insert>

// java语句
sqlSession.insert("UserMapper.saveUser", user);

Mybatis 修改

修改语句使用 <update></update> 标签,在映射文件中使用 parameterType 属性指定与数据库中对应的数据类型(完全限定名),借此来完成修改操作。修改操作使用的 API 是 sqlSession.update("命名空间.id", 实体对象);

<!-- 修改用户 -->
<update id="updateUser" parameterType="com.chrisniu.domain.User">
    update user set username = #{username}, birthday = #{birthday}, sex = #{sex}, address = #{address} where id = #{id}
</update>

// java语句
sqlSession.update("UserMapper.updateUser", user);

Mybatis 删除

修改语句使用 <delete></delete> 标签,在映射文件中使用 parameterType 属性指定传入所需的参数来完成删除操作。删除操作使用的 API 是 sqlSession.delete("命名空间.id", 参数对象);

在进行删除操作时,如果 parameterType 传入的类型是基本数据类型或者字符串类型且个数只有一个(即不存在多个属性),则 SQL 语句中的 #{} 内的值可以任意写,且标签中的 parameterType 属性可以不写;如果传入的是对象类型,则 #{} 需要与此对象类型的 get 方法后的单词小写进行对应。

<!-- 删除用户 -->
<delete id="deleteUser" parameterType="java.lang.Integer">
    <!-- 由于传入的对象就是一个整型,只有一个属性,因此 #{} 里面些什么都可以 -->
    delete from user where id = #{bothOK}
</delete>

// java语句
sqlSession.delete("UserMapper.deleteUser", 4);

5. Mybatis 核心配置文件

MyBatis 的配置文件包含了影响 MyBatis 行为的设置和属性信息。

配置文件的顶层结构在配置时,要按以下相对顺序

  • configuration - 配置

    • properties - 属性

    • settings - 设置

    • typeAliases - 类型别名

    • typeHandlers - 类型处理器

    • objectFactory - 对象工厂

    • plugins - 插件

    • environments - 环境配置

      • environment - 环境变量
        • transactionManager - 事务管理器
        • dataSource - 数据源
    • databaseIdProvider - 数据库厂商标识

    • mappers - 映射器

MyBatis 常用配置解析

1) properties 标签

在各标签中配置属性标签 <property></property> 时,可以将配置信息单独抽取成一个 properties 文件。如配置环境配置中的数据源时,将 jdbc 的配置信息单独提取出来。

jdbc.properties 文件,其中的数据存储采用 key=value 的形式。

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///mybatis_db?characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456

MyBatis 的核心配置文件,引入外部的 properties 文件后,占位符使用 ${} ,内部的字段值要与 properties 文件中的 key 值匹配。

<!-- 加载外部的properties文件 -->
<properties resource="jdbc.properties"></properties>

<!-- environments配置的是运行环境 -->
<environments default="develop">
    <environment id="develop">
        <!-- 当前的事务管理器是JDBC -->
        <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>
2) typeAliases 标签

对 Java 类的完全限定名设置一个短的别名,目的是简化映射文件中对 Java 类型的设置。

默认情况下,Java 官方为一些预定义类设置了别名,包装类的别名对应基本数据类型名,String 的别名为 string 等。

设置别名的方式有两种:

  • 第一种是使用 <typeAlias></typeAlias> 标签中的 type 和 alias 属性来对单一的类设置别名;
  • 第二种是使用 <package></package> 标签中的 name 指定需要设置别名的类存放的目录,然后将此目录内的全部类都设置别名为类名,且不区分大小写。
<!-- 设置类的别名 -->
<typeAliases>
    <!-- 方式一:给单个实体起别名 -->
    <!-- <typeAlias type="com.chrisniu.domain.User" alias="user"></typeAlias> -->
    <!-- 方式一:给单个实体起别名,别名就是类名,不区分大小写 -->
    <package name="com/chrisniu/domain"/>
</typeAliases>
3) plugins 标签

MyBatis 可以使用第三方的插件来对功能进行扩展。其中分页助手 PageHelper 是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。

  1. 在 pom.xml 文件中导入通用 PageHelper 坐标。

    <!-- 分页助手 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>3.7.5</version>
    </dependency>
    <dependency>
        <groupId>com.github.jsqlparser</groupId>
        <artifactId>jsqlparser</artifactId>
        <version>0.9.1</version>
    </dependency>
    
  2. 在 MyBatis 核心配置文件中配置 PageHelper 插件。

    <!-- 分页助手的插件 -->
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <!-- dialect:指定方言,即表示数据库使用的是mysql,则使用分页时就是用mysql的limit -->
        <property name="dialect" value="mysql"/>
    </plugin>
    
  3. 测试与使用。

    @Test
    public void testPageHelper(){
        // 向数据库执行查询操作之前,设置分页参数。中间不进行任何代码
        // 参数1表示当前页,参数2表示每页显示多少条数据
        PageHelper.startPage(1,2);
        List<User> users = userMapper.selectAll(null);
        // 此时查询到的结果就是分页后第一页的两条结果
        for(User user : users){ 
            System.out.println(user); 
        } 
    }
    
    // 其他分页的其它数据,泛型为查询结果封装到哪个实体类,参数为集合users,表示利用分页查询出来的集合数据
    PageInfo<User> pageInfo = new PageInfo<>(users);
    System.out.println("总条数:" + pageInfo.getTotal()); 
    System.out.println("总页数:" + pageInfo.getPages()); 
    System.out.println("当前页:" + pageInfo.getPageNum()); 
    System.out.println("每页显示长度:" + pageInfo.getPageSize()); 
    System.out.println("是否第一页:" + pageInfo.isIsFirstPage()); 
    System.out.println("是否最后一页:" + pageInfo.isIsLastPage());
    
4) environment 标签

完成对数据库环境的配置,支持多个环境配置。此标签的 id 属性指定当前环境的名称。

  • <transactionManager></transactionManager> 标签配置事务管理器。

    使用此标签的 type 属性配置事务管理器,事务管理器的类型有两种:JDBC 和 MANAGED。

    • JDBC:此配置直接使用 JDBC 的提交和回滚设置,依赖于从数据源得到的连接来管理事务作用域。
    • MANAGED:此配置几乎没做什么,从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。Mybatis 与 Spring 整合后,事务交给 Spring 容器管理。
  • <dataSource></dataSource> 标签配置当前数据源的类型(数据库连接的方式)。

    使用此标签的 type 配置数据源的类型,数据源常用类型有三种:UNPOOLED、POOLED 和 JNDI。

    • UNPOOLED:此数据源的实现为每次被请求时都需要打开和关闭连接。
    • POOLED:此数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
    • JNDI:此数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
5) mappers 标签

加载 MyBatis 的映射文件,映射方式有四种:

  • 使用 resource 属性对相对于类路径的映射文件进行引用。如 <mapper resource="mapper/UserMapper.xml"/>
  • 使用 url 加载映射文件。如 <mapper url="file:///var/mappers/UserMapper.xml"/>
  • 使用映射器接口的完全限定类名。如 <mapper class="org.mybatis.builder.userMapper"/>
  • 使用包内的映射器接口全部注册为映射器。如 <package name="org.mybatis.builder"/>

6. MyBatis API 相关接口

SqlSession 工厂构建器 SqlSessionFactoryBuilder

SqlSessionFactoryBuild().build(InputStream inputStream); 方法通过加载 MyBatis 核心配置文件的输入流的方式来构建一个 SqlSessionFactory 即 SqlSession 类的工厂类对象。

// MyBatis核心配置文件的路径
String resource = "sqlMapConfig.xml";
// 以输入流的形式加载配置信息
// Resources是一个工具类,在org.apache.ibatis.io包中,此类可以从类路径下、文件系统或一个web URL中加载资源文件。
InputStream inputStream = Resources.getResourceAsStream(resource);
// 新建SqlSession工厂构建器的实例
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 使用构造器实例构造SqlSession工厂
SqlSessionFactory factory = builder.build(inputStream);

SqlSession 工厂对象 SqlSessionFactory

SqlSessionFactory 有多个个方法创建 SqlSession 实例。常用的有如下两个:

  • openSession() - 默认开启一个事务,但此事务不会自动提交,因此在进行增删改操作时,需要手动提交该事务来将更新操作持久化到数据库中。
  • openSession(boolean autoCommit) - 参数表示事务是否自动提交,如果为 true,则表示无需手动提交事务。
// 3.获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();

SqlSession 会话

SqlSession 实例可以实现所有执行语句提交或回滚事务获取映射器实例的方法。

// 查询操作,可以查询一条数据或多条数据
<T> T selectOne(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter);
// 插入操作
int insert(String statement, Object parameter);
// 修改操作
int update(String statement, Object parameter);
// 删除操作
int delete(String statement, Object parameter);
// 事务的提交和回滚
void commit();
void rollback();

7. MyBatis 的基本原理

  1. 先进行 MyBatis 的核心配置文件(一般命名 SqlMapConfig.xml)和映射配置文件(一般命名 Mapper1.xml …)。核心配置文件中主要配置数据源、事务,并引入映射配置文件;映射配置文件中主要配置要执行的 SQL 语句,传入、传出参数等。

  2. 使用 SqlSessionFactoryBuilder 对象通过 build 方法构建 SqlSession 的工厂对象 SqlSessionFactory。此过程的操作:

    1. 进行初始化配置:使用 dom4j 解析了配置文件,将映射配置文件中的每个 SQL 配置都封装成一个 MappedStatement 对象。每个 MappedStatement 对象中封装了多个属性,分别是要执行的 SQL 语句、resultType 和 parameterType 等参数,此过程就是将多个属性封装成了一个对象;然后将配置文件生成的多个 MappedStatement 对象封装成一个 map 集合,此集合的 key 保存的是每个 SQL 配置的 statementId(namespace.id 对应的字符串值),value 保存的是对应的 MappedStatement 对象。
    2. 创建了 SqlSessionFactory 工厂类对象。
  3. SqlSessionFactory 对象通过 openSession() 方法创建 SqlSession 会话对象。SqlSession 对象并不会直接操作数据库,而是委派给执行器 Executor 进行执行。

    执行器的底层就是 JDBC,JDBC 需要得到要执行的 SQL 语句、参数以及返回数据封装得到结果对象,而这些数据都封装在 MappedStatement 对象中。当 SqlSession 执行操作时,会将执行的操作的标识和参数类传给执行的函数,执行器就会根据这些参数在存放 MappedStatement 对象的 map 集合中找到对应的 mappedStatement,然后去执行底层的 JDBC 操作。

  4. 执行 JDBC 操作后,最终操作数据库或封装返回数据。

8. MyBatis 的 DAO 层开发

8.1 传统开发方式

  1. 编写 UseMapper 接口。

    public interface UserMapper {
        public List<User> findAll() throws Exception;
    }
    
  2. 编写 UserMapper 实现类。

    public class UserMapperImpl inplements UserMapper {
        @Override
        public List<User> findAll() throws Exception {
            // 1.加载核心配置文件
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            // 2.获取sqlSessionFactory工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            // 3.获取SqlSession对象
            SqlSession sqlSession = sqlSessionFactory.openSession();
            // 4.执行sql,参数就是statementId,即配置sql语句时的userspace.id
            List<User> users = sqlSession.selectList("UserMapper.findAll");
            // 5.关闭连接
            sqlSession.close();
            
            return users;
        }
    }
    
  3. 编写 UserMapper.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="UserMapper">
        <!-- 查询所有 -->
        <select id="findAll" resultType="com.chrisniu.domain.User">
            select * from user
        </select>
    </mapper>
    

存在的问题

  1. 实现类中,存在 MyBatis 模板代码重复。即加载核心配置文件、获取 SqlSession 工厂对象和获取 SqlSession 对象的操作。
  2. 实现类调用方法时,xml 中的 SQL statement 硬编码到 Java 代码中。即 statementId 硬编码在 Java 中,对 xml 文件中的 statementId 进行修改时也需修改大量 Java 代码中的方法参数。

由于出现的问题都存在于实现类中,因此考虑是否可以只写接口,不写实现类?

8.2 代理开发方式

采用 Mybatis 的基于接口代理方式实现持久层的开发。基于接口代理方式的开发只需要编写 Mapper 接口,Mybatis 框架会动态生成实现类的对象

开发流程

规范:Mapper 接口开发方法只需编写 Mapper 接口(相当于 Dao 接口),由 Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同 Dao 接口实现类方法。

  • Mapper.xml 映射文件中的 namespace 要与 Mapper 接口的完全限定名相同。
  • Mapper 接口中的成员方法名要与 Mapper.xml 映射文件中定义的每个 statement 的 id 相同。
  • Mapper 接口方法的输入参数类型要与 Mapper.xml 映射文件中定义的每个 SQL 的 parameterType 的类型相同。
  • Mapper 接口方法的返回参数类型要与 Mapper.xml 映射文件中定义的每个 SQL 的 resultType 的类型相同。
  1. 编写 UserMapper 接口。

    public interface UserMapper {
        public List<User> findAll() throws Exception;
    }
    
  2. 编写 UserMapper.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="UserMapper">
        <select id="findAll" resultType="com.chrisniu.domain.User">
            select * from user
        </select>
    </mapper>
    
  3. 映射文件的配置。

    • 使用映射器接口的完全限定类名。在使用此方式时,UserMapper.xml 映射配置文件在 resources 中的目录结构与 UserMapper.java 接口文件在 java 中的目录结构须保持一致。如 <mapper class="com.chrisniu.mapper.UserMapper"/> ,其中映射配置文件位于 src/main/resources/com/chrisniu/mapper/UserMapper.xml,接口文件位于 src/main/java/com/chrisniu/mapper/UserMapper.java。
    • 使用包内的映射器接口全部注册为映射器。如 <package name="com.chrisniu.mapper"/> 。此方法仍须满足相同目录结构。
    <!-- 引入映射配置文件 -->
        <mappers>
            <!-- 导入xml文件 -->
            <!--<mapper resource="com/chrisniu/mapper/UserMapper.xml"/>-->
            <!-- 将接口加载为映射器 -->
            <!--<mapper class="com.chrisniu.mapper.UserMapper"/>-->
            <!-- 将包内的接口全部注册为映射器 -->
            <package name="com.chrisniu.mapper"/>
        </mappers>
    
  4. 进行测试。虽然测试时仍然需要导入核心配置、获取工厂实例并获取 SqlSession 实例,但此操作是在测试中操作,而传统开发方式则需要将这些操作冗余在 Mapper 的实现类中。

    public void test1() throws IOException {
        	// 导入配置、获得工厂、获得SqlSession对象
            InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
            SqlSession sqlSession = sqlSessionFactory.openSession();
        
            // 获得Mapper代理对象
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            // 执行查询
            User user = userMapper.findUserById(1);
            System.out.println(user);
        
            // 释放资源
            sqlSession.close();
        }
    
执行原理

此时 DAO 层只有一个接口,但接口并不进行实际工作。

  1. getMapper(UserMapper.class) 方法利用接口的类型作为参数,得到的 UserMapper 接口的实现类对象实际上是由 MapperProxy 代理产生的代理对象(底层是基于 JDK 动态代理产生的代理对象)。即 userMapper 对象底层实际上是 MapperProxy 产生的对象。

    **代理对象调用接口中的任意方法时,底层的 invoke() 都会执行。**即当前的 userMapper 对象调用接口内的 findUserById() 方法时,底层执行的其实是 mapperProxy.invoke() 方法。

  2. MapperProxy 对象调用 invoke() 方法时,最终调用了 mapperMathod.excute(sqlSession, args) 方法。

  3. 在 excute 方法中,执行工作的还是 sqlSession,也即调用的是 sqlSession 的 API 方法。

MyBatis 复杂映射

1. MyBatis 高级查询

resultMap 属性与标签

映射配置文件中,进行 SQL 编写时,resultType 指定的实体类的属性名要与数据库表中的字段名保持一致(即 get 和 set 方法后面的单词小写与数据库表字段一致),才可以将查询到的结果自动封装到实体类中。

如果实体类的属性名与数据库表中的字段名不一致,则可以使用 resultMap 属性搭配 resultMap 标签 实现将数据手动封装到实体类中。

如果数据库中的字段名与实体类中的属性名一致,则在 SQL 操作标签中使用参数 resultType 将查询结果封装到指定的实体类中;

如果数据库中的字段名与实体类中的属性名不一致,则使用 resultMap 标签实现实体类的成员变量名与数据库表的字段名之间的映射关系,然后再在 SQL 操作标签中使用 resultMap 属性来传入 resultMap 标签的 id,然后封装到对应类中。一个 resultMap 标签可以被使用多次

<!-- resultMap标签的id属性是此标签的唯一标识,type属性是对应的封装后的实体类 -->
<resultMap id="userResultMap" type="user">
    <!-- 手动配置映射关系 -->
    <!-- id标签用来配置主键,property属性对应实体类中的成员变量名,column属性对应数据库表中的字段名 -->
    <id property="idMy" column="id"/>
    <!-- result标签配置表中的其它字段,property属性对应实体类中的成员变量名,column属性对应数据库表中的字段名 -->
    <result property="usernameMy" column="username"/>
    <result property="birthdayMy" column="birthday"/>
    <result property="sexMy" column="sex"/>
    <result property="addressMy" column="address"/>
</resultMap>

<select id="findAllResultMap" resultMap="userResultMap">
    select * from user
</select>

多条件查询

多条件查询有三种方式:使用 #{arg0} ~ #{argn}#{param1} ~ #{paramn} 获取参数通过 @Param() 注解获取参数使用 Pojo 对象传递参数

1) #{...} 的方式

使用 #{arg0} ~ #{argn}#{param1} ~ #{paramn} 获取多个参数,其中 arg 的下标从 0 开始,param 的下标从 1 开始。此方式无需配置 parameterType,将方法传入的参数按顺序放入占位符 #{...} 中然后在数据库端进行查询。

<select id="findByIdAndUsername" resultType="user">
    <!--select * from user where id = #{arg0} and username = #{arg1}-->
    select * from user where id = #{param1} and username = #{param2}
</select>
2) @Param() 注解

在接口的成员方法的参数前使用 @Param 注解的形式,相当于给参数设置一个别名,此时在映射配置文件中就可以使用注解中的别名对占位符 #{...} 中的参数进行填充。

public interface UserMapper {
    public List<User> findByIdAndUsername(@Param("id") Integer id, @Param("username") String username);
}
<mapper namespace="com.lagou.mapper.UserMapper">
    <select id="findByIdAndUsername" resultType="user">
        select * from user where id = #{id} and username = #{username}
    </select>
</mapper>
3) Pojo 对象(推荐)

Pojo 对象本质上就是一个对象,这种方法将需要传递的多个属性封装到对象中,然后将此对象作为映射文件的 parameterType 参数的值。其中占位符 #{...} 中的参数名就是传入对象的成员变量名(get 方法名的小写)。

<select id="findByIdAndUsername" parameterType="user" resultType="user">
    select * from user where id = #{id} and username = #{username}
</select>

模糊查询

模糊查询有两种方式:#{...} ${...}

在 SQL 语句中,模糊查询为 select * from user where username like '%王%' ,因此接口传入的就是带有模糊查询符号 %...% 的字符串。

// 映射接口中的抽象方法
public interface UserMapper {
    public List<User> findByUsername(String username);
}

// 测试方法中传入模糊字段
@Test
public void testFindByUsername() throws Exception {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<User> list = userMapper.findByUsername1("%王%");
    for (User user : list) {
        System.out.println(user);
    }
}
1) #{...}

使用占位符 #{username} 传入字符串参数,{} 内部可以为任意参数名,且不需要加单引号 ' '

<mapper namespace="com.lagou.mapper.UserMapper">
    <select id="findByUsername" parameterType="string" resultType="user">
        select * from user where username like #{username}
    </select>
</mapper>
2) ${...}

使用拼接符 ${value} 传入字符串参数,{} 内部只能为 value,且需要加单引号 ' '

<mapper namespace="com.lagou.mapper.UserMapper">
    <select id="findByUsername" parameterType="string" resultType="user">
        select * from user where username like '${value}'
    </select>
</mapper>
#{}${} 的区别
  • #{} 表示一个占位符。
    • 通过 #{} 可以实现 preparedStatement 向占位符中设置值(JDBC 预编译的占位符传值),自动进行 Java 类型和 JDBC 类型转换(Java 类和数据库类的转换),#{} 可以有效防止 SQL 注入。
    • 占位符 #{} 方式设置参数时,转换为 SQL 语句在数据库执行时,会自动为占位符两侧添加单引号 ' ',因此无需手动添加。
    • #{} 可以接收简单类型(基本数据类型和 String 类)或 Pojo 属性值。
    • 如果 parameterType 传输单个简单类型值(单个基本数据类型或 String 类),#{} 括号中名称可以随意设置。
  • ${} 表示 SQL 的原样拼接。
    • 通过 ${} 将 parameterType 传入的内容拼接在 SQL 语句中,且不进行 Java 类型与 JDBC 类型的转换,会出现 SQL 注入问题。
    • 拼接符 ${} 方式拼接 SQL 语句时,不会添加单引号 ' ',因此需要在两侧添加单引号 '${value}'
    • ${} 可以接收简单类型(基本数据类型和 String 类)或 Pojo 属性值。
    • 如果 parameterType 传输单个简单类型值(单个基本数据类型或 String 类),#{} 括号中的字段只能为 value。

2. MyBatis 映射文件深入

2.1 返回主键

返回主键的场景:在向数据库插入一条数据后,希望立即拿到此记录在数据库中的主键值。

如 MySQL 中主键设置的是自增长的 id 值,而传入数据时不需要传入此主键值,但是想要得到此主键值。

1) userGeneratedKeys 属性

useGeneratedKeys 属性声明是否将数据库中的主键返回,keyProperty 属性表示将返回的主键值封装到实体类中的哪个属性。

<!-- Java中定义 public void saveUser(User user); 接口来返回主键 -->

<!-- useGeneratedKeys="true"声明需要返回主键 -->
<!-- keyProperty="id"表示把返回主键的值封装到user实体的id属性中 -->
<insert id="saveUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
    insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address}) </insert>

只适用于主键自增的数据库,MySQL 和 SqlServer 支持,Oracle 不支持。

2) selectKey 标签(推荐)

MySQL 内置的函数 SELECT LAST_INSERT_ID(); 用于查询最近一条插入数据的主键值。

<selectKey></selectKey> 标签支持所有类型的数据库。order 属性声明当前 selectKey 标签内的 SQL 语句执行在此标签外部 SQL 语句的前或后,值可以为 BEFOREAFTERkeyProperty 属性声明数据库中的主键对应的列名;keyProperty 属性声明返回的主键要存储在实体类中的哪个属性;resultType 属性声明主键的类型。

<!-- Java中定义 public void saveUser(User user); 接口来返回主键 -->

<insert id="saveUser" parameterType="user">
    <!-- selectKey适用范围广,支持所有类型数据库 -->
    <!-- keyColumn="id"指定数据库中主键的列名 -->
    <!-- keyProperty="id"指定主键封装到实体的id属性中 -->
    <!-- resultType="int"指定主键类型 -->
    <!-- order="AFTER"设置在sql语句执行前(后),执行此语句 -->
    <selectKey keyColumn="id" keyProperty="id" resultType="int" order="AFTER">
        SELECT LAST_INSERT_ID();
    </selectKey>
    insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address}) </insert>

2.2 动态 SQL

需要根据不同的场景条件执行不同的 SQL 语句时,需要使用动态 SQL,来对 SQL 语句进行动态拼接。

1) <if>

适用于在 SQL 操作时参数的个数不确定的情况,如在进行商品筛选时,有多个可选条件,可以限定某个条件或某些条件的组合限定,此时需要使用 <where></where> 标签与 <if></if> 搭配完成动态 SQL 查询操作。

<!-- Java中定义 public List<User> findByIdAndUsernameIf(User user); 返回查询结果集 -->

<!--  -->
<select id="findByIdAndUsernameIf" parameterType="user" resultType="user">
    SELECT * FROM user 
    <!-- where标签逻辑上相当于在查询语句后面添加`where 1=1`,但是如果内部没有<if>条件,则不会拼接where关键字 -->
    <where>
        <!-- if标签内部的test为判断条件 -->
        <if test="id != null">
            AND id = #{id} 
        </if>
        <if test="username != null">
            AND username = #{username}
        </if>
    </where>
</select>
2) <set> 动态更新

在 SQL 操作时,对符合条件的数据进行某个或某些字段的值修改,而修改的字段数与具体字段不定时,需要使用 <set></set> 标签与 <if></if> 搭配完成动态 SQL 修改操作。

<!-- Java中定义 public void updateIf(User user); 进行更新操作 -->

<update id="updateIf" parameterType="user">
    UPDATE user 
    <!-- set标签在执行时,自动为SQL语句添加set关键字,然后去掉最后一个条件的逗号 -->
    <set>
        <if test="username != null">
            username = #{username}, 
        </if> 
        <if test="birthday != null"> 
            birthday = #{birthday}, 
        </if> 
        <if test="sex != null"> 
            sex = #{sex}, 
        </if> 
        <if test="address != null"> 
            address = #{address}, 
        </if> 
    </set> WHERE id = #{id} 
</update>
3) <foreach> 多值查询

适用于在 SQL 操作时参数为有限个同种参数的情况,如在查询时,需要查询某属性的值在某个有限集合中的数据时,需要使用 <foreach></foreach> 标签。如需要查询 id 在 [1, 2, 3] 集合内的数据,相当于 select * from user where id in (1,2,3)

<foreach> 标签用于数据的循环遍历,属性有:

  • collection:代表要遍历的集合的类型。
  • open:代表语句的开始部分。
  • close:代表语句的结束部分。
  • item:遍历集合的每个元素,所生成的别名,相当于 foreach(int id: ids) 中,ids 是集合或数组,id 是元素别名。在标签内部的 #{} 内填充的变量名需要与 item 的值相同。
  • sperator:代表分隔符。

相当于拼接的 SQL 语句为:<where> 外部的 SQL 语句 + open 属性值 + #{id} + separator 属性值 + … + close 属性值。

<!-- Java中定义 public List<User> findByList(List<Integer> ids); 返回查询结果集合 -->

<!-- 如果查询条件为普通类型的List集合,collection属性值为:collection或者list -->
<select id="findByList" parameterType="list" resultType="user" > 
    SELECT * FROM user 
    <where> 
        <foreach collection="collection" open="id in(" close=")" item="id" separator=",">
            <!-- #{}内部的变量名需要与item的值id相同 -->
            #{id} 
        </foreach> 
    </where> 
</select>

<!-- Java中定义 public List<User> findByArray(Integer[] ids); 返回查询结果集合 -->

<!-- 如果查询条件为普通类型 Array数组,collection属性值为:array -->
<select id="findByArray" parameterType="int" resultType="user"> 
    SELECT * FROM user 
    <where> 
        <foreach collection="array" open="id in(" close=")" item="id" separator=",">
            #{id} 
        </foreach> 
    </where> 
</select>

2.3 SQL 片段

在映射文件中,可能存在重复的 SQL 语句片段,可以使用 <sql></sql> 标签将这些重复的 SQL 片段抽取出来,属性 id 为唯一标识符,使用时利用 <include/> 标签引用,属性 refid 为需要引用的 SQL 片段的 id,可以实现 SQL 片段复用。

<!-- 抽取的sql片段,赋予唯一标识符id -->
<sql id="selectUser"> 
    SELECT * FROM `user` 
</sql>

<select id="findByList" parameterType="list" resultType="user" > 
    <!--引入sql片段-->
    <include refid="selectUser"/>
    <where> 
        <foreach collection="collection" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where> 
</select> 

<select id="findByArray" parameterType="integer[]" resultType="user"> 
    <!--引入sql片段--> 
    <include refid="selectUser"></include> 
    <where> 
        <foreach collection="array" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

3. MyBatis 多表查询

关系型数据库表之间的关系有:一对一多对一一对多多对多。MyBatis 中,将多对一关系视为一对一,如多个订单属于同一用户,对于每个订单来说,只能属于一个用户。

本质上关系有一对一(人与身份证号之间互相为一对一关系)、多对一(用户与订单关系中,对于订单来说,多个订单属于一个用户,为多对一关系)、一对多(用户与订单关系中,对于用户来说,一个用户有多个订单,为一对多关系)、多对多(学生与选修课程来说,一个学生可以选多门课程,一门课程可以有多名学生选取,为多对多关系)。

多表构建

构建 orders 表表示订单,外键 uid 指向 user 表的主键,则这两张表存在多对一关系。构建 sys_role 表表示角色,与 user 之间存在多对多关系,因此存在中间表 sys_user_role,其中的 userid 指向 user 表的主键,roleid 指向 sys_role 的主键。

-- ---------------------------- 
-- Table structure for orders 
-- ---------------------------- 
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` ( 
    `id` INT(11) NOT NULL AUTO_INCREMENT, 
    `ordertime` VARCHAR(255) DEFAULT NULL, 
    `total` DOUBLE DEFAULT NULL, 
    `uid` INT(11) DEFAULT NULL, 
    PRIMARY KEY (`id`), 
    KEY `uid` (`uid`), 
    CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ---------------------------- 
-- Records of orders 
-- ---------------------------- 
INSERT INTO `orders` VALUES ('1', '2020-12-12', '3000', '1'); 
INSERT INTO `orders` VALUES ('2', '2020-12-12', '4000', '1'); 
INSERT INTO `orders` VALUES ('3', '2020-12-12', '5000', '2');

-- ---------------------------- 
-- Table structure for sys_role 
-- ---------------------------- 
DROP TABLE IF EXISTS `sys_role`; 
CREATE TABLE `sys_role` ( 
    `id` INT(11) NOT NULL AUTO_INCREMENT, 
    `rolename` VARCHAR(255) DEFAULT NULL, 
    `roleDesc` VARCHAR(255) DEFAULT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ---------------------------- 
-- Records of sys_role 
-- ---------------------------- 
INSERT INTO `sys_role` VALUES ('1', 'CTO', 'CTO'); 
INSERT INTO `sys_role` VALUES ('2', 'CEO', 'CEO');

-- ----------------------------
-- Table structure for sys_user_role 
-- ---------------------------- 
DROP TABLE IF EXISTS `sys_user_role`; 
CREATE TABLE `sys_user_role` ( 
    `userid` INT(11) NOT NULL, 
    `roleid` INT(11) NOT NULL, 
    PRIMARY KEY (`userid`,`roleid`), 
    KEY `roleid` (`roleid`), 
    CONSTRAINT `sys_user_role_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `sys_role` (`id`), 
    CONSTRAINT `sys_user_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `user` (`id`) 
) ENGINE=INNODB DEFAULT CHARSET=utf8;

-- ---------------------------- 
-- Records of sys_user_role 
-- ---------------------------- 
INSERT INTO `sys_user_role` VALUES ('1', '1'); 
INSERT INTO `sys_user_role` VALUES ('2', '1'); 
INSERT INTO `sys_user_role` VALUES ('1', '2'); 
INSERT INTO `sys_user_role` VALUES ('2', '2');

3.1 一对一(多对一)

场景:用户表与订单表的关系中,从订单角度来看,每个订单只属于一个用户,因此可以看做是一对一关系。

-- 一对一查询语句
-- 查询所有订单,同时查询每个订单所属的用户
SELECT * FROM orders o LEFT JOIN USER u ON o.`uid`=u.`id`;

当前查询出来的表相当于由两部分组成:左边是订单信息,右边是订单所属的用户及用户自身的信息。此时如果构建 Order 类,封装 orders 表的三个字段,此时 User 类和 Order 类都无法完成查询到结果的封装。由于需要查询的是订单属于哪个用户,则可以为 Order 类封装一个 User 类的属性 user,来存储订单所属用户的信息。

// 封装表示订单的类Order
public class Order {
    // 表示当前订单自身的属性
    private int id;
    private Date orderTime;
    private double money;
    private int uid;
    // 表示当前订单属于哪个用户
    private User user;
}

// OrderMapper接口,定义findAllWithUser方法实现多表查询
public interface OrderMapper { 
    public List<Order> findAllWithUser(); 
}

为多表查询的 findAllWithUser() 方法配置映射文件 OrderMapper.xml,其在 resources 中的路径需要与 OrderMapper 接口在 java 中的路径一致且同名。

<mapper namespace="com.chrisniu.mapper.OrderMapper">
    <!-- 使用resultMap用来对应数据库中字段名与实体类中的字段名的一个映射 -->
    <!-- 由于查询返回的结果是两个表,且将多表查询后其中一个表的结果存储在一个实体类中
         因此也可以使用resultMap标签完成将返回的多个数据封装到一个类属性中 -->
    <resultMap id="orderMap" type="com.chrisniu.domain.Order">
        <id column="id" property="id"/>
        <result column="ordertime" property="orderTime"/>
        <result column="total" property="money"/>
        <result column="uid" property="uid"/>

        <!-- 一对一(多对一)使用association标签关联
             property="user" 封装实体的属性名,与Order类中的属性对应
             javaType="user" 封装实体的属性类型,是实体类的全限定名 -->
        <association property="user" javaType="com.chrisniu.domain.User">
            <!-- id标签同样存储此表的主键,其表的主键应该是id,但是与orders表中的主键同名
                 而其主键其实与orders中的uid对应,因此使用uid的值作为当前表的主键值
			    也可以通过对查询到的属性起别名的方式进行区分,在SQL语句中取别名
                 其它属性column对应数据库的字段名,property对应user类的类名 -->
            <id column="uid" property="idMy"/>
            <result column="username" property="usernameMy"/>
            <result column="birthday" property="birthdayMy"/>
            <result column="sex" property="sexMy"/>
            <result column="address" property="addressMy"/>
        </association>
    </resultMap>
    
    <select id="findAllWithUser" resultMap="orderMap">
        select * from orders o left join user u on o.uid=u.id
    </select>
</mapper>

3.2 一对多

场景:在用户表与订单表的关系中,从用户角度来看,一个用户可以有多个订单,则用户与订单之间是一对多的关系。

-- 一对多查询语句
-- 查询所有用户,同时查询出每个用户具有的订单
SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.`id` = o.`uid`;

编写实体类 User,由于每个 User 可以有多个订单信息,每个订单信息对应一个 Order 类,因此需要在 User 类中添加一个 List<Order> 属性来存放多个订单信息。

public class User {
    private int id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    // 代表当前用户具备的订单列表,包含多个Order实例
    // 其中Order包含id、ordertime和money属性
    private List<Order> orderList;
}

// UserMapper接口,定义findAllWithOrder方法实现一对多查询
public interface UserMapper { 
    public List<User> findAllWithOrder();
}

为多表查询的 findAllWithOrder() 方法配置映射文件 UserMapper.xml。将属性封装到单个类中使用 <association> 标签,而将每一条数据的属性封装到一个类,且有多条数据时,使用 collection 标签。

<resultMap id="userOrderMap" type="com.chrisniu.domain.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="birthday" property="birthday"/>
    <result column="sex" property="sex"/>
    <result column="address" property="address"/>
    <!-- 一对多使用collection标签关联
         property="orderList" 封装到集合的属性名,对应User类中的集合属性
         ofType="order" 封装集合的泛型类型,对应集合的泛型对应的实体类 -->
    <collection property="orderList" ofType="com.chrisniu.domain.Order">
        <id column="oid" property="id"/>
        <result column="ordertime" property="orderTime"/>
        <result column="total" property="money"/>
    </collection>
</resultMap>

<select id="findAllWithOrder" resultMap="userOrderMap">
    SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.id=o.uid
    <!-- 此处oid的用处是为orders表中查到的数据,给其主键起的一个别名,相当于orders表中主键的那一列的字段名变成oid -->
</select>

3.3 多对多

场景:在用户表和角色表的关系中,一个用户可以有多个角色,一个角色可以有多个用户使用,则用户与角色之间是多对多的关系。

多对多的关系在数据库表中的体现为:通过 user_role 表进行连接。

user
user_role
role

查询的目的是得到 user 表中的用户数据和 role 表中的角色数据,而这两张表没有直接的关联,因此需要使用中间表 user_role,进行三表联查。

-- 一对多查询语句
-- 查询所有用户,同时查询出每个用户所有的角色
SELECT * FROM USER u   -- 用户表 
	LEFT JOIN sys_user_role ur ON u.`id` = ur.`userid`   -- 左外连接中间表	 
	LEFT JOIN sys_role r ON ur.`roleid` = r.`id`;   -- 左外连接中间表

-- 使用起别名的方式
-- 则当前方式下,映射文件中resultMap中的collection内部id标签的column属性的值就可以是rid
SELECT u.*,r.id rid,r.rolename,r.roleDesc FROM user u 
    LEFT JOIN sys_user_role ur ON ur.userid = u.id
    LEFT JOIN sys_role r ON ur.roleid = r.id

在编写完 SQL 语句后,由于目的是查询所有用户以及用户所属角色,因此从角色方面来看,多对多与一对多相似。也即多对多查询与一对多查询的区别主要在 SQL 语句。

public class User {
    private int id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    // 代表当前用户所属的角色列表
    private List<Role> roleList;
}

public class Role {
    private int id;
    private String roleName;
    private String roleDesc;
}

// UserMapper接口,定义findAllWithRole方法实现一对多查询
public interface UserMapper { 
    public List<User> findAllWithRole(); 
}

为多表查询的 findAllWithRole() 方法配置映射文件 UserMapper.xml。

需要注意的是,column 对应的是 SQL 中多表查询到的视图中的字段名,因此可以使用起别名的方式区分同名字段,或者使用主键对应中间表中的外键字段名。

<resultMap id="userRoleMap" type="com.chrisniu.domain.User">
   <id column="id" property="id"/>
   <result column="username" property="username"/>
   <result column="birthday" property="birthday"/>
   <result column="sex" property="sex"/>
   <result column="address" property="address"/>
   
   <collection property="roleList" ofType="com.chrisniu.domain.Role">
       <id column="roleid" property="id"/>
       <result column="rolename" property="roleName"/>
       <result column="roleDesc" property="roleDesc"/>
   </collection>
</resultMap>

<select id="findAllWithRole" resultMap="userRoleMap">
   SELECT * FROM USER u LEFT JOIN sys_user_role ur ON u.id = ur.userid LEFT JOIN sys_role r ON ur.roleid = r.id
</select>

总结

  • 多对一(一对一)配置:使用 <resultMap> + <association> 做配置。

  • 一对多配置:使用 <resultMap> + <collection> 做配置。

  • 多对多配置:使用 <resultMap> + <collection> 做配置。

  • 一对多和多对多配置基本相同,区别在于 SQL 语句的编写。

4. MyBatis 嵌套查询

嵌套查询:将多表查询中的联合查询语句,拆分成多个单表查询,再使用 MyBatis 语法进行嵌套。

-- 需求:查询一个订单,同时查询出该订单所属的用户
-- 1. 联合查询
SELECT * FROM orders o LEFT JOIN USER u ON o.`uid`=u.`id`;
-- 2. 嵌套查询 
	-- 2.1 先查询订单 
	SELECT * FROM orders 
	-- 2.2 再根据订单uid外键,查询用户 
	SELECT * FROM user WHERE id = #{根据订单查询到的uid} 
	-- 2.3 最后使用MyBatis,将以上二步嵌套起来 ...

4.1 一对一(多对一)

将多表查询的 SQL 拆分成多个单表查询的 SQL。

-- 先查询订单 
SELECT * FROM orders; 
-- 再根据订单uid外键,查询用户 
SELECT * FROM user WHERE id = #{订单的uid};

分别在 OrderMapper 和 UserMapper 接口中编写方法。

public interface OrderMapper { 
    // 查询所有的订单信息
    public List<Order> findAllWithUser(); 
}

public interface UserMapper { 
    // 根据订单信息中的uid外键,就可以根据int类型的id查询用户
    public User findById(int id); 
}

需要对两个接口对应的 xml 文件进行配置。执行过程为:

  1. 先执行 OrderMapper.xml 文件的 <select> 标签内的语句,然后利用 <resultMap> 将查询到的数据封装到 Order 实体中。
  2. <resultMap> 中对查询到的数据进行封装,然后使用 <association> 标签对 Order 中的 User 属性进行封装。
  3. javaType 属性表示要将此属性封装到哪个实体类,select 属性表示需要查询的第二句 SQL 语句的 statementId,即 namespace.idcolumn 的值是需要传入第二条 SQL 语句的参数。
  4. 再执行 UserMapper.xml 文件中对应的语句,并将结果返回到 OrderMapper.xml 文件中的 <association> 处,即可完成封装。
  5. 最后完成查询封装。
<!-- 一对一嵌套查询 --> 

<!-- OrderMapper.xml -->
<resultMap id="orderMap" type="com.chrisniu.domain.Order">
    <id column="id" property="id"/>
    <result column="ordertime" property="orderTime"/>
    <result column="total" property="money"/>
    
    <!-- 这是执行的第一条SQL语句,问题:1.如何执行第二条SQL?2.第二条SQL的uid如何 -->
    <!-- select属性:就是需要调用的第二条SQL在配置文件中的namespace.id
         column属性:当前第一个SQL查询出来的表中,作为参数传入第二条SQL的字段名 -->
    <!-- 根据订单中的 uid 外键,查询用户表 -->
    <association property="user" javaType="com.chrisniu.domain.User" column="uid" select="com.chrisniu.mapper.UserMapper.findById"/>
</resultMap>

<select id="findAllWithUser" resultMap="orderMap" >
    SELECT * FROM orders
</select>


<!-- UserMapper.xml -->
<select id="findById" parameterType="int" resultType="com.chris.domain.User"> 
    SELECT * FROM `user where id = #{uid} 
</select>

4.2 一对多

SQL 语句

-- 先查询用户
SELECT * FROM user; 
-- 再根据用户id主键,查询订单列表 
SELECT * FROM orders where uid = #{用户id};

Mapper 接口

public interface UserMapper { 
    public List<User> findAllWithOrder(); 
}

public interface OrderMapper { 
    public List<Order> findByUserId(int uid); 
}

映射文件

<!-- 一对多嵌套查询 --> 

<!-- UserMapper.xml -->
<resultMap id="userMap" type="com.chris.domain.User"> 
    <id column="id" property="id"/>
    <result column="username" property="username"/> 
    <result column="birthday" property="birthday"/> 
    <result column="sex" property="sex"/>
	<result column="address" property="address"/>
    
    <!-- ofType属性:查询 -->
    <!-- 根据用户的主键id,查询订单表 --> 
    <collection property="orderList" column="id" ofType="com.chris.domain.Order" select="com.chrisniu.mapper.OrderMapper.findByUserId"/> 
</resultMap>

<select id="findAllWithOrder" resultMap="userMap"> 
    SELECT * FROM user
</select>


<!-- OrderMapper.xml -->
<select id="findByUserId" parameterType="int" resultType="com.chris.domain.Order"> 
    SELECT * FROM orders where uid=#{id}
</select>

4.3 多对多

SQL 语句

-- 先查询用户 
SELECT * FROM user; 
-- 再根据用户id主键,查询角色列表 
SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{用户id};

Mapper 接口

public interface UserMapper { 
    public List<User> findAllWithRole();
}

public interface RoleMapper { 
    public List<Role> findByUserId(Integer uid);
}

映射文件

<!-- 多对多嵌套查询 --> 

<!-- UserMapper.xml -->
<resultMap id="userAndRoleMap" type="user"> 
    <id column="id" property="id"/>
    <result column="username" property="username"/> 
    <result column="birthday" property="birthday"/> 
    <result column="sex" property="sex"/>
	<result column="address" property="address"/>
    
    <!-- 根据用户id,查询角色列表 --> 
    <collection property="roleList" column="id" ofType="com.chris.domain.Role" select="com.chris.mapper.RoleMapper.findByUserId"/>
</resultMap> 

<select id="findAllWithRole" resultMap="userAndRoleMap">
	SELECT * FROM user
</select>


<!-- RoleMapper.xml -->
<select id="findByUserId" parameterType="int" resultType="com.chris.domain.Role"> 
    SELECT r.id,r.role_name roleName,r.roleDesc roleDesc FROM sys_role r INNER JOIN sys_user_role ur ON r.id = ur.roleid WHERE ur.userid = #{uid}
</select>

总结

  • 一对一(多对一)配置:使用 <resultMap> + <association> 做配置,通过 column 条件,执行 select 查询。

  • 一对多配置:使用 <resultMap> + <collection> 做配置,通过 column 条件,执行 select 查询。

  • 多对多配置:使用 <resultMap> + <collection> 做配置,通过 column 条件,执行 select 查询。

  • 优点:简化多表查询操作;缺点:执行多次 SQL 语句,浪费数据库性能。

MyBatis 加载策略和注解开发

MyBatis 加载策略

立即加载

在进行一对多或多对多查询时,如查询用户及其订单信息时,是在查询用户的同时,将用户相关的订单信息一同查询(一同加载)出来,这种加载策略是立即加载。

延迟加载

同样查询用户及其订单信息时,可以只查询用户信息,当需要用户相关的订单信息时,再根据用户关联的 id 查询订单信息,这种加载策略是延迟加载。也就是在需要用到这部分数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载

延迟加载是基于嵌套查询来实现的

在查询某信息时,将其关联信息也查询出来,是立即加载;只查询出需要的信息,其关联信息在使用时才查询,是延迟加载。

延迟加载表现为,当前并没有任何方法调用嵌套查询的数据时,不进行加载;当有任意方法需要用到这部分数据时,此时进行加载。

优点:先从单表查询,需要时再从关联表进行关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表的速度要快。

缺点:因为只有当需要用到关联数据时,才会进行数据库查询(相当于二次数据库查询),这样在大批量数据查询时,由于查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。

在多表查询中,一对多,多对多:通常情况下采用延迟加载一对一(多对一):通常情况下采用立即加载

延迟加载的实现

1. 局部延迟加载

<association><collection> 标签中都有一个 fetchType 属性,通过修改此属性的值,可以修改局部加载策略,此属性的值为 lazy 时表示懒加载策略,值为 eager 时表示立即加载策略。

<resultMap id="orderMap" type="com.chrisniu.domain.Order">
        <id column="id" property="id"/>
        <result column="ordertime" property="orderTime"/>
        <result column="total" property="money"/>
        <!-- fetchType="lazy" 使用延迟加载(懒加载)策略,fetchType="eager" 使用立即加载策略 -->
        <association property="user" javaType="com.chrisniu.domain.User" column="uid" 
                     select="com.chrisniu.mapper.UserMapper.findById" fetchType="lazy"/>
    </resultMap>

设置触发延迟加载的方法

家在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,但是在你调用当前对象的 equals、clone、hashCode、toString 方法时也会触发关联对象的查询。

可以在核心配置文件中使用 lazyLoadTriggerMethods 配置项覆盖掉上面四个方法。

<settings> 
	<!-- 所有方法都会延触发迟加载 -->
	<!-- 要使用此字段对特定方法的延迟加载进行评比 -->
	<setting name="lazyLoadTriggerMethods" value="toString()"/> 
</settings>
2. 全局延迟加载

在 MyBatis 的核心配置文件中可以使用在 <settings> 标签内的 <setting> 标签修改全局加载策略。

<settings> 
    <!--开启全局延迟加载功能--> 
    <setting name="lazyLoadingEnabled" value="true"/> 
</settings>

局部的加载策略优先级高于全局的加载策略。即如果在全局配置中开启延迟加载(懒加载),则在映射文件中不配置 fetchType 属性的语句遵循全局配置,映射文件中配置 fetchType 的语句按映射文件为准。

通常在开启全局延迟加载时,对一对一查询的加载策略修改为立即加载。

MyBatis 缓存

MyBatis 缓存的必要性

当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来后,可以保存在缓存中。当用户再次查询这些数据时,无需再通过数据库查询,直接查询缓存,可以减少网络连接和数据库查询带来的资源损耗,从而提高查询效率、减少高并发访问带来的系统性能问题。

经常查询一些不经常发生变化的数据时,可以使用缓存来提高查询效率。Mybatis中缓存分为 一级缓存二级缓存

1. 一级缓存

一级缓存是 SqlSession 级别 的缓存,是 默认开启

所以在参数和 SQL 语句完全一样的情况下,使用 同一个 SqlSession 对象 多次 调用同一个 Mapper 方法(执行同一个 SQL 语句且参数相同),只执行一次 SQL 操作,因为使用 SqlSession 进行第一次查询后,MyBatis 会将查询结果放在缓存中,当再查询时,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。

当使用 SqlSession 进行增加、更新、删除操作,或是 SqlSession 调用 clearCache() | commit() | close() 方法时,都会清空此 SqlSession 的一级缓存

结合一级缓存的查询过程:

  1. 第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息。如果缓存中没有此数据,再从数据库查询用户信息。

  2. 得到用户信息,将用户信息存储到一级缓存中。缓存区本质上是一个 map 集合,其 key 就是此 SQL 查询语句 + 参数,对应的 value 是对查询到的结果进行封装的 实体对象 中。

  3. 第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否存在 id 为 1 的用户信息,缓存中存在,则直接从缓存中获取此用户信息。

  4. 如果 SqlSession 去执行commit 操作(执行插入、更新、删除),会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

在映射配置时,如果使用 flushCache 属性的值为 true 时,表示自动清空缓存。

2. 二级缓存

二级缓存是 namspace 级别(可以跨 SqlSession,也即 Mapper 映射级别)的缓存,是默认不开启的

开启二级缓存时,多个 SqlSession 可以在同一 namespace 下的 mapper 标签内的 SQL 语句查询到的结果进行共享。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWxEcBIf-1653319955618)(…/…/markdown_img/1653121152.png)]

二级缓存的开启需要进行配置,实现二级缓存时,MyBatis 要求返回的 pojo 必须是可序列化的,也就是 要求实体类实现 Serializable 接口。只需要在映射XML文件配置 <cache/> 就可以开启二级缓存。

<!-- 核心配置文件 -->
<settings> 
    <!--因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。
        1. 值为 true 代表开启二级缓存;为 false 代表不开启二级缓存。 --> 
    <setting name="cacheEnabled" value="true"/> 
</settings>

<!-- 映射文件 -->
<mapper namespace="com.chrisniu.dao.UserMapper"> 
    <!-- 2. 表示当前namespace的映射开启二级缓存 --> 
    <cache></cache> 
    
    <!-- 3. <select>标签中设置useCache="true" 代表当前这个statement要使用二级缓存。 
         如果不使用二级缓存可以设置为false 
         注意:针对每次查询都需要最新数据的 SQL,要设置成useCache="false",禁用二级缓存。 --> 
    <select id="findById" parameterType="int" resultType="user" useCache="true"> 
        SELECT * FROM `user` where id = #{id} 
    </select> 
</mapper>

关于一级缓存和二级缓存:只有当 SqlSession 执行 sqlSession.commit()sqlSession.close() 方法时,一级缓存中的内容才会刷新到二级缓存

二级缓存中所有的 sqlSession 执行的 select 操作得到的数据都会被缓存,而任一 sqlSession 执行了 insert/update/delete 操作后,会情况此 namespace 所属的二级缓存。

MyBatis 的二级缓存因为是 namespace 级别,所以在进行多表查询时会产生脏读问题(在多表查询时,比如查询用户信息并查询用户的订单信息时,需要对 user 表和 order 表进行多表查询,且查询结果缓存在 userMapper 所属的二级缓存中;而当对订单表中的数据进行更新时,使用的是 orderMap 进行更新,同时更新的是 orderMap 所属的二级缓存中的数据,而 userMapper 所属的二级缓存中的数据未更新,因此再进行用户及订单信息查询时,读到的是脏数据)。因此 MyBatis 的二级缓存在应用中不会使用,而是使用 redis 作为第三方缓存。

MyBatis 开启二级缓存后,查询的顺序是 二级缓存 -> 一级缓存 -> 数据库。当从数据库中查询到数据时,先存到一级缓存,当一级缓存的 sqlSession 执行 sqlSession.commit()sqlSession.close() 方法时,才存到二级缓存。

MyBatis 注解开发

MyBatis 常用的 CRUD 注解:

// 增删改查操作
@Insert // 实现新增,代替了<insert></insert>
@Delete // 实现删除,代替了<delete></delete>
@Update // 实现更新,代替了<update></update>
@Select // 实现查询,代替了<select></select>

// 多表映射
@Result // 实现结果集封装,代替了<result></result>
@Results // 可以与@Result一起使用,封装多个结果集,代替了<resultMap></resultMap>
@One // 实现一对一结果集封装,代替了<association></association>
@Many // 实现一对多结果集封装,代替了<collection></collection>

1. MyBatis 注解实现 CRUD

  1. 编写 UserMapper 接口。

    public interface UserMapper {
        // 查询操作
        // 注解的参数值只有一个时,value = 可以省略
        @Select(value = "SELECT * FROM user")
        public List<User> findAll();
        
        // 查询操作
        // 传入pojo对象作为参数时,SQL语句的参数同样使用占位符#{},内部的字段名是对象的get方法名的单词首字母小写
        @Insert("INSERT INTO user(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})")
        public void saveUser(User user);
        
        // 查询操作
        @Update("UPDATE user SET username = #{username},birthday = #{birthday},sex = #{sex},address = #{address} WHERE id = #{id}")
        public void update(User user);
        
        // 查询操作
        @Delete("DELETE FROM user WHERE id = #{id}")
        public void delete(int id);
    }
    
  2. 编写核心配置文件。

    <!-- 使用了注解替代的映射文件,所以只需要加载使用了注解的Mapper接口即可 --> 
    <mappers> 
        <!-- 扫描使用注解的Mapper接口文件 --> 
        <mapper class="com.chrisniu.mapper.UserMapper"/>
        <!-- 也可以将一个包内的全部接口都扫描 -->
        <package name="com.chrisniu.mapper"/>
    </mappers>
    
  3. 测试代码。可以将重复代码提取出来,被 @Before 注解标注的方法先于 @Test 注解标注的方法;在 @Test 注解标注的方法执行完之后,再执行 @After 注解标注的方法。

    class MybatisTest {
        private SqlSession sqlSession;
        
        @Before
        public void before() throws IOException {
            // 1.加载核心配置文件
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            // 2.获取sqlSessionFactory工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            // 3.获取SqlSession对象
            sqlSession = sqlSessionFactory.openSession();
        }
        
        @After
        public void after() {
            // 5.提交事务
            sqlSession.commit();
            // 6.关闭连接
            sqlSession.close();
        }
        
        // 查询 
        @Test 
        public void testFindAll(){ 
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
            List<User> list = userMapper.findAll(); 
            for (User user : list) { 
                System.out.println(user); 
            } 
        }
        
        // 添加 
        @Test 
        public void testSave(){ 
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
            User user = new User(); 
            user.setUsername("于谦"); 
            user.setBirthday(new Date()); 
            user.setSex("男"); 
            user.setAddress("北京德云社"); 
            userMapper.saveUser(user); 
        }
        
        // 更新 
        @Test 
        public void testUpdate(){ 
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
            User user = new User(); 
            user.setId(49); 
            user.setUsername("郭德纲"); 
            user.setBirthday(new Date()); 
            user.setSex("男"); 
            user.setAddress("北京德云社"); 
            userMapper.update(user); 
        }
        
        // 删除 
        @Test 
        public void testDelete(){ 
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
            userMapper.delete(49); 
        }
    }
    

2. 使用注解实现复杂映射

使用 @Results/@Result/@One/@Many 注解组合完成复杂映射的配置。

注解说明
@Results代替标签 <resultMap>,此注解中可以使用单个 @Result 注解,也可以使用 @Result 注解集合。
@Results(@Resut)()})@Results({@Resut)(), @Resut)(), @Resut)(), ...})
@Result代替标签 <id>/<result>@Result 注解的属性:column - 数据库的列名;property - 需要装配的实体类的属性名;one - 需要使用的 @One 注解,@Result(one = @One)表示一对一关系;many - 需要使用的 @Many 注解,@Result(many = @Many)表示一对多关系。
@One代替标签 <assocation>,是多表查询的关键,在注解中用来指定子查询返回单一对象。
@One 注解的属性:select - 指定用来进行多表查询的 SQL 语句,使用时 @Result(column = " ", property = " ", one = @One(select = " "))
@Many代替标签 <collection>,是多表查询的关键,在注解中用来指定子查询返回对象集合。
同样属性为 select,使用时 @Result(column = " ", property = " ", many = @Many(select = " "))
2.1 一对一查询

查找订单信息及每个订单所属的用户。

SQL 语句

SELECT * FROM orders; 
SELECT * FROM user WHERE id = #{订单的uid};

接口及注解

// 订单的接口
public interface OrderMapper { 
    // 先查找订单信息
    @Select("SELECT * FROM orders")
    // 结果封装,相当于<resultMap>标签
    // 如果只有一个属性与字段名不一致,需要添加映射,则只添加一个 @Result即可
    @Results({
        // column - 数据库中的字段名;property - 实体类的属性
        // id = true - 表示当前字段是数据库中的主键
        @Result(id = true, column = "id", property = "id"), 
        @Result(column = "ordertime", property = "orderTime"), 
        @Result(column = "total", property = "money"), 
        // javaType - 表示当前封装到的类型
        @Result(property = "user", javaType = User.class, 
                // column - 是传递给后面 one/many 中操作的主键,也即当前表的外键,传递给后续 SQL 操作的参数
                // @One 注解的参数 select 的值是另一个 SQL 的全路径,就是接口中的某方法
                // 此处其实是拿到findById方法返回的数据,如果User类属性与表中字段不匹配,则需要在findById的属性添加results注解完成映射
                column = "uid", one = @One(select = "com.chrisniu.mapper.UserMapper.findById", 
                           				  fetchType = FetchType.EAGER))
    })
    public List<Order> findAllWithUser(); 
}

// 用户的接口
public interface UserMapper { 
    @Select("SELECT * FROM user WHERE id = #{id}")
    public User findById(int id); 
}
2.2 一对多查询

查找用户信息及每个用户拥有的订单。

SQL 语句

SELECT * FROM user; 
SELECT * FROM orders WHERE uid = #{用户id};

接口及注解

public interface UserMapper { 
    // 先查找用户信息
    @Select("SELECT * FROM user") 
    @Results({ 
        @Result(id = true, column = "id", property = "id"), 
        @Result(column = "username", property = "username"), 
        @Result(column = "brithday", property = "brithday"), 
        @Result(column = "sex", property = "sex"), 
        @Result(column = "address", property = "address"), 
        // javaType是一个List的类别
        @Result(property = "orderList", javaType = List.class, column = "id" , 
                many = @Many(select = "com.chrisniu.mapper.OrderMapper.findByUserId")) 
    })
    public List<User> findAllWithOrder(); 
}

public interface OrderMapper { 
    @Select("SELECT * FROM orders WHERE uid = #{uid}") 
    public List<Order> findByUserId(int uid); 
}
2.3 多对多查询

查找订单信息及每个订单所属的用户。

SQL 语句

SELECT * FROM user; 
SELECT * FROM sys_role r INNER JOIN sys_user_role ur 
	ON r.id = ur.userid WHERE ur.userid = #{用户id};

接口及注解

public interface UserMapper { 
    @Select("SELECT * FROM user") 
    @Results({ 
        @Result(id = true, column = "id", property = "id"), 
        @Result(column = "brithday", property = "brithday"), 
        @Result(column = "sex", property = "sex"), 
        @Result(column = "address", property = "address"), 
        @Result(property = "roleList", javaType = List.class, column = "id" , 
                many = @Many(select = "com.lagou.mapper.RoleMapper.findByUserId")) 
    })
    public List<User> findAllWithRole(); 
}

public interface RoleMapper { 
    @Select("SELECT * FROM role r INNER JOIN user_role ur ON r.id = ur.rid WHERE ur.uid = #{uid}") 
    public List<Role> findByUid(Integer uid); 
}

基于注解的二级缓存

  1. 首先,核心配置文件开启二级缓存的支持。

    <settings> 
        <!-- ,所以这一步可以省略不配置。 
             值为true代表开启二级缓存;为false代表不开启二级缓存。 --> 
        <setting name="cacheEnabled" value="true"/> 
    </settings>
    
  2. 然后,在 Mapper 接口中使用注解配置二级缓存。

    @CacheNamespace
    public interface UserMapper {...}
    

基于注解的延迟加载

不论在 @One 还是 @Many 注解中,都有 fetchType 属性。

fetchType = FetchType.LAZY      // 表示懒加载
fetchType = FetchType.EAGER     // 表示立即加载
fetchType = FetchType.DEFAULT   // 表示使用全局的延迟加载配置

XML 配置 与 注解开发 的优劣分析

  1. 注解开发和 xml 配置相比,从开发效率来说,注解编写更简单,效率更高。

  2. 从可维护性来说,注解如果要修改,必须修改源码,会导致维护成本增加;而 xml 维护性更强。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值