MyBatis学习从练气到化虚

MyBatis从练气到化虚


前言:
学习就像修炼,只要你付出了努力,就会有进步。有人说成功是99份的努力和1份的天赋,而这1份的天赋限制了你的高度;但是笔者想说99%的人是都没有做到这99份努力的,我们还远远没有努力到需要拼天赋的程度,就好比说若是付出了99份努力我们可以成为架构师,而成为CTO则很需要那一份天赋的存在,但是大部分人是都没有成为架构师,为什么呢?自然是我们努力的程度远远不够。

一、MyBatis的由来

MyBatis是当下较为流行的ORM框架之一,他的作用就是帮助我们从数据库中获取数据的,他的底层封装了JDBC,使得程序编码变得更简洁方便,ORM框架早就是项目中不可或缺的重要一环了,那什么是ORM呢?

1.什么是ORM

Object Relational Mapping:对象关系映射,对象关系型映射中的对象指的是POJO,关系则指的是关系型数据库,映射指的是将POJO与关系型数据库中的表进行映射起来。解释完ORM的意思,那他的作用也就很明了了,就是帮助程序将对象与数据进行映射,这样就会方便程序开发者来操作数据库,这也就是ORM的作用了。我们今天要说的MyBatis自然就是ORM的一种落地实现了,MyBatis前身是iBatis,甚至可以说他俩就是一款ORM框架,只不过是更改了名称而已。

2.iBatis的由来

iBatis是一款半自动的ORM框架,这个半自动是相对于Hibernate来说的,因为使用Hibernate是完全不需要我们去写sql的,所以Hibernate被称为全自动框架,但是全自动也有自己的弊端,对于复杂场景的支持不够友好,所以才有了iBatis的生存空间,IBatis诞生于2001年,是由Clinton Begin发起的一个开源项目,在2010年被谷歌托管然后更名为MyBatis,至此MyBatis诞生了,在2013年后MyBatis迁移到了Github一直到现在,他支持数据的持久化,支持定制化sql,存储过程以及高级映射可以使用注解也可以使用xml来开发,是一款优秀的持久层的ORM框架。

3.为什么选择Mybatis

可以发现当下在java后端中使用最多的ORM框架还是MyBatis,首先因为他开源,社区活跃度比较高,技术也成熟,其次笔者感觉主要还是缺乏一款革命性的ORM产品,就像Spring的出现一样,Spring的出现就定义了java开发的新模式(MyBatis-plus只是对MyBatis进行了扩展,增加了一些插件,其实还是MyBatis),废话不多说了,一起来总结下MyBatis的使用吧。

二、MyBatis的基本使用

1.准备数据库

1)创建数据库

create database `mybatis`;
use `mybatis`;

2)创建一张商户信息表

create  table if not EXISTS `mybatis`.`supplier_info`(
  `oid` int(20) not null ,
  `company_name` varchar(100) not null ,
  `legeal_person` varchar(20) not null ,
  `id_card` varchar(20),
  primary key(`oid`)
)engine=INNODB default charset = utf8;

3)插入几条数据备用

insert into mybatis.supplier_info VALUES
('1','Huawei','任正非','00001'),
('2','alibaba','马云','00002'),
('3','lenovo','杨元庆','00003');

到这里数据就已经准备完了,下面就可以开始真正的实战了。

2.创建工程,增加实体类

数据环境已经准备完毕,接下来我们需要通过maven来创建一个父子工程(使用单个maven工程也是完全可以的),创建父子工程的教程这里不细说,若是有不明白的参考这里:Maven创建父子工程详解,下面笔者就直接使用maven创建出的父子工程来展示这个学习案例了,然后我们在api子工程中添加一个实体类,如下,这个代码里使用的都是lombok的注解,同时建议大家使用这个小框架,他可以大大简化我们的开发流程,而且现在大部分项目中都使用这个小框架。

package com.cheng;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author pcc
 * @version 1.0.0
 * @className SupplierInfo
 * @date 2021-07-27 15:20
 */
@Data//省略get、set、tostring等等方法
@Accessors(chain = true)//开启实体的链式操作
@AllArgsConstructor//全参构造
@NoArgsConstructor//无参构造
public class SupplierInfo {
    Integer oid;
    String companyName;
    String legealPerson;
    String idCard;
}

3.引入jar包依赖

我们去mavn仓库中找到MyBatis的依赖,如下,我们使用最新版本的MyBatis版本3.5.7这是21年四月的
在这里插入图片描述

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
</dependency>

还有一些其他的相关的包,比如连接数据库的包,测试包,热部署包,此外还有jdk版本的声明,文件导出的配置都在这里一起展示了,必须要注意的是使用Mybatis必须添加文件导出配置,不然maven编译项目时不会把mapper.xml文件编译到class文件中,具体配置如下:

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

    <groupId>com.cheng</groupId>
    <artifactId>mybatis-father</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>mybatis-son-one</module>
        <module>mybatis-api</module>
    </modules>

    <properties>
        <mybatis-version>3.5.6</mybatis-version>
        <mysql-connector-version>5.1.38</mysql-connector-version>
        <junit-version>4.13</junit-version>
        <lombok-version>1.18.18</lombok-version>
        <springboot-version>2.3.4.RELEASE</springboot-version>
        <springboot-web-version>2.3.4.RELEASE</springboot-web-version>
        <druid-version>1.2.4</druid-version>
        <devtools-version>2.4.4</devtools-version>
        <mybatis-api-version>1.0-SNAPSHOT</mybatis-api-version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!--导入api包-->
            <dependency>
                <groupId>com.cheng</groupId>
                <artifactId>mybatis-api</artifactId>
                <version>${mybatis-api-version}</version>
            </dependency>
            <!--mybatis包-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>${mybatis-version}</version>
            </dependency>
            <!--数据库连接驱动包-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-version}</version>
            </dependency>
            <!--数据源-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid-version}</version>
            </dependency>
            <!--单元测试包-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit-version}</version>
            </dependency>
            <!--lombok包-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok-version}</version>
            </dependency>
            <!--springboot依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot</artifactId>
                <version>${springboot-version}</version>
            </dependency>
            <!--springboot的web依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot-web-version}</version>
            </dependency>
            <!--导入热部署依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <version>${devtools-version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <!--指定当前工程jdk的版本-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <!--导出静态资源文件,防止xml配置文件未加载-->
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

</project>

4.子工程中引入所需的jar包

这里就是将当前工程需要的jar包引进来就行,因为需要连接数据库,所以我们需要引入实体类包,mybatis的包,数据库连接驱动包,连接池包,对外提供接口的话还需要boot包,boot-web包等等,这里展示下引入的包,如下所示:

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

    <artifactId>mybatis-son-one</artifactId>

    <dependencies>
        <!--导入api包-->
        <dependency>
            <groupId>com.cheng</groupId>
            <artifactId>mybatis-api</artifactId>
        </dependency>
        <!--mybatis包-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>
        <!--数据库连接驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!--单元测试包-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--springboot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!--springboot的web依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    </dependencies>
</project>

5.配置Mybatis的配置文件

我们创建了一个父工程,一个api实体类工程,一个mybatis的工程,并且都引入了对应的需要的jar包,基础条件已经准备完成,接下来在nybatis子工程中配置mybatis的配置文件mybatis-config.xml。这个也是官方推荐的命名,建议都使用这个名称来命名,这样比较友好,下面展示下mybatis-config.xml的配置详情,里面已经列出了各个配置项的作用,这里就不重复赘述了,其中建议开启useSSL=true,笔者这里是使用的false,此外url中还需要声明编码的配置这样可以防止数据库查询出的数据乱码问题。配置mapper文件时,我们还可以使用包扫描,这里先只配置了加载单一的配置文件。

<?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"/><!--默认使用JDBC的事务管理,不需要改变-->
            <dataSource type="POOLED"><!--连接池配置,模式使用pooled-->
                <property name="driver" value="com.mysql.jdbc.Driver"/><!--驱动类配置-->
                <!--数据库url,SSL开启需要服务器身份验证,useUnicode=true$characterEncoding=UTF-8保证存取数据不会乱码-->
                <property name="url" value="jdbc:mysql://192.168.150.100:3306/mybatis?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8&amp;"/>
                <!--数据库用户名-->
                <property name="username" value="root"/>
                <!--数据库密码-->
                <property name="password" value="super"/>
            </dataSource>
        </environment>
    </environments>
    <!--配置mapper文件地址-->
    <mappers><!--使用resource声明资源路径时必须使用斜杠不能使用点-->
        <mapper resource="com/cheng/dao/SupplierMapper.xml"/>
    </mappers>
</configuration>

6.写dao接口与mapper.xml文件

准备好了工程、实体类、mybatis配置文件,我们就可以写dao接口了,先提供一个查询所有信息的方法,如下:

package com.cheng.dao;

import com.cheng.SupplierInfo;
import java.util.List;
/**
 * @author pcc
 * @version 1.0.0
 * @className SupplierDao
 * @date 2021-07-28 16:51
 */
public interface  SupplierDao {
    List<SupplierInfo> getSupplierInfo();
}

传统的dao接口中会有daoimpl来提供实现,当我们使用mybatis时,就不需要再去写daoimpl了,我们只需要为每个接口都提供一个对应的mapper.xml文件即可,为什么只提供一个接口就行了呢?而不需要去提供接口的实现,时间上mybatis会根据接口和对应mapper.xml文件来为我们自动生成实现类。所以我们就不需要提供实现类,只需要mapper.xml中关注sql的实现即可。这里展示下简单的一个mapper.xml文件,必须要注意的是namespace(命名空间)必须是dao的全限定名,此外若是数据库的列名称与实体类的名称不一致,我们就需要使用resultMap来进行映射。

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.cheng.dao.SupplierDao">
    <resultMap type="com.cheng.SupplierInfo" id="suppliermap">
        <id column="oid" property="oid"/>
        <result column="company_name" property="companyName"/>
        <result column="legeal_person" property="legealPerson"/>
        <result column="id_card" property="idCard"/>
    </resultMap>

    <select id="getSupplierInfo" resultMap="suppliermap">
		select * from mybatis.supplier_info
	</select>
</mapper>

7.测试数据获取

怎么使用Mybatis来连接数据库呢?这有一个标准的流程,如下所以,这就是使用Mybatis获取数据库数据的标准流程,乍一看比jdbc还复杂,使用Mybatis到底简化了什么呢?其实最主要的简化还是在dao层的实现类上,我们不需要在去写一些冗余的代码,在mapper.xml中写sql,可以让我们只关注sql的实现,而省略冗余的jdbc代码的操作。

    @Test
    public void testMybatis() throws Exception{
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try(SqlSession sqlSession = sqlSessionFactory.openSession()){
        SupplierDao supplierDao = sqlSession.getMapper(SupplierDao.class);
        List<SupplierInfo> list = supplierDao.getSupplierInfo();
        list.forEach( supplierInfo ->{
            System.out.println(supplierInfo.toString());
        });}catch (Exception e){
            e.printStackTrace();
        }
    }

下面展示下上面例子的运行结果,到这里mybatis的基本整合算是已经完成了,其实和简单,初次看可能内容不算少,多看两遍就会发现就这一点东西。
在这里插入图片描述

7.反思MyBatis的执行流程

  • 1)MyBatis如何找到mapper.xml中对应的sql?
    刚刚使用mybatis时肯定会有这种疑问,我在dao包下面放入自己的dao接口与mapper.xml怎么就自己去执行了sql呢?MyBatis能实现这个功能,其实主要归功于三点内容,第一点:就是dao与mapper.xml必须同包,第二点:我们在mapper.xml中都配置了namespace,这个namespace必须指向这个mapper.xml文件对应的dao接口文件,根据这两点Mybatis可以知道,我们这个mapper.xml到底是为了谁建立的,但依然还是不知道哪个方法应该对应到哪个sql,那就需要第三点了。第三点:mapper.xml中支持select、delete、insert、update等标签,且每个标签都必须声明id,MyBatis就是根据这个id来找到接口中的对应的方法的。根据这三点MyBatis就可以精准的找到每个dao中的方法对应的sql,然后根据sql来实现dao接口实现类的动态生成。
  • 2)可能有人会发现,在上面的代码中笔者获取了sqlsession后,使用完毕并没有进行关闭,这是为什么呢?这个其实和Mybatis没有关系,是jdk7以后增加的资源关闭方式,以前我们都是使用try-catch-finally来关闭连接,但是若是在finally中发生了异常就会关闭失败,因此jdk7以后提供了try-with-resource的资源关闭方式,也就是上面的写法,我们直接将需要关闭的资源声明在try后面的括号内即可,jvm会自动帮我们关闭。当然自动关闭也是有前提的,当前接口或者类必须继承或者实现了java.lang.AutoCloseable这个类才行。

三、增删改查的实现

在第二部分笔者已经介绍了一个简单的查询多条数据的操作,在实际的工作场景中,我们肯定会使用到所有的CRUD场景,并且增、删、改,我们是需要提交事务的,不然数据并不会真正刷新到我们的表中,下面一起看下CRUD的实现以及各个场景中的的关键知识点吧。

1.增加数据的实现

刚刚已经构建完成了的内容我们是不需要再去重复操作的了,比如mybatis-config.xml这个配置文件,我们已经配置完成,无需再次操作,比如dao对应的mapper.xml文件也已经存在我们也无需再次操作了,其实我们需要操作的就是在dao中新增一个插入数据的方法,然后在mapper.xml中提供这个插入实现的sql即可。

  • dao中新增插入方法如下
    //插入一条商户信息
    void insertSupplierInfo(SupplierInfo supplierInfo);
    
  • Mapper.xml中增加对应的sql
    <insert id="insertSupplierInfo" parameterType="com.cheng.SupplierInfo">
        insert into mybatis.supplier_info (oid, company_name, legeal_person, id_card) values (#{oid},#{companyName},#{legealPerson},#{idCard});
    </insert>
    
    可以看到如果新增一个方法,我们需要写的东西其实很少很少,只需要写个接口方法,然后在mapper.xml中写入sql即可,是不是非常简单呢,这也就是我们使用mybatis的原因了,不过mapper.xml中还是有需要说明的地方,首先id必须与接口方法保持一致,我们已经知道了,其次是parameterType这个参数是第一次出现,顾名思义他的作用就是生命传入的参数是个什么类型的,与我们直接实现daoimpl时传入参数是一个道理,mybatis也是需要通过这个参数来构建实现方法的传参的,这里使用的是类型的全限定名,后期我们可以使用别名包扫描就不用写全限定名这么麻烦了,这个后面再说。此外我们在写insert语句时出现了#{}这个操作符,这个操作符是用来获取属性的,当前传入的是个对象类型,我们可以直接使用#{}传入对象的属性名,来获取对象的属性值。
  • 测试插入
    @Test
    public void testInsert(){
        try(SqlSession sqlSession = MybatisUtil.getSqlSession()){
            SupplierDao supplierDao = sqlSession.getMapper(SupplierDao.class);
            SupplierInfo supplierInfo = new SupplierInfo().setOid(4).setCompanyName("tencent").setIdCard("45").setLegealPerson("马化腾");
            supplierDao.insertSupplierInfo(supplierInfo);
            sqlSession.commit();
        }
    }
    
    这块测试代码也是需要说下,因为每次获取sqlsession的代码都是一样的,所以笔者对其进行了封装,这个太简单就不展示了,此外还可以发现笔者直接将获取sqlsession的代码放在了try里,这个其实上面已经讲过这个是jdk7提供的资源释放方式,只要实现或者继承了AutoCloseable接口都可以使用这种方式关闭资源。并且因为此处不会有异常产生,所以笔者也没有写catch,所以就只有try部分了。try代码块里面的SupplierInfo创建时怎么可以一直set呢?这个是因为使用了lombok的链式操作,我们只需要为SupplierInfo增加一个注释就可以实现这种方式,这种操作方式可以简化不必要的代码书写,笔者感觉还是挺好的,除了这些之外我们一定要注意的是插入操作需要提交事务,事务默认是开启的,不提交数据是不会真正刷新到表里的。
    @Data//省略get、set、tostring等等方法
    @Accessors(chain = true)//开启实体的链式操作------------------就是这行注解的作用
    @AllArgsConstructor//全参构造
    @NoArgsConstructor//无参构造
    public class SupplierInfo {
        Integer oid;
        String companyName;
        String legealPerson;
        String idCard;
    }
    

2.删除数据的实现

增加删除已经实现了,现在来一起看下数据的删除吧,我们根据数据的主键进行删除,笔者主键的命名是oid。

  • dao中新增删除方法如下:
    //根据oid删除一条商户信息
    void deleteByOid(int oid);
    
  • Mapper.xml中增加对应的sql
    <delete id="deleteByOid" parameterType="int">
        delete from mybatis.supplier_info where oid = #{oid}
    </delete>
    
    到这里其实就已经编码完成了,我们可以发现增加方法其实只需要在dao接口中增加接口方法,然后mapper.xml中增加sql即可。这真的很方便,省去了大量的重复工作。sql中的id我们已经说过了,她必须和接口方法的方法名一致。不过这里的parameterType好像和刚刚传的不一样,刚刚我们是传了一个实体类的全限定名,这里只有一个int,怎么回事呢?这里必须要说的是对于基本数据类型和string类型,当他们作为参数时,我们可以直接写类型名称不需要写全限定名,为什么呢?因为Mybatis已经帮我们引入了这些常用的基本数据类型,我们写int,Mybatis就会知道这是要使用基本数据类型int了。以后我们使用别名策略后,我们自己写的实体类也可以这么写。此外还需要说明的是当传入参数是基本数据类型时,我们甚至可以不写parameterType,这样也不会有任何问题,sql中的获取参数的方式不用改变。测试代码千篇一律这里就不重复展示了。

3.修改数据的实现

  • dao中新增修改方法如下:
    //根据oid修改商户信息
    void updateByOid(SupplierInfo supplierInfo);
    
  • Mapper.xml中增加对应的sql
    <update id="updateByOid" parameterType="com.cheng.SupplierInfo">
        update mybatis.supplier_info set company_name = #{companyName},legeal_person=#{legealPerson},id_card=#{idCard} where oid = #{oid}
    </update>
    
    修改方法其实没有什么额外需要说的,与增加删除其实都差不多。

4.查询数据的实现

其实查询应该才是我们使用最为广泛的操作,查询我们还可以根据查询条件的多少分为单条件查询、多条件查询,可以根据返回结果的多少分为查询单条、查询多条等几种情况。因为查询也是应用最多场景最多的操作,笔者这里就模拟下这四种场景来总结下查询:单条件查询、多条件查询、返回单条的查询、返回多条的查询。

  • 1)单条件查询,同时返回单条数据
    • dao中新增单条件查询方法如下
      //单条件查询,返回单条数据
      SupplierInfo selectByOid(int oid);
      	~~~
      
    • Mapper.xml中增加对应的sql
      <select id="selectByOid" resultType="com.cheng.SupplierInfo">
          select oid,company_name,legeal_person,id_card from mybatis.supplier_info where oid = #{oid}
      </select>
      
      这样就写完了,测试代码因为都差不多,笔者就不展示了,然后运行就会发现只查到了oid其他参数怎么都是null呢?这里就要引出resultMap了,当数据库中的列名于实体类的字段名不一致时,我们需要使用resultMap来将他妈呢一一对应起来,且使用resultMap时必须列出所有的了列。所以若是字段名很多就会比较麻烦,还是建议大家实体类的字段名与数据库的列名都保持一致,这样会省很多事,下面展示下使用resultMap来映射实体类的属性与数据库的列名:
      <resultMap type="com.cheng.SupplierInfo" id="suppliermap">
          <id column="oid" property="oid"/>
          <result column="company_name" property="companyName"/>
          <result column="legeal_person" property="legealPerson"/>
          <result column="id_card" property="idCard"/>
      </resultMap>
      <select id="selectByOid" resultMap="suppliermap">
      select oid,company_name,legeal_person,id_card from mybatis.supplier_info where oid = #{oid}
      </select>
      
      如上所示,这样我们就把resultMap用起来了,这样在运行就不会有问题了。使用resultMap时需要注意的是列与属性的声明不要写反了,这也是一个很容易犯的错误,此外一个resultMap是可以被重复使用的,我们在使用resultMap时只需要在对应的select中的resultMap属性的值声明为resultMap标签的id即可。
  • 2)多条件查询,返回多条数据
    这里展示的是多条件查询时返回了多条数据的实例,同样也可以单条件返回多条,不过既然单条件查询和返回多条都明白了,其他组合场景肯定也就明白了,笔者感觉应该不需要介绍这么细致,毕竟程序员中应该没有傻子。这里必须要说的就是parameterMap了,在以前的版本中是支持使用parameterMap来传递多个参数的,但是在最新的MyBatis官方文档中已经明确指明这是一个废弃的参数了,建议大家以后也不要使用了,如果想传递多个参数还是使用包装类即可,官方也是建议使用parameterType就行,废话不多说,来一起看下吧。
    • dao中新增多条查询方法如下:
      //多条件查询,返回多条数据
      List<SupplierInfo> selectByName(SupplierInfo supplierInfo);
      
    • Mapper.xml中增加对应的sql:
      <select id="selectByName" parameterType="com.cheng.SupplierInfo" resultMap="suppliermap">
      	select oid,company_name,legeal_person,id_card from mybatis.supplier_info where legeal_person = #{legealPerson} or company_name = #{companyName}
      </select>
      
      这样一个多条查询的sql我们就写完了,这里我们是通过将条件都封装到了实体类中来进行传参的,这种也是推荐方式。此外我们可以发现查询的结果无论是单条还是多条,我们都是使用的同一个resultMap,这里就必须要说明一下了,接口中方法的返回类型我们关注的应该都是数据库中的表对应的实体类,而不是外面的集合类,就像这两个例子一样,返回结果无论是单条还是多条,我们直接用一个实体类接即可,当有多条返回结果时,MyBatis会自动根据接口的方法返回类型将实体类进行进一步封装,这是一个自动的过程,无需我们手动干预,我们只需要将返回类型声明为表对应的实体类即可。

5.万能传参方式之Map

说到这里必须要说的就是Map了,在第四部分时,我们是通过实体类来包装的多个参数,然后将多个参数传递到了sql中,那若是没有对应的实体类怎么办?就必须去写对应的实体类了吗,其实可以不用,我们完全可以使用Map来进行代替。在增删改查中只要是传入的参数我们都可以使用Map来传递这些参数,而不需要实体类,实际上在笔者目前所处的项目中都是使用的Map而不是使用的实体类来传递这个参数,下面我们通过一个插入的例子来了解下Map的使用吧(增删改查没区别,这里就是用插入介绍了):

  • dao中新插入的方法如下:
    //万能的Map---插入演示
    void insertByMap(Map<String,Object> map);
    
  • Mapper.xml中增加对应的sql:
    <insert id="insertByMap" parameterType="map">
        insert into mybatis.supplier_info (oid,legeal_person,company_name,id_card) value (#{oid},#{legeal_person},#{company_name},#{id_card})
    </insert>
    
    这里我们可以看到parameterType直接传入的是map,这是因为Mybatis自动装载了Map类型,所以我们可以直接写map,这相当于给Map起的别名(因为Mybatis自动封装了,其实我们可以省去parameterType,但是为了可读性,我们一般需要加上)。下面展示下测试代码和运行结果截图:
    @Test
    public void testMap(){
        try(SqlSession sqlSession = MybatisUtil.getSqlSession()){
            SupplierDao mapper = sqlSession.getMapper(SupplierDao.class);
            Map<String,Object> map = new HashMap<>();
            map.put("oid",6);
            map.put("legeal_person","cheng");
            map.put("company_name","孤独的风");
            map.put("id_card","00006");
            mapper.insertByMap(map);
            sqlSession.commit();
        }
    }
    
    在这里插入图片描述
    这就是我们刚刚插入的一条数据,可以发现我们使用map轻松实现了插入,其实无论是插入还是删改查,都可以使用Map来进行传参,这样就省去了书写实体类的麻烦,不过有些场景下实体类还是必要的。此外这种使用Map传参的方式还广泛应用于使用中台的场景,若是大型项目中使用的是数据中台,数据中台一般都是使用Map来接收传参的,这样就避免了需要导入其他系统的千千万万的实体类,而且实体类还有可能重复,所以使用Map还是一种比较好的选择,当然有实体类也可以使用实体类来传参。

6.总结增删改查

到这里我们已经把常用的增删改查都介绍了一遍,这里对增删改查做个小小的总结。上面内容看下来会发现MyBatis的使用很简单,我们只需要导入MyBatis的包与数据库驱动包就可以使用了。

  • 1)使用时需要创建一个MyBatis的配置文件,官方推荐我们命名为mybatis-conifg.xml,在这里我们可以配置MyBatis连接数据库的信息和加载dao接口对应的mapper.xml文件;
  • 2)另外一个就是需要创建对应的mapper.xml文件了,这个文件里首先开头需要指定namespace,这个值必须与dao接口的全限定名保持一致,
  • 3)然后我们就可以写自己的sql了,sql中支持CRUD,我们可以根据自己的需要场景来书写sql,此外写sql是需要注意参数parameterType、resultType、resultMap等项,parameterType和resultType在不使用包扫描时我们应该传入类型的全限定名,resultMap是为了解决表列名与实体类属性名不对应的情况的,
  • 4)此外我们获取参数时,若是基本数据类型和string我们可以省去parameterTyep,可以在sql中直接使用#{}来获取参数,写sql时还需要注意不要带分号,不然是会有问题的。
  • 5)最后一点需要说的是无论是返回单条还是多条我们都应该将返回类型声明为表结构对应的实体类而不是集合,
  • 6)此外最为重要的事情就是增删改需要提交事务,不然数据是不会刷新到表中的。
  • 7)maven资源文件导出问题,只需要在pom.xml中配置下即可,第一部分已经提供了这个配置,这里不重复展示了。
  • 8)Mybatis内置的类型作为参数时我们可以省略,比如基本数据类型、String、Map等,但是为了可读性通常会将Map使用parameterType声明出来。

四、增删改查的高级场景

这一章节才是工作中的高频重点,希望看到此文的小伙伴,一定要将这个章节弄懂弄透,对于工作中的帮助绝对会很大,我们学会了基本的增删改查不难,这个章节才是真正的工作中使用的场景,比如模糊查询、sql注入、resultMap结果映射、动态sql等等这些都是非常重要的。

1.模糊查询的两种实现方式

模糊查询我们可以通过两种方式来实现,

  • 第一种
    我们将匹配符写在sql使用双引号将通配符包围起来,然后使用#{}或者${}来获取参数,如下所示:
    <select id="selectByLike" parameterType="string" resultMap="suppliermap">
        select * from mybatis.supplier_info where company_name like "%"#{companyName}"%"
    </select>
    
    使用这种方式进行模糊查询时,我们调用dao接口中接口方式时直接传入查找内容即可如下所示:
    //测试模糊查询
    @Test
    public void testLike(){
        try(SqlSession sqlSession = MybatisUtil.getSqlSession()){
            SupplierDao mapper = sqlSession.getMapper(SupplierDao.class);
            List<SupplierInfo> supplierInfos = mapper.selectByLike("n");
            supplierInfos.forEach(supplierInfo -> {
                System.out.println(supplierInfo.toString());
            });
        }
    }
    
    这样我们就可以完成一次模糊查询了。
  • 第二种
    第一种模糊查询时通过在sql中加入统配符(这里只展示通配符的使用)实现,此外我们还可以使用在传参中加入通配符的方式来实现模糊匹配,mapper.xml文件中则直接使用#{}来获取参数即可,如下所示:
    <select id="selectByLike" parameterType="string" resultMap="suppliermap">
       	select * from mybatis.supplier_info where company_name like #{companyName}
    </select>
    
    然后我们在调用dao接口中的方法的位置来传入通配符,如下所示:
    在这里插入图片描述
    可以发现这样也实现了模糊查询,以上便是两种实现防护查询的方式,其实本质上都是一样的,就是想办法将通配符传入到sql中即可,一种是在sql中直接写,一种是通过java拼接进去,不过需要说的是这两种实现方式都有可能存在安全隐患,下面就来说说这个安全隐患。

2.sql注入问题

上面介绍了两种模糊查询的场景,其实他们都是存在安全隐患的,主要还是在乎我们怎么使用,无论是第一种还是第二种都有可能存在安全隐患,不过这有一个前提那就是我们使用的是${}来获取参数,使用它来获取参数时MyBatis会将参数解析后直接拼接到sql里面,无论传进来的是什么都会直接拼接,这样若是传进来的是一个 某某条件后面跟了一个 or 1=1,那此时所有数据都会查出来,这样就是sql注入了,笔者使用第二种案例来演示下sql注入;

  • 复现sql注入
    第一步将sql改成使用${}来获取参数,如下所示:
    <select id="selectByLike" parameterType="string" resultMap="suppliermap">
        select * from mybatis.supplier_info where company_name like ${companyName}
    </select>
    
    第二步传入一个条件后面拼接 or 1=1
    java代码如下所示:
    //测试模糊查询
    @Test
    public void testLike(){
        try(SqlSession sqlSession = MybatisUtil.getSqlSession()){
            SupplierDao mapper = sqlSession.getMapper(SupplierDao.class);
            List<SupplierInfo> supplierInfos = mapper.selectByLike("'n' or 1=1");
            supplierInfos.forEach(supplierInfo -> {
                System.out.println(supplierInfo.toString());
            });
        }
    }
    
    下面展示下运行结果,可以发现所有数据均被查出来了,这就是sql注入了。
    在这里插入图片描述
  • 预防sql注入
    看了上面的例子应该知道了,sql注入的关键就是使用了${}来获取参数了,只要我们使用#{}来获取参数其实是不会有问题的,那为什么使用#{}获取参数就不会有问题了呢,因为#{}是将数据解析长字符串拼接到sql中的,无论你传入的是什么我都把你当成查询的字符串拼接进去,这样其实就不会有问题了,因此在实际的开发中,所有的场景中我们应该是能用#{}来获取参数的就不要使用${}来获取参数了。

3.分页实现

分页的实现有很多种,不过万变不离其总,所有的分页实现底层都还是要依赖于limit的,所以若是没有强制要求,我们使用limit其实效率才是最高的,不过日常开发中可能也会用分页插件,比如PageHelper,也可能会使用RowBounds等分页实现,不过推荐还是使用limit来进行原始分页。
使用limit来实现分页则是很简单的,我们只需要在sql中加入limit即可,同时将起始位置下标与每页的条数传进来即可。比如sql写成如下即可(传参没什么需要说的,这里就不重复介绍了):

<select id="getSupplierInfo" resultMap="suppliermap">
	select * from mybatis.supplier_info limit #{offset},#{pageSize}
</select>

原始分页实现就这么简单,若是需要使用PageHelper或者RowBounds等其他分页方式,可直接百度教程还是不少的。

五、Mapper.xml文件详解

1.resultMap结果映射

resultMap被官方称为最强大、最重要的元素,足可见他的用处了,事实上只要我们使用了MyBatis就肯定使用了resultMap了,我们正常使用resultType时,其实底层是会默认去调用resultMap将表的列名与属性名进行一一对应的,只不过正常情况下他们名称一致的话,MyBatis就帮我们做了这些事情,就省去了我们显示写resultMap的代码(MyBatis帮我们写),但是若是数据表中的列名与javaBean的属性名不一致呢?这就要求我们自己做映射了,因为此时MyBatis已经不能直接将列名与属性名进行对应起来了。其实一开始的例子,笔者就使用了resultMap,因为笔者建立的表与javaBean的属性值是不对应的。通过刚开始的例子我们可以发现resultMap使用起来并不费劲,不过呢,其实处理数据库表列名与属性名不对应的情况我们也可以不使用resultMap,还有一种原始的解决办法,我们先来看下这个原始的办法。

  • 使用起别名做结果映射
    这个就比较简单了,只需要我们在sql中为列名与属性名不对应的值进行起别名就行,这个起笔名就是sql中起别名,这样我们直接声明resultType就可以成功映射到javaBean了,比如下方的操作:
    <select id="getSupplierInfo" resultType="supplierInfo">
    	select oid, company_name as companyName, legeal_person as legealPerson, id_card idCard from mybatis.supplier_info
    </select>
    
    这样我们也是可以解决映射问题的,但是这种并不是一个好的选择,这种写法会让代码更加的冗余,使用resultMap才是最好的选择,下面来再看看resultMap的写法
  • 使用resultMap做结果映射
    使用resultMap就很简单了,我们只需要将属性名与字段名不一致的进行列出来即可,一致的是可以省略的,解决上面的问题我们这样操作即可
     <resultMap type="supplierInfo" id="suppliermap">
        <id column="oid" property="oid"/>
        <result column="company_name" property="companyName"/>
        <result column="legeal_person" property="legealPerson"/>
        <result column="id_card" property="idCard"/>
    </resultMap>
    
    <select id="getSupplierInfo" resultMap="suppliermap">
    	select * from mybatis.supplier_info
    </select>
    
    可以发现resultMap的基础使用很简单,只需要在resultMap中提供result映射即可,result中的column表示数据库表中的列名,property表示javaBean中的属性名,此外就没了,使用resultMap来做结果映射简单明了。此时可能会感觉好像我使用起别名的方法比resultMap更简单啊,这里就需要注意了在真实的业务场景中是没有独立的对象的,对象与对象之间肯定有一对一、一对多、多对一、多对多的关系,这时仅仅依赖sql就处理不了了,还是得用resultMap才行,这些下面会说,这里就不重复说了。
  • resultMap支持的操作之id
    这个id标签上面的例子中已经使用了其实,id的作用很单一就是映射数据库主键到javaBean的唯一标识的。就是这么简单,官方的解释是使用id可以提高操作效率,包括在有一对多、多对一的场景中我们使用id都是一个更好于result的选择。
  • resultMap支持的操作之result
    result用于映射普通的列名到属性名,这个没有太深的需要说的,列名使用column,属性名使用property,这样MyBatis就会自动的帮我们完成列名到属性的映射了,当然了MyBatis的底层还是需要去调用实体类的set方法,所以我们必须提供实体类的set方法才是可以的,不然这个操作MyBatis也完成不了,若是不提供set方法,MyBatis返回给我们的将是一个对象的地址。

2.resultMap一对多的映射

一对多,比如常见的场景一个店铺对应多个订单,如下的pojo

/**
 * @author pcc
 * @version 1.0.0
 * @className ShopInfo
 * @date 2021-08-07 08:51
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class ShopInfo {
    private int oid;
    private String shopName;
    private List<OrderInfo> orderInfoList;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class OrderInfo {
    private int oid;
    private int orderNo;
}

从上面的代码中可以看到一个店铺对应了多个订单信息,在这种情况下我们应该怎么处理呢?首先需要明确的是数据库表中我我们肯定不能建立对应类型,我们都是通过外键来关联的,但是我们实体类又想接收到对象,此时该如何处理呢?不用急Mybatis为我们提供了collection来解决这一场景,话不多说看看接口与Mapper.xml该如何写:

--------- 接口代码 ---------
public interface ShopMapper {
    ShopInfo queryShopByOid(int oid);
}

--------- Mapper.xml代码 ---------
<resultMap id="shopMap" type="shopInfo">
    <result column="shop_oid" property="oid"/>
    <result column="shop_name" property="shopName"/>
    <collection property="orderInfoList" ofType="OrderInfo">
        <result column="oid" property="oid"/>
        <result column="orderNo" property="orderNo"/>
    </collection>
</resultMap>
<select id="queryShopByOid" resultMap="shopMap">
    select ord.oid,ord.orderNo,ord.shop_oid,shop.shop_name from order_info ord
  	left join shop_info shop on ord.shop_oid = shop.oid
  	where ord.shop_oid = #{oid}
</select>

如上所示我们就完成了一对多的操作,一对多时我们必须使用collection,同时使用property来声明集合的参数名,使用ofType声明集合的泛型类,这里需要注意的是必须使用ofType不能使用javaType,下面要说的association是使用javaType,这里也不知道MyBatis为啥非要这么设计,都使用javaType去处理应该也是可以实现的,作为使用者,我们只能去遵守人家的规范了。
此外还要一点必须要说的是,我们还可以再collection中声明select语句的id来实现数据的映射,不过这种方式不推荐使用,作为了解即可,工作中要是碰到这种写法,知道是怎么回事就行,使用官方推荐也是这种。

3.resultMap多对一的映射

一对多时,比如店铺与订单的关系,一个店铺可以有多个订单,若是我们在店铺中维护订单信息那么就是这么写

/**
 * @author pcc
 * @version 1.0.0
 * @className ShopInfo
 * @date 2021-08-07 08:51
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class ShopInfo {
    private int oid;
    private String shopName;
    private List<OrderInfo> orderInfoList;
}

这种就是一对多,不过其实我们还可以不在店铺中维护订单信息,也就是去掉代码中的这一行:private List orderInfoList;,我们可以在订单中声明店铺信息,这样对于单条订单来说与店铺的关系就是一对一,对于多条订单来说可能是多对一也可能是多对多,比如下:

/**
 * @author pcc
 * @version 1.0.0
 * @className OrderInfo
 * @date 2021-08-07 08:55
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class OrderInfo {
    private int oid;
    private int orderNo;
    private ShopInfo shopInfo;
}

这样是不是发现一些什么了?其实一对多与多对一解决的是一个场景问题,在一个问题中我们只需要使用其中一种策略即可,使用了多对一就无需使用一对多了。比如有这样一个场景:一个商户对应多个店铺,一个店铺又对应了多个订单。那我们设计表机构时应该怎么设计呢?其实有两种的标准策略:①商户表维护店铺信息,店铺表维护订单信息;这样就是两次一对多来设计的表机构。②店铺表维护商户信息,订单表维护店铺信息;这样就是两次多对一来设计的表结构。事实上我们都是使用第二种来设计表,也就是使用多对一的方式来设计表结构,在多的一方来维护少的一方,查询时也是从多的一方开始查询。
上面一点是笔者结合工作感悟做的一点点扩展,言归正传,虽然只使用一对多或者多对一其中一种就可以解决问题,但是学习时,我们肯定是都需要学的。所以上面的例子我们在反过来讲一遍就是多对一了,此时,我们在订单表中维护店铺信息,pojo的代码在上面已经展示这里就不展示了,这里展示下接口和mapper.xml的代码,如下:

----------- 接口代码 -----------
public interface OrderMapper {
    List<OrderInfo> queryOrderByShopOid(int oid);
}

----------- Mapper.xml代码 -----------
<resultMap id="orderListMap" type="orderInfo">
    <result column="oid" property="oid"/>
    <result column="orderNo" property="orderNo"/>
    <association property="shopInfo" javaType="ShopInfo">
        <result column="shop_oid" property="oid"/>
        <result column="shop_name" property="shopName"/>
    </association>
</resultMap>
<select id="queryOrderByShopOid" resultMap="orderListMap">
  select ord.oid,ord.orderNo,ord.shop_oid,shop.shop_name from order_info ord
  left join shop_info shop on ord.shop_oid = shop.oid
  where ord.shop_oid = #{oid}
</select>

这样我们就完成了多对一的代码的编写,测试代码这里就不展示了,可以看到我们使用association标签来完成了单条订单与店铺的一对一关系,事实上MyBatis底层会根据订单表的外键shop_oid来去查询店铺信息,然后通过association里面的result标签,将店铺信息映射到指定的JavaType类型行,最后再将数据交给属性,这样就完成了外键到对象的转换。,不过这并不是唯一的解决方法,我们还可以再association中声明另一个select语句来完成数据查询,这样就是使用显示的去查询而不是使用MyBatis默认的方式查询,建议都是使用第一种,且工作中都是使用第一种。另一中方式有兴趣学习的参见官方文档:MyBatis3官方文档
这里额外说明下,不要让自己的一张表中既有一对多也有多对一的场景,这种属于表结构设计的不合理,会增加编码和阅读的难度,这是一种不合理的设计,我们应该尽量避免。

4.动态sql之where、if

动态sql是工作中的重中之重,这块工作中百分百会遇到的,所以不要存在侥幸心理,这块是必须得懂且会用才行的。动态sql主要就是为了方便sql的拼写,以及根据不同场景对条件进行动态拼接从而来省去使用JDBC时拼接sql语句的苦恼。
先来看下if和where这两个动态sql标签的使用,先来说if吧,if标签用来做判断使用的,若是if内的表达式返回真,则sql拼接上if标签内的内容,比如有如下sql,我们在test中写表达式,若是表达式为真则拼接if语句中的内容,同时if语句还支持嵌套使用。

<select id="queryProduct" resultType="productInfo">
    select * from product_info where 1=1
    <if test="pid !=null">
        and pid = #{pid}
    </if>
    <if test="productNo !=null">
        and productNo = #{productNo}
    </if>
</select>

从上面的例子中我们可以看到有这么一句 “where 1=1” 为什么要写这么一句呢?写这样一个恒等式是因为我们不能确定下面拼接sql时具体哪一段为真这样就不能确定第一个拼接的片段会不会有“and”这个词,若是出现了“and”sql肯定会执行不了,所以我们加了“where 1=1” 这个恒等式,这样下面无论先拼接哪一个都不会出现where 后 就跟着and了。其实我们完全可以不用写“where 1=1”,我们使用where标签一样可以 做到这样,where标签就是为了这个场景而生的,现在把上面的内容改写成具有where标签的写法,如下:

-------- 接口方法 --------
List<ProductInfo> queryProduct(Map<String,Object> map);

-------- Mapper.xml ---------
<select id="queryProduct" parameterType="map" resultType="productInfo">
    select * from product_info
    <where>
        <if test="pid !=null and pid!= ''">
            and pid = #{pid}
        </if>
        <if test="productNo !=null and productNo != ''">
            and productNo = #{productNo}
        </if>
    </where>
</select>

这就是where与if的使用了,下面对上面的代码测试下,看下结果,根据测试代码理论上只会有一条数据出现,事实上也是如此,说明我们写的没有问题,不过这里有一点是需要特别注意的,通常我们获取接口传递的参数时需要#或者$加上大括号才是可以的,但是在if的test表达式中是不需要加#或者$的,加了的话是获取不到数据的,这点切记,他与直接写在sql中的获取参数是不一样的。
在这里插入图片描述

5.动态sql之sql、include

sql标签主要是为了提高sql的复用,把多条sql共用的sql片段抽离出来作为一个片段,在需要的地方使用include标签来引入sql片段,不过sql片段使用过多,真的不利于代码阅读,这块在MyBatis官方文档上已经删掉了,笔者感觉MyBatis应该是不推荐使用所以删掉了sql的使用介绍,不过目前的版本中仍然可以使用sql片段,笔者碰到过一个项目,里面大量使用了sql片段,致使阅读起来真的很费劲,需要来回去找各种sql片段。所以笔者也是建议对于逻辑简单的可复用的部分进行重复使用,对于逻辑复杂的部分就不要使用sql片段了,真的不利于代码的阅读。我们还使用以上的例子来展示sql、include这两个标签的使用,更改后的sql如下:

<sql id="productColumn">
    pid,productNo,productName
</sql>
<select id="queryProduct" parameterType="map" resultType="productInfo">
    select
    <include refid="productColumn"/>
    from product_info
    <where>
        <if test="pid !=null and pid!= ''">
            and pid = #{pid}
        </if>
        <if test="productNo !=null and productNo != ''">
            and productNo = #{productNo}
        </if>
    </where>
</select>

这里需要说明的是sql标签可以抽离任何一块内容作为sql片段,然后在其他地方引入,不过一般使用最多的还是对列名的共用,和一些共性条件,不过笔者不建议共用复杂的条件,还是那句话,影响代码的阅读。

6.动态sql之trim、where、set

where标签在上面已经介绍过了,这里先说下set标签吧,set与where的用法非常类似,我们使用where标签是为了省去判断条件语句中and的位置。同样的问题在更新语句可能也会出现,比如说我们更新的字段都是使用逗号分割,若是传入的某些字段为空我们是不用更新的,那还是要判断逗号出现的问题是不是最后一次,也很麻烦,因此有了set标签。下面使用一个更新语句来展示下set标签的使用。

------- 接口方法 -------
void updateProduct(ProductInfo productInfo);

------- Mapper.xml -------
<update id="updateProduct" parameterType="productInfo">
    update product_info
    <set>
        <if test="productNo != null">
            productNo = #{productNo},
        </if>
        <if test="productName != null">
            productName = #{productName},
        </if>
    </set>
    <where>
        <if test="pid != null">
            and pid = #{pid}
        </if>
    </where>
</update>

可以看到使用set标签后我们为每个待拼接的片段都加了逗号分割,但最后语法并没有问题,说明set标签可以动态的为我们去除多余的逗号,值得注意的是,笔者这里使用的参数是javaBean而不是上个例子中的Map了,两者共同的特点就是,我们可以直接使用属性,而不是使用#或者$来获取,更不用通过对象点属性的方式获取属性。切记无论传参是javaBean还是Map都是直接使用属性或者Map的键即可。
set标签与where标签都会用了,就需要来说下trim标签了,其实无论是where还是set都是trim的特殊形式,我们可以使用trim来达到where的效果或者set的效果。我们先将上面sql中的where标签替换成使用trim(修剪)标签来看下,如下:

<update id="updateProduct" parameterType="productInfo">
    update product_info
    <set>
        <if test="productNo != null">
            productNo = #{productNo},
        </if>
        <if test="productName != null">
            productName = #{productName},
        </if>
    </set>
    <trim prefix="where" prefixOverrides="and|or">
        <if test="pid != null">
            and pid = #{pid}
        </if>
    </trim>
</update>

改成使用trim标签以后测试发现是没有任何问题的,那这个trim标签的作用是什么呢,如上的trim写法作用就是将trim标签内拼接出来的sql自动加上前缀where ,然后开头若是and或者or就会被覆盖或者叫删除,这不就是我们的where标签干得事吗,这么一看就知道trim标签是什么了吧。下面我们在使用trim标签来代替下set标签,如下:

<update id="updateProduct" parameterType="productInfo">
    update product_info
    <trim prefix="set" suffixOverrides=",">
        <if test="productNo != null">
            productNo = #{productNo},
        </if>
        <if test="productName != null">
            productName = #{productName},
        </if>
    </trim>
    <trim prefix="where" prefixOverrides="and|or">
        <if test="pid != null">
            and pid = #{pid}
        </if>
    </trim>
</update>

上面的sql中使用trim标签代替了set标签,prefix是添加前缀的意思,我们已经知道,上个例子中我们使用prefixOverrides来覆盖前缀多余的and或者or,这里使用suffixOverrides自然就是来覆盖末尾多余的逗号了,是不是发现trim标签很好用呢,其实无论是where还是set标签底层都是trim标签,在开发中我们也完全可以使用trim标签,trim标签支持四个属性prefix、suffix、prefixOverrides、suffixOverrides,他们的作用分别是增加前缀、增加后缀、前缀覆盖、后缀覆盖,使用trim标签可以灵活定制各种场景。

7.动态sql之choose、when、otherwise

这三个标签是一组的,是要一起使用的,我们使用java语言里的例子来类比这三个标签的作用:choose相当于switch,when相当于case,otherwise相当于default。事实上也是如此,不过when准确的说是相当于加了break的case。若是不明白java中的switch的语法,建议百度下再往下看,如果明白了switch的语法,那么对于choose、when、otherwise自然也就懂了。我们通过修改讲解if标签的例子来使用下这三个标签,如下:

<sql id="productColumn">
    pid,productNo,productName
</sql>
<select id="queryProduct" parameterType="map" resultType="productInfo">
    select
    <include refid="productColumn"/>
    from product_info
    <where>
        <choose>
            <when test="pid !=null">
                and pid = #{pid}
            </when>
            <when test="productNo != null">
                and productNo = #{productNo}
            </when>
            <otherwise>
                and 1=1
            </otherwise>
        </choose>
    </where>
</select>

下面展示下数据库的数据如下:
在这里插入图片描述
好了,然后我们来测试下,我们传入了两个条件pid为2,productNo为1。若是两个条件同时满足通过上面的数据库数据我们可以看到是肯定查不到数据的,但是下面展示查出了pid为2的数据,说明只有第一个条件生效了这就是choose的作用了。其实刚刚已经说了choose相当于switch,只要匹配了某个when,在执行完这个when后就会退出choose语句,其他的when语句与otherwise都不会执行了,若是所有的when都没有匹配就会进入到otherwise中,这就是choose了,可以说与java中的switch是一模一样了。
在这里插入图片描述

8.动态sql之foreach

foreach顾名思义他的作用就是遍历了,java8中我们可以通过foreach来使用lamdba表达式操作集合,这里的foreach也是用来操作集合的,他可以操作List、Map、set、数组等各种集合,通常我们会使用List与Map来进行传参,笔者在这里演示下如何使用foreach来遍历List与Map来拼接sql。

  • foreach遍历List
    下面是遍历List时的接口方法与Mapper.xml中的内容

    -------- 接口方法 --------
    List<ProductInfo> queryByForeach(List<Integer> list);
    -------- Mapper.xm --------
    <select id="queryByForeach" parameterType="list" resultType="productInfo">
       select * from product_info
       <trim prefix="where" prefixOverrides="and|or">
           <foreach collection="list" item="id" open="and (" close=")" separator="or">
               pid = #{id}
           </foreach>
       </trim>
    </select>
    

    可以看到foreach中的属性还是不少的,我们来分别介绍下这几个属性:
    ①collection:代表当前集合的名称,接口参数是list,这里就必须是list,与其对应
    ②item:集合的元素,下面使用#{}获取的就是这个值,他们是相互对应的。
    ③open:拼接的语句以什么开始这里是以and (开始,这里不用担心and会多,trim会自动去掉多余的and
    ④close:以什么结束,这里是以右括号结束
    ⑤separator:以什么分割,这里是以or条件进行分割。
    解释完这些是不是对foreach的使用就很明朗了呢,foreach其实就是通过遍历集合元素,将集合元素以separator的条件进行连接,最后头部拼上open的内容,尾部拼上close的内容,这就是foreach了,展示下这个例子的测试截图:
    在这里插入图片描述

  • foreach遍历Map
    我们知道若是这种Map结构:Map<String,String> ,我们是不需要使用foreach的,我们可以直接获取这种map里面的数据进行拼接sql,此时无需使用foreach,这种例子与使用javaBean传递数据是一个道理,都是可以直接获取的,那什么样的Map才需要使用foreach进行遍历呢,其实只有这种Map才需要使用foreach:Map<String,List> ,也就是说Map的值是集合的时候才需要使用foreach,当然这个值不一定是list也可以是set或者数组等,这种情况下的Map我们才需要使用foreach进行遍历,下面展示下代码:

    -------- 接口方法 --------
    List<ProductInfo> queryByForeach2(Map<String,List<Integer>> map);
    -------- Mapper.xm --------
    <select id="queryByForeach2" parameterType="map" resultType="productInfo">
        select * from product_info
        <trim prefix="where" prefixOverrides="and|or">
            <foreach index="offset" collection="id1" item="id" open="and (" close=")" separator="or">
                      pid = #{id}
            </foreach>
        </trim>
    </select>
    

    乍一看这个foreach也是好多标签啊,仔细一看其实与刚刚没差多少,只是多了一个index标签,下面还是一样一起来看下所有标签的意思吧:
    ①index:表示下标的意思,这个一般不怎么使用
    ②collection:表示集合,之类的集合名称应该是Map中值为集合的那个键的名称,这里显示id1,这里是需要与Map的键对应的。
    ③item:上面也说了,就是元素的代号。
    ④open:开头拼接的内容
    ⑤close:末尾拼接的内容
    ⑥separator:中间的分隔符
    看了上面的解释可以发现使用Map与list有两点区别,第一点就是Map中多了index下标,第二点就是Map中的collection应该是集合对应的键。在使用list时,collection就是集合的名称。foreach的用法也就这些了,掌握后回去看看是不是发现也不困难。

六、mybatis-config.xml文件详解

mybatis-config.xml配置文件才是MyBatis的核心,只掌握CRUD只能算是MyBatis的入门,想要对MyBatis如臂指使那么他的配置文件是必须精通的,我们先看下这个配置文件都支持哪些配置,如下所示:这些是笔者从MyBatis的中文官方文档copy下来的,有兴趣的可以去看下这个文档:MyBatis3中文文档

配置
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:

configuration(配置)
	properties(属性)
	settings(设置)
	typeAliases(类型别名)
	typeHandlers(类型处理器)
	objectFactory(对象工厂)
	plugins(插件)
	environments(环境配置)
	environment(环境变量)
	transactionManager(事务管理器)
	dataSource(数据源)
	databaseIdProvider(数据库厂商标识)
	mappers(映射器)

上面展示了所有的13项可供选择的配置,虽然有一些是我们平时用不到的一些场景,但是这里笔者会将所有标签都进行解释说明,下面我们就来一个一个看下这些标签的使用吧。

1.configuration标签

这是一个一级标签,下面的所有标签都是二级或者三级标签,也就是说所有的标签的位置都应该在configuration这个标签里面来配置,这也就是configuration标签的作用了。这里不单独展示这个标签了,在后面的例子中一起展示,因为他的作用很单一就是配置文件的顶层标签。

2.properties标签

properties标签与其他框架里面的properties一样,都是用来声明属性值的,比如maven中我们可以使用peoperties来声明一些jar包的版本号,这里我们也可以使用properties来做类似的事情,我们可以使用properties来声明数据库连接的url、用户名、密码等信息。如下所示:

<?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来配置属性-->
        <property name="mysql.driver" value="com.mysql.jdbc.Driver"/>
        <property name="mysql.url" value="jdbc:mysql://192.168.150.100:3306/mybatis?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8&amp;"/>
        <property name="mysql.user" value="root"/>
        <property name="mysql.password" value="super"/>
    </properties>

    <environments default="development"><!--支持多环境配置-->
        <environment id="development"><!--环境配置-->
            <transactionManager type="JDBC"/><!--默认使用JDBC的事务管理,不需要改变-->
            <dataSource type="POOLED"><!--连接池配置,模式使用pooled-->
                <property name="driver" value="${mysql.driver}"/><!--驱动类配置-->
                <!--数据库url,SSL开启需要服务器身份验证,useUnicode=true$characterEncoding=UTF-8保证存取数据不会乱码-->
                <property name="url" value="${mysql.url}"/>
                <!--数据库用户名-->
                <property name="username" value="${mysql.user}"/>
                <!--数据库密码-->
                <property name="password" value="${mysql.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!--配置mapper文件地址-->
    <mappers>
        <mapper resource="com/cheng/dao/SupplierMapper.xml"/>
    </mappers>
</configuration>

在上面的配置文件中,笔者将驱动类、url、用户名、密码都配置到了properties中,然后我们在配置这些信息是就可以直接使用${}来获取这些信息了,这里需要注意用法,获取配置在properties中的信息时使用${}获取,它外面是有双引号包裹的,此时是不能使用#{}来获取值的。除了使用properties来声明一些属性值来达到统一管理这些值的目的以外,我们还可以使用properties标签来引入外部的配置文件,比如如下的操作。
创建一个mysql.properties文件,这个文件中的内容如下:

mysql.driver=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://192.168.150.100:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf8
mysql.user=root
mysql.password=super

然后我们在mybatis-config.xml中的properties标签中导入这个配置文件即可,也是可以达到上面同样的目的的。我们只需要将properties更改为如下样式即可。而获取这些值的操作还是使用${}来进行获取。这里需要注意的是mysql.properties文件的路径问题,笔者将其和mybatis-config.xml都放在了resource下面,所以是直接导入即可,若是将该文件放在了别处,只需要导入该文件的项目路径即可,同时rul写在properties文件中时就可以不需要&amp了这里需要注意(带上也不会报错)。

<properties resource="mysql.properties">
</properties>

到这里properties的所有用法就介绍完了,是不是也不难呢,总结下就是两个用法要不直接在properties中来定义属性值,要么通过引入配置文件的形式来加载这些属性,获取时我们都使用${}来获取即可,若是既引入了外部文件,同时也在properties文件中声明了属性,此时出现了某些属性重复定义的场景的话,会以外部配置文件配置为准。

3.settings标签

这个标签下的配置才是MyBatis的灵魂,这里支持的配置项有很多,不过大部分我们都用不到,这里只介绍常用的配置,其他的建议需要时去官方文档看,不过笔者可以清楚的告诉你,你不会用到。所以看看我写的也就够了哈哈。下面列出一些常用的配置项:

  • cacheEnabled 二级缓存配置
    全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,模式是开启模式。前面这句话是官方描述,注意其实他说的就是MyBatis的二级缓存,一级缓存是sqlsession级别的,默认就是开启。这里操作的是二级缓存,关于MyBatis的缓存,后面会有一个小节专门阐述,这里不做过多的描述了。
  • useGeneratedKeys 自动生成主键
    这个没啥用,因为在实际的开发中,主键都是自己控制生成的,不会使用过这种自动生成的主键,不过需要知道,我们可以通过他来自动生成主键
  • localCacheScope 配置一级缓存
    默认就是使用一级缓存:支持在同一个sqlsession中多次做相同查询时从缓存中获取,这个不用动,也不用改,知道就好。
  • mapUnderscoreToCamelCase 数据库字段自动转驼峰
    这个配置还是有些意思的,可以将数据库中使用下划线命名的列名自动与实体类的驼峰命名进行一一对应,当我们开启她时,就无需在resultMap中去将实体类的属性名于数据表的列名进行一一映射了,这样就省去了我们自己写resultMap了。该值默认是false,也就是不开启的状态,不过这个配置只能自动转换resultMap,对于里面的association与collection则不能实现自动转换,所以使用时有利有弊,权衡利弊后再使用。
  • logImpl 日志工程
    MyBatis默认不使用日志,若是我们想要去排查sql语句,如果可以输出sql则无疑会是更有利于排查问题的,MyBatis支持SLF4J、LOG4J、LOF4J2等日志的实现,其中SLF4J、LOG4J是我们常用的日志实现。

settings支持的配置还有很多比如日志啊等等,不过我们基本都不用,当下后端服务基本都是Springboot,在Springboot中我们已经没有了mybatis-config.xml配置文件了,所有的都是使用yml或者properties来配置所有场景的属性,其实需要我们配置的不多,记住上面几种比较有用的即可。

4.typeAliases标签

顾名思义这个标签的作用就是为类型起别名的,在我们在mapper中写sql时,我们可以发现当传入参数是基本数据类型或者是String、Map时,我们只需要写成int、string、map,就可以正常使用这些参数了,这是为什么呢?这就是因为MyBatis已经为他们起了别名了,因此我们声明parameterType或者是resultType时就不需要写类的全限定名了,而只写他的别名即可,这也是typeAliases的作用:简化书写。我们可以通过typeAliases来实现单个类的别名配置,也可以使用包扫描的方式将对应包下面的所有类都起上别名,如下所示:

  • 为单个类配置别名
    <typeAliases>
        <typeAlias type="com.cheng.SupplierInfo" alias="supplier"/>
    </typeAliases>
    
    如上所示,在type里传入类的全限定名,在alias中为该类起别名,别名通常的命名方式都是首字母小写的驼峰命名法,当然这个名称也可以不遵守这个规范,可以任意起。
  • 别名包扫描
    <typeAliases>
        <package name="com.cheng"/>
    </typeAliases>
    
    上面便是包扫描的别名配置了,当一个包下面有多个类时,为每个类都配置别名无疑是很费时费劲的,这时我们就可以通过包扫描的方式来配置别名,此时默认配置的别名就是首字母小写的类名,当有多个包是我们也可以使用多个package标签类配置不同的包,不过使用包扫描时我们不好定制化的为每个类起别名,不过这个也是可以解决的,当我们是用的是别名包扫描时,我们可以通过在实体类上加如下注解来实现别名的定制化:
    @Alias("supplier")
    public class SupplierInfo {
        Integer oid;
        String companyName;
        String legealPerson;
        String idCard;
    }
    
    别名的配制就这些东西,我们想要为单一的类配置别名,直接使用typeAlias即可,若是想要省事呢直接使用包扫描即可,若是对于包扫描默认的别名不喜欢此时就必须借助Alias这个注解来帮我们定制化别名了,下面展示下MyBatis提供的别名:
别名映射的类型
_bytebyte
_longlong
_shortshort
_intint
_integerint
_doubledouble
_floatfloat
_booleanboolean
stringString
byteByte
longLong
shortShort
intInteger
integerInteger
doubleDouble
floatFloat
booleanBoolean
dateDate
decimalBigDecimal
bigdecimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList
collectionCollection
iteratorIterator

5.typeHandlers标签

这个标签与下面的objectFactory标签是基本不可能用到的,先来说下当前标签的作用吧,这个标签用来当数据库中的类型与java类型不能做自动映射时,我们来手动添加一个处理器映射器来建立数据库类型到java类型的映射,就目前的场景是不需要做这种处理的,因此这个标签作为了解即可,若是以后真有必要这么做去参考官方文档吧。官方文档地址:MyBatis3官方文档

6.objectFactory标签

每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如:

// ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
  public Object create(Class type) {
    return super.create(type);
  }
  public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

这个里面的例子笔者是借用的官方文档,因为这个标签实在是用不到,我们也不会去重新定义对象的生成方式,这是没有必要的行为,这个标签作为了解即可。

7.plugins标签

相信知道MyBatis的人应该都知道MyBatisPlus,那MyBatisPlus是什么呢?其实MyBatisPlus就是基于MyBatis然后增加了一些插件而已,然后将MyBatis的使用变得更方便智能,这个插件就是通过plugins来实现的,除了MyBatisPlus还有很多插件支持。MyBatis提供了一套机制用以支持插件的使用,插件插件就是随插随用的感觉,MyBatis提供了Interceptor接口,若是想要定义插件只需要实现该接口即可,不过需要说的是插件会修改MyBatis的核心功能,如果没有对MyBatis的底层核心代码有非常深的了解,建议不要干这种事情,因为这是非常危险的。下面展示下插件的实现作为了解即可:

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

然后们就需要将手写的插件引入到MyBatis的配置文件中,如下:

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

这部分内容在精通MyBatis之前建议只是了解下,笔者也是如此,并没有精通这门技术,待以后再深入研究这里面的东西好了。

8.environments标签 和 enviroment 标签

这两个标签就放一起说了,因为他们是配合使用的;就像上面的properties和property一样。enrironments用来声明多环境配置的,我们可以在这个标签里面配置多个enrironment标签也就是配置多个环境,而environment就是用来配置单个环境的,且多个环境时每个environment都有自己的id,environments就是通过配置自己的default属性的值来寻找具体使用哪个环境的,废话不多说我们演示一下:

<?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="mysql.properties">
        <!--<property name="mysql.driver" value="com.mysql.jdbc.Driver"/>-->
        <!--<property name="mysql.url" value="jdbc:mysql://192.168.150.100:3306/mybatis?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8&amp;"/>-->
        <!--<property name="mysql.user" value="root"/>-->
        <!--<property name="mysql.password" value="super"/>-->
    </properties>

    <environments default="test"><!--支持多环境配置-->
        <environment id="pro"><!--环境配置-->
            <transactionManager type="JDBC"/><!--默认使用JDBC的事务管理,不需要改变-->
            <dataSource type="POOLED"><!--连接池配置,模式使用pooled-->
                <property name="driver" value="${mysql.driver}"/><!--驱动类配置-->
                <!--数据库url,SSL开启需要服务器身份验证,useUnicode=true$characterEncoding=UTF-8保证存取数据不会乱码-->
                <property name="url" value="${mysql.url}"/>
                <!--数据库用户名-->
                <property name="username" value="${mysql.user}"/>
                <!--数据库密码-->
                <property name="password" value="${mysql.password}"/>
            </dataSource>
        </environment>
        <environment id="test"><!--环境配置-->
            <transactionManager type="JDBC"/><!--默认使用JDBC的事务管理,不需要改变-->
            <dataSource type="POOLED"><!--连接池配置,模式使用pooled-->
                <property name="driver" value="${mysql.driver}"/><!--驱动类配置-->
                <!--数据库url,SSL开启需要服务器身份验证,useUnicode=true$characterEncoding=UTF-8保证存取数据不会乱码-->
                <property name="url" value="${mysql.url}"/>
                <!--数据库用户名-->
                <property name="username" value="${mysql.user}"/>
                <!--数据库密码-->
                <property name="password" value="${mysql.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!--配置mapper文件地址-->
    <mappers>
        <mapper resource="com/cheng/dao/SupplierMapper.xml"/>
    </mappers>
</configuration>

如上所示笔者写了两个enrivonment标签,一个id是test一个id是pro,我们可以根据environments的标签值来选择具体使用哪个环境,这就是environments和environment的作用了。

9.transactionManager标签

顾名思义这个标签是用来配置事务管理的,默认情况下使用jdbc的事务管理。同时MyBatis还支持另一种配置MANAGED,下面说下这两种事务的区别:

  • JDBC:这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
  • MANAGED:这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。
    通过对比这两种事务的方式我们可以发现我们只能使用JDBC来进行管理了,不然就没有事务了,配置方式在介绍environment标签时已经有了,这里只做单个标签的展示,不然占用的篇幅太大,如下:
<transactionManager type="JDBC"/>

说到事务就必须得提下spring的事务管理了,因为到目前为止后端项目基本都是使用spring来进行构建的,spring支持声明式事务与编程式事务,因为编程式事务对代码进入量比较高,我们一般使用声明式事务。spring既然已经提供了事务支持我们在使用MyBatis时就不需要在进行事务管理了,一旦发生异常spring会使用自己的事务将业务回滚。在spring中我们一般使用DataSourceTransactionManager来做事务管理,此时我们就不需要在MyBatis中使用事务了,若是对spring的事务不太明白可以点击下面的链接,这篇文章里第四节介绍redis的事务时笔者整理了常见的一些事务处理 redis从练气到化虚

10.dataSource标签

这个标签也是enviroment的子标签,他的作用就是用来声明数据库连接的一些信息的,通过他可以声明数据库的连接池、驱动类、数据库服务器的用户名、数据库服务器密码等属性,这样我们就可以正常连接到数据库了,下面展示下datasource标签的内容:

<dataSource type="POOLED"><!--连接池配置,模式使用pooled-->
    <property name="driver" value="${mysql.driver}"/><!--驱动类配置-->
    <!--数据库url,SSL开启需要服务器身份验证,useUnicode=true$characterEncoding=UTF-8保证存取数据不会乱码-->
    <property name="url" value="${mysql.url}"/>
    <!--数据库用户名-->
    <property name="username" value="${mysql.user}"/>
    <!--数据库密码-->
    <property name="password" value="${mysql.password}"/>
</dataSource>

driver、url、username、passord这些应该都不用说了,都是直接从properties中配置的属性中获取对应的值即可,唯一值得说的是数据源的配置,也可以叫做连接池的配置,上面默认使用的是POOLED连接池,这是MyBatis的默认选择。此外还支持UNPOOLED、JNDI两种模式,那他们都是什么意思呢,一起看下:

  • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间
  • UNPOOLED:这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择
  • JNDI:这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
    看完了这三种模式的介绍,我们最为常用的还是POOLED,不过在真实的开发中我们是不可能直接使用MyBatis提供的连接池的,因为他的性能不够,通常都是引入第三方的连接池,比如c3p0、druid等连接池,因此这也无需再去深入研究各个模式下的详细参数配置了,我们只需要知道有这三种属性,我们在自己测试时使用POOLED即可,其余POOLED模式下的其他参数直接使用默认即可。

11.databaseIdProvider标签

这个标签只能说是真的没大用,同时基本也不用。MyBatis提供该标签用以支持可以根据不同的数据库厂商来执行不同的语句,我们配置了如下内容MyBatis就会自动为我们查找当前的数据库的提供商:

<databaseIdProvider type="DB_VENDOR"><!--这是标准写法-->
  <property name="SQL Server" value="sqlserver"/><!--这里配置数据库的映射-->
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

MyBatis会自动将对应的值设置到databaseid中,然后我们在写CRUD的sql时便可以手动声明databaseid这就就可以实现根据数据库的提供商来执行sql了,但是基本没有这么用的,所以这个了解下即可。
在这里插入图片描述

12.mappers标签

mappers标签也是MyBatis标签的核心之一,这个标签的作用就是为了引入mapper.xml文件的。引入方式支持的有很多种,我们可以使用resource的方式,也可以使用url的方式,也可以使用class的方式,也可以使用包扫描的方式。总共是支持这四种方式来引入mapper.xml文件,不过其中url方式太不友好,没人用正常用的只有另外三种,下面介绍下这三种引入mapper.xml的方式:

  • 方式一:resource
    <mappers>
        <mapper resource="com/cheng/dao/SupplierMapper.xml"/>
    </mappers>
    
    先回忆下前面的内容:MyBatis通过Mapper.xml中的namaspace找到对应的接口类,通过sql的id找到接口的方法,然后MyBatis就可以自动为我们生产实现类,从而减去了我们手动书写实现类的麻烦。那发生这些得有个前提啊,前提就是MyBatis已经正常加载了Mapper.xml文件,若是没有正常加载Mapper.xml文件,自然是不可能为我们生产实现类的。而这里的mappers标签就是为了加载mapper.xml文件的。此外这里需要说明一个很可能碰到的问题:resource目录下的资源引入时使用的都是斜杠而不是点;这个是必须这么写的,若是我们不想让接口与mapper.xml写在一起,我们可以在resource下创建与dao接口同名的层级目录,然后将对应的mapper.xml文件放入也是可以的,这样编译时也不会有任何问题。但是在resource下创建时不能像创建包一样使用点来间隔目录层级,这里应该使用斜杠而且是必须使用斜杠,斜杠表示文件夹层级的意思,这里是需要特别注意的,这个错误很容易会犯,犯了这个错就会导致编译时找不到对应的Mapper.xml文件,而且这个错不容易排查。这些明确以后我们就继续看第二种加载Mapper.xml文件的方式吧
  • 方式二:class
    <mappers>
        <mapper class="com.cheng.dao.SupplierMapper"/>
    </mappers>
    
    如上所示便是使用class来加载Mapper.xml文件的方式,这里需要注意class中传入的是dao接口,那不是说mappes标签是为了加载Mapper.xml文件的吗?怎么又是加载dao接口了呢?这里就需要说下MyBatis提供的默认机制了,MyBatis提供了使用class的方式来加载Mapper.xml文件,但是使用这种方式必须有两个前提:①dao接口与mapper.xml文件名必须一致,②他们必须同包。这就是MyBatis允许我们使用class方式加载Mapper.xml的前提,因此我们是必须要满足的,满足这些条件后,我们可以根据传入的class的接口名来找到Mapper.xml文件进行加载(因为他们同名同包),这样也实现了Mapper.xml的加载,所以使用class进行加载Mapper.xml时必须保证接口与mapper.xml文件同包同名,不然就加载不了。
  • 方式三:package
    <mappers>
        <package name="com.cheng"/>
    </mappers>
    
    方式三其实就是方式二的进化版,他和方式二的原理一致,因此也要求我们所有的接口与其对应的Mapper.xml同名同包,只不过是方式三不需要我们一个一个的去单个的配置这些class,而是通过包扫描的方式将包下面所有的mapper.xml都加载,这点与别名包扫描是类似的。
    这样我们就看完了所有的加载Mapper.xml文件的方式,使用Resource的方式就是可以将Mapper.xml放置在任何位置,命名也不需要太规范,但是当Mapper.xml文件比较多时,要写很多很多的配置项也是很麻烦的,正常情况下我们还是使用包扫描的方式,只是使用这种方式时应该遵守MyBatis定好的规范:接口与其对应的Mapper.xm必须同名同包。

七、MyBatis注解开发详解

在介绍使用注解开发之前有必要声明一下,在MyBatis中使用配置文件开发才是官方推荐行为,而是使用配置文件开发他更利于维护,代码阅读起来也更优雅,使用注解开发会对代码有很高的侵入行为,而且对于复杂的查询有使用使用注解就会力不从心这点与Spring、Shiro、SrpingBoot等框架不一样,其他框架很多都是使用注解更简洁,但是对于MyBatis来说使用注解可能意味着更麻烦,不过对于简单的增删改查使用注解还是很方便的,下面就来一起看下如何使用注解来实现增删改查吧。

使用注解实现插入:@Insert

使用注解开发时最好实体类的属性名于列名保持一致,这样就不用映射了,不然会很麻烦,此外还有一点需要特别注意,使用注解时,我们就无法导入Mapper.xml文件了,我们需要使用类路径方式来加载,关于使用类路径加载和使用resource加载的区别,详情请看mybatis-config.xml详解的最后一节,如下所示:

    <mappers>
        <mapper resource="com/cheng/dao/SupplierMapper.xml"/>
        <mapper resource="com/cheng/orderdao/OrderMapper.xml"/>
        <mapper resource="com/cheng/shopdao/ShopMapper.xml"/>
        <mapper class="com.cheng.productdao.ProductMapper"/><--!使用类路经方式加载 -->
    </mappers>

然后我们就可以写接口了,下面展示下使用注解方式时接口的写法:

/**
 * @author pcc
 * @version 1.0.0
 * @className ProductMapper
 * @date 2021-08-07 14:14
 */
public interface ProductMapper {

    @Insert("insert into product_info(pid,productNo,productName) value(#{pid},#{productNo},#{productName})")
    void addProduct(ProductInfo productInfo);
}

下面是测试截图:
在这里插入图片描述
这样我们就插入成功了,可以发现使用注解开发还是很简单的。这里sql的书写与Mapper.xml中没有任何区别,所以也没有什么其他值得说的了。

使用注解实现删除:@Delete

@Delete、@Insert、@Update、@Select他们之间的关系与@Component、@Controller、@Service、@Repository他们之间的关系类似(compoment注解与Controller、Service、Repository注解的关系),都是可以混用的,底层其实都是去执行sql而已,只不过是为了区分业务场景,来划分出来的不同注解,其实都使用一种注解也是可以的,比如如下,使用@Select来做删除操作,也是没有任何问题的。

/**
 * @author pcc
 * @version 1.0.0
 * @className ProductMapper
 * @date 2021-08-07 14:14
 */
public interface ProductMapper {
    @Select("delete from product_info where pid = #{pid}")
    void deleteProductByPid(@Param("pid") int id);
}

下面是测试截图:

在这里插入图片描述
可以发现这里使用@Select也成功实现了删除,不过要是真正使用时建议还是使用Delete注解来实现删除操作,这样阅读起来更舒适一些。

使用注解实现修改:@Update

修改与增加、删除不同的地方就是注解与sql不同,其他也没什么区别,直接展示代码了:

/**
 * @author pcc
 * @version 1.0.0
 * @className ProductMapper
 * @date 2021-08-07 14:14
 */
public interface ProductMapper {

    @Update("update product_info set productName = #{productName} where pid = #{pid}")
    void updateProductByPro(ProductInfo productInfo);
}

下面是测试截图:
在这里插入图片描述

使用注解实现查询:@Select

通过增删改我们发现使用注解还是很轻松的,比使用Mapper.xml不要轻松太多,但是在真实的业务场景中最复杂的和使用最多的业务还是查询,增删改都不多,而查询中还有一对多、多对一的场景此时使用注解就不好实现了,所以还是建议使用Mapper.xml文件来开发。下面展示一则简单的模糊查询,这里使用了@Param注解,这个注解与@RequestBody类似,都是进行参数绑定的。

/**
 * @author pcc
 * @version 1.0.0
 * @className ProductMapper
 * @date 2021-08-07 14:14
 */
public interface ProductMapper {

    @Select("select * from product_info where productName like \"%\"#{productName}\"%\"")
    List<ProductInfo> queryProductByName(@Param("productName") String name);
}

下面是测试截图:
在这里插入图片描述
可以看到这个查询结果是正确的,刚刚我们插入了5条,删除了第五条,修改了第四条,然后就是这样了,说明增删改查使用注解来实现也是没有问题的,这样使用注解实现的增删改查就说完了,这里只是展示了基础的增删改查的注解实现,此外注解还支持复杂的动态sql等,但是会让接口看起来很臃肿,并不利于阅读,若是对于复杂的语句建议还是使用配置文件。

八、MyBatis的缓存详解

缓存这个概念应该都不会陌生,在这个大数据量高并发的时代,缓存技术是非常非常必要的,除非是做一些OA系统、后台管理系统等用户量比较少的系统时缓存使用的不是太多,在主流的项目中缓存已经是必备的点了,因此掌握各个场景的缓存也就显得十分必要,我们最常提起的其实还是项目中为整个项目做的缓存,现在通常使用redis来实现,与我们自己构建的项目比较类似的是很多框架也有相似的思想,多是为自己增加一些缓存管理,这些缓存无疑都有一个共性,就是为了提升自身的查询性能,减少不必要的资源消耗和内存消耗。Mybatis作为数据库交互的首选ORM框架,是我们日常最常用的数据库交互框架,他底层对JDBC进行了封装,使我们使用时更方便,让开发变得更简单,同时Mybatis为了减少不必要的数据库交互操作,为用户提供了两种缓存策略,即Mybatis的一级缓存和二级缓存,那Mybatis的一级缓存和二级缓存又是如何做到减少不必要的数据交互呢,一起来看下Mybatis怎么做的一级缓存和二级缓存吧。

1.一级缓存

一级缓存是sqlsession级别的,当在一次sqlsession中操作了同样的sql时,Mybatis默认从一级缓存中获取数据,而不会重复去数据库查询,我们使用Mybatis时连接数据库通常都是基于sqlsession来连接的,sqlsession的获取上面已经说过这里就不重复介绍了。Mybatis提供的一级缓存默认就是开启的,并且他是基于sqlsession的,我们知道sqlsession是基于与数据库建立的一次连接的,当连接消失时sqlsession就会被归还到连接池中,所以Mybatis的一级缓存只在当前请求内有效,释放连接后一级缓存也就没了。其实在上面介绍mybatis-config.xml中的settings时笔者也介绍了一下一级缓存,就是下面这个标签:
在这里插入图片描述
我们可以看到,一级缓存模式就是sqlsession级别的,就是为了减少同一个sqlsession的重复查询。

2.一级缓存什么场景会失效

下面几种常见常见会导致一级缓存的失效:
①sqlsession被关闭或者归还到连接池时,缓存失效,因为sqlsession已经不存在了,一级缓存也就没了。
②sqlsession中使用了删除、修改等导致数据变更的操作也会引起一级缓存的失效。
③使用flushcache也可以让缓存手动失效。
④整合Spring与MyBatis时未开启Spring的事务。

3.二级缓存

二级缓存需要手动开启,在mapper.xml中全局开启二级缓存使用<cache/>就可以完成二级缓存的开启,二级缓存是基于namespace的,也可以说是基于mapper的(也可以针对一条语句开启二级缓存,直接在select语句中添加缓存标签即可),当多次sqlsession的请求都是同一个mapper.xml文件时且查询的数据一样,就会走到二级缓存中,Mybatis会直接从二级缓存后去数据返回,而不会去数据库查,那二级缓存数据是哪里来的呢?当然是从一级缓存中来的了,当一级缓存失效时会查看二级缓存是否开启,若是开启了,则会将数据放入二级缓存。
二级缓存默认是开启的,他的开关是在mybatis-config.xml中的setting中,如下所示:
在这里插入图片描述
那开启的为什么上面还说需要手动开启,这里需要说下,这里开启的是个总开关,每个mapper文件若是想要开启二级缓存还需要在mapper.xml中增加<cache/>标签即可。

4.二级缓存什么场景会失效

下面列出几种MyBatis二级缓存失效的场景:
①mapper.xml中的数据修改sql被调用时会引起二级缓存的失效
②二级缓存默认失效时间是60s也就是即使不做修改操作,60s以后二级缓存的数据也会消失
③当二级缓存中默认最多存储512条数据,数据满了以后,二级缓存采用的默认丢弃策略与redis一致都是LRU,即最近最少使用的会被删除,也就是说会删除存在比较长且使用次数比较少的缓存,这种情况也会导致二级缓存中部分对象不可用的情况

5.Spring整合Mybatis必须开启事务,否则一级缓存不生效

Spring与Mybatis整合不使用事务时会引起一级缓存的失效,为什么呢?因为我们不使用Spring事务,默认就是使用Mybatis的事务,Mybatis的事务默认又是使用的JDBC的事务,每操作数据库一次就会提交事务,提交后也就释放了sqlsession的连接,这样即使我们在同一个service中多次操作同一个查询对象也不会有一级缓存的存在了。那开启Spring事务又怎么会一级缓存生效了呢?因为当我们开启Spring的事务时,本次方法内是共用一个sqlsession的,而一级缓存就是基于sqlsession的,所以此时一级缓存有效。

九、关于MyBatis的补充

1.作用域(Scope)和生命周期

这一块主要是通过阐述不同的作用域与生命周期可能会对程序造成的影响,因此我们需要在设计程序时充分的考虑对象的作用域与生命周期的问题。比如以下几方面都是我们应该思考和关注的:

  • SqlSessionFactoryBuilder
    SqlSessionFactoryBuilder的唯一作用就是用来加载核心配置文件mybatis-config.xml文件流的,使用完就没有了用处,因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
  • SqlSessionFactory
    SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
  • SqlSession
    每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。SqlSession使用完毕应该立即关闭或者放回连接池。使用try-with-resource即可正常释放。
  • 映射器实例
    这里说的映射器示例就是我们通过sqlsession.getMapper获得的接口实现类的实例。映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。

上面的规范已经了解完了,不能光纸上谈兵啊,我们照着上面的规范来进行编码,如下所示,就是符合上述声明周期和作用域的编码规范,也就是SqlSessionFactoryBuider是用完即丢的,SqlSessionFactory是只创建了一次的(这里只作展示,写的不是太严谨),sqlsession是用完即关的,映射器对象也是方法级别的,当然我们正常编程时其实也都是这么写的。

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.InputStream;

/**
 * @author pcc
 * @version 1.0.0
 * @className TestScope
 * @date 2021-08-02 13:30
 */
public class TestScope {
    static SqlSessionFactory sqlSessionFactory ;
    @Test
    public void testScope() throws Exception{
        String file = "mybatis-config.xm";
        InputStream inputStream = Resources.getResourceAsStream(file);
        if(sqlSessionFactory==null)
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try(SqlSession sqlSession = sqlSessionFactory.openSession()){
            SupplierDao mapper = sqlSession.getMapper(SupplierDao.class);
        }
    }
}

2. # 和 $ 的区别

使用MyBatis时获取参数,我们必须使用# 或者 $ 那他们有什么区别呢,首先我们看下官方的解释:

默认情况下,使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 
这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串,比如 ORDER BY 子句,这时候你可以
使用$ 来获取参数,此外,若是想将列名从外界传进来,我们也可以使用$但是他们都是有风险的,因此一般建议使用#来获取参数。

官方解释的可以说挺详细的,明确告诉我们使用#就和使用JDBC中的PreparedStatement一样他是安全的,使用$则是直接将参数拼接进sql,这样很容易造成sql注入的风险,因此建议我们使用#来获取参数。

3.多个参数的传参方式

普遍使用的有四种方法来实现多个参数的传参分别是:
①使用JavaBean来包装参数,这种方式实现请参照上文中:第三大节中的增删改查
②使用Map来包装参数,这种方式使用起来与javaBean并没有多大区别,请参照:第三大节中的“万能传参方式之Map”
③使用List来包装参数,这种方式需要借助foreach标签一起配合才可以,请参照:第五大节中的“动态sql之foreach”
④使用@param来修饰接口参数,这种方式无需任何其他配合,只需要我们为每个接口中的参数添加这个注解即可,然后在Mapper.xml中直接使用就行,下面展示下这种用法:

------- 接口方法 -------
List<ProductInfo> queryParam(@Param("pid") int id,@Param("productName") String name);
------- Mapper.xml -------
<select id="queryParam" resultType="productInfo">
    select * from product_info
    <trim prefix="where" prefixOverrides="and|or">
        <if test="pid != null">
            and pid = #{pid}
        </if>
        <if test="productName !=null">
            and productName like "%"#{productName}"%"
        </if>
    </trim>
</select>

下面展示下这种传参的测试截图:
在这里插入图片描述
可以发现这两个条件查询都没有问题。
那这么多方式都可以支持多条件的传递我们应该怎么选择呢,其实无论使用哪种都是没有任何问题的,不过平时使用最多的可能还是Map与javaBean来传递多参数,建议工作时也这么用即可,此外List准备来说他是传递的一个参数的多个可能值,而不是多个参数,若是强调必须是参数多个则不能使用List了。

4.Mapkey注解

可以将sql返回对象转换成Map,这样就可以不用写实体类了,其实作为偷懒是很有用的。
这是java代码的mapper接口

@MapKey("roleId")
    Map<Long,Map<String,Object>>  getRoleRelaPositionByRoleIds(@Param("roleIds") List<Long> roleIds);

这个注解的作用是将查询的列的信息将roleId列作为key,然后value的值是Map,这个map的key是列名,value是真正的值,而且这里可以看到已经将roleId转为了Long,若是有其他数字类型也会默认转为Long,所以使用时数字类型最好统一。

这是mapper.xml文件信息

    <select id="getRoleRelaPositionByRoleIds" resultType="java.util.Map">
        select roleRe.role_id as roleId,count(distinct positionRe.position_id) as positionCount,role.role_name as positionNames from am_position_role_rela roleRe
    </select>

返回信息如下:
在这里插入图片描述
需要注意的是Map的value也是Map,这里之所以是Map是以为在maper.xml文件中声明的对象是Map,和Mapkey是没有关系的。这点一定要注意,很容易弄混淆。
如果value想要使用对象类型也是可以的,java代码无需改变,只需要改变mapper.xml即可,如下:

    <select id="getRoleRelaPositionByRoleIds" resultType="com.sunacwy.am.bussiness.entity.AmRoleInfo">
        select roleRe.role_id as id,count(distinct positionRe.position_id) as positionCount,role.role_name as positionNames from am_position_role_rela roleRe
    </select>

也就是只改了resultType即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

归去来 兮

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

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

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

打赏作者

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

抵扣说明:

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

余额充值