又遇Mybatis

找到了实习,但感觉之前的知识还是很虚啊,而且又看看那些什么文章,感觉焦虑得很,就。。。再学学学。

感谢太平洋,让我有、时间做做技术积累(手动狗头,其实是摸鱼学习【x)也谢谢狂神,又一次刷刷他的视频了。

mybatis官网:https://mybatis.org/mybatis-3/zh/index.html#
github地址:https://github.com/mybatis/mybatis-3

简介

它是一个优秀的持久层框架。

持久层是做持久化工作的

  • 持久化是把程序数据从持久状态和瞬时状态转化的过程
  • 内存断电即失
  • 数据库、io文件可以持久化存放。

持久层:帮助程序员把数据存到数据库

  • 完成持久化工作的代码块
  • 层级界限非常明显的
  • 简化、框架、自动化
  • 不用它也行,它很容易上手

(这块。。怎么说,“软知识”,多看看书or以后有更深的感悟再补些东西)

hello world

这块写一个mybatis的入门案例,链接官网:https://mybatis.org/mybatis-3/zh/getting-started.html#

搭建环境

数据库:(手写一下SQL,虽然干活的时候直接用工作。。。)

create database mybatis ;

use mybatis;

create table user(
	id int(20) not null ,
	name VARCHAR(30) DEFAULT NULL,
	pwd VARCHAR(30) DEFAULT NULL,
	PRIMARY key (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

INSERT INTO mybatis.`user` VALUES (1, 'tim', '123');
INSERT INTO mybatis.`user` VALUES (2, 'timmm', '1fd23');
INSERT INTO mybatis.`user` VALUES (3, 'jam', '1aa23');
INSERT INTO mybatis.`user` VALUES (4, 'fadsm', 'abc');
INSERT INTO mybatis.`user` VALUES (5, 'ff', 'gg');

新建maven项目,先把目录清空,然后在父工程中导入依赖:

<dependencies>
    <!--mysq驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.25</version>
    </dependency>
    <!--mybatis-->
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    <!--junit-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

创建子模块:mybatis-helloworld

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--核心配置文件-->
<configuration>
    <!--环境,可以配置多套,然后选择默认的环境-->
    <environments default="development">
        <environment id="development">
            <!--事务管理,默认使用JDBC的事务管理 -->
            <transactionManager type="JDBC"/>
            <!--连接数据库相关的东西,看名字跟着写就是了-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8&amp;useUnicode=true&amp;serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="admin"/>
            </dataSource>
        </environment>
    </environments> 
</configuration>

按照官网的做法,其实有几步是固定的,所以直接改改,写成一个工具类。

public class MybatisUtils {
    private static SqlSessionFactory sqlSessionFactory;
    static {
        try {
            // 获取SqlSessionFactory对象
            String resource = "org/mybatis/example/mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。
      */
    public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
}

然后建dao的接口和相关xml。
这里口头描述就很。。。抽象,最后我会放一张目录的截图,这样就清晰了。

UserDao

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

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">
<!--namespace绑定一个对应的dao(或者mapper)接口-->
<mapper namespace="com.tanjiaming99.dao.UserDao">
    <!--
        select就是查询语句
        id对应namespace的接口UserDao中的方法名,
        resultType是返回结果,这里要写全限定类名(以后可以设置别名,更简短)
        中间写sql语句
    -->
    <select id="getUserList" resultType="com.tanjiaming99.pojo.User">
        select *
        from mybatis.user
    </select>
</mapper>

下面就可以开始测试了!

写一段测试代码:

public class UserMapperTest {
    @Test
    public void testGetUserList(){
        // 获取一个SqlSession
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        // 拿到mapper接口就行了
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.getUserList().forEach(System.out::println);
        sqlSession.close();
    }
}

运行,发现居然报错了。。。其实是前面埋了两个坑。

  1. 没有注册mapper

核心配置文件mybatis-config.xml中,需要注册一个个mapper。

<mappers>
    <!--每个mapper.xml都要在此注册-->
    <!--resource需要的是path,直接Path From Source Root-->
    <mapper resource="com/tanjiaming99/dao/UserMapper.xml"/>
</mappers>
  1. 资源过滤问题

在pom.xml中加入,然后clean即可。
以后见到maven就可以把这段加上。。。

<!--maven的资源过滤问题-->
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>

做完上面两步之后,成功展示所有的用户。

User(id=1, name=tim, pwd=123)
User(id=2, name=timmm, pwd=1fd23)
User(id=3, name=jam, pwd=1aa23)
User(id=4, name=fadsm, pwd=abc)
User(id=5, name=ff, pwd=gg)

CRUD

上面写出来了一个入门案例后,可以根据这个架子写一些CRUD的操作了。
这时可以知道,只要改接口、Mapper和测试类,别的都不用改了。

查询

根据id获取user

User getUserById(Integer id);
<select id="getUserById" resultType="com.tanjiaming99.pojo.User">
    select  * from mybatis.user where id = #{id}
</select>
@Test
public void testGetUserById(){
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(mapper.getUserById(5));
    }
}

这样一套改下来,就完成这个功能了。

模糊查询

补充一个模糊查询。
其实主要有两种形式:

  1. 在Java代码层面的传递通配符。

    <select id="getUserLike" resultType="com.tanjiaming99.pojo.User">
    
        select *
        from mybatis.user 
        where name like #{name} 
    </select>
    

    可以看到SQL语句中没有%,所以我们会在代码中写。

    @Test
    public void testLike() {
        try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            // 在Java代码中传递通配符% 
            mapper.getUserLike("%im%").forEach(System.out::println);
            sqlSession.commit();
        }
    }
    

    这样可以防止SQL注入,因为限制好了一个值,用户就耍不了花样。

  2. 在SQL语句中拼接

    小心注入噢。。。

    <select id="getUserLike" resultType="com.tanjiaming99.pojo.User">
    
        select *
        from mybatis.user
    #         有SQL注入的风险
        where name like "%" #{name} "%"
    </select>
    

    那么在Java代码中只需要传入想模糊查询的字段,通配符%已经限定好了。

新增

int addUser(User user);

这里要注意,传入的是一个User对象,所以需要在parameterType中写清楚对象引用。

同时,对象中的值是使用#来取的,而且这里的类名与数据库名一致,不用考虑映射。(如果不一致,需要进行结果集映射)

<!--在parameterType中写参数类型,-->
<insert id="addUser" parameterType="com.tanjiaming99.pojo.User">
    insert into mybatis.user
    values (#{id}, #{name}, #{pwd})
</insert>

别忘了事务!增删改都要提交事务。

@Test
public void testAddUser(){
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(mapper.addUser(new User(6, "66666", "this is 666")));
        // 需要提交事务!
        sqlSession.commit();
    }
}

修改

既然知道怎么插入了,那修改也是差不多的。。

int updateUser(User user);
<update id="updateUser" parameterType="com.tanjiaming99.pojo.User">
    update user
    set name = #{name},
        pwd  = #{pwd}
    where id = #{id};
</update>
@Test
public void testUpdateUser(){
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(mapper.updateUser(new User(6, "77777", "this is 77")));
        // 需要提交事务!
        sqlSession.commit();
    }
}

删除

int deleteUser(Integer id);
<delete id="deleteUser">
    delete
    from user
    where id = #{id};
</delete>
@Test
public void testDeleteUser(){
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(mapper.deleteUser(6));
        // 需要提交事务!
        sqlSession.commit();
    }
}

CRUD小结

这个CRUD真是无聊啊。。。做来做去都是一样的,暂时来说这个也就这样啦,没啥难度。【x】

这里要加或者要改东西,首先要确定写什么,所以就在接口上写东西;然后再对应的mapper中写sql,最后就能测试了。

要记得一些xml的参数,比如resultMap、parameterType。还有记得提交事务

补充小技巧——万能的map

体验一下,部分修改的sql。

以增加一个用户为例。

int addUser2(Map<String, Object> map);

我们这样写xml

<insert id="addUser2" parameterType="map">
    insert into mybatis.user
    values (#{userId}, #{userName}, #{password})
</insert>

为什么会有像userId、userName这样奇奇怪怪的字段呢?

原来它是与map里面的k-v对应
写个测试类就知道了。

@Test
public void testMap(){
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        // 万能的map,需要什么东西就把它丢到map里面
        // 不过可能维护起来有、麻烦
        Map<String, Object> map = new HashMap<>();
        // 需要什么字段,就把它放进去
        map.put("userId", 9);
        map.put("userName", "这是一个9");
        map.put("password", "9999");
        mapper.addUser2(map);
        sqlSession.commit();
    }
}

如果实体类中的表或字段的参数过多,可以考虑用map。
因为key的高度自定制化。

“野路子”?但是看着挺方便的,在旧的项目里面确实看到这样写,就是没有注释很头疼。

配置解析

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

重要的地方,就是配置

环境配置

尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

在environments中,default参数可以选定使用哪个环境,比如开发、测试等。。多套环境的情况。

还有就是,Mybatis默认事务管理器是JDBC,连接池是POOLED。

详情可以察看官网列举信息。

属性

这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。

即可以通过properties属性来引用配置文件。

就是比如连数据库那些用户名、密码这些,可以写在配置文件上,而不用写在代码里面。

比如,写一个db.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8&useUnicode=true&serverTimezone=Asia/Shanghai
username=root
password=admin

然后,在核心配置文件中引入。

注意!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>
    <properties resource="db.properties"/>
    ......其它代码省略,properties按顺序是在最上面

发现,里面可以写properties,所以我们也可以在注解中自己配置。

<properties resource="db.properties">
    <property name="username" value="root"/>
</properties>

把db.properties中的username删除,下面的同样会读取到username。

不过这里有个问题,这两个谁的优先级高?
试一下嘛,把properties中的名字乱改。
发现,还能运行,因此外部配置文件的优先级高

别名

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。

简单地说,看看mapper,会发现那些名字太了,可以让别名缩写。

它有两种方式,一种是给包起,另一种是给一个个实体类写。

typeAlias

<typeAliases>
    <typeAlias type="com.tanjiaming99.pojo.User" alias="user"/>
</typeAliases>

(好像还不区分大小写诶。。。甚至写成UsEr都可以)
这种方式比较自由,可以自定义

package

在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。

就是会自动扫描实体类的包,然后可以用它的小写的来用。。

<typeAliases>
    <package name="com.tanjiaming99.pojo"/>
</typeAliases>

@Alias("alias")

用注解可以覆盖包扫描的别名,这样同样可以自定义。

不过这个注解需要放在实体类头,注意别导错包。

另外,这里好像又涉及到优先级的问题。从官网上可知,@Alias>package,而typeAlias和package不能共存,所以@Alias和typeAlias谁优先级高呢?

经过测试,发现还是注解的优先级高


其实有一些Java常见的类型的别名,比如int、Integer。
具体可以看这里,总的来说,就是基本数据类型要加_,而包装类则是其小写。

设置

这里有很多设置,记不完的【x,但是可以看看一些常用的。

  • cacheEnabled
    • 缓存是否加载,默认为true
  • lazyLoadingEnabled
    • 懒加载,默认为false
  • mapUnderscoreToCamelCase
    • 驼峰命名自动映射,即把user_id => userId。
    • 默认关闭,需要打开。
  • logImpl
    • 开日志,有很多选项。
    • 一般偷懒会开STDOUT_LOGGING,当然如果配了LOG4J,就选它。

映射器

就是让接口的方法找到它在哪能找到那些SQL语句,告诉 MyBatis 到哪里去找映射文件。

其实有四种方式,但最常用的就两三种,这里仅作简单展示。

  • resource

该mapper的资源路径。

<mappers>
    <mapper resource="com/tanjiaming99/dao/UserMapper.xml"/>
</mappers>
  • 使用class绑定

就是想查的是哪个mapper就直接填上那个mapper的引用。

但是,它有、要求:

  1. 接口和Mapper配置文件必须同名
  2. 接口和Mapper配置文件必须在同一个包下。
<mappers>
    <mapper class="com.tanjiaming99.dao.UserMapper"/>
</mappers>
  • package

使用扫描包进行注入。
要注意的点同上!


但是呢,我发现后面使用springboot之后,在Mapper上加一个@Mapper注解,再写写yml,好像就什么都不用管了,不用管同不同包同不同名。。。。

解决属性名和字段名不一致——ResultMap

resultMap部分在官网的位置。

还是跟旧的项目一样,但是把User中的pwd改成password,这样就与数据库的字段不一致了。

查询,会发现所有的password都是null。

怎么解决?

  1. 起别名

    select id, name, pwd as password from mybatis.user

    直接改sql语句。

  2. resultMap 结果集映射

使用

修改xml的标签。

<select id="getUserById" resultMap="userMap">
    select *
    from mybatis.user
    where id = #{id}
</select>

首先,把resultType修改成resultMap。接下来就应该创建一个userMap了。

<resultMap id="userMap" type="user">
    <result column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="pwd" property="password"/>
</resultMap>

这个是为了数据库中的列与实体类的属性相对应~

  • type:实体类
  • column:对应数据库中的字段。
  • property:对应user属性名。

其实,它本来就会自动映射,比如当你实体类属性与数据库字段一样的时候,就不用管了,它会隐式地自动创建一个resultMap,然后把字段和属性相匹配。

日志

排错小助手——日志。

mybatis内置了很多日志工厂,在属性中,可以设置logImpl,详情见官网or上文的属性中。

常用有很多,比如LOG4j、STDOUT_LOGGING

使用mybatis的标准日志工厂。

在mybatis-config.xml中,按正确的位置添加:

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

当然,也可以换成LOG4J,只是换个参数就行。

关于日志的知识,还是拆开另一个专题来用,现在就只是知道导入、粘别人的配置、使用,然后就没了……

分页

SQL语句中,分页通常是:select * from user limit startIndex, pageSize

首先,写一个分页的接口。

List<User> getUserByLimit(Map<String, Integer> map);

它利用map传入分页所需的两个参数:startIndex和pageSize。

<select id="getUserByLimit" resultType="user" parameterType="map">
    select *
    from mybatis.user
    limit #{startIndex}, #{pageSize}
</select>

这样就能用起来了。

@Test
public void testGetUserByLimit() {
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        Map<String, Integer> map = new HashMap<>();
        map.put("startIndex", 1);
        map.put("pageSize", 2);
        mapper.getUserByLimit(map).forEach(System.out::println);
    }
}

感觉没啥问题,但是本质上还是在写SQL,不够面向对象 ?

所以还有一种使用RowBounds的分页。(当然,它没有上面的方式快。)

<select id="getUserByRowBounds" resultMap="userMap">
    select *
    from mybatis.user
</select>

它就正常的查询。不再使用SQL进行分页,感觉好像mp的Page。。。

这个看着感觉还行

@Test
public void testGetUserByRowBounds() {
    try (SqlSession sqlSession = MybatisUtils.getSqlSession();) {
        RowBounds rowBounds = new RowBounds(0, 2);
        // 通过Java代码层面实现
        sqlSession.selectList("com.tanjiaming99.dao.UserMapper.getUserByRowBounds", null, rowBounds).forEach(System.out::println);
    }
}

多对一

以老师、学生为案例,从学生角度上,多个学生关联一个老师, 是多对一;从老师角度上,是一种集合的感觉,一个老师有很多个学生。

数据库环境搭建。

CREATE TABLE `teacher`
(
    `id`   INT(10) NOT NULL,
    `name` VARCHAR(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE = INNODB
  DEFAULT CHARSET = utf8;

INSERT INTO teacher(`id`, `name`)
VALUES (1, '秦老师');

CREATE TABLE `student`
(
    `id`   INT(10) NOT NULL,
    `name` VARCHAR(30) DEFAULT NULL,
    `tid`  INT(10)     DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `fktid` (`tid`),
    CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE = INNODB
  DEFAULT CHARSET = utf8;
INSERT INTO `student` (`id`, `name`, `tid`)
VALUES (1, '小明', 1);
INSERT INTO `student` (`id`, `name`, `tid`)
VALUES (2, '小红', 1);
INSERT INTO `student` (`id`, `name`, `tid`)
VALUES (3, '小张', 1);
INSERT INTO `student` (`id`, `name`, `tid`)
VALUES (4, '小李', 1);
INSERT INTO `student` (`id`, `name`, `tid`)
VALUES (5, '小王', 1);

然后创建对应的实体类、接口、xml等。。。

多对一

完成需求:查询所有的学生信息(以及对应的老师信息)。

首先写出sql语句

select s.id, s.name, t.name
from student s,
     teacher t
where s.tid = t.id

发现。。不大妥,这个写sql很容易,但是要把它赋值到对象上?。。

这里写两种方法,感觉已经学过几次了,然而再看回来还是很。。。

想查的东西,就在下面的代码里面,想通过mapper里面的getStudent中获得所有的信息,难道写上上面那条sql就行了?好像不大行。。所以还得做一些手脚。

@Test
public void testGetStudents(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    // 期望能查出来老师和学生的信息,然而。。。teacher为null
    mapper.getStudent().forEach(System.out::println);
    sqlSession.close();
}

在mapper中,下面这条东西肯定是查不出来teacher的,即使换成上面那条sql也一样,因为它不认识teacher啊。

从现在学到的知识来看,我觉得编写这个还是有、压力的。

按照查询嵌套处理

<select id="getStudent" resultMap="StudentTeacher">
    select *
    from student
</select>

分开查,那就可以按照查出学生的id来来得到老师。

<select id="getStudent" resultMap="StudentTeacher">
    select *
    from student
</select>
<select id="getTeacher" resultType="teacher">
    select *
    from teacher
    where id = #{id}
</select>

但是它们两个现在还是没啥关系呀,所以需要写一个resultMap把它们关联起来~。

这里我好像只知道各个参数想表达的意思,这个是关联了student里面的teacher属性,对应数据库的字段是tid,然后在Java的类型是teacher,但是还是不知道怎么查?所以最后写上一个查询语句。

<resultMap id="StudentTeacher" type="student">
    <result column="id" property="id"/>
    <result column="name" property="name"/>
    <!--剩下的是Teacher,这个属性怎么处理呢?-->
    <!--对象使用association,集合用collection-->
    <association property="teacher" column="tid" javaType="teacher" select="getTeacher"/>
</resultMap>

按照结果嵌套处理

一般使用这种方式,非常直观。。。

测试不变,只改xml。

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

<resultMap id="StudentTeacher" type="student">
    <!--上面的SQL语句中起了别名,这里的column就写那个别名-->
    <result property="id" column="sid"/>
    <result property="name" column="sname"/>
    <!--这里就不能写什么tname了(虽然tname确实是teacher里面一个属性)-->
    <!--直接写它的java类型,就是它查出来的结果了-->
    <association property="teacher" javaType="teacher">
        <result property="name" column="tname"/>
        <result property="id" column="tid"/>
    </association>
</resultMap>

话说那个javaType,我好像不大懂怎么用,但是我发现,mybatis会自动推断,没写的话也能知道它是teacher。

ummmm暂时不大会,就先用着吧。

多对一

一个老师拥有多个学生,对于老师而言,就是一对多的关系。

这样的话,就改变一下实体类。

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Student {
    private Integer id;
    private String name;
    private int tid;
}

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Teacher {
    private Integer id;
    private String name;
    // 学生集合
    private List<Student> students;
}

完成需求:获取指定老师下的所有学生及老师信息。

同样,可以写出SQL语句:

select t.name tname, t.id tid, s.id sid, s.name sname
from student s,
        teacher t
where s.tid = t.id
    and t.id = #{tid}

这里其实都差不多,注意一下ofType参数就行,它代表集合中的参数,和关联的javaType一个意思。

不过这里用IDEA,会解析不了students和tid,不知道为什么。。。

另外,记得在接口中使用@Param绑定参数,不然的话又是找不到(需要和参数名一致才行,我这里计划绑的是tid但是没绑,所以找不到tid。。)

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

<resultMap id="TeacherStudent" type="teacher" >
    <result property="id" column="tid"/>
    <result property="name" column="tname"/>
    <!--集合中的泛型,使用ofType-->
    <collection property="students" ofType="student">
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <result property="tid" column="tid"/>
    </collection>
</resultMap>

这个是按结果嵌套处理,非常容易理解,下面来整那个非常困难的按照查询嵌套处理。


这种方式,怎么说。。。确实是sql上非常简单,两条都是简单查询,但是要把它们映射起来就需要做一些复杂的操作了。

这里因为是学生集合,所以使用collection标签。

然后在那些参数,ummmm怎么说呢,突然有、理解吧。首先你要对哪个属性或集合(property)进行映射,然后属性的类型(javaType)是什么?集合中的元素的类型(ofType)又是什么?
然后就可以写另一个简单查询(select),但是简单查询的参数(tid)怎么传过去呢?就只好使用column字段了。

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

<resultMap id="TeacherStudent" type="teacher">
    <collection property="students" javaType="ArrayList" ofType="student" select="getStudentByTeacherId" column="id" />
</resultMap>

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

小结

  • 关联:association,多对一
  • 集合:colllection,一对多
  • javaType和ofType。
    • javaType用于指定实体类中属性的类型。
    • ofType用于指定映射到集合中的元素的类型。(泛型中的约束?或者那些pojo实体类?)

动态SQL

动态SQL就是指根据不同的条件生成不同的SQL语句。

而且,它在sql层面上,可以加点逻辑判断。

官网在此:https://mybatis.org/mybatis-3/zh/dynamic-sql.html#

环境搭建

sql语句:

CREATE TABLE `blog`(
`id` VARCHAR(50) NOT NULL COMMENT '博客id',
`title` VARCHAR(100) NOT NULL COMMENT '博客标题',
`author` VARCHAR(30) NOT NULL COMMENT '博客作者',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`views` INT(30) NOT NULL COMMENT '浏览量'
)ENGINE=INNODB DEFAULT CHARSET=utf8;

插入数据(别人的)

@Test
public void addBlogTest() {
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    Blog blog = new Blog();
    blog.setId(IDUtils.getId());
    blog.setTitle("Mybatis");
    blog.setAuthor("狂神说");
    blog.setCreateTime(LocalDateTime.now());
    blog.setViews(9999);

    mapper.addBlog(blog);

    blog.setId(IDUtils.getId());
    blog.setTitle("Java");
    mapper.addBlog(blog);

    blog.setId(IDUtils.getId());
    blog.setTitle("Spring");
    mapper.addBlog(blog);

    blog.setId(IDUtils.getId());
    blog.setTitle("微服务");
    mapper.addBlog(blog);
    sqlSession.commit();
    sqlSession.close();
}

if

以带条件的查询为例:

List<Blog> queryBlogIf(Map map);

在Mapper中,使用if标签。

它里面写上测试的条件,如果通过了,它才会继续拼if里面的语句。

(PS:我在最近的实习中发现,如果前端传一个""字符串过来,它也会进去条件里面,所以一般我会再加一个**and title != ''**这样的语句在里面,防止出现奇奇怪怪的情况)

<select id="queryBlogIf" resultType="blog">
    select * from mybatis.blog where 1= 1
    <if test="title != null">
        and title = #{title}
    </if>
    <if test="author != null">
        and author = #{author}
    </if>
</select>

由此可以看到,当我需要一些条件的时候,只要往map里面丢就行。

@Test
public void queryIf(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    Map map = new HashMap();
//        map.put("title", "Mybatis");
    map.put("author", "狂神说");
    List<Blog> blogs = mapper.queryBlogIf(map);
    blogs.forEach(System.out::println);
    sqlSession.close();;
}

where

首先,对于上面例子的语句中,那个where 1=1显得很奇怪,难道没有什么方法去掉这种拼接吗?
有的,使用where标签!

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

修正:

<select id="queryBlogIf" resultType="blog">
    select * from mybatis.blog
    <where>
        <if test="title != null">
            and title = #{title}
        </if>
        <if test="author != null">
            and author = #{author}
        </if>
    </where>
</select>

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

仍然是按条件查询,但是这次是只要其中一个条件,而不是所有条件。

这里傻了,没有加上where,choose并不是替代where的啊,只是说在where中选择一个条件而已,并不是全部条件都选,你可以有多个条件,但是成立的只会是最上面的。

<select id="queryBlogChoose" resultType="com.tanjiaming99.pojo.Blog" parameterType="map">
    select * from blog
    /*这里还是要套一个where。(当然要了,不然怎么条件查询呢!)*/
    <where>
        <choose>
            <when test="title != null">
                and title = #{title}
            </when>
            <when test="author != null">
                and author = #{author}
            </when>
            <otherwise>
                and views = #{views}
            </otherwise>
        </choose>
    </where>
</select>

set

其实这个set,和上面的and、or都一样的,就是在更新的时候,需要set a=xx, b=xx, c=xx这样,但是这里面的,(逗号)一样有and和or那种拼接上的问题,所以加上这个标签,在更新的时候就不用担心这么多了。

与where相似。

当然,虽然它能去掉,逗号,但是却不能添加,逗号,如果没写,会报错的。。

根据id更新Blog。

<update id="updateBlog">
    update blog
    <set>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="author != null">
            author = #{author},
        </if>
        <if test="views != null">
            views = #{views},
        </if>
    </set>
    where id = #{id}
</update>

trim

其实这个和where、set标签一样的,是它的“爸爸”,它们的操作都是根据trim来的,或者说,where和set是trim的自定义标签。

where:前缀加WHERE,前端覆盖是and或or,看它名字就能猜到。会把前缀多的给去掉。同时要小心,它后面有个空格!

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

再看看set,它是处理末尾的逗号嘛,那就%……
和上面也差不多嘛,反正看着它的名字就知道怎么用了。

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

foreach

其实我也用过。。自定义批量查询。
参数里面有一个集合,参数名是list。我命名它里面的每一小项为id,左边右边则是(),用逗号分隔。

<select id="queryBatchKeywordLevel" resultType="com.pc.censor.api.model.entity.KeywordLevel">
    select * from csr_keyword_level where keyword_id in
    <foreach collection="list" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

SQL片段

其实是实现了SQL语句的复用,以后想要引用一段重复的SQL时,就用include引进来。

<sql id="select_column">
    id as 'id',
    config_name as 'configName',
    config_key as 'configKey',
    config_value as 'configValue',
    description as 'description',
    create_at as 'createAt',
    create_by as 'createBy',
    update_at as 'updateAt',
    update_by as 'updateBy'
</sql>
<select id="queryAll" resultType="com.pc.censor.api.model.entity.Config">
    select 
    <include refid="select_column" />
    from csr_config
</select>

缓存

这里重点写写它的一级缓存和二级缓存,别的更多的理论信息就不写了。

缓存其实是在多次查询相同信息时,直接从缓存中拿就比从数据库中拿快了,缓存是放在内存中的临时数据,它是解决高并发情况下的性能问题

一级缓存

默认情况下,只有一个缓存开启。

本地缓存,是SqlSession级别的。

public void updateSet() {
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    // ...缓存有效
    sqlSession.close();
}

二级缓存

手动开启,基于namespace级别的。

  • 一个会话查询一条数据,这条数据就会被放在当前会话的一级缓存中。
  • 如果当前会话关闭了,这个会话对应的一级缓存就会没有了。
  • 但是我们还需要它,就把这个一级缓存中的数据保存到二级缓存中。
  • 新的会话查询信息时,从二级缓存中获取内容。
  • 不同的mapper查出的数据会放在自己对应的缓存中。

验证一级缓存

SqlSession级别的缓存,在一个SqlSession中,查询两个相同的记录。查看日志!

@Test
public void test(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user1 = mapper.queryUserById(2);
    System.out.println(user1);
    System.out.println("------------");
    User user2 = mapper.queryUserById(2);
    System.out.println(user2);
    System.out.println("------------");
    System.out.println(user1 == user2);

    sqlSession.close();
}

可以看到,它确实是被缓存了,而且只执行一次sql查询。

Opening JDBC Connection
Created connection 1926343982.
==>  Preparing: select * from mybatis.user where id = ?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, timmm, 1fd23
<==      Total: 1
User(id=2, name=timmm, pwd=1fd23)
------------
User(id=2, name=timmm, pwd=1fd23)
------------
true
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72d1ad2e]
Returned connection 1926343982 to pool.

Process finished with exit code 0

失效情况(官网):

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的1024个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

测试:insert、update 和 delete 操作可能会改变原来的数据,必定会刷新缓存(即使改的是不原来的那个)

@Test
public void test(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user1 = mapper.queryUserById(2);
    System.out.println(user1);
    System.out.println("------------");
    System.out.println("修改了9号用户");
    mapper.updateUser(new User(9, "小99", "9999"));
    System.out.println("------------");
    User user2 = mapper.queryUserById(2);
    System.out.println(user2);
    System.out.println("------------");
    System.out.println(user1 == user2);

    sqlSession.close();
}

查看结果,发现确实是刷新了,也重新回数据库中查东西了。

Opening JDBC Connection
Created connection 1926343982.
==>  Preparing: select * from mybatis.user where id = ?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, timmm, 1fd23
<==      Total: 1
User(id=2, name=timmm, pwd=1fd23)
------------
修改了9号用户
==>  Preparing: update user set name = ?, pwd = ? where id = ?;
==> Parameters: 小99(String), 9999(String), 9(Integer)
<==    Updates: 1
------------
==>  Preparing: select * from mybatis.user where id = ?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, timmm, 1fd23
<==      Total: 1
User(id=2, name=timmm, pwd=1fd23)
------------
false
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72d1ad2e]
Returned connection 1926343982 to pool.

还有一个大杀招:清理缓存。

sqlSession.clearCache();


一级缓存默认开启,只在一次SqlSession有效,在开始连接到关闭的过程中。

(可以想象成一个map ?查的东西就丢到map中,用完就丢了。)

验证二级缓存

首先,要在配置中显式地开启全局缓存:
<setting name="cacheEnabled" value="true"/>

然后,在mapper中加入缓存标签。

<cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>

开始测试了。

@Test
public void test() {
    SqlSession sqlSession1 = MybatisUtils.getSqlSession();
    SqlSession sqlSession2 = MybatisUtils.getSqlSession();
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

    User user1 = mapper1.queryUserById(2);
    System.out.println(user1);
    sqlSession1.close();

    User user2 = mapper2.queryUserById(2);
    System.out.println(user2);
    System.out.println(user1 == user2);
    sqlSession2.close();
}

这里是走了缓存的,因为只有一个SqlSession挂掉之后,才会把东西放到二级缓存中,所以这里特意把sqlSession1给关掉,也可以看看下面的日志,会发现只查了一次,而且还hit一下。

Cache Hit Ratio [com.tanjiaming99.dao.UserMapper]: 0.0
Opening JDBC Connection
Created connection 1130894323.
==>  Preparing: select * from mybatis.user where id = ?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, timmm, 1fd23
<==      Total: 1
User(id=2, name=timmm, pwd=1fd23)
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@436813f3]
Returned connection 1130894323 to pool.
Cache Hit Ratio [com.tanjiaming99.dao.UserMapper]: 0.5
User(id=2, name=timmm, pwd=1fd23)
true

注意噢,它是mapper级别的,不同的mapper之间是没关系的。

一些理论

作用域和生命周期

生命周期和作用域是至关重要的,因为错误的使用会导致非常严重的并发问题。

SqlSessionFactoryBuilder

  • 它一旦创建了 SqlSessionFactory,就不再需要它了。
  • 最佳应用:局部变量

SqlSessionFactory

  • SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
  • 可以想象成数据库连接池。
  • 最佳应用:单例模式或者静态单例模式。

SqlSession

  • 每个线程都应该有它自己的 SqlSession 实例。
  • 连接到连接池的一个请求。
  • SqlSession 的实例不是线程安全的,因此是不能被共享的。
  • 用完赶紧关,否则资源被占用。
  • 最佳应用:请求或方法作用域。

mybatis执行流程剖析

可以看回创建的代码,再走走

  1. Resources获取加载全局配置文件。
  2. 实例化SqlSessionFactoryBuilder构造器
  3. 解析配置文件流XMLConfigBuilder
  4. 这个对象会得到所有的配置信息,解析时放在Configuration中。
  5. 得到配置后,将SqlSessionFactory实例化。
  6. Transactional事务管理器。
  7. executor执行器,核心!
  8. 创建sqlSession
  9. 实现CRUD。(万一执行失败,回滚到6)
  10. 查看是否执行成功,不成功可能回滚(即到6的事务管理器)
  11. 提交事务,然后关闭。

缓存

image.png