Mybatis

1. 框架简介

1.1 三层架构

软件开发常用的架构是三层架构,之所以流行是因为有着清晰的任务划分,一般包括以下三层:

  • 持久层:主要完成与数据库相关的操作,即对数据库的增删改查。
    • 因为数据库访问的对象一般称为Data Access Object(简称DAO),所以有人把持久层叫做DAO层。
  • 业务层:主要根据功能需求完成业务逻辑的定义和实现。
    • 因为它主要是为上层提供服务的,所以有人把业务层叫做Service层或Business层。
  • 表现层:主要完成与最终软件使用用户的交互,需要有交互界面(UI)。
    • 因此,有人把表现层称之为web层或View层。

三层架构之间调用关系为:表现层调用业务层,业务层调用持久层。

各层之间必然要进行数据交互,我们一般使用Java实体对象来传递数据。

在这里插入图片描述

1.2 框架

1.2.1 什么是框架?

框架就是一套规范。既然是规范,你使用这个框架就要遵守这个框架所规定的约束。

框架可以理解为半成品软件,框架做好以后,接下来在它基础上进行开发。

1.2.2 为什么使用框架?

框架为我们封装好了一些冗余,且重用率低的代码。并且使用反射与动态代理机制,将代码实现了通用性,让开发人员把精力专注在核心的业务代码实现上。

1.2.3 常见的框架

企业中最常用的组合:Spring + Spring MVC + MyBatis(SSM)

  • 持久层框架:专注于解决数据持久化的框架。常用的有mybatis、hibernate、spring jdbc等等。
  • 表现层框架:专注于解决与用户交互的框架。常见的有struts2、spring mvc等等。
  • 全栈框架:能在各层都给出解决方案的框架。比较著名的就是spring。

2. Mybatis简介

2.1 Mybatis简介

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

mybatis历史

MyBatis 本是apache的一个开源项目iBatis, 2010年6月这个项目由apache software foundation 迁移 到了google code,随着开发团队转投到Google Code旗下,iBatis正式改名为MyBatis ,代码于2013 年11月迁移到Github。

Github地址:https://github.com/mybatis/mybatis-3/

2.2 ORM思想

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

O(对象模型): 实体对象,即我们在程序中根据数据库表结构建立的一个个实体JavaBean

R(关系型数据库的数据结构): 关系数据库领域的Relational(建立的数据库表)

M(映射): 从R(数据库)到O(对象模型)的映射,可通过XML文件映射

ORM作为一种思想,帮助我们跟踪实体的变化,并将实体的变化翻译成sql脚本,执行到数据库中去,也就是将实体的变化映射到了表的变化。

3. Mybatis快速入门

3.1 MyBatis开发步骤

MyBatis官方文档:http://www.mybatis.org/mybatis-3/

案例需求

通过mybatis查询数据库user表的所有记录,封装到User对象中,打印到控制台上

步骤分析
SQLyog
1. 创建mybatis_db数据库和user表
IDEA
2. 创建maven项目,导入依赖(MySQL、mybatis、junit)
3. 创建User实体类
4. 编写映射配置文件UserMapper.xml(ORM思想)
5. 编写核心配置文件SqlMapConfig.xml
	1.数据库环境配置
	2.映射关系配置的引入(引入映射配置文件的路径)
6. 编写测试类
	1.加载核心配置文件
	2.获取sqlSessionFactory工厂对象
	3.获取sqlSession会话对象
	4.执行sql
	5.打印结果
	6.释放资源
目录结构

在这里插入图片描述

3.2 代码实现

3.2.1 创建user数据表
CREATE DATABASE `mybatis_db`;

USE `mybatis_db`;

CREATE TABLE `user` (
`id` INT(11) NOT 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,'子慕','2020-11-11 00:00:00','男','北京海淀'),(2,'应颠','2020-12-12 00:00:00','男','北京海淀');
3.2.2 创建maven项目,导入依赖(MySQL、mybatis、junit)

pom.xml

<!--指定编码及版本-->
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.11</java.version>
    <maven.compiler.source>1.11</maven.compiler.source>
    <maven.compiler.target>1.11</maven.compiler.target>
</properties>

<!--引入相关依赖-->
<dependencies>
    <!--引入mybatis依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.4</version>
    </dependency>

    <!--引入mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>

    <!--引入junit-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>

</dependencies>
3.2.3 编写User实体
public class User {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    // getter/setter 略
}
3.2.4 编写UserMapper映射文件

约束头可去官方文档查看

namespace:Copy Reference(java目录下接口文件)

resultType:Copy Reference(java目录下JavaBean)

以上两步添加< typeAliases>标签后可起别名

<?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">
    <!--namespace : 命名空间:与id属性共同构成唯一标识 namespace.id: user.findAll Copy Reference
        resultType: 返回结果类型(自动映射封装):要封装的实体的全路径 Copy Reference
    -->

    <!-- 查询所有 -->
    <!-- SqlMapConfig.xml中typeAliases批量起别名 别名就是类名 不区分大小写 -->
    <!-- <select id="findAll" resultType="com.lagou.domain.User"> -->
    <select id="findAll" resultType="USER">
        select * from user
    </select>
</mapper>
3.2.5 编写MyBatis核心文件

约束头可去官方文档查看,标签要按官方文档顺序写,否则报错

resource:Copy Path From Source Root(resources目录下xml文件)

class:Copy Reference(java目录下接口文件)

name:Copy Reference(java目录下接口包名)

<?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"></properties>

    <!--设置别名-->
    <typeAliases>
        <!--方式一:给单个实体起别名-->
       <!-- <typeAlias type="com.lagou.domain.User" alias="user"></typeAlias>-->
        <!--方式二:批量起别名 别名就是类名,且不区分大小写-->
        <package name="com.lagou.domain"/>
    </typeAliases>


    <!--environments: 运行环境-->
    <environments default="development">
        <environment id="development">
                <!--当前的事务管理器是JDBC-->
            <transactionManager type="JDBC"></transactionManager>
                <!--数据源信息 POOLED:使用mybatis的连接池-->
            <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>
        <!-- Copy Path From Source Root -->
        <!-- <mapper resource="mapper\UserMapper.xml"></mapper>-->
        
        <!--使用该方式:接口和映射文件需要同包同名 Copy Reference-->
       	<!-- <mapper class="com.lagou.mapper.UserMapper"></mapper>-->

        <!--批量加载映射 Copy Reference-->
        <package name="com.lagou.mapper"/>
    </mappers>

</configuration>

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///mybatis_db?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=940412
3.2.6 编写测试类

MybatisTest

@Test
public void mybatisQuickStart() 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 : namespace.id
    List<User> users = sqlSession.selectList("userMapper.findAll");

    //5. 遍历打印结果
    for (User user : users) {
        System.out.println(user);
    }

    //6. 关闭资源
    sqlSession.close();
}

3.3 Mybatis增删改查

3.3.1 UserMapper.xml
<mapper namespace="userMapper">
    <!--namespace : 命名空间:与id属性共同构成唯一标识 namespace.id: user.findAll-->

    <!--新增用户-->
    <!--#{} : mybatis中的占位符,等同于JDBC中的?
        parameterType :指定接收到的参数类型 -->
    <insert id="saveUser" parameterType="user">
        insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})
    </insert>

    <!--更新用户-->
    <update id="updateUser" parameterType="user">
        update user set username = #{username},birthday = #{birthday},sex = #{sex},address = #{address} where id = #{id}
    </update>

    <!--删除用户 java.lang.Integer-->
    <!-- 只传一个参数时,如基本数据类型或String #{随便写} 因为{}里不论是什么 传进来的值都被赋给它 -->
    <delete id="deleteUser" parameterType="int">
        delete from user where id = #{abc}
    </delete>

</mapper>
3.3.2 MybatisTest
注意事项
  • Sql语句中使用#{实体属性名}方式引用实体中的属性值
  • 增删改操作涉及数据库数据变化,所以要使用sqlSession对象显式地提交事务,即 sqlSession.commit()
    • sqlSessionFactory.openSession(true); // 自动提交事务
/*
    测试新增用户
*/
@Test
public void testSave() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession(true); // 自动提交事务


    User user = new User();
    user.setUsername("自动提交事务");
    user.setBirthday(new Date());
    user.setSex("男");
    user.setAddress("北京海淀");

    sqlSession.insert("userMapper.saveUser", user);

    sqlSession.close();

}


/*
    测试更新用户
 */
@Test
public void testUpdate() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();


    User user = new User();
    user.setId(5);
    user.setUsername("lucy");
    user.setBirthday(new Date());
    user.setSex("女");
    user.setAddress("北京朝阳");

    sqlSession.update("userMapper.updateUser", user);

    // 手动提交事务
    sqlSession.commit();
    sqlSession.close();

}


/*
    测试删除用户
*/
@Test
public void testDelete() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    sqlSession.delete("userMapper.deleteUser", 4);

    // 手动提交事务
    sqlSession.commit();
    sqlSession.close();

}

4. Mybatis基本原理介绍

在这里插入图片描述

5. Mybatis的dao层开发使用

5.1 传统开发方式

步骤
  1. 编写UserMapper接口

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

    以查询操作为例:

    public class UserMapperImpl implements UserMapper {
    	@Override
    	public List<User> findAll() throws Exception {
    		// 加载配置文件
            InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
            // 获取SqlSessionFactory工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
            // 获取SqlSession会话对象
            SqlSession sqlSession = sqlSessionFactory.openSession();
            
            // 执行sql
            List<User> list = sqlSession.selectList("UserMapper.findAll");
            // 释放资源
            sqlSession.close();
            return list;
    	}
    }
    

    每个方法都要重复写以下步骤:

    // 加载配置文件
    InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
    // 获取SqlSessionFactory工厂对象
    SqlSessionFactory sqlSessionFactory = new
    SqlSessionFactoryBuilder().build(is);
    // 获取SqlSession会话对象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    
  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="user">
    		select * from user
    	</select>
    </mapper>
    
  4. 测试

    @Test
    public void testFindAll() throws Exception {
        // 创建UserMapper 实现类
        UserMapper userMapper = new UserMapperImpl();
        // 执行查询
        List<User> list = userMapper.findAll();
        for (User user : list) {
        	System.out.println(user);
        }
    }
    
问题
  • 实现类中,存在mybatis模板代码重复

  • 实现类调用方法时,xml中的sql statement 硬编码到java代码中

    List<User> list = sqlSession.selectList("UserMapper.findAll");
    

硬编码:是将数据直接嵌入到程序或其他可执行对象的源代码中的软件开发实践,与从外部获取数据或在运行时生成数据不同。 硬编码数据通常只能通过编辑源代码重新编译可执行文件来修改。

思考

能否只写接口,不写实现类,只编写接口和Mapper.xml即可?

  • 因为在dao(mapper)的实现类中,对sqlsession的使用方式很类似,因此mybatis提供了接口的动态代理。

5.2 代理开发方式

基于接口代理方式的开发只需要程序员编写 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="user">
    		select * from user
    	</select>
    </mapper>
    
  3. 测试

    注意:重复的模板代码写在测试类中,而不是持久层中,学习Spring后就都不用写了

    @Test
    public void testFindAll() throws Exception {
        // 加载核心配置文件
        InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得SqlSessionFactory工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        // 获得SqlSession会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        
        // 获得Mapper代理对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        
        // 执行查询
        List<User> list = userMapper.findAll();
        for (User user : list) {
        	System.out.println(user);
        }
        // 释放资源
        sqlSession.close();
    }
    
Mybatis基于接口代理方式的内部执行原理

我们的持久层现在只有一个接口,而接口是不实际干活的,那么是谁在做查询的实际工作呢?

下面通过追踪源码看一下:

  1. 通过追踪源码我们会发现,我们使用的mapper实际上是一个代理对象,是由MapperProxy代理产生的。

在这里插入图片描述

  1. 追踪MapperProxy的invoke方法会发现,其最终调用了mapperMethod.execute(sqlSession, args)

    在这里插入图片描述

  2. 进入execute方法会发现,最终工作的还是sqlSession

    在这里插入图片描述

  3. 总结

    在这里插入图片描述

6. Mybatis映射配置文件

https://mybatis.org/mybatis-3/zh/sqlmap-xml.html

在这里插入图片描述

6.1 动态SQL

https://mybatis.org/mybatis-3/zh/dynamic-sql.html

6.2 SQL片段

映射文件中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的

<!--抽取的sql片段-->

<sql id="selectUser">
    select * from user
    /*select * from user where id in (1,2,3)*/
</sql>

<!--动态sql的foreach标签:多值查询:根据多个id值查询用户-->
<select id="findByList" parameterType="list" resultType="user">
    <!--引入sql片段-->
    <include refid="selectUser"/>
    <where>
        <!--
                collection : 代表要遍历的集合元素,通常写collection或者list
                open : 代表语句的开始部分
                close : 代表语句的结束部分
                item : 代表遍历结合中的每个元素,生成的变量名
                separator: 分隔符
             -->
        <foreach collection="collection" open="id in (" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

7. MyBatis核心配置文件

https://mybatis.org/mybatis-3/zh/configuration.html

8. Mybatis高级查询

8.1 ResutlMap属性

建立对象关系映射

  • resultType:如果实体的属性名(JavaBean中)与表中字段名(MySQL建表语句)一致,将查询结果自动封装到实体类中
  • ResutlMap:如果实体的属性名与表中字段名不一致,可以使用ResutlMap实现手动封装到实体类中
案例
实体

在这里插入图片描述

数据表

在这里插入图片描述

编写ResutlMap

在这里插入图片描述

8.2 多条件查询

需求

根据id和username查询user表

方式

使用pojo对象传递参数

步骤
UserMapper接口
public interface UserMapper {
	public List<User> findByIdAndUsername(User user);
}
UserMapper.xml
<mapper namespace="com.lagou.mapper.UserMapper">
    <select id="findByIdAndUsername" parameterType="com.lagou.domain.User"
    resultType="com.lagou.domain.User">
    	select * from user where id = #{id} and username = #{username}
    </select>
</mapper>
测试
@Test
    public void test() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 当前返回的 其实是基于UserMapper所产生的代理对象:底层:JDK动态代理 实际类型:proxy
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user1 = new User();
        user1.setId(1);
        //user1.setUsernameabc("子慕");

        List<User> users = mapper.findByIdAndUsername(user1);
        for (User user : users) {
            System.out.println(user);
        }
        sqlSession.close();

    }

8.3 模糊查询

8.3.1 方式一 #{}
UserMapper接口
public interface UserMapper {
	public List<User> findByUsername(String username);
}
UserMapper.xml
<mapper namespace="com.lagou.mapper.UserMapper">
    <!--id : 标签的唯一标识
        type: 封装后实体类型-->
    <resultMap id="userResultMap" type="com.lagou.domain.User">
        <!--手动配置映射关系-->
        <!--id: 用来配置主键-->
        <id property="id" column="id"></id>
        <!-- result: 表中普通字段的封装-->
        <result property="username" column="username"></result>
        <result property="birthday" column="birthday"></result>
        <result property="sex" column="sex"></result>
        <result property="address" column="address"></result>
    </resultMap>
    
    <select id="findByUsername" resultMap="userResultMap" parameterType="string">
        <!-- #{}在mybatis中是占位符,引用参数值的时候会自动添加单引号 -->
        select * from user where username like #{username}
    </select>
</mapper>
测试
@Test
    public void test() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 当前返回的 其实是基于UserMapper所产生的代理对象:底层:JDK动态代理 实际类型:proxy
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        List<User> users = mapper.findByUsername("%子慕%");
        for (User user : users) {
            System.out.println(user);
        }

        sqlSession.commit();
        sqlSession.close();

    }
8.3.2 方式二 ${}
UserMapper接口
public interface UserMapper {
	public List<User> findByUsername2(String username);
}
UserMapper.xml
<mapper namespace="com.lagou.mapper.UserMapper">
    <select id="findByUsername2" resultMap="userResultMap" parameterType="string">
        <!--parameterType是基本数据类型或者String的时候,${}里面的值只能写value
            ${}: sql原样拼接
        -->
        select * from user where username like '${value}'
    </select>
</mapper>
测试
@Test
    public void test() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 当前返回的 其实是基于UserMapper所产生的代理对象:底层:JDK动态代理 实际类型:proxy
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        List<User> users = mapper.findByUsername2("%子慕%");
        for (User user : users) {
            System.out.println(user);
        }

        sqlSession.close();

    }
8.3.3 【笔试题】#{} 与 ${} 区别
  • #{}:表示一个占位符号
    • 通过 #{} 可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换,#{}可以有效防止sql注入
    • #{} 可以接收简单类型值或pojo属性值
    • 如果parameterType传输单个简单类型值, #{} 括号中名称随便写
  • ${}:表示拼接sql串
    • 通过 ${} 可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换,会出现sql注入问题
    • ${} 可以接收简单类型值或pojo属性值
    • 如果parameterType传输单个简单类型值, ${} 括号中只能是value

8.4 多表查询

关系型数据库表关系分为 一对一、一对多、多对多

* 人和身份证号就是一对一
    一个人只能有一个身份证号
    一个身份证号只能属于一个人
    
* 用户和订单就是一对多,订单和用户就是多对一
    一个用户可以下多个订单
    多个订单属于同一个用户
    
* 学生和课程就是多对多
    一个学生可以选修多门课程
    一个课程可以被多个学生选修
    
* 特例
	一个订单只从属于一个用户,所以mybatis将多对一看成了一对一
sql环境准备
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');
项目目录

在这里插入图片描述

8.4.1 关联查询
一对一(多对一)

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对一查询的需求:查询所有订单,与此同时查询出每个订单所属的用户

一对一查询语句
SELECT * FROM orders o LEFT JOIN USER u ON o.`uid`=u.`id`;
Order实体
public class Orders {

    private Integer id;
    private String ordertime;
    private Double total;
    private Integer uid;

    // 表示当前订单属于哪个用户 association
    private User user;
}
OrderMapper接口
public List<Orders> findAllWithUser();
OrderMapper.xml
<resultMap id="orderMap" type="com.lagou.domain.Orders">
    <id property="id" column="id"/>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>


    <!--
            association : 在进行一对一关联查询配置时,使用association标签进行关联
            property="user" :要封装实体的属性名
            javaType="com.lagou.domain.User" 要封装的实体的属性类型
    -->
    <association property="user" javaType="com.lagou.domain.User">
        <id property="id" column="uid"></id>
        <result property="username" column="username"></result>
        <result property="birthday" column="birthday"></result>
        <result property="sex" column="sex"></result>
        <result property="address" column="address"></result>
    </association>
</resultMap>

<select id="findAllWithUser" resultMap="orderMap">
    SELECT * FROM orders o LEFT JOIN USER u ON o.uid = u.id
</select>
测试
@Test
    public void test1() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);

        List<Orders> orders = mapper.findAllWithUser();

        for (Orders order : orders) {
            System.out.println(order);
        }

        sqlSession.close();
    }
一对多

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对多查询的需求:查询所有用户,与此同时查询出该用户具有的订单

一对多查询语句
SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.`id` = o.`uid`;
User实体
public class User {

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    // 表示多方关系:集合 : 代表了当前用户所具有的订单列表 collection
    private List<Orders> ordersList;
}
UserMapper接口
public List<User> findAllWithOrder();
UserMapper.xml
<resultMap id="userMap" type="com.lagou.domain.User">
    <id property="id" column="id"></id>
    <result property="username" column="username"></result>
    <result property="birthday" column="birthday"></result>
    <result property="sex" column="sex"></result>
    <result property="address" column="address"></result>


    <!--
            collection : 一对多使用collection标签进行关联
        -->
    <collection property="ordersList" ofType="com.lagou.domain.Orders">
        <id property="id" column="oid"></id>
        <result property="ordertime" column="ordertime"/>
        <result property="total" column="total"/>
        <result property="uid" column="uid"/>
    </collection>

</resultMap>


<select id="findAllWithOrder"  resultMap="userMap">
    SELECT u.*,o.id oid,o.ordertime,o.total,o.uid FROM orders o RIGHT JOIN USER u ON o.uid = u.id
</select>
测试
@Test
    public void test2() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        List<User> allWithOrder = mapper.findAllWithOrder();

        for (User user : allWithOrder) {
            System.out.println(user);
        }

        sqlSession.close();
    }
多对多

用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用

多对多查询的需求:查询所有用户同时查询出该用户的所有角色

多对多查询语句
SELECT
	*
FROM
	USER u -- 用户表
	LEFT JOIN user_role ur -- 左外连接中间表
		ON u.`id` = ur.`uid`
	LEFT JOIN role r -- 左外连接中间表
		ON ur.`rid` = r.`id` ;
* 多对多的配置跟一对多很相似,难度在于SQL语句的编写
小结
* 多对一(一对一)配置:使用<resultMap>+<association>做配置

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

* 多对多配置:使用<resultMap>+<collection>做配置
8.4.2 嵌套查询

嵌套查询就是将原来多表查询中的联合查询语句拆成单个表的查询,再使用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,将以上二步嵌套起来
一对一(多对一)

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对一查询的需求:查询所有订单,与此同时查询出每个订单所属的用户

一对一查询语句
-- 先查询订单
SELECT * FROM orders;

-- 再根据订单uid外键,查询用户
SELECT * FROM `user` WHERE id = #{订单的uid};
Order实体
public class Orders {

    private Integer id;
    private String ordertime;
    private Double total;
    private Integer uid;

    // 表示当前订单属于哪个用户 association
    private User user;
}
OrderMapper接口
public List<Orders> findAllWithUser2();
OrderMapper.xml
<resultMap id="orderMap2" type="com.lagou.domain.Orders">
    <id property="id" column="id"/>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>

    <!--问题:1.怎么去执行第二条sql , 2.如何执行第二条sql的时候,把uid作为参数进行传递-->
    <association property="user" javaType="com.lagou.domain.User"
                 select="com.lagou.mapper.UserMapper.findById" column="uid" fetchType="eager"/>
</resultMap>

<!--一对一嵌套查询-->
<select id="findAllWithUser2" resultMap="orderMap2">
    SELECT * FROM orders
</select>
UserMapper接口
public User findById(Integer id);
UserMapper.xml
<select id="findById" resultType="com.lagou.domain.User" parameterType="int">
    SELECT * FROM user WHERE id = #{id}
</select>

9. MyBatis加载策略

9.1 什么是延迟加载

就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据,延迟加载也称懒加载。

* 在一对多中,当我们有一个用户,它有个100个订单
    在查询用户的时候,要不要把关联的订单查出来?
    在查询订单的时候,要不要把关联的用户查出来?
* 回答
    在查询用户时,用户下的订单应该是,什么时候用,什么时候查询
    在查询订单时,订单所属的用户信息应该是随着订单一起查询出来
  • 优点: 先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
  • 缺点: 因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
  • 注意: 延迟加载是基于嵌套查询来实现的
    • 在多表中:
      • 一对多,多对多:通常情况下采用延迟加载
      • 一对一(多对一):通常情况下采用立即加载

9.2 设置触发延迟加载的方法

9.2.1 局部延迟加载
总结
  1. association和collection标签:fetchType="lazy"

  2. sqlMapConfig.xml

    <settings>
        <!-- `equals`、`clone`、`hashCode`、`toString` 都会延迟加载-->
        <setting name="lazyLoadTriggerMethods" value="toString()"/>
    </settings>
    
  3. 测试:调用对应get方法

需求

一对多嵌套查询:查询所有用户及关联的订单信息

一对多查询语句
-- 先查询用户
SELECT * FROM `user`;

-- 再根据用户id主键,查询订单列表
SELECT * FROM orders where uid = #{用户id};
UserMapper接口
public List<User> findAllWithOrder();
UserMapper.xml
<!-- 开启一对多 延迟加载 -->
<resultMap id="userOrderMap" type="com.lagou.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"></result>
    <result property="birthday" column="birthday"></result>
    <result property="sex" column="sex"></result>
    <result property="address" column="address"></result>

    <!--fetchType="lazy" : 延迟加载策略
        fetchType="eager": 立即加载策略
     -->
    <collection property="ordersList" ofType="com.lagou.domain.Orders" column="id" select="com.lagou.mapper.OrderMapper.findByUid" fetchType="lazy"></collection>
</resultMap>

<select id="findAllWithOrder" resultMap="userOrderMap">
    SELECT * FROM USER
</select>
OrderMapper接口
public List<Order> findByUid(Integer uid);
OrderMapper.xml
<select id="findByUid" parameterType="int" resultType="com.lagou.domain.Orders">
        SELECT * FROM orders WHERE uid = #{uid}
</select>
配置sqlMapConfig.xml

在配置了延迟加载策略后,在你调用当前对象的 equalsclonehashCodetoString方法时也会触发关联对象的查询。

我们可以在配置文件中使用lazyLoadTriggerMethods配置项覆盖掉上面四个方法,实现延迟加载效果。

<settings>
    <!-- `equals`、`clone`、`hashCode`、`toString` 都会延迟加载-->
    <setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
测试
/*
       一对多嵌套查询:查询所有用户及关联的订单信息
   */
    @Test
    public void test5() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        List<User> allWithOrder2 = mapper.findAllWithOrder2();

        for (User user : allWithOrder2) {
            System.out.println(user);

            // 延迟加载:要用到该用户的订单信息
            System.out.println(user.getOrdersList());
        }

        sqlSession.close();
    }

9.2.2 全局延迟加载

配置sqlMapConfig.xml
<settings>
    <!--开启全局延迟加载功能-->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- `equals`、`clone`、`hashCode`、`toString` 都会延迟加载-->
    <setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
注意

局部的加载策略优先级高于全局的加载策略。

10. MyBatis缓存

当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来保存在缓存中。当用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。

减少网络连接和数据库查询带来的损耗,从而提高我们的查询效率,减少高并发访问带来的系统性能问题。

10.1 一级缓存

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

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

10.1.1 验证
@Test
public void testOneCache() throws IOException {

    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 根据id查询用户信息
    // 第一次查询,查询的数据库
    User user1 = userMapper.findById(1);
    System.out.println(user1);

    // clearCache: 手动清空缓存
    //sqlSession.clearCache();

    // 第二次查询,查询的是一级缓存
    User user2 = userMapper.findById(1);
    System.out.println(user2);

    sqlSession.close();

}

在这里插入图片描述
在这里插入图片描述

我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是Mybatis提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询id为1的记录时,并没有发出sql语句从数据库中查询数据,而是从一级缓存中查询。

10.1.2 分析

一级缓存是SqlSession范围的缓存,执行SqlSession的C(增加)U(更新)D(删除)操作,或者调 用clearCache()、commit()、close()方法,都会清空缓存,避免脏读。

<!-- 每次查询时,都会清除缓存 -->
< select flushCache="true"></select>

在这里插入图片描述

10.2 二级缓存

二级缓存是namspace级别(跨sqlSession)的缓存,是默认不开启的。

mybatis的二级缓存因为是namespace级别,所以在进行多表查询时会产生脏读问题,需要使用第三方的缓存技术解决问题。

11. MyBatis注解

11.1 常用注解

* @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>

11.2 基本CRUD

UserMapper接口
public interface UserMapper {
    
    @Select("SELECT * FROM `user`")
    public List<User> findAll();
    
    @Insert("INSERT INTO `user`(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})")
    public void save(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(Integer id);
}
核心配置文件
<!--我们使用了注解替代的映射文件,所以我们只需要加载使用了注解的Mapper接口即可-->
<mappers>
    <!--扫描使用注解的Mapper类-->
    <mapper class="com.lagou.mapper.UserMapper"></mapper>
</mappers>

<!--或者指定扫描包含映射关系的接口所在的包也可以-->
<mappers>
    <!--扫描使用注解的Mapper类所在的包-->
    <package name="com.lagou.mapper"></package>
</mappers>

11.3 复杂映射开发

一对多查询

需求:查询一个用户,与此同时查询出该用户具有的订单

一对多查询语句
SELECT * FROM `user`;
SELECT * FROM orders where uid = #{用户id};
UserMapper接口
/*
        查询所有用户,及关联的订单信息
     */
@Select("select * from user")
    @Results({
            @Result(property = "id",column = "id",id = true),
            @Result(property = "username",column = "username"),
            @Result(property = "birthday",column = "birthday"),
            @Result(property = "sex",column = "sex"),
            @Result(property = "address",column = "address"),
        
            @Result(property = "ordersList",javaType = List.class,column = "id",many = @Many(select = "com.lagou.mapper.OrderMapper.findOrderByUid",fetchType = FetchType.LAZY))
    })
    public List<User> findAllWithOrder();
OrderMapper接口
/*
        根据传递过来的用户id,查询该用户所具有的订单信息
     */
@Select("select * from orders where uid = #{uid}")
    public List<Orders> findOrderByUid(Integer uid);
测试
@Test
    public void testOneToMany(){
        
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        sqlSession = sqlSessionFactory.openSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        List<User> allWithOrder = mapper.findAllWithOrder();

        for (User user : allWithOrder) {
            System.out.println(user);
            System.out.println(user.getOrdersList());
        }
        sqlSession.commit();
        sqlSession.close();
    }

11.4 小结

注解开发和xml配置优劣分析

  • 注解开发和xml配置相比,从开发效率来说,注解编写更简单,效率更高
    • 建议单表操作注解方式,效率更高
  • 从可维护性来说,注解如果要修改,必须修改源码,会导致维护成本增加,xml维护性更强
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值