Loading

MyBatis学习笔记

1 简介

1.1 什么是MyBatis

  • 持久层框架
  • 定制化SQL、存储过程和高级映射
  • 使用简单的XML或注解配置,映射原生类型、接口和POJO为数据库中的记录

1.2 持久化

数据持久化

  • 持久化就是将程序的数据在持久状态和瞬时状态转化的过程
  • 数据库、IO文件持久化

1.3 持久层

Dao层

  • 完成持久化工作的代码块

1.4 为什么需要MyBatis

  • 原生JDBC的缺点
    原生的JDBC操作复杂,需要获取连接、获取PreparedStatement、写入Sql、预编译参数、执行Sql、封装结果、释放连接等一系列操作,比较麻烦
    并且JDBC中SQL语句是硬编码在程序中的,数据库层和Java编码耦合,维护修改Sql语句时比较麻烦

  • Hibernate框架的缺点
    无法定制SQL,是一个全映射框架,做部分字段映射很难

而MyBatis将重要的步骤(即编写SQL语句)抽取出来,可以人工定制,其他步骤实现自动化
编写SQL语句是通过配置文件实现,好维护,能解决数据库的优化问题
MyBatis底层就是对原生JDBC的一个简单封装
既将Java编码与SQL语句分离开来,还不会失去自动化功能,是一个半自动的持久化框架

2 第一个MyBatis程序

搭建环境-->导入MyBatis-->编写代码-->测试

2.1 搭建环境

  1. 创建数据库
  2. 创建项目
  3. 导入JDBC驱动,MyBatis包
  4. 配置mybatis的核心xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <!--每个Mapper.xml都要在核心配置文件中注册-->
  <mappers>
    <mapper resource="com/hjc/dao/UserMapper.xml"/>
  </mappers>
</configuration>
  1. 从xml文件中构建SqlSessionFactory,我们把构建SqlSessionFactory的方法放在工具类的静态方法内
public class MyBatisUtils {
    
    private static SqlSessionFactory sqlSessionFactory;
    
    static {
        try {
            String resource = "mybatis-config.xml";
	    InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static sqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
}

2.2 编写代码

  1. 编写实体类(pojo)
  2. 编写Dao接口
public interface UserDao {
    List<User> getUserList();
}
  1. 编写UserMapper.xml文件(接口实现类由原来的UserDaoImpl转变为Mapper配置文件)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.hjc.dao.UserDao">
  <!--select查询语句-->
  <select id="getUserList" resultType="com.hjc.pojo.User">
    select * from user;
  </select>
</mapper>
  1. 在核心配置文件中注册UserMapper.xml

    <!--每个Mapper.xml都要在核心配置文件中注册-->
    <mappers>
      <mapper resource="com/hjc/dao/UserMapper.xml"/>
    </mappers>
    
    1. 在默认的Maven配置下,UserMapper.xml文件应该放在resources文件夹下,也就是resources/com/hjc/dao/UserMapper.xml路径
    2. 要想把UserDao和UserMapper.xml文件放在一起,需要更改Maven构建配置
    3. 如果不这样做,在构建时,Mapper文件无法被打包到target文件夹中
<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

2.3 测试代码

public class UserDaoTest {
    
    @Test
    public void testGetUserList() {
        //获得SqlSession对象
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
        //方式一:getMapper
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        List<User> userList = userDao.getUserList();
        
        //方式二:不建议使用
        //List<User> userList = sqlSession.selectList("com.hjc.dao.UserDao.getUserList");
        
        for (User user : userList)
            System.out.println(user);
        
        //关闭SqlSession
        sqlSession.close();
    }
}

3 CRUD

3.1 namespace

namespace中的包名要和Dao/Mapper接口的包名一致

3.2 select

查询语句

  • id:就是对应的namespace中的方法名
  • resultType:sql语句执行的返回类型
  • parameterType:sql语句的参数类型
  1. 编写接口
//根据id查询用户
User getUserById(int id);
  1. 编写对应Mapper.xml文件中的sql语句
<select id="getUserById" parameterType="Integer" resultType="com.hjc.pojo.User">
    select * from user where id = #{id}
</selects>
  1. 测试
@Test
public void testGetUserById() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    User user = userDao.getUserById(1);
    System.out.println(user);
    sqlSession.close();
}

3.3 insert

  1. 编写接口
//插入用户
void addUser(User user);
  1. 编写对应Mapper.xml文件中的sql语句
<insert id="addUser" parameterType="com.hjc.pojo.User">
    insert into user (id, name, pwd) values (#{id}, #{name}, #{pwd})
</insert>
  1. 测试
@Test
public void testAddUser() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    userDao.addUser(new User(5, "test", 123456));
    //需要提交事务
    sqlSession.commit();
    sqlSession.close();
}

3.4 update

  1. 编写接口
//更新用户
void updateUse(User user);
  1. 编写对应Mapper.xml文件中的sql语句
<update id="updateUser" parameterType="com.hjc.pojp.User">
    update user set name = #{name}, pwd = #{pwd} where id = #{id}
</update>
  1. 测试
@Test
public void testUpdateUser() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    userDao.updateUser(new User(5, "test", 123456));
    //需要提交事务
    sqlSession.commit();
    sqlSession.close();
}

3.5 delete

  1. 编写接口
//删除用户
void deleteUserById(int id);
  1. 编写对应Mapper.xml文件中的sql语句
<delete id="deleteUserById" parameterType="Integer">
    delete from user where id = #{id}
</delete>
  1. 测试
@Test
public void testDeleteUserById() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    userDao.deleteUserById(5);
    //需要提交事务
    sqlSession.commit();
    sqlSession.close();
}

注意点:MyBatis默认不会自动提交事务,需要手动提交事务

3.6 Map

如果实体类或数据库中表的字段或参数过多,我们应当考虑使用Map作为sql的参数

void addUserByMap(Map<String, Object> map);
<insert id="addUserByMap" parameterType="Map">
    insert into user (id, pwd) values (#{userId}, #{userPwd})
</insert>
@Test
public void testAddUserByMap() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    Map<String, Object> map = new HashMap<>();
    map.put("userId", 6);
    map.put("userPwd", "123456");
    userDao.addUserByMap(map);
    //需要提交事务
    sqlSession.commit();
    sqlSession.close();
}
  • Map传递参数,直接在sql中取出key即可
  • 对象传递参数,直接在sql中取对象的属性即可
  • 只有一个基本类型参数的情况下,可以直接在sql中取到
  • 多个参数的情况,用Map,或者注解

3.7 模糊查询

List<User> getUserLike(String name);
<select id="getUserLike" parameterType="String" resultType="com.hjc.pojo.User">
    select * from user where name like #{name}
</select>
@Test
public void testGetUserLike() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    List<User> userList = userDao.getUserLike("%test%");
    for (User user : userList)
        System.out.println(user);
    sqlSession.close();
}
  • 在Java代码执行的时候,传递通配符"% %"
List<User> userList = userDao.getUserLike("%test%");
  • 在sql拼接中使用通配符"% %"
select * from user where name like "%"#{name}"%"

3.8 总结参数传递的多种情况

在编写mapper映射文件的时候,parameterType属性其实是可有可无的,也就是说,不写这个属性,一般也会成功

一般在SQL语句中获取参数值使用#{}来获取,也可以使用${}来获取

{}是参数预编译的方式,参数的位置都是用?来替代,参数是后来预编译设置进去的

${}不是参数预编译的方式,而是直接和SQL语句进行拼串
两者相比,#{}更加安全,不存在SQL注入的问题,而${}不安全,会有SQL注入的问题
但是,如果数据库的表名也要作为参数传入的话,就只能使用${}获取,因为表名不支持参数预编译

  1. 传入单个参数

当方法传入单个参数的时候,我们在SQL语句中获得参数是用#{参数名}来获取的,实际上,大括号内的参数名可以随便取

  1. 传入多个参数

当方法传入多个参数的时候,如果我们只是在SQL语句中使用#{参数名1}#{参数名2}来获取多个参数,这样是不能成功的
因为传入多个参数,MyBatis会自动将这些参数封装在一个map中,map中的key是参数的索引,那么,我们就不能在大括号内写入参数名,
而是使用#{0}#{1}或者#{param1}#{param2}来获取第一个参数值和第二个参数值

另外,如果一定要用参数名来获取传入多个参数情况下的参数值,我们推荐在dao层的接口方法的参数前面加上@Param注解,
比如int getUserByNameAndPassword(@Param("name") String name, @Param("pwd") String pwd),这样的话,我们在写SQL语句的时候就能直接使用#{参数名}来获取参数值

<select id="getUserByIdAndName" resultType="com.hjc.pojo.User">
    select * from user where name=#{name} and pwd=#{pwd}
</select>
  1. 传入pojo

如果方法的参数为pojo类,那么在SQL语句中直接使用#{pojo的属性名}来获取对应属性的值

  1. 传入map

我们可以手动将传入的多个参数封装进一个map中,那么在SQL语句中可以使用#{key}来获取对应的值,key为map的键

3.9 查询返回list

如果需要查询结果返回list,那么在写mapper映射文件的时候,resultType属性写入list集合内部元素的类型,MyBatis会自动将查询结果封装进list中

3.10 查询返回map

  1. 如果查询返回单条记录,要封装在map中返回,那么列名作为key,值作为value
<select id="getUserByIdReturnMap" resultType="map">
    select * from user where id=#{id}
</select>

执行这个配置,返回的map是key为列名或者属性名,value为对应的值

  1. 如果查询返回多条记录,封装到map中返回,那么主键作为key,单条记录对象作为value
<select id="getUsersReturnMap" resultType="com.hjc.pojo.User">
    select * from user 
</select>

此外,在接口方法上还要指明作为key的属性

@MapKey("id")
Map<Integer, User> getUsersReturnMap();

注意点:如果返回多条记录封装到map中,那么返回类型resultType要写封装到map中value的类型,和查询返回list相同,另外,在接口方法上还要指定一个属性作为map的key

4 配置解析

4.1 核心配置文件

  • mybatis-config.xml
  • 配置文件会影响MyBatis行为的设置和属性信息
configuration(配置)
    properties(属性)
    settings(设置)
    typeAliases(类型别名)
    typeHandlers(类型处理器)
    objectFactory(对象工厂)
    plugins(插件)
    environments(环境配置)
        environment(环境变量)
            transactionManager(事务管理器)
            dataSource(数据源)
    databaseIdProvider(数据库厂商标识,用来数据库移植,一般不用)
    mappers(映射器)

4.2 环境配置(environments)

environments标签中定义了配置的环境,包括事务管理器和数据源

MyBatis可以配置成适应多种环境,但每个SqlSessionFactory实例只能选择一种环境

要切换其他环境,只需要将environments标签的default属性换成其他environment标签的id属性

MyBatis默认的事务管理器就是JDBC,连接池POOLED

在和Spring进行整合的时候,事务管理器和数据源交给Spring来做

4.3 属性(properties)

可以通过properties属性来实现引用外部配置文件,属性可以在Java属性文件(properties文件)中配置,也可以通过properties元素的子元素来传递

  1. 编写外部配置文件db.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/test_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username=root
password=123456
  1. 在核心配置文件中引入properties文件
<properties resource="db.properties"/>
  1. 也可以在核心配置文件中增加属性
<properties resource="db.properties">
    <property name="username" value="root"/>
    <property name="password" value="111111"/>
</properties>
  1. 如果两个地方都配置了同一字段,优先使用外部文件配置的信息

4.4 类型别名(typeAliases)

在mappers的配置文件中,一般resultType属性要求我们写全限定类名,我们可以使用类型别名定义一个别名,那么在写resultType的时候就不需要写全限定类名了

  • 类型别名是为Java类型设置一个短的名字,为了减少全限定类名的冗余,别名不区分大小写
<typeAliases>
    <typeAlias type="com.hjc.pojo.User" alias="User"/>
</typeAliases>
  • 指定一个包名,MyBatis会在包名下面搜索需要的JavaBean,默认别名就是这个类的类名,不区分大小写,也可以配合注解@Alias使用
<typeAliases>
    <package name="com.hjc.pojo"/>
</typeAliases>
  • 在实体类比较少的时候,使用第一种方式;实体类比较多,建议第二种

另外,MyBatis已经为常见的Java类型内建了相应的类型别名,比如int -> _int,String -> string,别名不区分大小写,我们在自己定义别名的时候注意不要与这些别名冲突

4.5 设置(settings)

这项属性可以改变MyBatis的运行时行为

  • cacheEnabled:全局开启或关闭所有映射器配置文件中已配置的任何缓存
  • lazyLoadingEnabled:开启或关闭延迟加载
  • logImpl:指定MyBatis所有日志的具体实现
<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="logImpl" value="LOG4J"/>
</settings>

4.6 映射器(mappers)

注册绑定Mapper文件

  1. 方式一
<mappers>
  <mapper resource="com/hjc/dao/UserMapper.xml"/>
</mappers>
  1. 方式二:使用class文件绑定注册
<mappers>
    <mapper class="com.hjc.dao.UserMapper"/>
</mappers>

注意点

使用此方法必须

  • 接口和其Mapper配置文件必须同名
  • 接口和其Mapper配置文件必须在同一个包下
  1. 方式三:使用扫描包进行绑定注册
<mappers>
    <mapper package="com.hjc.dao"/>
</mappers>

注意点

  • 接口和其Mapper配置文件必须同名
  • 接口和其Mapper配置文件必须在同一个包下
    或者
  • Mapper配置文件的路径和接口的包路径一致

4.7 其他配置

  • typeHandlers(类型处理器)
  • objectFactory(对象工厂)
  • plugins插件
    • mybatis-generator-core
    • mybatis-plus
    • 通用mapper

4.8 生命周期和作用域

生命周期和作用域至关重要,错误使用会导致并发问题

SqlSessionFactoryBuilder

  • 一旦创建了SqlSessionFactory,就不再需要SqlSessionFactoryBuilder了
  • 局部变量

SqlSessionFactory

  • 相当于数据库连接池
  • 一旦被创建就在应用的运行期间一直存在,没有理由丢弃它或重新创建另一个实例
  • SqlSessionFactory的最佳作用域是应用作用域
  • 使用单例模式或者静态单例模式

SqlSesison

  • 相当于连接到连接池的一个请求
  • 不是线程安全的,不能被共享
  • SqlSession的最佳作用域是请求或方法作用域
  • 用完之后要立即关闭,否则资源被占用

图中的每个Mapper代表一个具体的业务

5 实体类属性名和数据库字段名不一致问题

MyBatis自动封装结果集,前提是数据库列名和JavaBean属性名一一对应(不区分大小写),如果不一一对应,可以开启驼峰命名规则(数据库aaa_bbb -> 属性aaaBbb),可以起别名,还可以自定义结果集映射

如果User类中的属性名依次为id,name和password,而数据库中的列名以此为id,name和pwd,不一一对应

解决方法:

5.1 起别名

原来是

<select id="getUserById" parameterType="int" resultType="com.hjc.pojo.User">
    select * from user where id = #{id}
</select>

改为

<select id="getUserById" parameterType="int" resultType="com.hjc.pojo.User">
    select id, name, pwd password from user where id = #{id}
</select>

5.2 使用resultMap

结果集映射

<!--结果集映射-->
<resultMap id="UserMap" type="User">
    <!--id为主键列,result为非主键列-->
    <!--column为数据库字段,property为实体类属性-->
    <!--<id column="id" property="id"/>-->
    <!--<result column="name" property="name"/>-->
    <result column="pwd" property="password"/>
</resultMap>

<select id="getUserById" parameterType="int" resultMap="UserMap">
    select * from user where id = #{id}
</select>

对于简单的语句不需要配置显示的结果集映射,而对于复杂一点的语句需要描述它们的关系

6 获取自增主键和非自增主键的值

6.1 获取自增主键的值

当数据库的主键是自增的情况下,插入一条用户信息是不需要给id属性赋值的,那么如何才能获取到刚插入的用户的主键id呢

我们可以使用useGeneratedKeys="true"来获取自增主键的值

比如

<insert id="addUser" useGeneratedKeys="true" keyProperty="id">
    insert into user (name, pwd) values (#{name}, #{pwd})
</insert>

useGeneratedKeys="true"表示需要获取数据库自增的主键,keyProperty="id"表示将需要获取的自增主键封装给id属性

User user = new User(null, "test", 123456);
userDao.addUser(user);
SqlSession.commit();
System.out.println(user.getId());

这样就能获取到新插入user对象的id

6.2 获取非自增主键的值

如果数据库的主键不是自增的,那么在插入一条用户信息的时候就要带上主键值,但是,我们可能不知道数据库中的主键值已经用了多少,也就是说,有可能插入的主键已经在数据库中存在了,会产生冲突

我们可以使用selectKey标签在核心SQL语句运行之前先运行一个查询主键的SQL语句,再将查到的主键值赋值给JavaBean的对应属性

比如

<insert id="addUser">
    <selectKey order="BEFORE" resultType="integer" keyProperty="id">
        select max(id) + 1 from user
    </selectKey>
    insert into user (id, name, pwd) values (#{id}, #{name}, #{pwd})
</insert>

其中,order="BEFORE"表示在核心SQL语句之前运行,keyProperty="id"表示将查询到的值封装给id属性

User user = new User(null, "test", 123456);
userDao.addUser(user);
SqlSession.commit();
System.out.println(user.getId());

这样就能在数据库没有设定自增主键的情况下,安全地插入数据

7 日志

7.1 日志工厂

如果数据库操作出现异常,需要排错,那么日志就是最好的助手

以前可以使用sout打印,debug,现在有日志工厂

MyBatis使用内置的日志工厂提供日志功能,可以设置具体日志实现

STDOUT_LOGGING

在mybatis核心配置文件中配置日志

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

可以直接使用

7.2 LOG4J

  • LOG4J可以控制日志信息输送的目的地是控制台,文件,GUI组件

  • 可以控制每一条日志的输出格式

  • 可以定义每一条日志信息的级别

  • LOG4J在配置完后不能直接使用

  1. 先导入LOG4J的包
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

  1. 在resource文件夹下创建log4j.properties文件
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file

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

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/hjc.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
  1. 配置log4j为日志实现
<settings>
    <setting name="logImpl" value="LOG4J"/>
</settings>
  1. log4j的使用,测试运行

简单使用

  1. 在要使用log4j的类中,导入包 import org.apache.log4j.Logger;
  2. 日志对象,参数为当前类的class
private static Logger logger = Logger.getLogger(UserDaoTest.class);
  1. 日志级别
logger.info("info: 开始测试");
logger.debug("debug: 开始debug");
logger.error("error: 出现了错误");

8 分页

如果一次查询得到的数据量太大,会产生资源浪费的后果,所以要用到分页

  • 减少数据的处理量

8.1 使用limit分页

语法:select * from user limit startIndex, pageSize;
例子:select * from user limit 1, 3;

MyBatis中实现分页

  1. 编写接口,传输参数为map,map内存放start Index和pageSize
//分页查询
List<User> getUserByLimit(Map<String, Integer> map);
  1. 编写对应的Mapper.xml文件
<select id="getUserByLimit" parameterType="map" resultType="com.hjc.pojo.User">
    select * from user limit #{startIndex}, #{pageSize}
</select>
  1. 测试
@Test
public void testGetUserByLimit() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    Map<String, Integer> map = new HashMap<>();
    map.put("startIndex", 1);
    map.put("pageSize", 2);
    List<User> userList = userDao.getUserByLimit(map);
    for (User user : userList)
        System.out.println(user);
    sqlSession.close();
}

8.2 使用RowBounds分页

不在sql中使用limit,使用面向对象思想

  1. 编写接口
List<user> getUserByRowBounds();
  1. 编写对应的Mapper.xml文件
<select id="getUserByRowBounds" resultType="com.hjc.pojo.User">
    select * from user
</select>
  1. 测试
@Test
public void testGetUserByRowBounds() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    RowBounds rowBounds = new RowBounds(1, 2);
    //不使用Mapper来查询,使用以前的selectList来查询
    List<User> userList =
        sqlSession.selectList("com.hjc.dao.UserDao.getUserByRowBounds", null, rowBounds);
    for (User user : userList)
        System.out.println(user);
    sqlSession.close();
}

8.3 使用分页插件

MyBatis分页插件PageHelper,了解

9 使用注解

9.1 面向接口编程

在开发中,会选择面向接口编程

  • 根本原因是解耦,可拓展,提高复用
  • 分层开发中,上层不需要管具体的实现
  • 规范性更好

关于接口的理解

  • 从深层次的理解,接口是定义(规范、约束)与实现的分离
  • 接口的本身反映了系统设计人员对系统的抽象理解
  • 接口有两类
    • 第一类是对一个个体的抽象,对应为一个抽象体(abstract class)
    • 第二类是对一个个体某一方面的抽象,即形成一个抽象面(interface)
  • 一个个体可能有多个抽象面

9.2 注解

  1. 注解在接口内方法名上标注
@Select("select * from user")
List<User> getUsers();
  1. 需要在核心配置文件中绑定接口
<mappers>
    <mapper class="com.hjc.dao.UserDao"/>
</mappers>
  1. 测试
  • 本质:反射机制实现
  • 底层:动态代理

9.3 注解实现CRUD

可以在工具类内实现自动提交事务

public static sqlSession getSqlSession() {
    return sqlSessionFactory.openSession(true);
}
  1. 编写接口
public interface UserDao {
    
    @Select("select * from user where id = #{id}")
    User getUserById(@param("id") int id);
    
    @Insert("insert into user (id, name, pwd) values (#{id}, #{name}, #{pwd})")
    int addUser(User user);
    
    @Update("update user set name = #{name}, pwd = #{pwd} where id = #{id}")
    int updateUser(User user);
    
    @Delete("delete from user where id = #{uid}")
    int deleteUser(@Param("uid") int id);
}
  1. 绑定接口
<mappers>
    <mapper class="com.hjc.dao.UserDao"/>
</mappers>
  1. 测试

关于@Param注解

  • 基本类型的参数或者String类型,需要加上
  • 引用类型不需要加
  • 如果只有一个基本类型参数的话,可以忽略
  • 在sql语句中使用的参数就是注解内设定的属性名

10 MyBatis执行流程

11 Lombok

lombok是一个插件,通过注解来消除业务中冗长的代码,尤其对于POJO

使用步骤

  1. 在IDEA中安装Lombok插件
  2. 在项目中导入Lombok包
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
</dependency>
  1. 在实体类上加注解
    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. ……

12 多对一关系处理

mysql中多对一查询方式

  • 子查询
  • 联表查询

假设多个学生对应一个老师,那么先分别写对应实体类

@Data
public Student implements Serializable {
    private int id;
    private String name;
    //学生需要关联一个老师
    private Teacher teacher;
}
@Data
public Teacher implements Serializable {
    private int id;
    private String name;
}

要查出所有学生以及对应的老师,有两种方法

12.1 按照查询嵌套处理

思路:

  1. 查询所有的学生信息
  2. 根据查询出的学生tid,查询对应的老师,相当于子查询
<select id="getStudent" resultMap="StudentPlusTeacher">
    select * from student
</select>

<resultMap id="StudentPlusTeacher" type="Student">
    <!--主键-->
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <!--复杂的属性需要单独处理,对象用association,集合用collection
    javaType表示属性的类型-->
    <association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
</resultMap>

<select id="getTeacher" parameterType="int" resultType="Teacher">
    select * form teacher where id = #{id}
</select>

注:在核心配置文件中已经配置了别名,所以可以省去实体类的包路径

12.2 按照结果嵌套处理

使用association标签,表示联合了一个对象,其中javaType属性是指对象的类型

<select id="getStudent" resultMap="StudentPlusTeacher">
    select s.id as sid, s.name as sname, t.name as tname
    from student s, teacher t
    where s.tid = t.id
</select>

<resultMap id="StudentPlusTeacher" type="Student">
    <id property="id" column="sid"/>
    <result property="name" column="sname"/>
    <association property="teacher" javaType="Teacher">
        <result property="name" column="tname"/>
    </association>
</resultMap>

因为在查询语句中返回的只有teacher.name,没有teacher.id,所以不用在association内写上teacher.id的映射关系

另外,在写resultMap的时候也可以不用association封装,可以使用级联属性的方式封装

<resultMap id="StudentPlusTeacher" type="Student">
    <id property="id" column="sid"/>
    <result property="name" column="sname"/>
    <result property="teacher.name" column="tname"/>
</resultMap>

13 一对多关系处理

假设一个老师有多个学生,那么先分别写对应的实体类

@Data
public class Student implements Serializable {
    private int id;
    private String name;
    private int tid;
}
@Data
public class Teacher implements Serializable {
    private int id;
    private String name;
    //一个老师有多个学生
    private List<Student> students;
}

要查出指定老师及其对应的所有学生

13.1 按照结果嵌套处理

使用collection标签,表示联合了一个集合,其中ofType属性表示集合内元素的类型

<select id="getTeacher" parameterType="int" resultMap="TeacherPlusStudent">
    select s.id as sid, s.name as sname, t.name as tname, t.id as tid
    from student s, teacher t
    where s.tid = t.id and t.id = #{tid}
</select>

<resultMap id="TeacherPlusStudent" type="Teacher">
    <id property="id" column="tid"/>
    <result property="name" column="tname"/>
    <!--复杂的属性需要单独处理,对象用association,集合用collection
    ofType表示集合中的泛型信息-->
    <collection proerty="students" ofType="Student">
        <id property="id" column="sid"/>
        <result property="name" column="sname"/>
        <result property="tid" column="tid"/>
    </collection>
</resultMap>

13.2 按照查询嵌套处理

<select id="getTeacher" parameterType="int" resultMap="TeacherPlusStudent">
    select * from teacher where id = #{tid}
</select>

<resultMap id="TeacherPlusStudent" type="Teacher">
    <collection property="students" javaType="ArrayList" ofType="Student" select="getStudentByTeacherId" column="id"/>
</resultMap>

<select id="getStudentByTeacherId" parameter="int" resultType="Student">
    select * from student where tid = #{tid}
</select>

13.3 总结

  1. 关联:association,表示多对一
  2. 集合:collection,表示一对多
  3. javaType:用来指定实体类中属性的类型
  4. ofType:用来指定集合中的泛型信息,即集合内的pojo类型

14 动态SQL

动态SQL是指根据不同的条件生成不同的SQL语句,之前的项目写sql时,有时候会根据不同的条件拼接sql语句,现在可以使用动态SQL来实现

参考mybatis官方文档中对于动态SQL的解释

14.1 if

<select id="findActiveBlogWithTitleLike" resultType="Blog">
    SELECT * FROM BLOG
    WHERE state = ‘ACTIVE’
    <if test="title != null">
        AND title like #{title}
    </if>
</select>

14.2 choose、when、otherwise

相当于switch语句

<select id="findActiveBlogLike" resultType="Blog">
    SELECT * FROM BLOG WHERE state = ‘ACTIVE’
    <choose>
        <when test="title != null">
            AND title like #{title}
        </when>
        <when test="author != null and author.name != null">
            AND author_name like #{author.name}
        </when>
        <otherwise>
            AND featured = 1
        </otherwise>
    </choose>
</select>

14.3 where、set、trim

上面几个例子,sql语句中WHERE关键字后面都跟了默认的条件state = ‘ACTIVE’,如果把这条语句删了或者改为动态SQL,那么整个SQL语句会出现问题,比如

<select id="findActiveBlogLike" resultType="Blog">
    SELECT * FROM BLOG
    WHERE
    <if test="state != null">
        state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
</select>

如果if条件一个都不满足,那么sql变为

SELECT * FROM BLOG WHERE

很明显,这条sql语句是错误的

如果第一个id条件不满足,那么sql变为

SELECT * FROM BLOG WHERE AND title like #{title} ……

很明显,这条sql语句也是错误的

那么,MyBatis提供了where标签,可以解决这些问题。我们把上面的例子用where标签修改

<select id="findActiveBlogLike" resultType="Blog">
    SELECT * FROM BLOG
    <where>
        <if test="state != null">
            state = #{state}
        </if>
        <if test="title != null">
            AND title like #{title}
        </if>
        <if test="author != null and author.name != null">
            AND author_name like #{author.name}
        </if>
    </where>
</select>
<select id="findActiveBlogLike" resultType="Blog">
    SELECT * FROM BLOG
    <where>
        <choose>
            <when test="title != null">
                AND title like #{title}
            </when>
            <when test="author != null and author.name != null">
                AND author_name like #{author.name}
            </when>
            <otherwise>
                AND featured = 1
            </otherwise>
        </choose>
    </where>
</select>

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 ANDORwhere 元素也会将它们去除

另外,set标签类似于where标签,会动态地设置SET关键字,同时也会删掉无关的逗号

<update id="updateAuthorIfNecessary">
    update Author
    <set>
        <if test="username != null">
            username = #{username},
        </if>
        <if test="password != null">
            password = #{password},
        </if>
        <if test="email != null">
            email = #{email},
        </if>
        <if test="bio != null">
            bio = #{bio}
        </if>
    </set>
    where id = #{id}
</update>

set会自动在行首插入SET关键字,并根据传进来的参数是否为空决定sql语句的结构,比如是否需要删除行尾的逗号

trim标签与set标签等价

14.4 SQL片段

SQL片段:将sql语句的部分提取出来,方便复用

<sql id="test">
    <if test="state != null">
        state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
</sql>

<select id="findActiveBlogLike" resultType="Blog">
    SELECT * FROM BLOG
    <where>
        <include refid="test"/>
    </where>
</select>

注意:

  • 最好基于单表来定义SQL片段
  • SQL片段内不要存在where标签

14.5 foreach

foreach标签可以在dao层接口的方法传入参数为list集合的情况下,对list中数据进行遍历来组成SQL语句

比如,遇到IN语句的时候,可以使用foreach标签对集合进行遍历

<select id="selectPostIn" parameterType="list" resultType="domain.blog.Post">
    SELECT *
    FROM POST P
    WHERE ID in
    <!--传入参数为list集合-->
    <foreach item="item" index="index" collection="list" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值

另外,foreach也可以用在批量插入数据的情况下,比如我们要插入多条数据,那么方法传入的参数是一个list,在编写SQL语句的时候就可以使用foreach标签对list集合进行遍历来作为插入的VALUES。我们可以对比批量插入的多种方法,此方法的执行性能最好

总结

动态SQL就是在拼接SQL语句,只要保证SQL的正确性,就按照SQL的格式排列组合。先写出完整的SQL语句,再对应修改称为动态SQL实现通用

15 缓存

15.1 简介

  1. 什么是缓存
    • 存在内存中的临时数据
    • 将用户经常查询的数据放在缓存中,下次查询时不用再从数据库中获取,而是从缓存中获取,从而提高查询效率,解决高并发系统的性能问题
  2. 为什么使用缓存
    • 减少和数据库的交互次数,减少系统开销,提高系统效率
  3. 什么数据能使用缓存
    • 经常查询且不经常改变的数据

15.2 MyBatis缓存

  • MyBatis包含一个强大的查询缓存特性,可以非常方便地定制和配置缓存
  • MyBatis默认定义了两级缓存:一级缓存和二级缓存
    • 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存,线程级别的缓存)
    • 二级缓存需要手动开启和配置,是基于namespace级别的缓存,全局缓存
    • MyBatis定义了缓存接口Cache,可以通过实现Cache接口来自定义二级缓存

15.3 缓存原理

15.4 一级缓存

  • 一级缓存也叫本地缓存:SqlSession
    • 相当于一个map
    • 和数据库同一次会话(同一次SqlSession)期间查询到的数据会放在本地缓存中
    • 以后如果要获取相同的数据,可以直接从缓存中取,不需要从数据库中查询

测试

@Test
public void test() {
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    User user1 = userDao.getUserById(1);
    User user2 = userDao.getUserById(1);
    System.out.println(user1 == user2);
    sqlSession.close();
}

根据日志,我们可以看到sql只执行了一次,user1和user2是同一个

  • 缓存失效的情况
    • 不同的SqlSession,使用不同的一级缓存
    • 同一个方法,查询不同的数据
    • 在同一个SqlSession期间,执行任何一次增删改操作会改变原来的数据,缓存会刷新
    • 使用不同的Mapper进行查询
    • 手动清除缓存

15.5 二级缓存

  • 二级缓存也叫全局缓存,作用域比一级缓存要大
  • 基于namespace级别的缓存,一个命名空间对应一个二级缓存
  • 工作机制
    • 一次会话查询一条数据,这个数据会被放在当前会话的一级缓存中
    • 如果当前会话关闭了,那么对应的一级缓存就没了。但是二级缓存开启后,会话关闭之后,一级缓存中的数据会保存到二级缓存中
    • 新的会话查询数据,就可以从二级缓存中获取数据
    • 不同的Mapper查出的数据会放在对应的缓存中

步骤

  1. 在核心配置文件中开启全局缓存
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
  1. 在要使用二级缓存的Mapper.xml中开启二级缓存,可以自定义缓存属性
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
  1. 测试
@Test
public void test() {
    SqlSession sqlSession1 = MyBatisUtils.getSqlSession();
    UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
    User user1 = userDao1.getUserById(1);
    sqlSession1.close();
    //只有sqlSession1关闭后,一级缓存内的数据才会进入二级缓存
    SqlSession sqlSession2 = MyBatisUtils.getSqlSession();
    UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
    User user2 = userDao2.getUserById(1);
    sqlSession2.close();
    
    System.out.println(user1 == user2);
}

根据日志可以看到sql只执行了一次,且查到的User对象是同一个

注意:

  • 二级缓存对同一个Mapper下的数据才能起到提高效率的效果
  • 数据会先放在一级缓存中,只有当会话提交或者关闭后,才会放到二级缓存中
  • 每次查询的时候,会优先检查二级缓存,没有查到再查一级缓存,没有查到最后才查询数据库

15.6 自定义缓存EhCache

可以使用自定义的缓存,也可以使用第三方的缓存

EhCache是专业的Java进程内的缓存框架

16 PageHelper分页插件

PageHelper是MyBatis优秀的分页插件,可以帮我们解决分页查询上的问题,github仓库 PageHelper

使用方法参考此处

posted @ 2020-07-11 19:47  Kinopio  阅读(211)  评论(0编辑  收藏  举报