MyBatis详解

1.MyBatis介绍

1.1 什么是框架

框架(Framework)是一个框子——指其约束性,也是一个架子——指其支撑性。是一个基本概念上的结构,用于去解决或者处理复杂的问题。

框架是一个半成品,已经对基础的代码进行了封装并提供相应的API,开发者在使用框架是直接调用封装好的API可以省去很多代码编写,从而提高工作效率和开发速度。

1.2 什么是MyBatis

MyBatis 本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
  MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注SQL本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。

Mybatis

1.3 为什么要使用MyBatis?

  • 传统JDBC方式存在的缺陷:
    JDBC缺陷
  1. 使用传统方式JDBC访问数据库:
    (1)使用JDBC访问数据库有大量重复代码(比如注册驱动、获取连接、获取传输器、释放资源等);
    (2)JDBC自身没有连接池,会频繁的创建连接和关闭连接,效率低;
    (3)SQL是写死在程序中,一旦修改SQL,需要对类重新编译
    (4)对查询SQL执行后返回的ResultSet对象,需要手动处理,有时会特别麻烦;

  2. 使用mybatis框架访问数据库:
    (1)Mybatis对JDBC对了封装,可以简化JDBC代码
    (2)Mybatis自身支持连接池(也可以配置其他的连接池),因此可以提高程序的效率;
    (3)Mybatis是将SQL配置在mapper文件中,修改SQL只是修改配置文件,类不需要重新编译
    (4)对查询SQL执行后返回的ResultSet对象,Mybatis会帮我们处理,转换成Java对象。
      …

总之,JDBC中所有的问题(代码繁琐、有太多重复代码、需要操作太多对象、释放资源、对结果的处理太麻烦等),在Mybatis框架中几乎都得到了解决!

2.MyBatis入门案例

2.1 案例项目结构

项目结构

2.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.by</groupId>
    <artifactId>MyBatis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
    <!-- 项目编译JDK版本 -->
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <!-- 项目源码及编译输出的编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
        </dependency>
    </dependencies>
    <build>
        <!-- 如果不添加此节点src/main/java目录下的所有配置文件都会被漏掉。 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>

</project>

2.3 log4j.properties

# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

2.4 pojo

package com.by.pojo;

import lombok.Data;

import java.util.Date;

@Data
public class User {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

}

2.5 UserDao

public interface UserDao {
    public List<User> findAll();
}

2.6 UserDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:隔离sql,一般是接口名称的全类名-->
<mapper namespace="com.by.dao.UserDao">
    <!--
      id:和接口方法名保持一致
      resultType:和接口返回类型保持一致
     -->
    <select id="findAll" resultType="com.by.pojo.User">
        select * from user
    </select>
</mapper>

2.7 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>
    <!-- 和spring整合后 environments配置将废除-->
    <environments default="dev">
        <!-- dev环境 -->
        <environment id="dev">
            <!-- 配置事务的类型:type="JDBC | MANAGED" 两种方式
                 JDBC:表示使用JDBC中原生的事务管理方式
                 MANAGED:被管理,例如Spring
			-->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置连接数据库的信息:用的是数据源(连接池) -->
            <dataSource type="POOLED">
                 <!-- mysql5 -->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 告知 mybatis 映射配置的位置 -->
    <mappers>
        <mapper resource="com/by/dao/UserDao.xml"/>
    </mappers>
</configuration>
  • 注意mysql8的driver和url
<!-- mysql8 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&serverTimezone=Asia/Shanghai"/>

2.8 测试

public class MyBatisTest {

    private InputStream inputStream;
    private SqlSession sqlSession;

    @Before
    public void createSqlSession() throws IOException {
        // 加载配置文件
        String resource = "mybatis-config.xml";
        inputStream = Resources.getResourceAsStream(resource);

        // 创建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = 
            				new SqlSessionFactoryBuilder().build(inputStream);
        //获得数据库会话实例
        sqlSession = sqlSessionFactory.openSession();
    }

    @Test
    public void testFindAll(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        List<User> userList = userDao.findAll();
        for(User user : userList) {
            System.out.println(user);
        }
    }

    @After
    public void closeSqlSession() throws IOException {
        sqlSession.close();
        inputStream.close();
    }
}
  • 输出结果
DEBUG [main] - Created connection 1259639178.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14918a]
DEBUG [main] - ==>  Preparing: SELECT * FROM user 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 4
User(id=2, username=玛卡巴卡, birthday=null, sex=未知, address=花园宝宝)
User(id=7, username=唔西迪西, birthday=null, sex=未知, address=宝宝花园)
User(id=8, username=八个雅鹿, birthday=null, sex=未知, address=银河系)
User(id=9, username=张三丰, birthday=null, sex=男, address=湖南)

3.MyBatis执行流程

我们已经通过案例体验到了mybatis的魅力。现在来梳理一下MyBatis运行时的几个对象,我们需要搞清楚他们的作用,进而需要理解mybatis的整个工作流程和执行原理。

  • Resources

    加载配置文件,有一种是使用类加载进行加载,我们通过这个类的类加载器进行资源的加载。

  • SqlSessionFactoryBuilder

    构建SqlSessionFactory工厂对象需要的对象。采用了构建者模式,屏蔽了对象构建的细节。

  • SqlSessionFactory

    创建SqlSession对象所用。使用工厂模式创建,目的就是解耦合。

  • SqlSession

    创建代理对象,使用了代理模式

  • Executor

    操作数据库

  • MappedStatement

    存储SQL语句、参数、输出结果类型
    执行流程

4.CRUD

4.1 查询

查询时大多数场景都需要通过传递参数进行条件查询,使用jdbc时可以通过占位符实现参数绑定,那么MyBatis中要怎么实现参数绑定呢?

在MyBatis根据查询条件以及需求的不同可分为多种绑定方式。

4.1.1 单个参数绑定

  • id=“findUserById” 映射的方法名
  • parameterType=“java.lang.Integer” 参数的类型
  • resultType=“com.by.pojo.User” 返回值类型,会自动将查询结构装填到user对象中
    //单个参数传递
    public User findUserById(Integer id);
    <!--
        parameterType:指定输入参数的类型
        resultType:指定数据结果封装的数据类型
		#{id}:它代表占位符,相当于原来 jdbc 部分所学的?,都是用于替换实际的数据。
    -->
    <select id="findUserById" parameterType="java.lang.Integer" 
            				  resultType="com.by.pojo.User" >
        select * from user where id=#{id}<!--只有一个参数时,#{任意书写}-->
    </select>
    @Test
    public void testFindUserById(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user = userDao.findUserById(41);
        System.out.println(user);
    }

4.1.2 序号参数绑定

参数顺序不能随意改变,严格按照形参声明顺序

    //传递多个参数
    public User findUserByIdAndName(Integer id, String username);
     <select id="findUserByIdAndName" resultType="com.by.pojo.User" >
        SELECT * FROM user
        WHERE id = #{arg0} AND username = #{arg1} <!--arg0 arg1 arg2 ...-->
    </select>
	<select id="findUserByIdAndName" resultType="com.by.pojo.User" >
        SELECT * FROM user
        WHERE id = #{param1} AND username = #{param2} <!--param1 param2 param3 ...-->
    </select>
    @Test
    public void testFindUserByIdAndName(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user = userDao.findUserByIdAndName(41,"张三丰");
        System.out.println(user);
    }

4.1.3 注解参数绑定(推荐)

可随意改变参数顺序,不必遵守形参声明时的顺序

    //传递多个参数
    public User findUserByIdAndName2(@Param("id") Integer id,
                                     @Param("username")String username);
    <select id="findUserByIdAndName2" resultType="com.by.pojo.User" >
        SELECT * FROM user
        WHERE id = #{id} AND username = #{username}
    </select>
    @Test
    public void testFindUserByIdAndName2(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user = userDao.findUserByIdAndName2(41,"张三丰");
        System.out.println(user);
    }

4.1.4 对象参数绑定(推荐)

当参数较多且属于同一个对象时,可将该对象作为实参传递

    //使用对象属性进行参数绑定
    public User findUserByUserInfo(User user);
    <select id="findUserByUserInfo" parameterType="com.by.pojo.User" 
            									resultType="com.by.pojo.User">
        SELECT * FROM user
        WHERE id = #{id} AND username = #{username}<!--参数为对象时,#{属性名}-->
    </select>
    @Test
    public void testFindUserByName(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User userInfo = new User();
        userInfo.setId(41);
        userInfo.setUsername("张三丰");
        User user = userDao.findUserByUserInfo(userInfo);
        System.out.println(user);
    }

4.1.5 Map参数绑定

当参数较多且不属于同一对象,可存在map集合中传递,与传递对象类似

	//使用Map进行参数绑定
	public User findUserByMap(Map<String, Object> map);
    <select id="findUserByMap" 
            parameterMap="java.util.Map" resultType="com.by.pojo.User">
        SELECT * FROM user
        WHERE id = #{id} AND username = #{username}
    </select>
    @Test
    public void testFindUserByMap(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("id",41);
        map.put("username","张三丰");
        User user = userDao.findUserByMap(map);
        System.out.println(user);
    }

4.1.6 模糊查询

    //模糊查询
    public  List<User> findUserByName(String username);
    <select id="findUserByName" parameterType="string" resultType="com.by.pojo.User">
    	<!-- 方式1 -->
        <!-- select * from user where username like concat('%',#{username},'%') -->
        
        <!-- 方式2-->
        select * from user where username like '%${value}%'<!--${}括号中只能是value-->
    </select>
    @Test
    public void testFindUserByName(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        List<User> userList = userDao.findUserByName("张");
        for (User user : userList) {
            System.out.println(user);
        }
    }

4.1.7 sql注入

    //sql注入
    public User login(User user);
    <select id="login" parameterType="com.by.pojo.User" resultType="com.by.pojo.User">
        select * from user where username = '${username}' and password = '${password}'
    </select>
    @Test
    public void testLogin(){
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User userInfo = new User();
        userInfo.setUsername("张三丰' #");
        userInfo.setPassword("123");
        User user = userDao.login(userInfo);
        System.out.println(user);
    }

SQL注入
当在username值中加上' #,#后边的语句会被注释,因此登录的判断条件就变成了只有username符合就行,数据库数据非常危险,这种行为就称为SQL注入。

  • #{} 和${}的区别:
符号传单个简单类型时的参数名类型转换底层sql注入
#{}任意名称自动进行 java 类型和 jdbc 类型转换PreparedStatement防止
${}只能是value不会转换Statement不防止

4.1.8 聚合函数

  • 查询记录数
//聚合函数查询
public Integer getTotal();
<!--聚合函数查询-->
<select id="getTotal" resultType="int">
    SELECT COUNT(id) FROM user
</select>
@Test
public void testGetTotal(){
    Integer total = userDao.getTotal();
    System.out.println(total);
}

4.2 添加

  • userMapper(userDao)接口
void addUser(User user);    //添加
  • userMapper.xml
<insert id="addUser" parameterType="com.by.pojo.User" useGeneratedKeys="true" keyProperty="id">
	<!--
         主键回填:新增之后,获取新增记录的id值
         keyProperty="id":主键对应实体类的属性
         useGeneratedKeys="true"   主键自动回填到新增的对象中
         parameterType="com.by.pojo.User"   对象参数的类型
     -->
    INSERT INTO user (username,password, birthday , sex, address)
    values (#{username},#{password},#{birthday},#{sex},#{address})
</insert>
  • Test
    在创建sqlSession对象时,sqlSession = sqlSessionFactory.openSession(true);使用有参构造,实参为true,意为设置不自动回滚
@Test
public void test(){
	//Lombok链式传值
	addUser(new User().setUsername("熏悟空").setPassword("111").setSex("男").setAddress("花果山").setBirthday(new Date()));  
}
public void addUser(User user){
    userMapper.addUser(user);
    sqlSession.commit();      //添加后需要提交更改
    System.out.println("新增的记录id:"+user.getId());   //主键回填到了id属性中
}

4.3 修改

  • userMapper接口
void updateUserById(User user);
  • userMapper.xml
<update id="updateUserById" parameterType="com.by.pojo.User" >
    UPDATE user SET username = #{username},password = #{password},birthday=#{birthday},sex=#{sex},address=#{address} Where id=#{id}
</update>
  • Test
@Test
public void test(){
	//Lombok链式传值
	updateUserById(new User().setId(51).setPassword("111").setUsername("猪肛裂").setAddress("高老庄").setSex("男").setBirthday(new Date()));
}
private void updateUserById(User user) {
    userMapper.updateUserById(user);
    sqlSession.commit(); 		//修改后需要提交更改
}

4.4 删除

  • userMapper接口
void delUserById(Integer id);
  • userMapper.xml
<delete id="delUserById" parameterType="java.lang.Integer">
    DELETE FROM user where id=#{id};
</delete>
  • Test
@Test
public void test(){
	delUserById(50); 
}
public void delUserById(Integer id){
    userMapper.delUserById(id);
    sqlSession.commit(); 		//删除后需要提交更改
}

5.ORM映射

5.1 什么是ORM映射

定义:对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。

MyBatis只能自动维护库表”列名“与”属性名“相同时的对应关系,二者不同时无法自动ORM,如下:
ORM映射

5.2 列的别名

在SQL中使用 as 为查询字段添加列别名,以匹配属性名:

 <select id="findAll" resultType="com.by.pojo.Role" >
     select id, role_name as roleName, role_desc as roleDesc from role
 </select>

思考: 如果我们的查询很多,都使用别名的话写起来岂不是很麻烦,有没有别的解决办法呢?

5.3 结果映射

使用ResultMap标签手动映射,解决实体字段和数据表字段不一致的问题

 <!--
    id:和select查询标签的返回值保持一致
    type: 映射实体的全类名
-->
<resultMap id="findAllResultMap" type="com.by.pojo.Role">
    <id property="id" column="id" />

    <!--
        描述非主键字段的映射关系:
            property:实体类的属性
         column:数据表字段名称
    -->
   
    <result column="ROLE_NAME" property="roleName"/>
    <result column="ROLE_DESC" property="roleDesc"/>
</resultMap>
<select id="findAll" resultMap="findAllResultMap" >
    select * from role
</select>

6.配置文件

6.1 properties标签

  1. 我们一般会把数据库配置信息定义在一个独立的配置文件里面,比如db.properties:
   jdbc.driver=com.mysql.jdbc.Driver
   jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=UTF-8
   jdbc.username=root
   jdbc.password=1111

那么我们如何在mybatis的核心配置文件里面加载外部的数据库配置信息呢?

  1. 在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>
   <!--引入db.properties-->
   <properties resource="db.properties"></properties>
   <environments default="mysql">
       <environment id="mysql">
           <transactionManager type="JDBC"></transactionManager>
           <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="com/by/dao/UserDao.xml"/>
   </mappers>
</configuration>

6.2 typeAliases标签

查看mybatis源码可以看到 Mybatis 默认支持的别名:

type

我们也可以为实体类定义别名,提高书写效率:

  1. 定义别名
<?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>
    <typeAliases>
        <!--定义单个别名-->
        <typeAlias type="com.by.pojo.User" alias="user"></typeAlias>
        
        <!--批量定义别名-->
        <!--pojo包内所有类可直接使用小写类名代替-->
        <package name="com.by.pojo"></package>
    </typeAliases>
</configuration>
  1. 使用别名
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.by.dao.UserDao">
   <!--使用别名-->
   <select id="findAll" resultType="user">
       select * from user
   </select>
</mapper>
  1. debug查看注册的别名

别名

6.3 Mappers标签

Mappers标签的作用是用来在核心配置文件里面引入映射文件,引入方式有如下三种:

  1. 使用mapper映射文件的路径
<mappers>
    <mapper resource="com/by/dao/UserDao.xml"/>
</mappers>
  1. 使用mapper接口的路径
<mappers>
    <mapper class="com.by.dao.UserDao"></mapper>
</mappers>

注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同

  1. 使用mapper接口的包名批量引入
<mappers>
    <package name="com.by.dao"></package>
</mappers>

注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同

7.关联查询

7.1 什么是关联查询

  • 实体间的关系(拥有 has、属于 belong)

    • OneToOne:一对一关系(account ←→ user)

    • OneToMany:一对多关系(user ←→ account)

    • ManyToMany:多对多关系(user ←→ role)

  • 什么是关联查询

    关联查询,也称为多表查询,指的是在查询数据时,需要的数据不只在一张表中,可能在两张或多张表中。为了获取这些数据,需要同时操作这些表来查询数据。

  • 关联查询的语法

    指定“一方”关系时(对象),使用<association javaType="" >

    指定“多方”关系时(集合),使用<collection ofType="" >

7.2 一对一查询

需求:查询账户信息,关联查询用户信息。

分析:因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。

  • 实体类:Account
@Data
public class Account implements Serializable {

    private Integer id;
    private Integer uid;
    private Double money;
    //加入User类的对象作为Account类的一个属性
    private User user;

}
  • 接口:AccountMapper
public interface AccountMapper {
    List<Account> findAll();
}
  • 映射:AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.by.mapper.AccountMapper">
	<!-- 结果映射 -->
    <resultMap id="findAllResultMap" type="account">
        <id column="aid" property="id"/>
        <result column="uid" property="uid"/>
        <result column="money" property="money"/>
        <!-- 指定关系表中数据的封装规则 -->
        <association javaType="user" property="user">
            <id column="id" property="id"/>
            <result column="username" property="username"/>
            <result column="address" property="address"/>
            <result column="password" property="password"/>
            <result column="sex" property="sex"/>
            <result column="birthday" property="birthday"/>
        </association>
    </resultMap>
    <select id="findAll" resultMap="findAllResultMap">
        SELECT a.id aid,a.uid,a.money,u.* FROM account a,user u where uid=u.id
    </select>
</mapper>

7.3 一对多查询

需求:查询所有用户信息及用户关联的账户信息。

分析:用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息查询出来,此时左外连接查询比较合适。

  • 实体类:User
@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private Date birthday;
    private String sex;
    private String address;
    //加入List<Account>存储用户所拥有的账户
    private List<Account> accountList;
}
  • 接口:UserMapper
public interface UserMapper {
    List<User> findAll();
}
  • 映射:UserMapper.xml
<resultMap id="findAllResultMap" type="user">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="address" property="address"/>
    <result column="password" property="password"/>
    <result column="sex" property="sex"/>
    <result column="birthday" property="birthday"/>
    <!-- collection 是用于建立一对多中集合属性的对应关系
        ofType 用于指定集合元素的数据类型-->
    <collection property="accountList" ofType="account">
        <id column="aid" property="id"/>
        <result column="uid" property="uid"/>
        <result column="money" property="money"/>
    </collection>
</resultMap>
<!-- 配置sql语句使用左连接查询指定数据 -->
<select id="findAll" resultMap="findAllResultMap">
    SELECT u.*,a.id aid,a.uid,a.money FROM user u LEFT JOIN account a ON uid=u.id
</select>

7.4 多对多查询

需求:查询角色及角色赋予的用户信息。

分析:一个用户可以拥有多个角色,一个角色也可以赋予多个用户,用户和角色为双向的一对多关系,多对多关系其实我们看成是双向的一对多关系。

多对多

  • 实体类:Role
@Data
public class Role {
    private Integer id;
    private String roleName;
    private String roleDesc;
    //加入List<User> userList存储角色赋予的用户信息
    private List<User> userList;
}
  • 接口:RoleMapper
public interface RoleMapper {
    List<Role> findAll();
}
  • 映射:RoleMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.by.mapper.RoleMapper">
	<!--定义 role 表的 ResultMap-->
    <resultMap id="findAllRoleAndUserResultMap" type="role">
        <id column="rid" property="id"/>
        <result column="ROLE_NAME" property="roleName"/>
        <result column="ROLE_DESC" property="roleDesc"/>
        <collection property="userList" ofType="user">
            <id column="id" property="id"/>
            <result column="username" property="username"/>
            <result column="address" property="address"/>
            <result column="password" property="password"/>
            <result column="sex" property="sex"/>
            <result column="birthday" property="birthday"/>
        </collection>
    </resultMap>
    <!--查询所有role-->
    <select id="findAllRoleAndUser" resultMap="findAllRoleAndUserResultMap">
        SELECT r.id rid,r.ROLE_NAME,r.ROLE_DESC,u.* FROM role r LEFT JOIN user_role ON rid=r.id LEFT JOIN user u ON uid=u.id
    </select>
</mapper>

8.延迟加载

8.1 什么是延迟加载?

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

好处:

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

坏处

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

延迟加载和立即加载:

延迟加载:只有在使用到了对应的数据才进行查询,否则不进行查询。
立即加载:不管是否使用到了,只要一调用方法,就会立马查询出来。

8.2 一对一关联查询的延迟加载

  • 需求:通过账户id查找账户信息,以及该账户所属用户的信息

  • AccountMapper接口:

public interface AccountMapper {
    Account findAccountById(Integer id);
}
  • AccountMapper.xml映射:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.by.mapper.AccountMapper">
	<resultMap id="findAccountByIdResultMap" type="account">
        <id column="id" property="id"/>
        <result column="uid" property="uid"/>
        <result column="money" property="money"/>
         <!--
          property:属性名
          javaType:对象的类型
          select: 要调用的 select 映射的 id
          column : 传递给 select 映射的参数
          fetchType="lazy":懒加载,默认情况下是没有开启延迟加载的,局部配置
         -->
        <association
                property="user"
                javaType="com.by.pojo.User"
                select="com.by.mapper.UserMapper.findUserById"
                fetchType="lazy"
                column="uid"
        />
    </resultMap>
    <select id="findAccountById" resultMap="findAccountByIdResultMap" parameterType="int">
        select * from account where  id=#{id}
    </select>
</mapper>
  • 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="com.by.mapper.UserMapper">
	<select id="findUserById" resultType="com.by.pojo.User" parameterType="java.lang.Integer" >
        SELECT * FROM user WHERE id=#{id}
    </select>
</mapper>
  • 测试
 @Test
    public void testFindAccountById() {
    	//获得代理对象
        AccountMapper accountMapper = sqlSession.getMapper(AccountMapper.class);
        //执行方法,获取数据
        Account account = accountMapper.findAccountById(id);	

		System.out.println(account.getId())   //只访问id,不会读取user的数据

		System.out.println(account.getsUser())  //访问user时才会从数据库读取user数据
    }

8.3 一对多关联查询的延迟加载

  • 需求:根据用户id查询用户信息,以及该用户拥有的所有账户信息
  • UserMapper接口:
public interface UserMapper {
	User findUserById(Integer id);
}
  • 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="com.by.mapper.UserMapper">
	<resultMap id="findUserByIdResultMap" type="user">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="address" property="address"/>
        <result column="password" property="password"/>
        <result column="sex" property="sex"/>
        <result column="birthday" property="birthday"/>
        <!--
          property:属性名
          ofType:泛型
          select: 要调用的 select 映射的 id
          column : 传递给 select 映射的参数
          fetchType="lazy":懒加载,默认情况下是没有开启延迟加载的,局部配置
         -->
        <collection
                property="accountList"
                ofType="account"
                select="com.by.mapper.AccountMapper.findAccountByUid"
                fetchType="lazy"
                column="id"
        />
	</resultMap>
	<select id="findUserById" resultMap="findUserByIdResultMap" parameterType="int">
        select * from user where id=#{id}
    </select>
</mapper>
  • AccountMapper.xml映射:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.by.mapper.AccountMapper">
	<select id="findAccountByUid" resultType="user" parameterType="int">
        select * from account where uid=#{uid}
    </select>
</mapper>
  • 测试
 @Test
    public void testFindAll() {
        UserMapper userMapper = sqlSession.getMapper(UserMapper .class);
        User user = userMapper.findUserById(53);

		System.out.println(user.getUsername());		//只访问username,不会加载account的数据
		System.out.println(user.getAccountList()) 	//访问到AccountList时,才会执行获取account对应SQL语句,到数据库读取数据
    }

8.4 全局延迟加载

开启全局加载后,SQL操作都会变成懒加载模式,只有用到时才会执行对应SQL,读取数据。

推荐使用fetchType=“lazy”,写在标签上增加可读性。

<!-- 全局配置延迟加载策略 -->
    <settings>
        <!--  打开延迟加载的开关  -->
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>

9.动态SQL

9.1 什么是动态SQL?

MyBatis中的动态SQL是一种可以根据不同条件生成不同SQL语句的技术。它允许我们在映射文件中编写灵活的SQL语句,以便根据参数的不同情况来动态生成SQL语句。这种灵活性使得我们能够根据应用程序的需求来构建动态的查询语句。

9.2 if标签

作用:条件判断

test 属性:逻辑表达式完成条件查询

模拟筛选功能,可以根据 username 和 sex 来查询数据。
首先不使用 动态SQL 来书写

<select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.by.pojo.User">

    select * from user where username=#{username} and sex=#{sex}
    
</select>

上面的查询语句,我们可以发现,如果 #{username} 为空,那么查询结果也是空,如何解决这个问题呢?可以使用 if 标签来判断

<select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User">
    select * from user where
        <if test="username != null and username != ''">
           username=#{username}
        </if>
         
        <if test="sex != null and sex != ''">
           and sex=#{sex}
        </if>
</select>

我们发现,当sex为空时可以正常执行单条件查询,但是username为空时SQL会变成select * from user where and sex=#{sex} ,多了个and,这直接就报错了,那么如何解决这个问题呢?可做如下修改

<select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User">
    select * from user where 1=1
        <if test="username != null and username != ''">
           and username=#{username}
        </if>
         
        <if test="sex != null and sex != ''">
           and sex=#{sex}
        </if>
</select>

在where关键字后边加个 1=1就可以处理前面多的and,且不影响结果。

使用if标签时,每次都要添加一句1=1岂不是特别繁琐,如何解决?请看where。

9.3 where标签

作用:
1. 替换where关键字
2. 会动态的去掉第一个条件前的 and
3. 如果所有的参数没有值则不加where关键字

<select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User">
    select * from user
    <where>
        <if test="username != null and username != ''">
           username=#{username}
        </if>
         
        <if test="sex != null and sex != ''">
           and sex=#{sex}
        </if>
    </where>
</select>

9.4 set标签

作用:set标签用于动态包含需要更新的列, 替换set关键字,并会删掉额外的逗号

需求:当要修改某用户的数据时,如果某字段要修改的值为空,如何不对该字段进行修改,只修改其他有数据的字段?

首先不使用set标签进行实现

<!-- 根据 id 更新 user 表的数据 -->
<update id="updateUserById" parameterType="com.ys.po.User">
    update user u set
         <if test="username != null and username != ''">
             u.username = #{username},
         </if>
         <if test="sex != null and sex != ''">
             u.sex = #{sex}
         </if>
     
     where id=#{id}
</update>

当sex为空时不修改sex,只修改username字段这时拼接的SQL为:

update user u set u.username = #{username}, where id=#{id}

可见多了一个"," SQL语句直接无法运行,接下来使用set标签

<!-- 根据 id 更新 user 表的数据 -->
<update id="updateUserById" parameterType="com.ys.po.User">
    update user u
        <set>
            <if test="username != null and username != ''">
                u.username = #{username},
            </if>
            <if test="sex != null and sex != ''">
                u.sex = #{sex}
            </if>
        </set>
     
     where id=#{id}
</update>

set会自动去除多余的","且会自动判断是否需要set关键字。

9.5 trim标签

作用:trim标签可以代替where标签、set标签,并且可实现其特有功能。

  • 属性及作用:
属性作用
prefix添加一个字符串前缀,通常用于添加特定的字符或标记。
suffix添加一个字符串后缀,常用于添加特定的字符或标记。
prefixOverrides移除一个字符串前缀,当字符串以该前缀开头时,该前缀将被移除。
suffixOverrides移除一个字符串后缀,在字符串以该后缀结尾时,该后缀将被移除。
  • 新增数据
<!--示例:INSERT INTO user (username,sex) values(#{username},#{sex})-->

<insert id="addUserBy" parameterType="user" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user
    	<!-- 
        		添加前缀:(
        	 	添加后缀:)
        	 	移除后缀:,
        -->
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="username != null and username != ''">
                username,
            </if>
            <if test="sex != null and sex != ''">
                sex,
            </if>
        </trim>
        <!-- 
        		添加前缀:values(
        	 	添加后缀:)
        	 	移除后缀:,
        -->
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="sex != null and sex != ''">
                sex=#{sex},
            </if>
        </trim>
</insert>

9.6 foreach标签

作用:foreach标签实现循环逻辑,可以进行一个集合的遍历。

  • 属性和作用:
属性作用
collection取值list、array、map、@Param(“keyName”)、对象的属性名
item循环取出的具体对象
open起始符
close结束符
separator分隔符
  • mapper
  //批量删除
    public void delUserByIdList(@Param("idList") List<Integer> idList);
	//批量添加
    public void addUserList(@Param("userList") List<User> userList);
  • 批量新增记录
<insert id="addUserList" parameterType="list" >
    INSERT INTO user (username,sex) values
    <foreach collection="userList" item="user"  separator=",">
        (#{user.username},#{user.sex})
    </foreach>
</insert>
  • 批量删除记录
<delete id="delUserByIdList" parameterType="list">
     delete from user where id in
     <foreach collection="idList" item="id" open="(" close=")" separator=",">
         #{id}
     </foreach>
</delete>
  • 测试
@Test
public void addUserList(){
	UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<User> userList = new ArrayList<>();
	userList.add(new User().setUsername("大黄").setSex("男"));
    userList.add(new User().setUsername("小白").setSex("女"));
    userMapper.addUserList(userList);
}
@Test
public void delUserByIdList(){
	UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<Integer> idList = new ArrayList<>();
    idList.add(68);
    idList.add(69);
    userMapper.delUserByIdList(idList);
}
  • 测试结果
    批量添加
    批量删除

9.7 sql标签

作用:sql元素标签用来定义可重复使用的SQL代码片段

 //复杂条件查询
    public List<User> findByUser(User user);
  • mapper
 <!-- 定义SQL片段 -->
    <sql id="query_user_where">
        <if test="username!=null and username != ''">
            and username=#{username}
        </if>
        <if test="birthday!=null">
            and birthday=#{birthday}
        </if>
        <if test="sex!=null and sex != ''">
            and sex=#{sex}
        </if>
        <if test="address!=null and address != ''">
            and address=#{address}
        </if>
    </sql>

 <!-- 引入SQL片段 -->
    <select id="findByUser3" resultType="User">
        select * from user
        <where>
         <!-- 使用include标签引入SQL片段 -->
            <include refid="query_user_where"></include>
        </where>
    </select>

10.缓存

10.1 缓存介绍

  • 为什么使用缓存?

    首次访问时,查询数据库,并将数据存储到内存中;再次访问时直接访问缓存,减少IO、硬盘读写次数、提高效率

  • Mybatis中的一级缓存和二级缓存?

    • 一级缓存:

      它指的是mybatis中的SqlSession对象的缓存。当我们执行完查询之后,查询的结果会同时存在在SqlSession为我们提供的一块区域中。当我们再次查询同样的数据,mybatis会先去SqlSession中查询是否有,有的话直接拿出来使用。当SqlSession对象消失时,Mybatis的一级缓存也就消失了。

    • 二级缓存:

      它指的是Mybatis中SqlSessionFactory对象的缓存,由同一个SqlSessioFactory对象创建的SqlSession共享其缓存。

10.2一级缓存

一级缓存也叫本地缓存:与数据库同一次会话期间查询到的数据会放在本地缓存中。

以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;一级缓存就是一个Map

  • 测试一级缓存
    @Test
    public void testFindUserById() throws Exception{
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserDao userDao = sqlSession1.getMapper(UserDao.class);
        User user1 = userDao.findUserById(41);//执行SQL
        System.out.println("第一次查询:" + user1);
        User user2 = userDao.findUserById(41);//不执行SQL
        System.out.println("第二次查询:" + user2);

        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        userDao = sqlSession2.getMapper(UserDao.class);
        User user3 = userDao.findUserById(41);//执行SQL
        System.out.println("第三次查询:" + user1);
    }
  • 分析

一级缓存是SqlSession范围的缓存,当调用SqlSession的commit(),close()等方法时,就会清空一级缓存。

一级缓存

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

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

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

  • 测试清空一级缓存
    @Test
    public void testFindUserById() throws Exception{
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user1 = userDao.findUserById(41);//执行SQL
        System.out.println("第一次查询:" + user1);
        User user2 = userDao.findUserById(41);//不执行SQL
        System.out.println("第二次查询:" + user2);
        sqlSession.commit();   //执行commit()清空一级缓存
        
        User user3 = userDao.findUserById(41);//执行SQL
        System.out.println("第三次查询:" + user1);
    }

10.3 二级缓存

  • 开启二级缓存
  1. 在mybatis-config.xml 文件开启二级缓存
   <settings>
       <!-- 开启二级缓存的支持:默认值是ture,所以可省略 -->
       <setting name="cacheEnabled" value="true"/>
   </settings>
  1. 配置相关的Mapper映射文件
   <mapper namespace="com.by.dao.UserDao">
       <!-- 开启二级缓存的支持 -->
       <cache></cache>
  • 测试二级缓存
    @Test
    public void testSecondUserById(){
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserDao userDao = sqlSession1.getMapper(UserDao.class);
        User user1 = userDao.findUserById(41);//执行SQL
        System.out.println("第一次查询:" + user1);
        sqlSession1.commit();//二级缓存在sqlSession.commit()或者sqlSession.close()之后生效

        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
        User user2 = userDao2.findUserById(41);//不执行SQL
        System.out.println("第二次查询:" + user2);
    }
  • 分析

二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

二级缓存结构图:

二级缓存

  • 只有开启了二级缓存,在同一个Mapper下就有效
  • 所有的数据都会先放在一级缓存中;
  • 只有当会话提交,或者关闭的时候,才会提交到二级缓存中。
  • 测试清空二级缓存
    @Test
    public void testSecondUserById(){
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserDao userDao = sqlSession1.getMapper(UserDao.class);
        User user1 = userDao.findUserById(43);//执行SQL
        System.out.println("第一次查询:" + user1);
        sqlSession1.commit();

        SqlSession sqlSession3 = sqlSessionFactory.openSession();
        UserDao userDao3 = sqlSession3.getMapper(UserDao.class);
        userDao3.deleteUserById(41);//执行增删改操作会清除二级缓存,防止脏读
        sqlSession3.commit();

        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
        User user2 = userDao2.findUserById(43);执行SQL
        System.out.println("第二次查询:" + user2);
        sqlSession2.commit();
        sqlSession2.close();
    }

10.4 缓存总结

对比一级缓存二级缓存
范围sqlsessionsqlsessionFactory
配置默认开启<cache></cache>
走缓存同一个sqlsession同一个sqlsessionFactory
不走缓存不同sqlsession或两次查询之间执行了增删改级缓存不同sqlsessionFactory或两次查询之间执行了增删改
  • 48
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

这河里吗l

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值