3.4 MyBatis

一、引言

1.1 什么是框架

软件的半成品,解决了软件过程当中的普适性问题,从而简化了开发步骤,提高了开发的效率。

1.2 什么是ORM框架

  • ORM(Object Relational Mapping)对象关系映射,将程序中的一个对象与表中的一行数据一一对应
  • ORM框架提供了持久化类与表的映射关系,在运行时参照映射文件的信息,把对象持久化到数据库中

我们要学的MyBatis就是ORM框架。对象关系映射中,对象指Java对象(比如student,customer对象),关系指惯性型数据库(比如MySQL就是关系型数据库),映射指将将Java对象和关系型数据库中的表映射起来。

ORM框架的作用将程序中的一个对象与表中的一行数据一一对应,使对象持久化到数据库更加容易。

MyBatis就是再次封装JDBC。

1.3 使用JDBC完成ORM操作的缺点?

  • 存在大量的冗余代码。
  • 手工创建Connection、Statement等。
  • 手工将结果封装到实体对象。
  • 查询效率低,没有对数据访问进行过优化(Not Cache)。

二、MyBatis框架

2.1 概念

  • MyBatis本是Apache软件基金会的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了Google Code,并且改名为MyBatis。2013年11月迁移到Github。
  • MyBatis是一个优秀的基于Java的持久成框架,支持自定义SQL,存储过程和高级映射。
  • MyBatis对原有JDBC操作进行了封装,几乎消除了所有JDBC代码,使开发者只需关注SQL本身。(封装JDBC,暴露MyBatis的API,让我们精力只在SQL本身)
  • MyBatis可以使用简单的XML或Annotation来配置执行SQL,并自动完成ORM操作,将执行结果返回。(有了SQL,自动完成ORM操作)

2.2 访问与下载

官方网站:http://www.mybatis.org/mybatis-3/

下载地址:https://github.com/mybatis/mybatis-3/releases/tag/mybatis-3.5.1

其它版本查找方式:

官网首页点击:进入后点击下翻点击即可找到任意版本。

 当然我们不用下,我们后面用Maven,引入依赖即可。

 

三、构建Maven项目

3.1 新建项目

 构建Maven项目,gav坐标为

 创建好Maven项目后,我们要先将MyBatis环境搭建起来,搭建环境分为两步:1、导入依赖;2、加入MyBatis配置文件。

 

四、MyBatis环境搭建【重点】

4.1 pom.xml中引入

<!--依赖-->
<dependencies>
    <!--MyBatis核心依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.2</version>
    </dependency>
    <!--MySQL驱动依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.46</version>
    </dependency>
</dependencies>

 这里需要的依赖有两个,一个是MyBatis依赖,另一个是MySQL数据库。对于MyBatis依赖我们自然非常清楚,但是为什么需要MySQL呢?可以不要吗?答案是不可以,我们MyBatis是ORM框架,封装的是JDBC,是要操作数据库的,因此数据库本身的依赖还是要添加的。

4.2 创建MyBatis配置文件

注意,这里的MyBatis配置文件是放在resources下面的,其名字无所谓,MyBatis不要求。但最好起有意义的,我们这里是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="shine_config">
        <!--数据库相关配置   其中id是这个配置的标识,在environments上引用,让其生效-->
        <environment id="shine_config">
            <!--事务控制类型 这里选用的是JDBC事务控制,说白了就是connection控制事务,这里是单标签,你也可以用双标签-->
            <transactionManager type="JDBC"/>
            <!--开始是连接池   后面是数据库连接参数 就是驱动,url,数据库账号和密码-->
            <dataSource type="org.apache.ibatis.datasource.pooled.PooledDataSourceFactory">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!--原本我们的和用&符号即可,这里不能直接用,要转义,即&转为&amp;,用这5个字符代表一个字符&-->
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&amp;characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!--Mapper注册-->
    <mappers>
        <!--注册Mapper文件的所在位置-->
        <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
        <mapper resource="org/mybatis/example/BlogMapper.xml"/>
    </mappers>
</configuration>

 这个配置文件的格式在官网就可以找到。搜索MyBatis进去即可。

 这里重点关注<environments>标签下面的内容,其下面有许多<environment>子标签,当然我们这里只有一个;<environment>标签下面有数据库控制类型标签<tarnsactionManager>和数据库连接池标签<datasource>,在顺便说一下,这里的数据库连接池真是简单,这么写个标签即可;数据库连接池里面就是我们链接数据库需要的参数了,driver、url、user和password。

关于Mapper这里先不要管,后面会讲解。

想了解MyBatis配置的更详细解读,请自行搜索一下。

此外:还需要数据库的建立,这个不属于MyBatis的准备工作,但是没有是不行的,MySQL如下:

CREATE DATABASE mybatis_shine DEFAULT CHARSET = utf8;

USE mybatis_shine;

CREATE TABLE t_user (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50),
  PASSWORD VARCHAR(50),
  gender TINYINT,
  regist_time DATETIME
)DEFAULT CHARSET = utf8;

另外还有SQLyog工具插入了两条数据,注册日期随意。

五、MyBatis开发步骤【重点】

5.1 建表

关于建表,上面已经有了。

5.2 定义实体类

 根据现有的数据库创建一个实体。

这个实体类要和数据库字段对应。我们先回顾下数据库的字段

 这里建实体类如下:

package com.qf.entity;

import java.util.Date;
public class User { private Integer id; private String username; private String password; private Boolean gender; private Date registTime; //下面是有参、无参构造、getter和setter方法、toString方法 }

 我们注意:这里又一个细节,就是实体类中的registTime和数据库中的regist_time是不同的。这里先留着疑问。

5.3 定义DAO接口

 所谓的ORM关系映射,就是将数据库中的表与实体类映射到一起。即表中的一行,对应实体类的一个对象。

那么中间如何映射呢?我们总的指定好吧??首先我们要定义DAO接口,这个接口是做什么的??哎,都是疑问。

package com.qf.dao;
import com.qf.entity.User;
public interface UserDao {
    User queryUserById(Integer id);
}

 这个接口是做什么的??其实接口里面只有方法,比如这个方法queryUserById,就是通过id查询数据库中的数据,最终返回User对象。

那么问题来了??我给他一个接口,MyBatis就能帮我们查名??他就有这么神奇吗??答案是!不,他没有这么神奇,他还是需要SQL语句的。哎,那问题又来了,SQL语句写在哪里?当然是写在实现类里了!!

实现类走起:

package com.qf.dao;
import com.qf.entity.User;
class UserDaoImpl implements UserDao {
    @Override
    public User queryUserById(Integer id) {
        /*
        * Connection conn = ...
        * Statement stm = ....
        * String sql = "select id, username,password,gender,regist_time from t_user where id=?"
        * ResultSet rs = ...
        * if (rs.next()) {
        *   ...
        *   Integer id = rs.getInt("id");
        *   ....
        *   user.setId(id);
        *   .....
        * }
        * return user;
        * */

        return null;
    }
}

 其实你已经被我套路了??你看上面这个实现类,岂不就是JDBC吗?那我们还用MyBatis框架干嘛??

 其实MyBatis确实需要SQL语句,但是他不是写在实现类里面的,而是写在mapper映射文件里的,这是MyBatis的又一个配置文件。

5.4 编写Mapper.xml

 在Maven里,配置文件统一写在resources里面,我们习惯命名是:Dao接口名+mapper.xml,这里命名为UserDaoMapper.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 = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">
    <!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
    <!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
    <select id="queryUserById" resultType="com.qf.entity.User">
        select id,username,password,gender,regist_time
        from t_user
        where id=#{arg0}
    </select>
</mapper>

一个Mapper对应一个Dao,<mapper>标签里面的内容就是写SQL语句的,对应Dao里面的方法。

我们这里要说明一下,其实这个Mapper就相当于是Dao接口的实现类,只是因为以前的实现类用JDBC调用,有大量重复的代码,这些工作被MyBatis做了。我们这里写了Mapper映射文件,看似没有Dao实现类,其实是有的,只是这个实现类是由MyBatis做了,在运行时,MyBatis利用反射机制在内存中创建了Dao接口的实现类。后面我们调用的时候,其实还是调用实现类,只是这个实现类不是我们亲手写的罢了。

5.5 注册Mapper

 我们这里要说一下,虽然我们写了Mapper映射文件(MyBatis的又一个配置文件),但是MyBatis只认一个配置文件,就是最上面的MyBatis-config配置文件(我想他是根据配置文件的头识别出来的),那我们写的mapper映射文,MyBatis读取不到,怎么办呢?需要注册,非常简单,其实上面大家已经见过,就是MyBatis-config配置文件中有一个<mappers>标签,我们将所有的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="shine_config">
        <!--数据库相关配置   其中id是这个配置的标识,在environments上引用,让其生效-->
        <environment id="shine_config">
            <!--事务控制类型 这里选用的是JDBC事务控制,说白了就是connection控制事务,这里是单标签,你也可以用双标签-->
            <transactionManager type="JDBC"/>
            <!--开始是连接池   后面是数据库连接参数 就是驱动,url,数据库账号和密码-->
            <dataSource type="org.apache.ibatis.datasource.pooled.PooledDataSourceFactory">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!--原本我们的和用&符号即可,这里不能直接用,要转义,即&转为&amp;,用这5个字符代表一个字符&-->
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&amp;characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!--Mapper注册-->
    <mappers>
        <!--注册Mapper文件的所在位置-->
        <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
        <mapper resource="UserDaoMapper.xml"/>
    </mappers>
</configuration>

之所以这里的名字只是UserDaoMapper.xml,是因为他现在是和mybatis-config.xml放在一起的。下面是项目目录和编译后的目录。我们可以发现resources下的文件直接是在根目录下(classes目录下)。

5.6 测试

package com.qf.test;
import com.qf.dao.UserDao;
import com.qf.entity.User;
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 java.io.IOException;
import java.io.InputStream;
public class TestMyBatis {
    public static void main(String[] args) throws IOException {
        // MyBatis API
        // 1. 加载配置文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 2. 构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //3. 通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 4.通过SqlSession获得DAO实现类的对象
        UserDao mapper = sqlSession.getMapper(UserDao.class); //获取UserDao对应的实现类的对象

        // 5. 测试查询方法
        User user = mapper.queryUserById(1);
        User user1 = mapper.queryUserById(2);

        System.out.println(user);
        System.out.println(user1);
    }
}

 测试结果:

Thu Nov 26 17:22:08 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
User{id=1, username='shine', password='123', gender=true, registTime=null}
User{id=2, username='shine2', password='456', gender=false, registTime=null}

 哇,这不是我们数据库里面的内容吗?出来了,出来了,他带车笑容出来了。

 但是我们看到这里id注册日期是null,这是为什么呢?这就是我们上面遗留的疑问了,就是数据库里面的字段名要和实体类一样。

我们现在把实体类的字段名修改为regist_time,这样就和数据库保持一致了。修改后再次运行(注意用快捷键shift+f6把所有registTime修改为regist_time哦)

User{id=1, username='shine', password='123', gender=true, regist_time=Fri Nov 20 18:37:41 CST 2020}
User{id=2, username='shine2', password='456', gender=false, regist_time=Wed Jul 01 17:38:13 CST 2015}

这样就成功了,但是这是有问题的,你想,我们数据库里面的命名规则和Java变量命名规则不一样,比如regist_time在数据库中作为字段名挺好的,而registTime在Java类中作为字段名也也挺好。

 现在我们抛出如下两个问题:数据库里面的字段是如何和实体类映射到一起的,采用什么规则?如果不修改实体类的字段名,该如何保证实体类字段registTime可以获得值?

对于问题1:直接给出答案,是同名规则,即数据库中的字段和实体类的字段名字相同,才可以。其实这里说的不准确,并非是数据库中的字段名,而且通过SQL语句从数据库中查询出来的字段名。这里又三个概念:数据库字段;查询出来的字段(有时是别名,有时是数据库字段);实体类字段。

 不说废话了,就这个,MyBatis实现的同名规则,就是查询出来的字段和类字段一致,就会得到结果。

对于问题2:通过上面你的解释就很容易了,其实就是我们写SQL语句的时候起个别名就行了,SQL语句在哪里写??MyBatsi的第二种配置文件,即Mapper里面写的。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">
    <!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
    <!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
    <select id="queryUserById" resultType="com.qf.entity.User">
        select id,username,password,gender,regist_time as registTime
        from t_user
        where id=#{arg0}
    </select>
</mapper>

现在再次运行程序(这里要注意:User实体类的字段还要改回来哦,该为registTime)

Thu Nov 26 18:46:18 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
User{id=1, username='shine', password='123', gender=true, regist_time=Fri Nov 20 18:37:41 CST 2020}
User{id=2, username='shine2', password='456', gender=false, regist_time=Wed Jul 01 17:38:13 CST 2015}

我们这里讲一个问题:其实还是以前讲过的,数据库中的字段怎么就映射到实体上了,也就是说ORM是怎么做的。

六、细节补充

6.1 解决mapper.xml存放在resources以外路径中的读取问题。

在前面我们将mapper.xml映射文件存放在resources下面,这是很自然的,因为resources下面就是存放配置文件的。但是呢?这个mapper映射文件和Dao接口关联性非常大,因此我们想把他和Dao接口放在一起,即如下图所示。放就放吧,这有啥??

 此时我们运行一下:错误信息如下,注意错误信息有很多Cased by,我们要从下往上看,看第一个cased by。

D:\developer_tools\Java\jdk-8u221\bin\java "-javaagent:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\lib\idea_rt.jar=50185:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\bin" -Dfile.encoding=UTF-8 -classpath D:\developer_tools\Java\jdk-8u221\jre\lib\charsets.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\deploy.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\access-bridge-64.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\cldrdata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\dnsns.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jaccess.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jfxrt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\localedata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\nashorn.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunec.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunjce_provider.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunmscapi.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunpkcs11.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\zipfs.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\javaws.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jce.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfr.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfxswt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jsse.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\management-agent.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\plugin.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\resources.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\rt.jar;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\test-classes;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\classes;D:\developer_tools\Java\maven_repository_for3.5.4\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\developer_tools\Java\maven_repository_for3.5.4\mysql\mysql-connector-java\5.1.46\mysql-connector-java-5.1.46.jar com.qf.test.TestMyBatis
Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
### Error building SqlSession.
### The error may exist in UserDaoMapper.xml
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.io.IOException: Could not find resource UserDaoMapper.xml
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:80)
    at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:64)
    at com.qf.test.TestMyBatis.main(TestMyBatis.java:17)
Caused by: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.io.IOException: Could not find resource UserDaoMapper.xml
    at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:121)
    at org.apache.ibatis.builder.xml.XMLConfigBuilder.parse(XMLConfigBuilder.java:98)
    at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:78)
    ... 2 more
Caused by: java.io.IOException: Could not find resource UserDaoMapper.xml
    at org.apache.ibatis.io.Resources.getResourceAsStream(Resources.java:114)
    at org.apache.ibatis.io.Resources.getResourceAsStream(Resources.java:100)
    at org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement(XMLConfigBuilder.java:372)
    at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:119)
    ... 4 more

Process finished with exit code 1

此时我们发现提示找不到这个映射文件,我们想,哎,是啊,这个映射文件不是要在MyBatis配置文件mybatis-config.xml里面注册吗?你位置换了,不是要重新注册吗(就是修改位置)。于是我们做了如下修改:

<mappers>
    <!--注册Mapper文件的所在位置-->
    <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
    <mapper resource="com/qf/dao/UserDaoMapper.xml"/>
</mappers>

这时候就可以找到了,我们再次运行:

Caused by: java.io.IOException: Could not find resource com/qf/dao/UserDaoMapper.xml

这次错误变成不能找到:com/qf/dao/UserDaoMapper.xml,我就奇怪了,我们的这个包下确实有这个mapper映射文件呀,我刚刚剪切过去的。

 其实呢?我们看文件,不是看我们写代码的位置,而是看编译后的位置,即编译后生成的target目录。

 我们发现这个编译后生成的目录的dao文件夹下并没有映射文件,怎么办??很简单,我们复制一份就可以了。

 

 此时再次运行,正确结果就出现了。

D:\developer_tools\Java\jdk-8u221\bin\java "-javaagent:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\lib\idea_rt.jar=50695:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\bin" -Dfile.encoding=UTF-8 -classpath D:\developer_tools\Java\jdk-8u221\jre\lib\charsets.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\deploy.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\access-bridge-64.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\cldrdata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\dnsns.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jaccess.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jfxrt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\localedata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\nashorn.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunec.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunjce_provider.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunmscapi.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunpkcs11.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\zipfs.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\javaws.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jce.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfr.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfxswt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jsse.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\management-agent.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\plugin.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\resources.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\rt.jar;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\test-classes;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\classes;D:\developer_tools\Java\maven_repository_for3.5.4\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\developer_tools\Java\maven_repository_for3.5.4\mysql\mysql-connector-java\5.1.46\mysql-connector-java-5.1.46.jar com.qf.test.TestMyBatis
Thu Nov 26 19:06:14 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
User{id=1, username='shine', password='123', gender=true, regist_time=Fri Nov 20 18:37:41 CST 2020}
User{id=2, username='shine2', password='456', gender=false, regist_time=Wed Jul 01 17:38:13 CST 2015}

但是呢?我们不可能每次都复制吧?而且这还是一个mapper,万一以后有许多Dao接口(一个接口一张表嘛,表多,Dao自然多),那么mapper文件就多了。因此,赶快删掉哈。

那到底怎么办?首先我们要明白为什么我们从resources文件夹下,将mapper映射文件复制到java->com/qf/dao下不起作用。

这是Maven编译的编译规则造成的,默认情况下,maven认为你放在resources里面的就是配置文件,因此我们放这里的xml文件就会生效,会编译就target里面(我们最开始就是放在这里)。但是呢?如果你把他移动到java包下,这个包在maven眼里只有java文件,因此只有java文件才会被编译,而其他文件(比如这里的mapper.xml文件)是不会被编译进入target里面的。

那我们要怎么办呢?兜兜转转,其实方法非常简单,就是修改Maven的编译规则,怎么修改?在pom.xml文件中修改(这是Maven的配置文件)

这里我就贴下完整的pom.xml文件吧

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

    <!--项目配置-->
    <groupId>com.qf</groupId>
    <artifactId>hello-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!--依赖-->
    <dependencies>
        <!--MyBatis核心依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--MySQL驱动依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
    </dependencies>

    <build>
        <!--更改maven编译规则-->
        <resources>
            <resource>
                <!--资源目录-->
                <directory>src/main/java</directory>
                <includes>
                    <include>*.xml</include> <!-- 代表java目录下的所有xml文件 -->
                    <include>**/*.xml</include> <!-- 代表java目录下任意文件夹(包括子文件夹)下的所有xml文件 -->
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

</project>

 

这时候再次运行,就出现正确结果了。其实我这里出现了错误,不过自己已经纠正了,错误出的莫名其妙,然后是一连串,解决方式也是好奇怪的感觉,怎么说呢?我东墙破了,他却显示西墙破了,我修了西墙,发现问题没有解决,此时又不知怎么的暴露出东墙破了,我就修了东墙。可能我是菜鸟吧,这是唯一的解释。

不过上面的代码是没问题的。

6.2 properyies配置文件

 关于MyBatis的核心配置文件,就目前而言里面有两大标签,其中一个是关联Mapper文件的<mappers>标签,另外一个是<environments>标签,这里面有一个<environement>标签,在这个<environment>标签下,有两个字标签,一个是关于事务的<transcationManager>标签,另外一个是<dataSource>关于数据库连接的标签,这个<dataSource>标签里面的type属性指定了数据库连接池。该标签下的四个字标签<property>指定了连接数据库的四个内容:driver、url、username和password。

我们想这个连接数据库的四个标签内容是经常改变的。比如生产、测试、开发环境均不相同,那么连接数据库的地址也很有可能不相同,我们如果经常来这个MyBatis的核心配置文件里面修改是不合理的。为什么??因为里面东西多,你万一动了其它的怎么办?我们一般把经常修改的提取出来,放在另外一个配置文件,然后在这个MyBatis的核心配置文件中引用。这就有点想Mapper映射文件的引用了。当然Mapper映射文件肯定不能写在核心配置文件中,毕竟那么大,那么多,怎么写的下。好了,废话不多说,怎么把这些和数据库相关的四个属性提取出来,放在一个文件,并引用呢?

在resources下新建一个properties文件存放这四个属性,名字无所谓,为了见名知意,这里就叫jdbc吧,因此名字就是jdbc.properties。

里面内容如下:

jdbc.url=jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&characterEncoding=utf-8
jdbc.driver=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=root

这里需要说明的是:这里的四个名字,比如jdbc.url,你可以任意起名,比如a,比如a.url等等,都是可以的。但依然是为了见明知意,我们这么起名。

这里就是四个键值对,关于value值,就从MyBatis的核心配置文件中复制过来即可。但是这里需要注意的是,&这个符号在这里已经不需要转义,因此不再用&amp;了。

既然已经提取出来了,怎么在MyBatis核心配置文件中引用呢??其实非常简单。代码如下:红色标注出来。

<?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="jdbc.properties"></properties>
    <!--核心配置信息-->
    <environments default="shine_config">
        <!--数据库相关配置   其中id是这个配置的标识,在environments上引用,让其生效-->
        <environment id="shine_config">
            <!--事务控制类型 这里选用的是JDBC事务控制,说白了就是connection控制事务,这里是单标签,你也可以用双标签-->
            <transactionManager type="JDBC"/>
            <!--开始是连接池   后面是数据库连接参数 就是驱动,url,数据库账号和密码-->
            <dataSource type="org.apache.ibatis.datasource.pooled.PooledDataSourceFactory">
                <property name="driver" value="${jdbc.driver}"/>
                <!--原本我们的和用&符号即可,这里不能直接用,要转义,即&转为&amp;,用这5个字符代表一个字符&-->
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!--Mapper注册-->
    <mappers>
        <!--注册Mapper文件的所在位置-->
        <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
        <mapper resource="com/qf/dao/UserDaoMapper.xml"/>
    </mappers>
</configuration>

 对于<properties>标签引用文件,这里需要说明的是,里面直接写了jdbc.properties名称,而没有写resources/jdbc.properties。这里大家要明白是为什么。表面上看是因为这个文件和核心配置文件都在resources下,即同级。深层次的原因如下图所示(编码后的target目录):

 这才是真正这么写的原因。我们要明白。此时再次运行程序,没有任何问题。

6.3 类型别名

 现在我们看一下mapper映射文件,我们的结果返回是User对象,这里写的是com.qf.entity.User

<?xml version="1.0" encoding="GBK" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">
<!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
<!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
<select id="queryUserById" resultType="com.qf.entity.User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id=#{arg0}
</select>
</mapper>

你想后面我们的借口中有很多方法,增删改查四类方法,都有可能是和这个实体类相关的,也就是只要是查询方法返回这个实体类的,就要写com.qf.entity.User,这样写完整的类名是有点难受的,那能不能有简写方式呢?当然有,这里提供两个。

方式一:

<!--实体类别名-->
<typeAliases>
    <typeAlias type="com.qf.entity.User" alias="user_shine" />
</typeAliases>

在核心配置文件中添加<typeAliases>标签,然后给这个全类名实体类起别名为user_shine,然后在Mapper映射文件中引用。

<?xml version="1.0" encoding="GBK" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">
<!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
<!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
<select id="queryUserById" resultType="user_shine">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id=#{arg0}
</select>
</mapper>

这样运行就可以了。但是这样有一个缺陷,第一:后面我们会有许多实体类,那岂不是在<typeAliases>标签下写许多<typeAlias>吗?而且每个实体类都要想一个名字;第二,这个名字,起出来可能可读性不强,不如上面的shine_user。当然对于第二个缺陷,我们可以直接起名为User即可。

方式二:

依然是在核心配置文件中

<!--实体类别名-->
<typeAliases>
    <!--定义实体类所在的package,每个实体类会自动注册一个别名,该别名就是类名-->
    <package name="com.qf.entity" />
</typeAliases>

此时在Mapper映射文件中引用User即可,不需要写全类名。

<?xml version="1.0" encoding="GBK" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">
<!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
<!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
<select id="queryUserById" resultType="User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id=#{arg0}
</select>
</mapper>

运行之后依然得到正确结果。

需要提醒的是,方式一和方式二只能存在一个。我们这里推荐方式二。

6.4 创建log4j配置文件

 我们上面已经运用好了MyBatis进行了一次查询,那MyBatis为我们做了什么事情,从理论上,我们知道大概。但是如果我们想知道更加详细的怎么办?其实MyBatis是有日志的,只是因为现在我们还没有让他显现出来。怎么显现出来呢?分两步:

1.引入log4f依赖。

<!--log4j日志依赖-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2.创建并配置log4j.properties,这里需要注意,这个名字是固定的,不能修改。Log4j会自动去找这个文件。(这里先复制进去,关于内容怎么写,后面会有专题讲解日志)

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

此时再次运行不仅输出了结果,还有日志:

D:\developer_tools\Java\jdk-8u221\bin\java "-javaagent:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\lib\idea_rt.jar=58726:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\bin" -Dfile.encoding=UTF-8 -classpath D:\developer_tools\Java\jdk-8u221\jre\lib\charsets.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\deploy.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\access-bridge-64.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\cldrdata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\dnsns.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jaccess.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jfxrt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\localedata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\nashorn.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunec.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunjce_provider.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunmscapi.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunpkcs11.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\zipfs.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\javaws.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jce.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfr.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfxswt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jsse.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\management-agent.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\plugin.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\resources.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\rt.jar;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\test-classes;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\classes;D:\developer_tools\Java\maven_repository_for3.5.4\log4j\log4j\1.2.17\log4j-1.2.17.jar;D:\developer_tools\Java\maven_repository_for3.5.4\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\developer_tools\Java\maven_repository_for3.5.4\mysql\mysql-connector-java\5.1.46\mysql-connector-java-5.1.46.jar com.qf.test.TestMyBatis
log4j:WARN No such property [conversionPattern] in org.apache.log4j.ConsoleAppender.
Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
Class not found: org.jboss.vfs.VFS
JBoss 6 VFS API is not available in this environment.
Class not found: org.jboss.vfs.VirtualFile
VFS implementation org.apache.ibatis.io.JBoss6VFS is not valid in this environment.
Using VFS adapter org.apache.ibatis.io.DefaultVFS
Find JAR URL: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity
Not a JAR: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity
Reader entry: User.class
Listing file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity
Find JAR URL: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity/User.class
Not a JAR: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity/User.class
Reader entry: ����   4 P
Checking to see if class com.qf.entity.User matches criteria [is assignable to Object]
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Fri Nov 27 10:45:49 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 1277009227.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4c1d9d4b]
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where id=? 
==> Parameters: 1(Integer)
<==      Total: 1
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where id=? 
==> Parameters: 2(Integer)
<==      Total: 1
User{id=1, username='shine', password='123', gender=true, registTime=Fri Nov 20 18:37:41 CST 2020}
User{id=2, username='shine2', password='456', gender=false, registTime=Wed Jul 01 17:38:13 CST 2015}

Process finished with exit code 0

 日志这里我就不分析了,其实慢慢的你就看懂了。现在学的少,可能只能看懂一点点。

七、MyBatis的CRUD操作【重点】

7.1 查询

标签:<select id="" resultType"">

7.1.1 序号参数绑定

 我们这里在UserDao接口中再写一个方法进行演示:

package com.qf.dao;
import com.qf.entity.User;
public interface UserDao {
    User queryUserById(Integer id);
    User queryUserByIdAndUsername(Integer id,String username);
}

现在我们在Mapper中写Sql语句,并关联这个方法。

<?xml version="1.0" encoding="GBK" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">
    <!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
    <!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
    <select id="queryUserById" resultType="User">
        select id,username,password,gender,regist_time as registTime
        from t_user
        where id=#{arg0}
    </select>

    <select id="queryUserByIdAndUsername" resultType="User">
        select id,username,password,gender,regist_time as registTime
        from t_user
        where id=#{arg0} and username=#{arg1}
    </select>
</mapper>

这里需要说明的是:首先方法返回值这里不是用全类名,而是采用别名的方式(采用第二种起别名的方式,别名就是类名);其次要注意SQL语句中药起别名,为了是MyBatis的同名原则拿到值;最后是where绑定动态参数时(就是方法传过来的参数),采用的是arg0和arg1,这里需要说明的是,这两个变量分别可以拿到方法形参的第一个值和第二个值。

测试方法:

package com.qf.test;
import com.qf.dao.UserDao;
import com.qf.entity.User;
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 java.io.IOException;
import java.io.InputStream;
public class TestMyBatis {
    public static void main(String[] args) throws IOException {
        // MyBatis API
        // 1. 加载配置文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 2. 构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //3. 通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 4.通过SqlSession获得DAO实现类的对象
        UserDao mapper = sqlSession.getMapper(UserDao.class); //获取UserDao对应的实现类的对象

        // 5. 测试查询方法
        User user = mapper.queryUserById(1);
        User user1 = mapper.queryUserById(2);
        User user2 = mapper.queryUserByIdAndUsername(1, "shine");

        System.out.println(user);
        System.out.println(user1);
        System.out.println(user2);
        
    }
}

 运行结果如下:

D:\developer_tools\Java\jdk-8u221\bin\java "-javaagent:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\lib\idea_rt.jar=59397:D:\developer_tools\Java\JetBrains\IntelliJ IDEA 2017.3.5\bin" -Dfile.encoding=UTF-8 -classpath D:\developer_tools\Java\jdk-8u221\jre\lib\charsets.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\deploy.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\access-bridge-64.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\cldrdata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\dnsns.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jaccess.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jfxrt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\localedata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\nashorn.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunec.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunjce_provider.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunmscapi.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunpkcs11.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\zipfs.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\javaws.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jce.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfr.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfxswt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jsse.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\management-agent.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\plugin.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\resources.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\rt.jar;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\test-classes;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomy-batis\target\classes;D:\developer_tools\Java\maven_repository_for3.5.4\log4j\log4j\1.2.17\log4j-1.2.17.jar;D:\developer_tools\Java\maven_repository_for3.5.4\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\developer_tools\Java\maven_repository_for3.5.4\mysql\mysql-connector-java\5.1.46\mysql-connector-java-5.1.46.jar com.qf.test.TestMyBatis
log4j:WARN No such property [conversionPattern] in org.apache.log4j.ConsoleAppender.
Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
Class not found: org.jboss.vfs.VFS
JBoss 6 VFS API is not available in this environment.
Class not found: org.jboss.vfs.VirtualFile
VFS implementation org.apache.ibatis.io.JBoss6VFS is not valid in this environment.
Using VFS adapter org.apache.ibatis.io.DefaultVFS
Find JAR URL: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity
Not a JAR: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity
Reader entry: User.class
Listing file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity
Find JAR URL: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity/User.class
Not a JAR: file:/D:/IDEAworkspace/02_java_advance/3.4-mybatis/hellomy-batis/target/classes/com/qf/entity/User.class
Reader entry: ����   4 P
Checking to see if class com.qf.entity.User matches criteria [is assignable to Object]
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Fri Nov 27 10:55:29 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 1277009227.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4c1d9d4b]
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where id=? 
==> Parameters: 1(Integer)
<==      Total: 1
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where id=? 
==> Parameters: 2(Integer)
<==      Total: 1
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where id=? and username=? 
==> Parameters: 1(Integer), shine(String)
<==      Total: 1
User{id=1, username='shine', password='123', gender=true, registTime=Fri Nov 20 18:37:41 CST 2020}
User{id=2, username='shine2', password='456', gender=false, registTime=Wed Jul 01 17:38:13 CST 2015}
User{id=1, username='shine', password='123', gender=true, registTime=Fri Nov 20 18:37:41 CST 2020}

Process finished with exit code 0

这个结果不出意外,没什么好说的。

我们这里再用另一种序号参数绑定。即param0和param1.这等同于arg0和arg1.这里修改Mapper文件即可。

<select id="queryUserByIdAndUsername" resultType="User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id=#{param0} and username=#{param1}
</select>

运行一下:呀,报错了,部分错误如下

Caused by: org.apache.ibatis.binding.BindingException: Parameter 'param0' not found. Available parameters are [arg1, arg0, param1, param2]

这个错误说明了,如果是用arg序号参数绑定,那么是从0开始的,如果用param序号绑定参数则是从1,开始的,也就是说arg0对于param1,arg1对应param2.

修改后:

<select id="queryUserByIdAndUsername" resultType="User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id=#{param1} and username=#{param2}
</select>

毫无意外,结果正常展示。

7.1.2 注解参数绑定【推荐】

 对于上面的序号参数绑定,不能说不好,但是呢?怎么说,可读性太差了。有没有别的方式呢?那就是注解参数绑定,非常简单。

UserDao中方法(注意,这里的@Param中的value可以是任意值,但是我们建议和形参同名)

User queryUserByIdAndPassword(@Param("id") Integer id,@Param("password") String password);

Mapper中的SQL语句:

<select id="queryUserByIdAndPassword" resultType="User" >
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id = #{id} and password=#{password}
</select>

测试结果:成功查询到(测试代码和结果略)

7.1.3 Map参数绑定

对于上面的方法形参是两个,我们可以用args或者param的 序号参数绑定,也可以采用@Param的注解参数绑定。但是呢?对于这么多参数,我们有时候更愿意把他们汇总起来,成为一个形参,比如这里用Map汇总,Map嘛,就是key-value对,value就是参数值,key就是给这个参数起个名字,可以任意命名,不过还是建议和形参名字本身相同。

Map汇总我们的多个参数,Map中的key是什么值,将决定我们的映射文件mapper是什么值。

UserDao方法:

User queryUserByIdAndPassword2(Map map);

Mapper文件中的SQL语句:

<select id="queryUserByIdAndPassword2" resultType="User" >
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id = #{id} and password=#{password}
</select>

这里的#{xx}中,xx的取值和你存放的map中的key有关,即可key保持一致。

测试代码:

System.out.println("--------------");
Map map = new HashMap();
map.put("id",2);
map.put("password",456);
User user4 = mapper.queryUserByIdAndPassword2(map);
System.out.println(user4);

运行结果:不再写了,也是运行成功了。不过这里需要注意的是:Dao方法中的参数Map map前面没有用注解@Param哦,注意不要搞混了。

7.1.4 对象参数绑定

 当我们在做零散参数汇总时,除了用这个Map外,其实还有一个更常见的方式,把这些参数放在一个实体类中。

UserDao中的方法:这里需要传入的是User对象,当然你也可能用其它对象封装这些参数,看实际情况,因为这里需要的是id和password,二User实体类中有这两个属性,因此可以封装。

User queryUserByIdAndPassword3(User user);

重点提示:这里的形参User user和方法返回值User是一样的吗?不一样,完全不一样。之所以这里看着是一样的,即巧合(因为正好可以用User这个实体类封装参数),可是因为我们命名规范。

Mapper映射文件:

<select id="queryUserByIdAndPassword3" resultType="User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where id = #{id} and password=#{password}
</select>

大家想一下,这里的#{xx},里面的xx是什么?其实这里的xx是User实体类的属性名。

重要提示:这里面的#{id}和#{password}和方法返回值User有关系吗?看着有关系,其实本质是完全没有关系的,这里的#{id}和#{password}仅和我们方法传入的封装多个形参的类User有关,而这个封装多个形参的类User和方法返回值并没有直接关系。

测试代码:

System.out.println("--------------");
User user5 = new User(1, null, "123", null, null);
User user6 = mapper.queryUserByIdAndPassword3(user5);
System.out.println(user6);

运行结果:一样没有问题。

--------------
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where id = ? and password=? 
==> Parameters: 1(Integer), 123(String)
<==      Total: 1
User{id=1, username='shine', password='123', gender=true, registTime=Fri Nov 20 18:37:41 CST 2020}

 对于上面提到的这始终方式:

 我们怎么选择呢?如果只有一个参数,我们经常用@Param注解的方式,如果是多个参数,我们经常是用第四种,一个对象包装。

7.1.5 模糊查询

 我们现在想用模糊查询:

UserDao的方法:(这里需要说明一下,String username参数是我们马虎查询需要的字段,其实不一定要用username命名,准确的来说应该是username在数据库中存放数据的一部分字母。比如数据库中的名字有shine,shine1,那么我们模糊查询可能是username包含字母shin的。当然这个不说大家也是知道的);此外,这里用了注解参数绑定;还有模糊查询的结果是List即可,这个很正常吧,你查询到的很可能不是一条。

List<User> queryUserByUsername(@Param("username") String username);

Mapper映射文件中的SQL,虽然接口中的方法是一个List即可,其泛型是User,但是我们这里的reultType并没有用List<User>,而还是用的User。这里需要注意一下,虽然我们的结果是一个即可,即SQL语句查出来的多条数据,但是我们这里还是将这多条数据,一条条的封装成一个User对象。

<select id="queryUserByUsername" resultType="User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where username like %#{username}%
</select>

模糊查询大家都是知道的,比如像查username字段包含shin字段的,那么weher怎么写呢?where username like %shine%即可。由于这里是动态参数,一次是%#{username}%。

测试:我这里的输出即可用的是jdk1.8的Stream流哈,大家不会刻意换成正常的foreach。

System.out.println("************");
List<User> users = mapper.queryUserByUsername("shine");
users.stream().forEach(System.out::println);

 

结果:

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '%'shine'%' at line 3
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)

报错,这个错误是什么意思呢?不易仓了,我直接说结果吧,Mapper文件里的模糊查询,不能直接用%#{username}%,需要用函数将这三个字符拼接起来,此时Mapper文件中的SQL为:

<select id="queryUserByUsername" resultType="User">
    select id,username,password,gender,regist_time as registTime
    from t_user
    where username like concat('%',#{username},'%')
</select>

此时的运行结果就是正确的:

************
==>  Preparing: select id,username,password,gender,regist_time as registTime from t_user where username like concat('%',?,'%') 
==> Parameters: shine(String)
<==      Total: 2
User{id=1, username='shine', password='123', gender=true, registTime=Fri Nov 20 18:37:41 CST 2020}
User{id=2, username='shine2', password='456', gender=false, registTime=Wed Jul 01 17:38:13 CST 2015}

这里讲解模糊查询有三个目的:

1.怎么使用模糊查询

2.我们注意到这个模糊查询返回的是一个List集合,但是SQL的返回结果依然是User,至于为什么。SQL查询出来的是一条条数据,这个每一条依然是一个User对象。这算是解释吧,但是我想说的是,这不是MyBatis开发成这样吗?这有什么好为什么的?你可以自己开发个ORM框架,如果返回结果是List,你就用List承接,至于一条条数据,你写在泛型里面。当然我决定MyBatis没有这么做,可能是JDBC的原因吧。不过这是我猜的,JDBC早忘了。

3.这里就要强调拼接了,你不能直接写在一起,要用concat函数拼接模糊查询的字段,此外,这个concat是SQL函数,并非是MyBatis的函数。

7.2 删除

 下面我们就来讲增删改了,这三个很简单。原因有二:一是增删改本身没有太多的细节;二是在讲解查询时,关于参数的绑定都已经讲过了,这个在增删改里面同样是适用的。

UserDao接口中的方法:

void deleteUser(@Param("id") Integer id);

Mapper中的SQL语句:我们这里注意到,在<delete>标签中没有resultType属性。其实resultType属性只有在查询中有,而在增删改里面是没有的。这里又一个属性是parameterType属性,看名字就知道是参数类型,但是我其实很疑惑,原本查询的时候也有参数啊,怎么没有这个属性。后来经过百度才知道,其实这个属性增删改查都是不需要写的。其实参数的匹配不是通过类型,而是通过序号(args,param)、@Param、Map的key或者包装类的属性名。这里为什么写,可能是老师比较无聊吧。

<delete id="deleteUser" parameterType="int">
    delete from t_user
    where id = #{id}
</delete>

测试方法:

mapper.deleteUser(1);

运行结果:没有报任何错误。

但是我们看一下数据库,发现这条数据竟然还没有删除,这是为什么吗?不合理呀,我们已经删除了。这里就要提到一个查询锁不具备的为题,就是事务控制。MyBatis明确要求增删改需要事务控制,而查询不需要

MyBatis的控制事务只有两个方法,简单吧,一个就是开启事务,一个就是事务回滚。在学这两个方法前,先不要慌,让我们看看前面就出现的日志,我们一次还没分析过呢?

Opening JDBC Connection
Fri Nov 27 14:22:59 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 247944893.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@ec756bd]
==>  Preparing: delete from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Updates: 1

这个意思很明显,就是事务是关闭的,设置自动提交为false。这就是上面我们没有删除成功的原因,虽然程序执行没有问题,但是没有提交。

mapper.deleteUser(1);
sqlSession.commit();

这次再次刷新数据库,就发现删除成功了。

我们看下日志:

Fri Nov 27 14:40:25 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 247944893.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@ec756bd]
==>  Preparing: delete from t_user where id = ? 
==> Parameters: 1(Integer)
<==    Updates: 1
Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@ec756bd]

 

开始是MyBatis默认事务是关闭的,但是在执行了这个删除之后,事务提交了。因此也就删除成功了。

我们刚刚说过,关于事务,是有两个方法的,那另外一个是什么呢?就就是事务回滚。

mapper.deleteUser(2);
sqlSession.rollback();

刷新数据库,发现此次数据没有被删除,这是符合我们的预期的,因为事务回滚了。

再看下日志信息:

Fri Nov 27 14:43:16 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 247944893.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@ec756bd]
==>  Preparing: delete from t_user where id = ? 
==> Parameters: 2(Integer)
<==    Updates: 1
Rolling back JDBC Connection [com.mysql.jdbc.JDBC4Connection@ec756bd]

发现这里提到了日志回滚。

关于事务:在MyBatis中,增删改必须手动控制事务,而查不需要,因为他只是查询,没有对数据库做任何修改变动。事务有两个方法,一个就是提交commit,一个就是回滚rollback。

此外,增删改没有resultType属性,即没有返回值属性;而有了paramterType,但是是不需要的(增删改查都不需要)。

7.3 修改

 UserDao接口的方法:我们看到这里的形参里面没有@Param注解哈,要明白为什么吗?这是不同的方式。还有这里的User只是对参数的封装,这个前面已经讲过了。

void updateUser(User user);

Mapper中用SQL绑定这个方法:

<update id="updateUser" parameterType="User">
    update t_user
    set username=#{username},password=#{password},gender=#{gender},regist_time=#{registTime}
    where id=#{id}
</update>

这里的paramterType不需要写,但是写了的话,注意这里不是全类名,是因为其和resultType一样,已经在MyBatis核心配置文件中起了别名。

测试:注意这里要提交事务。这里需要说明的是,我们这里的gender用true和false,在数据库中自动会转化成1和0.数据库中tinyint对应java类中的boolean。

mapper.updateUser(new User(2,"shine_002","99999",true,new Date()));
sqlSession.commit();

查看数据库发现数据已经更新了。

7.4 添加

UserDao接口中的方法:

void insertUser(User user);

Mapper中的SQL语句:

<insert id="insertUser" parameterType="User">
    insert into t_user values(#{id},#{username},#{password},#{gender},#{registTime})
</insert>

测试:这里是id给的是null,是因为我们数据库设置的是自增,因此给null即可。有人可能想说没有这个字段行不,不行,因为你SQL语句中有这个字段,那有人说如果SQL中没有这个字段行不行,我只能告诉你,行。

mapper.insertUser(new User(null,"shine_000","00000",false,new Date()));
sqlSession.commit();

结果:数据库多了一条数据,现在是id=2和id=3这两条数据。

下面来做几个测试:

1.所有地方都不变,Mapper中的SQL的id部分,把id=#{id}修改为null。那么能正常添加进去吗?答案是能,我们这里修改并执行以下。

此时数据库为:

 

 2.如果我们手动指定id,即在测试中指定id为10000,当然此时Mapper中还要改回#{id},能执行吗?当然能!运行结果是:

 

 我们实际开发中是怎么处理主键的??

我们mapper中的SQL还是要正常的用#{id}取值,只是主键是自己给,还是用默认值,是我们测试时,或者调用是确定的。如果是null,就是默认自增,如果设计值,就传入确定值。

此外,我们还有其它补充知识:

1.我们上面的增删改返回值都是void,而查询是有结果的,比如User或者List<User>,但是呢?其实我们增删改也是有值的,结果是Integer,代表的是增删改的行数。我们这里修改一下。当然要注意哦,虽然你在UserDao方法中写了Integer类型的返回值,在Mapper中依然不能有resultType的属性哦。这里我们只需要修改Dao方法返回值,不需要修改映射文件(什么都不需要改),只是调用时可以获得这个结果。

Dao接口中增删改返回值:

Integer deleteUser(@Param("id") Integer id);

Integer updateUser(User user);

Integer insertUser(User user);

Mapper不用改,运行结果已插入为例:

Integer insertCounter = mapper.insertUser(new User(null, "shine_000", "00000", false, new Date()));
System.out.println("插入了"+insertCounter+"条数据");
sqlSession.commit();

毫无悬念,运行成功。控制台显示插入一条数据:

Created connection 384294141.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16e7dcfd]
==>  Preparing: insert into t_user values(?,?,?,?,?) 
==> Parameters: null, shine_000(String), 00000(String), false(Boolean), 2020-11-27 15:37:12.353(Timestamp)
<==    Updates: 1
插入了1条数据
Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16e7dcfd]

 

增删改三个都差不多,没什么好说的。就是增稍微有两点知识:其中1就是主键的处理,SQL中还是用#{id}取值,主键到底是自增还是确定看你的调用;2是如果你插入的某一条数据不想传值,可以用null,这个不单是对主键,其它比如username也是可以的,就是插入数据库里面也没有而已。

 7.5 主键回填

标签:<selectKey id="" parameterType="" order="AFTER|BEFFORE">

主键回填,这是啥,第一次看到的人都说很懵逼!学会的人都说这个必须有,很有用!我来给你解释一下哈。当然对于主键回填的理解要我们实操后才会真正明白。不过我先小小解释一番。

我们上面插入过数据,也重点说过对于主键的处理。在Mapper映射文件的SQL中,采用取值的方式,而在调用(测试)时有两种方式,一种是设置确定的id值,一种是设置为null。这里需要强调的是,之所以能够设置null,是因为我们数据库里面的数据是int型,切为自增的,如果你不填,他会自动生成。

现在有什么业务需求呢?就是你现在插入一个用户,没有主键,即主键自增。但是呢?你插入这条数据后,你还想得到他的id值,怎么得到??有人可能想,这简单,我可以再查询一次(不通过主键查,因为你不知道主键,可以通过名字查),但如果username是重复的呢?有人可能很疑惑,为什么yo组合杨的绣球,插入就插入嘛,你还跟我要啥主键。那我再举个例子,如果现在是一个订单表,你插入一条数据,没有设置主键(设置null,数据库采用自增策略),但是呢?插入后你想给用户返回插入到数据库的这条数据的主键是什么,怎么办?这很常见吧,你要展示啊。

先不慌,我们先看下面的例子:

User user = new User(null, "shine_000", "00000", false, new Date());
Integer insertCounter = mapper.insertUser(user);
System.out.println(user.getId());
sqlSession.commit();

此时我们插入数据库一条数据,这条数据的主键是null,当我们查询数据库时,发现里面插入了一条数据,并且根据主键自增设计,起主键是1002.我们在测试程序中打印这条插入的数据的id,控制台显示null,这很正常,因为本来这个对象就没有id数据:

Fri Nov 27 16:38:47 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 384294141.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16e7dcfd]
==>  Preparing: insert into t_user values(?,?,?,?,?) 
==> Parameters: null, shine_000(String), 00000(String), false(Boolean), 2020-11-27 16:38:47.034(Timestamp)
<==    Updates: 1
null
Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16e7dcfd]

 

 

重点来了,如果此时我们用什么方法,能够给这个传入的对象的id设置id值(这个id值来自数据库),那么这就是主键回填。想必此时你已经大致知道什么是主键回填了。如果不懂,请继续看。

7.5.1 通过last_insert_id()查询主键

此时我们修改下Mapper映射文件关于这条插入的SQL语句。增加如下内容:

<insert id="insertUser" parameterType="User">
    <selectKey resultType="int" keyProperty="id" order="AFTER">
        select last_insert_id();
    </selectKey>
    insert into t_user values(#{id},#{username},#{password},#{gender},#{registTime})
</insert>

增加了之后,你很懵逼,哎,怎么这么多懵逼。不慌,我们先运行一下此时的测试代码,再来解释:

Fri Nov 27 16:45:33 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 990355670.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3b07a0d6]
==>  Preparing: insert into t_user values(?,?,?,?,?) 
==> Parameters: null, shine_000(String), 00000(String), false(Boolean), 2020-11-27 16:45:33.805(Timestamp)
<==    Updates: 1
==>  Preparing: select last_insert_id(); 
==> Parameters: 
<==      Total: 1
10003
Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3b07a0d6]

此时插入的user本来没有id值,但是现在有了,而且是10003,和数据库中的是一致的。

现在我们来讲解一下Mapper文件中新增的内容,然后再解释下为什么现在就插入的数据就有了id值(主键回填了,这名字真贴切)。

<selectKey resultType="int" keyProperty="id" order="AFTER">
    select last_insert_id();
</selectKey>

1.select就是查询的意思,select+key就是查询主键的意思,即查询数据库中某条数据的主键值;

2.里面的三条属性先不讲,先说SQL语句,如果你没有忘记SQL语句,则对这个方法应该是知道的,last_insert_id()是查询数据库中最新一次插入数据的主键(这个主键必须是自增的,且是int类型,当然其实我不知道有没有这条要求)。

3.关于属性resultType是这条查询语句的结果类型,显然这里是int类型。怎么说呢?主键一般也就两种类型,一种就是int,一种是String。

 

4.keyProperty是什么?他是我们插入对象中需要回填的那个属性字段。不知道理解了没,我们从数据库得到最新插入的id嘛,这个id是干嘛的,是回填的,回填到哪里?回填到我们插入的这个对象里(因为我们这个对象开始没有设置id,采用自增策略)。我们我们回填到这个对象的那个属性?就是对应主键的这个属性。这里的keyProperty就是写这个属性。

5.order是什么?order有两个取值,一个是AFTER,一个是BEFORE。这里是AFTER,以上是我们查询主键这个方法要在执行<insert>语句之后执行,那是自然了,因为只有插入了数据,数据库生成了主键后,你才能查询到这个主键。这里你可能还是不理解AFTER的含义,没事,下面会有BEFORE的例子,到时候你就理解了。

这里说明一下最近写项目用到了这个主键回填,上面的sql添加数据有id,其实也可以不用写id,实际项目也不会加这个字段。当然加了也不错。(这是给大家的提示,怕大家误解,是不是用主键回填,必须在添加时有这个id字段)

7.5.2 通过uuid()查询主键

我们上面的主键在数据库中设置的是int类型,且自增。这样我们可以在insert插入数据后,用selectkey标签(last_insert_id())得到主键,进而紫宗主键回填。

但是实际中,我们的主键不一定是int类型。有人会想?啊,那你不会都设置成int类型?设置成其他类型比如varchar类型(数据库是varchar,java对象中叫String)字符串类型干嘛??我们且看下面的例子:

如果在分布式架构中,有一个订单数据表内容非常多,我们不得已将其放在两个数据库中,这两个数据库放在两台服务器中。此时我们可以将主键设置为int吗?不可以!为什么?因为如果只有一台机器,我们设置int类型,并且自增,自然是不会重复的。但是现在是天台机器,这两台机器的int类型是很有可能重复的。那怎么办?设置成字符串类型。设置成字符串类型就可以了吗?当然不是这么简单。

我们这里要讲一个概念,UUID,在数据库中SQL语句里面存着这样一个函数uuid(),我们用数据库执行select uuid();这条语句,其运行结果如下:

 

 我们发现这个结果是又32个字母组成的随机数(其实说32不准确,如果包含4个-的话,就是36了)。这个随机数是全球唯一的。他会根据时间和你创建的地点而创建。总之能够保证全球独一个(其实我是不信的,我百度了一下,基本说的都是有可能相同,只是概率很小,我想也是)。不过我们就当做是全球唯一的就好了。

好,现在我们设计一个数据表,里面的主键是varchar(32)类型,在设置一个实体类与其对应,再设置Dao和mapper进行查询(当然mapper里面的SQL要进行主键回填)。

1.数据表设计:注意这里的主键已经是字符串类型了(在数据库是varchar类型,为什么里面是32,因为uuid是32位)

CREATE TABLE t_student(
  id VARCHAR(32) PRIMARY KEY,
  NAME VARCHAR(50),
  gender TINYINT
)DEFAULT CHARSET = utf8;

2.设置对应的实体类。ORM就是Object和relational(关系型数据表)之间的映射:这里的gender设置为boolean类型,我们要知道为什么。

package com.qf.entity
public
class Student { private String id; private String name; private boolean gender;
//下面的无参、空参、getter和setter、toString省略 }

3.StudentDao接口设计,里面只有插入一个方法:这里我们要清楚,插入的返回值是Integer,意思是影响了几条数据,即插入了几条数据,但是在Mapper中依然是没有resultType这个属性的;其次这里的方法参数是对传入形参的包装,即本身是几个形参,现在包装到一个类中了。

package com.qf.dao;
import com.qf.entity.Student;
public interface StudentDao {
    Integer insertStudent(Student student);
}

4.Mapper映射文件:

<?xml version="1.0" encoding="GBK" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.StudentDao">
    <insert id="insertStudent" parameterType="Student">
        <selectKey resultType="String" keyProperty="id" order="BEFORE">
            select uuid();
        </selectKey>
        insert into t_student values(#{id},#{gender},#{name})
    </insert>
</mapper>

此时我们看到这里的Mapper映射文件,在插入语句中有这个主键回填的设置,而且这里的主键并非是整型自增,而是字符串类型。下面我们看一下主键回填的标签及属性:

1.<selectKey>这个就是主键回填的标签,根据标签字面意思得知其于主键有关;

2.这里的SQL语句依然是SQL的函数,获得一个长度为32位的唯一标识字符串(带-是36位)。

3.resultType标签是指这个函数的返回结果是String类型,注意这里的类型以Java对象判断,如果是在数据库里面则是Varchar。

4.keyProperty是说该结果在对应的属性中的属性名。

5.order是说该函数在插入语句之前执行,这很好理解,因为我们要在SQL执行前,拥有主键。(而前面的用AFTER是因为数据库会自增设置主键)

5.核心配置文件修改,我们每次加入一个Mapper文件,都要在核心配置文件中注册:

 

<mapper resource="com/qf/dao/StudentDaoMapper.xml" />

6.测试

Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'id' at row 1

竟然报错:错误原因是:id太长,这是因为生成的uuid是包括无效的'-'这个符号的,我们需要替换一下。我们数据库中设计的varchar是32位,如果uuid包括'-'则是36位(32+4)。

7.再次修改Mapper映射文件为:

<?xml version="1.0" encoding="GBK" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.StudentDao">
    <insert id="insertStudent" parameterType="Student">
        <selectKey resultType="string" keyProperty="id" order="BEFORE">
            select replace(uuid(),'-','');
        </selectKey>
        insert into t_student values(#{id},#{name},#{gender})
    </insert>
</mapper>

这里修改运用了SQL的函数。将生成的uuid中的“-”替代掉。

查看控制台和数据库:

Mon Nov 30 09:48:00 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 690521419.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2928854b]
==>  Preparing: select replace(uuid(),'-',''); 
==> Parameters: 
<==      Total: 1
==>  Preparing: insert into t_student values(?,?,?) 
==> Parameters: 1551e8e532ae11eba8c4842afd16a15d(String), shine_001(String), false(Boolean)
<==    Updates: 1
Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@2928854b]
Student{id='1551e8e532ae11eba8c4842afd16a15d', name='shine_001', gender=false}

数据库:

 

 八、MyBatis工具类【重点】

8.1 封装工具类

封装工具类具体代码细节这里不扣可以自行百度下怎么写工具类:

创建util包,创建工具类

package com.qf.util;

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 java.io.IOException;
import java.io.InputStream;

/*
* 1.加载配置
* 2.创建SqlSessionFactory
* 3.创建Session
* 4.事务管理
* 5. Mapper获取
* */
public class MyBatisUtil {
    private static SqlSessionFactory sqlSessionFactory;
    // 创建ThreadLocal绑定当前线程中的SqlSession对象
    private static final ThreadLocal<SqlSession> t1 = new ThreadLocal<SqlSession>();
    static { //加载配置信息,并构建session工厂
        // 1.加载配置文件
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSession openSession() {
        SqlSession sqlSession = t1.get();
        if (sqlSession == null) {
            sqlSession = sqlSessionFactory.openSession();
            t1.set(sqlSession);
        }
        return sqlSession;
    }

    public static void closeSession() {
        SqlSession sqlSession = t1.get();
        sqlSession.close();
    }

    public static void commit() {
        SqlSession sqlSession = openSession();
        sqlSession.commit();
        closeSession();
    }

    public static void rollback() {
        SqlSession sqlSession = openSession();
        sqlSession.rollback();
        closeSession();
    }

    public static <T> T getMapper(Class<T> mapper) {
        SqlSession sqlSession = openSession();
        return sqlSession.getMapper(mapper);
    }
}

 

8.2 测试工具类

package com.qf.test;
import com.qf.dao.StudentDao;
import com.qf.dao.UserDao;
import com.qf.entity.Student;
import com.qf.entity.User;
import com.qf.util.MyBatisUtil;
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 java.io.IOException;
import java.io.InputStream;

public class TestMyBatis {
    public static void main(String[] args) throws IOException {
        // Student
        StudentDao studentMapper = MyBatisUtil.getMapper(StudentDao.class);
        Student student = new Student(null, "test_util", true);
        studentMapper.insertStudent(student);
        MyBatisUtil.commit();
    }

    public void test1() throws IOException {
        // MyBatis API
        // 1. 加载配置文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 2. 构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //3. 通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 4.通过SqlSession获得DAO实现类的对象
        StudentDao mapper = sqlSession.getMapper(StudentDao.class);

        Student student = new Student(null, "shine_001", false);
        mapper.insertStudent(student);

        sqlSession.commit();

        System.out.println(student);
    }
}

查看数据库,插入成功,里面有两条数据。

九、ORM映射【重点】

9.1 MyBatis自动ORM失效

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

 

 其实上面我们遇到过这个问题,对于上面的t_user表,库中的字段有regist_time,而类中的属性对应的是registTime。此时用MyBatis的同名规则(默认就是这个),会映射失败,上面我们采用的方式是SQL语句中起别名。

9.2 方案一:列的别名

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

这个上面我们用过,就是给regist_time起别名为属性名(registTime)

9.3 方案二:结果映射 (ResultMap - 查询结果的封装规则)

通过<resultMap id="" type="">映射,匹配列名与属性名。

 

 我们也在自己的代码中实际运用一下,将当初的代码改一下。这里需要说明一下,这里的type属性相当远当初的resultType属性,而id是给该映射取个别名,方便引用。

<?xml version="1.0" encoding="GBK" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace = 所需实现的借口全限定名-->
<mapper namespace="com.qf.dao.UserDao">

    <resultMap id="user_resultMap" type="User" >
        <!--定义更复杂的 映射规则-->
        <id column="id" property="id"></id>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>
        <result column="gender" property="gender"></result>
        <result column="regist_time" property="registTime"></result>
    </resultMap>

    <!--id = 所需重写的借口抽象方法 resultType = 查询后所需返回的对象类型-->
    <!--由于这里是根据id查询,方法参数就是id,我们在sql语句中药动态获取,因此用#{arg0},其中arg0代表方法参数的第一个参数-->
    <!--<select id="queryUserById" resultType="User">
        select id,username,password,gender,regist_time as registTime
        from t_user
        where id=#{arg0}
    </select>-->

    <select id="queryUserById" resultMap="user_resultMap">
        select id,username,password,gender,regist_time
        from t_user
        where id=#{arg0}
    </select>
</mapper>

测试:

public static void main(String[] args) throws IOException {
    UserDao mapper = MyBatisUtil.getMapper(UserDao.class);
    User user = mapper.queryUserById(2);
    System.out.println(user);
}

发现查询成功。大家可能会说,那我永远彩云方案一,就可以了,不用管方案二。其实并非这样,方案二还有其他更加复杂的映射,下一节我们会讲到。

十、MyBatis处理关联关系-多表链接【重点】

我们以前在讲MySQL时说过,表之间的关系有一对一,一对多(也可以叫多对一),多对的。那么我们设计的相应实体之间的关系也就是这三种。

实体键的关系:关联关系(拥有has,属于belong)

  • OneToOne:一对一关系(Passenger-Passport),一个乘客拥有一张护照,一张护照从属于一个乘客;
  • OneToMany:一对多关系(Employee-Departmnet),一个员工从属于一个部门,一个部门有许多员工。
  • ManyTony:多对多关系(Student--Subject),一个学生有许多门课,一个课程有许多学生。

Table建立外键关系

 

 

 Entitu添加关系属性

 

 Mapper中将属性与列名对应

 

10.1 OneToOne

sq语句,这里建了两个表,分别是旅客和护照,是一对一关系,这个表没什么特别说明的。就两个点,一个是外键,一个是护照表中的外键要唯一(即外键的值来自于主键,且只能有一个)。试想,如果不是唯一,也就是一个护照可以被两个旅客拥有。

CREATE TABLE t_passengers(
  id INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(50),
  sex VARCHAR(1),
  birthday DATE
)DEFAULT CHARSET = utf8;

CREATE TABLE t_passports (
  id INT PRIMARY KEY AUTO_INCREMENT,
  nationality VARCHAR(50),
  expire DATE,
  passenger_id INT UNIQUE,
  FOREIGN KEY (passenger_id) REFERENCES t_passengers(id)
)DEFAULT CHARSET = utf8;

INSERT INTO t_passengers VALUES(NULL,'shine_01','f','2018-11-11');
INSERT INTO t_passengers VALUES(NULL,'shine_02','m','2019-12-12');

INSERT INTO t_passports VALUES(NULL,'China','2030-12-12',1);
INSERT INTO t_passports VALUES(NULL,'America','2035-12-12',2);

实体类:我们看下数据库中的表,t_passengers,这个表有几个字段呢?4个,因此要建一个实体类Passengers,里面拥有四个字段;下面是t_passports这个表有几个字段呢?4个字段,因此要建一个实体类Passports,里面有4个属性(这是错误的),我们这里要对这个从表说明一下,这里虽然有四个字段,但是实体类只需要有三个属性即可,即关联的从键passenger_id不需要,至于为什么不需要,你先记着,其实写到后面你就有体会了,其实有这个字段是多余的。

实体类经过上面的分析就可以了吗?当然不是,如果仅仅是上面的Passengers里面4个字段,Passports里面有3个字段。那怎么体现一对一的关系呢?其实上面的这两个实体类中的7个字段(4+3)分别为该实体类的基本属性。这两个实体类分别还各有一个关系属性。比如Passenger实体类,里面有个关系属性的字段Passport passport,用于存储该旅客的护照信息;同理,Passport实体类里面拥有一个关系属性的字段Passenger passenger,用于存储旅客信息。

Passenger实体类

package com.qf.entity;
import java.util.Date;
public class Passenger
{
    // 基本属性
    private Integer id;
    private String name;
    private Boolean sex;
    private Date birthday;

    // 存储旅客的护照信息 : 关系属性
    private Passport passport;
  // 下面是无参、全参;getter和getter;toString方法
  // 对于passport只需要get方法即可。该get方法是方便后面测试打印
}

Passport实体类

package com.qf.entity;
import java.util.Date;
public class Passport
{
    // 基本属性
    private Integer id;
    private String nationality;
    private Date expire;

    // 存储旅客信息 : 关系属性
    private Passenger passenger;
  // 下面是无参、全参;getter和getter;toString方法
  // 对于passport只需要get方法即可。
该get方法是方便后面测试打印
}

PassengerDAO接口:这个接口没什么说的,就是通过id查询Passenger实体类。但是你要知道这个实体类里面是有一个关系属性Passport的,因此后面的SQL语句就要用到联接,这样则会从数据库中查询到7个属性(不是8个,因为t_passport表中的外键我们不需要查询出来,后面你能体会到查出来也是多余的)。只是重点是查询出来的7个字段值,如何封装在这5个属性(4个基本属性+1个关系属性)的Passenger实体类中。

package com.qf.dao;
import com.qf.entity.Passenger;
public interface PassengerDAO
{
    // 通过旅客的id,查询旅客信息及该旅客拥有的护照信息  称为关联查询  由称为级联查询
    Passenger queryPassengerById(Integer id);
}

 

Mapper映射文件

 首先我们在SQLyog里面用SQL语句查询一下,这一步是为了获得正确的SQL语句,因为这个语句毕竟有点复杂嘛!

这里的SQL语句要解释一下:首先这里是查询七个字段,前面已经说过为什么了;其次每个字段前面都有表明,这是因为两个表中都有id属性,如果没有表名无法区分,当然你可以只在两个id属性中提供表名,其它地方不写;然后我们在t_passports.id属性起别名为passId(省略了AS),这是因为如果不起别名,查询结果有两个id,无法区分。

SELECT t_passengers.id,t_passengers.name,t_passengers.sex,t_passengers.birthday,
t_passports.id passId,t_passports.nationality,t_passports.expire
FROM t_passengers JOIN t_passports
ON t_passengers.id = t_passports.passenger_id
WHERE t_passengers.id =1;

运行结果是:

 

Mapper文件版本1

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.PassengerDAO">
    <!-- 查询旅客及其护照 -->
    <select id="queryPassengerById" resultType="Passenger">
        SELECT t_passengers.id,t_passengers.name,t_passengers.sex,t_passengers.birthday,
        t_passports.id passId,t_passports.nationality,t_passports.expire
        FROM t_passengers JOIN t_passports
        ON t_passengers.id = t_passports.passenger_id
        where t_passengers.id = # {id}
    </select>
</mapper>

这个Mapper非常简单,我们以前就学过,只是一个查询语句,里面是SQL代码。但是这样写就可以了吗?我们注意到resultType是Passenger,代表的是接口中方法queryPassengerById的结果,这个方法也确实是这个结果。然后会根据同名的方法(SQL语句查询出来的结果的名字和实体类的属性名)进行匹配。显然是不可能同名的,因为查询出来是七个字段,而这个属性是5个字段(4+1)。怎么办呢?这时就要用到上节用到的resultMap了。

 Mapper文件版本2

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.PassengerDAO">
    <resultMap id="passenger_passport" type="Passenger">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="sex" property="sex"></result>
        <result column="birthday" property="birthday"></result>

        <!-- 描述passId nationlity expire和passport映射规则 -->
        <association property="passport" javaType="Passport">
            <id column="passId" property="id"></id>
            <result column="nationality" property="nationality"></result>
            <result column="expire" property="expire"></result>
        </association>
    </resultMap>

    <!-- 查询旅客及其护照 -->
    <select id="queryPassengerById" resultMap="passenger_passport">
        SELECT t_passengers.id,t_passengers.name,t_passengers.sex,t_passengers.birthday,
        t_passports.id passId,t_passports.nationality,t_passports.expire
        FROM t_passengers JOIN t_passports
        ON t_passengers.id = t_passports.passenger_id
        where t_passengers.id = #{id}
    </select>
</mapper>

 

将上述Mapper文件注册到MyBatis的核心配置文件中

<mappers>
    <!--注册Mapper文件的所在位置-->
    <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
    <mapper resource="com/qf/dao/UserDaoMapper.xml"/>
    <mapper resource="com/qf/dao/StudentDaoMapper.xml" />
    <mapper resource="com/qf/dao/PassengerDaoMapper.xml" />
</mappers>

 

测试 

public static void main(String[] args) throws IOException {
    PassengerDAO mapper = MyBatisUtil.getMapper(PassengerDAO.class);
    Passenger passenger = mapper.queryPassengerById(1);
    System.out.println(passenger.getPassport());
    System.out.println(passenger);
}

 

-----------------------------来一条分割线吧!!--------------------------

这就完了吗?确实是完了,但是呢?为了熟练,我们再翻过来查询一下。即根据ID查询护照信息,里面包含该护照所属的旅客。(这种查询被称为关联查询,或级联查询)

数据库已经准备妥当;

Dao接口设计:

package com.qf.dao;
import com.qf.entity.Passport;
import org.apache.ibatis.annotations.Param;
public interface PassportDAO {
    // 通过护照的id,查询护照信息及该护照所属的旅客信息  称为关联查询  由称为级联查询
    Passport queryPassportById(@Param("id") Integer id);
}

 

Mapper文件设计:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.PassportDAO">

    <resultMap id="passport_passenger" type="passport">
        <id column="id" property="id"></id>
        <result column="nationality" property="nationality"></result>
        <result column="expire" property="expire"></result>

        <association property="passenger" javaType="Passenger">
            <id column="passId" property="id"></id>
            <result column="name" property="name"></result>
            <result column="sex" property="sex"></result>
            <result column="birthday" property="birthday"></result>
        </association>
    </resultMap>

    <select id="queryPassportById" resultMap="passport_passenger">
        select t_passports.id,nationality,expire,t_passengers.id passId,name,sex,birthday
        from t_passports join t_passengers
        on passenger_id = t_passengers.id
        where t_passports.id = #{id}
    </select>
</mapper> 

 

注册到MyBatis核心配置文件当中:其实这里细心的人会发现一个小小的细节,就是我这里的xml文件全类名中的Dao字样和原xml文件大小写不一致,这也说明了他们之间注册时不区分大小写。但是强烈建议一致。

<mappers>
    <!--注册Mapper文件的所在位置-->
    <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
    <mapper resource="com/qf/dao/UserDaoMapper.xml"/>
    <mapper resource="com/qf/dao/StudentDaoMapper.xml" />
    <mapper resource="com/qf/dao/PassengerDaoMapper.xml" />
    <mapper resource="com/qf/dao/PassportDaoMapper.xml" />
</mappers>

 

测试:

public static void main(String[] args) throws IOException {
    PassportDAO mapper = MyBatisUtil.getMapper(PassportDAO.class);
    Passport passport = mapper.queryPassportById(1);
    System.out.println(passport.getPassenger());
    System.out.println(passport);
}

 

结果:

Tue Dec 01 19:15:00 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 580718781.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@229d10bd]
==>  Preparing: select t_passports.id,nationality,expire,t_passengers.id passId,name,sex,birthday from t_passports join t_passengers on passenger_id = t_passengers.id where t_passports.id = ? 
==> Parameters: 1(Integer)
<==      Total: 1
Passenger{id=1, name='shine_01', sex=false, birthday=Sun Nov 11 00:00:00 CST 2018}
Passport{id=1, nationality='China', expire=Thu Dec 12 00:00:00 CST 2030}

 

10.2 OneToMany

 一对多关系,我们这里用员工-部门举例:一个员工属于一个部门,一个部门可以拥有多个员工。这里的一对应的是部门,多对应的是员工。因此我们叫一对多或多对一关系。

数据库:这里的数据库,是两个表,其中外键在员工表中,其它字段分别就是描述员工和部门各自信息的字段。部门表里面是3个字段,员工表里面是4个字段,其中一个是外键。我们在部门里面插入了两条数据,分别是"教育部"和"研发部";而在员工表中插入四条数据,其中"shine01","shine02","张三","李四"。其中前两个属于教育部,后两个属于研发部。

CREATE TABLE t_departments (
  id INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(50),
  location VARCHAR(50)
 )DEFAULT CHARSET = utf8;
 
 CREATE TABLE t_employees(
  id INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(50),
  salary DOUBLE,
  dept_id INT,
  FOREIGN KEY (dept_id) REFERENCES t_departments(id) 
 )DEFAULT CHARSET = utf8;
 
INSERT INTO t_departments VALUES(1,"教学部","北京"),(2,"研发部","上海");
INSERT INTO t_employees VALUES(1,"shine01",10000.5,1),(2,"shine02",2000.5,1),
                (3,"张三",9000.5,2),(4,"李四",8000.5,2);
                

 

对应实体类:

Department

package com.qf.entity;
public class Department {
    private Integer id;
    private String name;
    private String location;
  //下面是有参、无参构造;getter和setter方法;toString方法 }

 

Employee

package com.qf.entity;
public class Employee {
    private Integer id;
    private String name;
    private Double salary;
  //
下面是有参、无参构造;getter和setter方法;toString方法
}

 

上面只是描述了这两个表锁对应实体类的基本属性,请注意员工表对应的实体类是没有外键属性的(前面已经讲过为什么了)。还有一点要注意,这两个实体类除了要有基本属性,还得有关系属性:

对于Employees部门,还要加以下字段

// 员工从属的部门信息
private Department department;
//下面是该属性的getter和setter方法

 

对于Department部门,还要加以下字段。请注意,这里就和一对一不同了,一个部门(一的一方)会有多个员工,因此是一个员工的List集合。

// 存储部门所有员工信息
private List<Employee> employees;
//下面是该属性的getter和setter方法

 

下面我们来写DAO,我们先写一个部门的DAO,做部门的查询,用部门查询去关联以下员工。

package com.qf.dao;
import com.qf.entity.Department;
import org.apache.ibatis.annotations.Param;
public interface DepartmentDAO {
    // 查询部门,及其所有员工信息
    Department queryDepartmentById(@Param("id") Integer id);
}

 

对应的Mapper映射文件:

先写Mapper映射文件模板:这里result到底选择resultType还是选择resultMap,根据经验,显然是resultMap。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.DepartmentDAO">

    <select id="queryDepartmentById" resultMap="">
        
    </select>
</mapper>

 

测试SQL语句,这种复杂的语句一定要测试一下。要是不测,万一错了,你定位不到错误就麻烦了。测试可以在SQLyog里面,也可以在IDEA自带的database里面。这个在SQLyog里面测试通过。注意这里的SQL没有起别名,我们在mapper中运用时要起别名。

SELECT t_departments.id,t_departments.name,location,t_employees.id,t_employees.name,salary
FROM t_departments JOIN t_employees
ON t_departments.id = t_employees.`dept_id`
WHERE t_departments.id = 1;

 

结果:

 

Mapper文件:其实这整个查询最难的就是这里。我们是根据Department的id查询的,其实按道理,我们查询id是1的则只会出来一条信息,那就是id=1,name=教育部,location=北京。但是呢!由于我们是级联查询,因此查询出来的是两条数据。即id是1的部门里面有两个员工,分别是id为1的shine01和id为2的shine02。那么这两条数据我们怎么封装呢?显然是用resultMap了。首先我们要明白,查询出来的结果是Department,他有三个普通字段和一个List<Employee>的关系字段。其中SQL语句查询出来的这两条数据的前三个字段是一样的,即是一个Department的普通字段,我们就正常封装即可;后三个显然是能封装成两个Employee对象,放在List集合中。我们这里标识list即可用collection标签,其中properties就是Department类中的关系字段属性名,ofType是这个关系属性名的集合中的泛型。请注意,这里MyBatis设计时这里写的是泛型,而不是List<泛型>,因为我们只要翻译出这个泛型就可以了。不管这个即可里面有几条数据,封装规则都是一样的。

此外Mapper文件中记得取别名。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.DepartmentDAO">

    <resultMap id="dep_emp" type="Department">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="location" property="location"></result>
        
        <!--  emp_id emp_name salary employees  -->
        <collection property="employees" ofType="Employee">
            <id column="emp_id" property="id"></id>
            <result column="emp_name" property="name"></result>
            <result column="salary" property="salary"></result>
        </collection>
    </resultMap>

    <select id="queryDepartmentById" resultMap="dep_emp">
        SELECT t_departments.id,t_departments.name,location,t_employees.id emp_id,t_employees.name emp_name,salary
        FROM t_departments JOIN t_employees
        ON t_departments.id = t_employees.`dept_id`
        WHERE t_departments.id = #{id};
    </select>
</mapper>

 

注册:

<mapper resource="com/qf/dao/DepartmentDAOMapper.xml" />

 

测试:

public static void main(String[] args) throws IOException {
    DepartmentDAO mapper = MyBatisUtil.getMapper(DepartmentDAO.class);
    Department department = mapper.queryDepartmentById(1);
    System.out.println(department.getEmployees());
    System.out.println(department);
}

 

控制台显示的结果:

Wed Dec 02 14:50:33 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 815674463.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@309e345f]
==>  Preparing: SELECT t_departments.id,t_departments.name,location,t_employees.id emp_id,t_employees.name emp_name,salary FROM t_departments JOIN t_employees ON t_departments.id = t_employees.dept_id WHERE t_departments.id = ?; 
==> Parameters: 1(Integer)
<==      Total: 2
[Employee{id=1, name='shine01', salary=10000.5}, Employee{id=2, name='shine02', salary=2000.5}]
Department{id=1, name='教学部', location='北京'}

 

好,上面是查询部门(一的一方),然后里面含有List<Employee>(多的一方)。现在我们反过来查询,即查询Employee(多的一方),然后包含Department关系属性。其实这里就很有趣,你想呀,我们是根据Employee的id查,是吧,那能查出几条,1条。然后这一个员工属于几个部门,显然是一个。那么SQL语句执行后就是一条数据。因此这里的一切都和一对一关系的写法一样。你可能不懂,等写完你就知道了。

一对多,通过一查询,则会有List即可,resultMap标签里面是collection标签;如果从多的一方查,则里面还是对应一个,resultMap标签里面则是associate标签,此时就又变回了一对一。

DAO接口:

package com.qf.dao;
import com.qf.entity.Employee;
import org.apache.ibatis.annotations.Param;
public interface EmployeeDAO {
    // 查询员工信息 并且 查到对应的部门信息
    Employee queryEmployeeById(@Param("id") Integer id);
}

 

Mapper文件:

先写SQL语句

SELECT t_employees.id,t_employees.name,t_employees.salary,t_departments.id dep_id,t_departments.name dep_name,t_departments.location
FROM t_employees JOIN t_departments
ON t_employees.`dept_id` = t_departments.`id`
WHERE t_employees.id = 1;

 

运行结果:

 Mapper文件,这里根据一对一写就可以了,没什么难度。虽然没什么难度,不过我还是解释一部分:首先这个<resultMap>标签我就不说了,我们需要复杂的关系映射时使用;里面的id属性是我们起的名字,方便我们下面引用,type属性是这这个ResultMap返回的结果,你想我们的查询是不是查询Employee,那么返回的不就是Employee吗?注意,这里用的是别名,前面已经讲过了;<resultMap>标签里面前三个是Employee类的基本属性,其中一个是主键,后面一个<association>代表的关系属性的封装,之所以这里不用<collection>,是因为这个关系属性只有一个对象,不是List集合;<association>标签里面的property属性是Department类中的关系属性名;javaType是该关系属性名的类型,需要注意的是如果是返回的集合,比如上一个一对多查询这里依然是一个基本类型,即写的是List集合中的泛型。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.EmployeeDAO">
    <resultMap id="emp_dep" type="Employee">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="salary" property="salary"></result>

        <association property="department" javaType="Department">
            <id column="dep_id" property="id"></id>
            <result column="dep_name" property="name"></result>
            <result column="location" property="location"></result>
        </association>
    </resultMap>
    <select id="queryEmployeeById" resultMap="emp_dep">
        SELECT t_employees.id,t_employees.name,t_employees.salary,t_departments.id dep_id,t_departments.name dep_name,t_departments.location
        FROM t_employees JOIN t_departments
        ON t_employees.`dept_id` = t_departments.`id`
        WHERE t_employees.id = #{id};
    </select>
</mapper> 

 

注册

<mapper resource="com/qf/dao/EmployeeDAOMapper.xml" />

 

测试

public static void main(String[] args) throws IOException {
    EmployeeDAO mapper = MyBatisUtil.getMapper(EmployeeDAO.class);
    Employee employee = mapper.queryEmployeeById(1);
    System.out.println(employee.getDepartment());
    System.out.println(employee);
}

 

结果:

Wed Dec 02 15:28:15 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 1169794610.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@45b9a632]
==>  Preparing: SELECT t_employees.id,t_employees.name,t_employees.salary,t_departments.id dep_id,t_departments.name dep_name,t_departments.location FROM t_employees JOIN t_departments ON t_employees.`dept_id` = t_departments.`id` WHERE t_employees.id = ?; 
==> Parameters: 1(Integer)
<==      Total: 1
Department{id=1, name='教学部', location='北京'}
Employee{id=1, name='shine01', salary=10000.5}

 

10.3 ManyToMany

 下面我们来学习多对多关系,学了一对一、一对多,这里学习多对多非常简单。我们用学生和课程演示,一个学生会选多个课程,一个课程会有多个学生。

 

 

 上面这张表要看懂,我们建立表students和表subjects间的关系时,要建立三张表。t_students表保存student的基本属性,t_subjects表保存subject的基本属性,然后两表之间用一个表关联起来。其实想一下为什么不直接像一对多关系中用两个外键关联呢?比如students表中有个外键,关联subjects表中的主键;而表subjects中有一个外键,关联student里面的主键。

其实我这想法很妙啊,那哪还需要第三张表?多省事,不会思考的人都是呆瓜。当然每个人角度可能不一样哈,可能这里我想到了,你是想到了别的地方。

但是,我这种思路是不对的,就一个问题,你怎么插入数据,在一对多关系中,我们插入有外键的表的前提是先有外键约束的那张表(即主键)。那么这样问题来了,你怎么插入?每个表都要求另一张表现插,这不就成了线程的死锁问题了吗?有人想,哎,那我设计个数据库像解决线程死锁问题一样去解决这个问题,别想多了,不是我们要学习的东西了。

对于上面中的两个类,每个都有一个List集合,包含着一系列另一个属性。

先建表,SQL如下:这里在建表t_stu_sub时,外键地方的括号不能省略,而且也不能用表名.id。前两个表就是基本属性,第三张表进行关联。后学生表插入两条数据,课程表插入两条数据。关联表插入四条数据,代表课程1有两个人上,课程2有两个人上;当然你也可以说第一个学生上两个课,第二个学生上两个课。

还有一点中间表两个列都是外键,每个列都是可以重复的。但是两个列合起来我们设置为主键,意思是1001学生选02课程,这只能有一条数据。

此外,我们看到student类的sex属性,以前我们用的是TinyInt类型,用0和1代表;这里用的是Varchar(1),用f和m代表。这两种都可以。

CREATE TABLE t_students(
  id INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(50),
  sex VARCHAR(1)
)DEFAULT CHARSET = utf8;

CREATE TABLE t_subjects(
  id INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(50),
  greade INT
)DEFAULT CHARSET = utf8;

CREATE TABLE t_stu_sub(
  student_id INT,
  subject_id INT,
  FOREIGN KEY (student_id) REFERENCES t_students(id),
  FOREIGN KEY (subject_id) REFERENCES t_subjects(id),
  PRIMARY KEY (student_id,subject_id)
)DEFAULT CHARSET = utf8;

INSERT INTO t_students VALUES(1,"shine",'m'),(2,"张三",'f');
INSERT INTO t_subjects VALUES(1001,"JavaSE",1),(1002,"JavaWeb",2);
INSERT INTO t_stu_sub VALUES(1,1001),(1,1002),(2,1001),(2,1002);

 

对应实体类:上面是三张表,几个实体类??两个哈,记得多对多关系,表是三个,实体类依然是两个,每个实体类拥有自己的基本属性和一个关系属性(List集合)

Student类:注意我们这里的Sex属性是Boolean,他会自动转化(真的吗?那0对应f还是m,我们拭目以待)

package com.qf.entity;
public class Student2 {
    private Integer id;
    private String name;
    private Boolean sex;
  //下面是有参,无参;toString;getter和setter方法 }

 

Subject类:

package com.qf.entity;
public class Subject {
    private Integer id;
    private String name;
    private Integer grade;
  //
下面是有参,无参;toString;getter和setter方法
}

 

上面我们描述的只是基本属性,还有关系属性:

Student类:

private List<Subject> subjects;
//及getter和setter方法

 

Subject类:

private List<Student> students;
//及getter和setter方法

 

Dao:

我们查询课程,然后查询得到选该课程的List学生集合或者是查询学生,然后得到该学生选择的课程的List集合。两者是相似的,我们选用前者。

package com.qf.dao;
import com.qf.entity.Subject;
import org.apache.ibatis.annotations.Param;
public interface SubjectDAO {
    Subject querySubjectById(@Param("id") Integer id);
}

 

Mapper文件:

先测SQL语句

SELECT t_subjects.`id`,t_subjects.`name`,t_subjects.`greade`,
       t_students.`id` stu_id,t_students.`name` stu_name,t_students.`sex`
FROM t_subjects JOIN t_stu_sub
ON t_subjects.`id` = t_stu_sub.`subject_id` 
JOIN t_students
ON t_students.`id` = t_stu_sub.`student_id`
WHERE t_subjects.`id` = 1001;

 

结果:

 

 

 Mapper文件:这个Mapper文件我没啥讲的了吧,和前面一样。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.SubjectDAO">

    <resultMap id="sub_stu" type="Subject">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <result column="grade" property="grade"></result>

        <collection property="students" ofType="Student2">
            <id column="stu_id" property="id"></id>
            <result column="stu_name" property="name"></result>
            <result column="sex" property="sex"></result>
        </collection>
    </resultMap>
    <select id="querySubjectById" resultMap="sub_stu">
        SELECT t_subjects.`id`,t_subjects.`name`,t_subjects.`greade`,
       t_students.`id` stu_id,t_students.`name` stu_name,t_students.`sex`
        FROM t_subjects JOIN t_stu_sub
        ON t_subjects.`id` = t_stu_sub.`subject_id`
        JOIN t_students
        ON t_students.`id` = t_stu_sub.`student_id`
        WHERE t_subjects.`id` = #{id};
    </select>
</mapper> 

 

注册:

<mapper resource="com/qf/dao/SubjectDAOMapper.xml" />

 

测试:

public static void main(String[] args) throws IOException {
    SubjectDAO mapper = MyBatisUtil.getMapper(SubjectDAO.class);
    Subject subject = mapper.querySubjectById(1001);
    System.out.println(subject.getStudents());
    System.out.println(subject);
}

 

测试结果:

Wed Dec 02 16:43:04 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 831236296.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@318ba8c8]
==>  Preparing: SELECT t_subjects.`id`,t_subjects.`name`,t_subjects.`greade`, t_students.`id` stu_id,t_students.`name` stu_name,t_students.`sex` FROM t_subjects JOIN t_stu_sub ON t_subjects.`id` = t_stu_sub.`subject_id` JOIN t_students ON t_students.`id` = t_stu_sub.`student_id` WHERE t_subjects.`id` = ?; 
==> Parameters: 1001(Integer)
<==      Total: 2
[Student2{id=1, name='shine', sex=false}, Student2{id=2, name='张三', sex=false}]
Subject{id=1001, name='JavaSE', grade=null}

 

关于查询学生,聪的得到List<Subject>这里就没啥说的了,一模一样。

10.4 关系总结

在一对一中:双方都是一,都添加对象作为关系属性。 

在一对多中:一方:添加集合;多放,添加对象。

在多对多红:双方都是多,都添加集合。

双方均可建立关系属性,建立关系属性后,对应的Mapper文件中需要使用<ResultMap>完成多表映射。

持有对象关系属性,使用<association property="dept" javaType="department">

持有集合关系属性,使用<collection property="emps" ofType="employee">

十一、动态SQL【重点】

MyBatis的映射文件中支持在基础SQL上添加一些逻辑操作,并动态拼接成完整的SQL之后再执行,以达到SQL复用、简化编程的效果。

我们这里再建一个Maven工程进行学习:

gav坐标:

 

 

 Pom文件修改:这里面我们就先只加两个依赖,一个是MySQL驱动依赖,一个是MyBatis依赖;另外,我们在build标签中设置了xml文件兑取位置,方便我们把DAO接口和Mapper映射文件放在一起(为什么要写前面已经写的很清楚了)。

<?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>org.qf</groupId>
    <artifactId>hellomybatis_02</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!--    MyBatis核心依赖    -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!--    MySQL驱动依赖      -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
    </dependencies>

    <build>
        <!--更改maven编译规则-->
        <resources>
            <resource>
                <!--资源目录-->
                <directory>src/main/java</directory>
                <includes>
                    <include>*.xml</include> <!-- 代表java目录下的所有xml文件 -->
                    <include>**/*.xml</include> <!-- 代表java目录下任意文件夹(包括子文件夹)下的所有xml文件 -->
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>

 

Mybatis核心配置文件

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

    <!--实体类别名-->
    <typeAliases>
        <!--定义实体类所在的package,每个实体类会自动注册一个别名,该别名就是类名-->
        <package name="com.qf.entity" />
    </typeAliases>
    <!--核心配置信息-->
    <environments default="shine_config">
        <!--数据库相关配置   其中id是这个配置的标识,在environments上引用,让其生效-->
        <environment id="shine_config">
            <!--事务控制类型 这里选用的是JDBC事务控制,说白了就是connection控制事务,这里是单标签,你也可以用双标签-->
            <transactionManager type="JDBC"/>
            <!--开始是连接池   后面是数据库连接参数 就是驱动,url,数据库账号和密码-->
            <dataSource type="org.apache.ibatis.datasource.pooled.PooledDataSourceFactory">
                <property name="driver" value="${jdbc.driver}"/>
                <!--原本我们的和用&符号即可,这里不能直接用,要转义,即&转为&amp;,用这5个字符代表一个字符&-->
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!--Mapper注册-->
    <mappers>
        <!--注册Mapper文件的所在位置-->
        <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
 
    </mappers>
</configuration>

 

外部参数文件:jdbc.properties

jdbc.url=jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&characterEncoding=utf-8
jdbc.driver=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=root

 

数据库设计:这里只有一张表是t_user,这个表上面已经创建过了。

 

 

 对应实体类:这里需要说明一点,在数据库中gender是TinyInt类型,这里是Boolean类型;regist_time是DateTime类型,这里是Date类型(java.util包下)

package com.qf.entity;
import java.util.Date;
public class User {
    private Integer id;
    private String username;
    private String password;
    private Boolean gender;
    private Date registTime;
  //下面是无参、有参;toString;getter和setter方法 }

 

Dao接口,这个接口里面有几个方法:

package com.qf.dao;
import com.qf.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserDAO {
    List<User> queryUsers();
    User queryUserById(@Param("id") Integer id);
    Integer deleteUser(@Param("id") Integer id);
    Integer updateUser(User user);
    Integer insertUser(User user);
}

 

DAO接口对应的Mapper文件:这里的第一个查询语句,返回的虽然是一个List,但是这里还是用resultType="User";这里要注意一个问题,那就是delete中的parameterType是int,而不是java.lang.Integer,其实这里我是存疑的,因为这里按道理要么用全类名,要么是别名,可是Integer并没有起别名,这个疑问先保留,看后面我们会不会遇到;对于最后一个插入,我们的SQL里面是不需要写id的,因为id设置的是自增。当然,该字段在SQL中也是可以存着的,因为有时候你可能会自己设计id,虽然基本上不可能。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.dao.UserDAO">
    <select id="queryUsers" resultType="User">
        select id,username,password,gender,regist_time registTime
        from t_user
    </select>
    <select id="queryUserById" resultType="User">
        select id,username,password,gender,regist_time registTime
        form t_user
     where id =#{id}
</select> <delete id="deleteUser" parameterType="int"> delete from t_user where id = #{id} </delete> <update id="updateUser" parameterType="User"> update t_user set username = #{username},password=#{password},gender=#{gender},regist_time=#(registTime) where id=#{id} </update> <insert id="insertUser" parameterType="User"> <selectKey order="AFTER" resultType="int" keyProperty="id"> select last_insert_id(); </selectKey> insert into t_user values(#{id},#{username},#{password},#{gender},#{registTime}) </insert> </mapper>

 

Mapper注册到核心配置文件:

<mappers>
    <!--注册Mapper文件的所在位置-->
    <!--mapper.xml默认建议存放在resources中,路径不能以/开头-->
    <mapper resource="com/qf/dao/UserDAOMapper.xml"></mapper>
</mappers>

 

MyBatisUtil类:这个在上一个项目中已经有了,我们拿过来。

package com.qf.util;

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 java.io.IOException;
import java.io.InputStream;

/*
* 1.加载配置
* 2.创建SqlSessionFactory
* 3.创建Session
* 4.事务管理
* 5. Mapper获取
* */
public class MyBatisUtil {
    private static SqlSessionFactory sqlSessionFactory;
    // 创建ThreadLocal绑定当前线程中的SqlSession对象
    private static final ThreadLocal<SqlSession> t1 = new ThreadLocal<SqlSession>();
    static { //加载配置信息,并构建session工厂
        // 1.加载配置文件
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSession openSession() {
        SqlSession sqlSession = t1.get();
        if (sqlSession == null) {
            sqlSession = sqlSessionFactory.openSession();
            t1.set(sqlSession);
        }
        return sqlSession;
    }

    public static void closeSession() {
        SqlSession sqlSession = t1.get();
        sqlSession.close();
    }

    public static void commit() {
        SqlSession sqlSession = openSession();
        sqlSession.commit();
        closeSession();
    }

    public static void rollback() {
        SqlSession sqlSession = openSession();
        sqlSession.rollback();
        closeSession();
    }

    public static <T> T getMapper(Class<T> mapper) {
        SqlSession sqlSession = openSession();
        return sqlSession.getMapper(mapper);
    }
}

 

测试:

前面我们用main方法测试,显然不是非常方便,这里我们就用Junit测试,首先引入依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>test</scope>
</dependency>

 

建包和测试类:

package com.qf;

public class MyBatisTest {
}

 

此时就准备停当了。我们开始讲动态SQL吧。

11.1  <sql>

 我们观察上面的Mapper文件中的两个SQL语句,非常相像,第一个是查询所有的用户,第二个是根据id查询用户。公共的sql语句部分如下:

select id,username,password,gender,regist_time registTime
from t_user

 

对于这个重复现象会有如下问题:1、代码量大,冗余;2、如果数据库中标的某个字段名字变化了,比如username变成了user,则需要修改两处;如果有天我们查询时,不想查询id了,则需要删除两处。

那该怎么办?我们很自然的想到有没有什么方法将公共部分提取出出来,然后在这里引用。此时我们就接触到动态SQL的第一个标签<sql>。

Mapper变化如下:<sql>标签下的id属性是起的名字,方便引用。

<!--  抽取重复sql片段,id为起的名字,方便引用  -->
<sql id="user_field">
    select id,username,password,gender,regist_time registTime
    from t_user
</sql>
<select id="queryUsers" resultType="User">
    <!--  引用sql片段  -->
    <include refid="user_field"></include>
</select>
<select id="queryUserById" resultType="User">
    <!--  引用sql片段  -->
    <include refid="user_field"></include>
    where id=#{id}
</select>
# 这里只写了两个<select>标签相关内容,其它部分略

 

测试:

package com.qf;

import com.qf.dao.UserDAO;
import com.qf.entity.User;
import com.qf.util.MyBatisUtil;
import org.junit.Test;

import java.util.List;

public class MyBatisTest {
    @Test
    public void test1() {
        UserDAO mapper = MyBatisUtil.getMapper(UserDAO.class);
        List<User> users = mapper.queryUsers();
        User user = mapper.queryUserById(2);
        users.stream().forEach(System.out::println);
        System.out.println("====================================");
        System.out.println(user);

    }
}

 

结果:报错,显示时区不能被识别什么的,这是为什么呢?之前我们写为什么没有这个错误。那是因为我们这里的mysql的驱动用的是最新版本8.0.22,你可以看一下依赖。在用该驱动时需要设置时区。其实就是在url上添加时区即可。

Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the 'serverTimezone' configuration property) to use a more specific time zone value if you want to utilize time zone support.
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)

 

修改jdbc.properties文件:

jdbc.url=jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.driver=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=root

 

再次测试结果:

D:\developer_tools\Java\jdk-8u221\bin\java.exe -ea -Didea.test.cyclic.buffer.size=1048576 -javaagent:C:\Users\YCKJ3911\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\202.8194.7\lib\idea_rt.jar=53018:C:\Users\YCKJ3911\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\202.8194.7\bin -Dfile.encoding=UTF-8 -classpath C:\Users\YCKJ3911\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\202.8194.7\lib\idea_rt.jar;C:\Users\YCKJ3911\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\202.8194.7\plugins\junit\lib\junit5-rt.jar;C:\Users\YCKJ3911\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\202.8194.7\plugins\junit\lib\junit-rt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\charsets.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\deploy.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\access-bridge-64.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\cldrdata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\dnsns.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jaccess.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\jfxrt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\localedata.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\nashorn.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunec.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunjce_provider.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunmscapi.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\sunpkcs11.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\ext\zipfs.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\javaws.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jce.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfr.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jfxswt.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\jsse.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\management-agent.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\plugin.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\resources.jar;D:\developer_tools\Java\jdk-8u221\jre\lib\rt.jar;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomybatis_02\target\test-classes;D:\IDEAworkspace\02_java_advance\3.4-mybatis\hellomybatis_02\target\classes;C:\Users\YCKJ3911\.m2\repository\org\mybatis\mybatis\3.5.5\mybatis-3.5.5.jar;C:\Users\YCKJ3911\.m2\repository\mysql\mysql-connector-java\8.0.22\mysql-connector-java-8.0.22.jar;C:\Users\YCKJ3911\.m2\repository\com\google\protobuf\protobuf-java\3.11.4\protobuf-java-3.11.4.jar;C:\Users\YCKJ3911\.m2\repository\junit\junit\4.13.1\junit-4.13.1.jar;C:\Users\YCKJ3911\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit4 com.qf.MyBatisTest,test1
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
User{id=2, username='shine_002', password='99999', gender=true, registTime=Fri Nov 27 15:04:09 CST 2020}
User{id=3, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:16:05 CST 2020}
User{id=4, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:23:40 CST 2020}
User{id=10000, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:25:45 CST 2020}
User{id=10001, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:37:12 CST 2020}
User{id=10002, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:38:47 CST 2020}
User{id=10003, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:45:34 CST 2020}
====================================
User{id=2, username='shine_002', password='99999', gender=true, registTime=Fri Nov 27 15:04:09 CST 2020}

Process finished with exit code 0

 

出来结果了,但是细心的人发现上面有一句话,这是什么意思呢?其实就是commysql.jdbc.Driver已经被弃用了。这里选择的是com.mysql.cj.jdbc.Driver驱动,并自动注入。最终还是出现正确据俄国了,但是这个提示闹心啊,我们怎么修改,很简单,就是把jdbc.properties文件里面的驱动换一下即可:

jdbc.url=jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=root

 

测试再次运行,依然得到正确结果,但是已经没有上面的提示了。

至此:我们动态SQL中的第一个标签<sql>已经学完了,其作用就是提取重复代码,方便其它地方复用。

11.2 <if>

 其实当我们看到if这个单词的时候,就知道这个标签的大致意思是判断。只是我们不知道怎么用,用在什么场合。

假设现在我们增加一个需求,根据用户名查询User用户(之前已经有两个,一个是查询所有用户,一个是根据id查询用户)。

这个很简单,我们来写一下:

Dao接口中增加方法:

User queryUserByName(@Param("username") String username);

 Mapper文件中添加对应SQL:和根据用户id查询基本一样,复制一下,修改即可

<select id="queryUserByName" resultType="User">
    <include refid="user_field"></include>
    where username=#{username}
</select>

 

测试: 

@Test
public void test2() {
    UserDAO mapper = MyBatisUtil.getMapper(UserDAO.class);
    User user = mapper.queryUserByName("shine_002");
    System.out.println(user);
}

 

 测试没什么说的,结果不再展示。

tips:这里做一个小提示,我昨天新装了IDEA,用ToolBox安装的,此时再在xml文件中写中文注释,会报错,什么2字节什么的错误。不知大家遇到没有。我到网上搜了,解决方法很简单,就是把xml文件上面的UTF-8修改为UTF8或者utf8或者GBK或者gbk。但是我没有修改,暂时也没有写中文注释。因为我觉得这样虽然看似解决了问题,但是这个xml的头是我从官网拿下来的,而且以前一直这么用,我觉得应该还有更加科学的方式解决,虽然暂时还不清楚。

 好了,上面这个需求我们解决了!但是这样就完美吗?增伤改查着四个SQL语句中查询是最普遍的,如果有一天他再让你用gender查,或者密码查(这个是我举例子,应该没有业务用密码查)。怎么办?当然我们可以定义接口方法,每个方法写一个SQL,这样做当然可以,但是这里会有大量重复,也没有必要。此时我们引入动态SQL的<if>标签。

其实解决方式非常简单,就是将DAO中的这两个方法合成一个方法:原本这两个方法分别是接收id和username属性,现在不再如此,而是接收一个User属性。当User中id不为空,就用id查询,当username属性不为空,则用username查询。这里传User类是为了能够封装这两个属性。

package com.qf.dao;
import com.qf.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserDAO {
    List<User> queryUsers();
    /*User queryUserById(@Param("id") Integer id);
    User queryUserByName(@Param("username") String username);*/
    
    User queryUser(User user);
    
    Integer deleteUser(@Param("id") Integer id);
    Integer updateUser(User user);
    Integer insertUser(User user);
}

 

对应的mapper文件:这里的where里面到底是写id还是写username呢?我们要做判断,此时就用到了动态SQL中的<if>标签

<!--<select id="queryUsers" resultType="User">
    <include refid="user_field"></include>
</select>
<select id="queryUserById" resultType="User">
    <include refid="user_field"></include>
    where id=#{id}
</select>-->
<select id="queryUser" resultType="User">
    <include refid="user_field"></include>
    where id =XXX username =XXX
</select>

 

修改后:如果id不为空而username为空则用id查询,如果username不为空而id为空则用username查询。注意:此时我们要调用时要约束一下:其中一个必须为空另一个必须有值。要不然用动态SQL拼接出来的SQL是错误的。

这里的test属性是判断条件,里面的id!=null中的id其实是从传入参数User中取id值,相当于#{id},只是在这里不能写(或者说不用写)#{id}。

<select id="queryUser" resultType="User">
    <include refid="user_field"></include>
    where
    <if test="id!=null">
        id=#{id}
    </if>
    <if test="username!=null">
        username=#{username}
    </if>
</select>

 

测试:

@Test
public void test3() {
    UserDAO mapper = MyBatisUtil.getMapper(UserDAO.class);
    User user = new User();
    user.setId(2);
    User user1 = mapper.queryUser(user);
    System.out.println(user1);
}

@Test
public void test4() {
    UserDAO mapper = MyBatisUtil.getMapper(UserDAO.class);
    User user = new User();
    user.setUsername("shine_002");
    User user1 = mapper.queryUser(user);
    System.out.println(user1);
}

 

最终测试出来结果:略

这里说一下:比如这里用名字查询,如果数据库中有多个同名的,但是方法返回值是一个User,那么会报错吗?返回结果是什么?答案:不会报错,会返回一条数据。

这就是动态SQL的第二个标签<if>,用于判断,传过来的一般就是一个对象,根据这个对象里面的信息判断怎么写sql语句。

11.3 <where>

下面我们来做一个复杂一点的查询,因以后我们的查询条件可能会变的更加复杂。再做一个查询,传入的是User类型,返回的是List<User>集合。

Dao接口中的方法:

List<User> queryUser2(User user);

 Mapper中的文件:下面的sql查询的是where username=xxx or gender

<select id="queryUser2" resultType="User">
    <include refid="user_field"></include>
    where
    <if test="username!=null">
        username=#{username}
    </if>
    <if test="gender!=null">
        or gender=#{gender}
    </if>

</select>

测试:where name=shine_000 or gender=true

package com.qf;

import com.qf.dao.UserDAO;
import com.qf.entity.User;
import com.qf.util.MyBatisUtil;
import org.junit.Before;
import org.junit.Test;

import java.security.PublicKey;
import java.util.List;


public class MyBatisTest {
    UserDAO mapper;

    @Before
    public void init() {
        mapper = MyBatisUtil.getMapper(UserDAO.class);
    }

    @Test
    public void test5() {
        User user = new User();
        user.setUsername("shine_000");
        user.setGender(true);
        List<User> users = mapper.queryUser2(user);
        users.stream().forEach(System.out::println);
    }

}

 测试结果:

User{id=2, username='shine_002', password='99999', gender=true, registTime=Fri Nov 27 15:04:09 CST 2020}
User{id=3, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:16:05 CST 2020}
User{id=4, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:23:40 CST 2020}
User{id=10000, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:25:45 CST 2020}
User{id=10001, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:37:12 CST 2020}
User{id=10002, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:38:47 CST 2020}
User{id=10003, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:45:34 CST 2020}

 其实你看到这里很懵逼,这和<where>有什么关系呢?等一等,我们先将MyBatis的日志配上。还记得如何配吗?引入log4j依赖,再添加配置文件(名字是确定的,不能自定义)即可。

 依赖:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

 

log4j.properties文件

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

 

再次运行:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a3d8174]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user where username=? or gender=?
==> Parameters: shine_000(String), true(Boolean)
<==      Total: 7
User{id=2, username='shine_002', password='99999', gender=true, registTime=Fri Nov 27 15:04:09 CST 2020}
User{id=3, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:16:05 CST 2020}
User{id=4, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:23:40 CST 2020}
User{id=10000, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:25:45 CST 2020}
User{id=10001, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:37:12 CST 2020}
User{id=10002, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:38:47 CST 2020}
User{id=10003, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:45:34 CST 2020}

 

我们发现这里就是where username=? or gender=?,之所以拼接处这个句子,是因为username和gender都是非空的。

如果这里的gender是空的,即测试时不设置gender的值,那么拼接出来的sql语句是什么?我们这里以username设置为shine_002,而gender不设置为例子测试:

@Test
public void test6() {
    User user = new User();
    user.setUsername("shine_002");
    List<User> users = mapper.queryUser2(user);
    users.stream().forEach(System.out::println);
}

 

运行结果:

Opening JDBC Connection
Created connection 1932274274.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@732c2a62]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user where username=?
==> Parameters: shine_002(String)
<==      Total: 1
User{id=2, username='shine_002', password='99999', gender=true, registTime=Fri Nov 27 15:04:09 CST 2020}

 

我们看到这里拼接出出来的sql语句是where username=?,后面没有gender,这是很正常的,毕竟gender为空。

别犯困,重点来了。那如果我们没有设置username,而设置了gender会怎么样?

测试:

@Test
public void test6() {
    User user = new User();
    user.setGender(true);
    List<User> users = mapper.queryUser2(user);
    users.stream().forEach(System.out::println);
}

 

结果:我挑选重点显示:第一句是拼出的SQL语句,第二句是说sql语法错误。其实我们已经发现了,这里拼写的sql是不对的,是where or gender = ?这显然不对,之所以这么拼写出来就是因为我们的username=null。

### SQL: select id,username,password,gender,regist_time registTime         from t_user               where                                 or gender=?
Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'or gender=1' at line 7

 

重点真的来了!!!

我们的<where>就是解决这个问题的,解决什么问题,就是sql语句中where后面可能以or或者and开头的问题。这里举例是or,其实and也是如此,比如这里什么时候开头会是or,就是username=null的时候,这时候就会错误。

解决后的mapper,注意,这里添加了<where>标签后,原有的where关键字就没有了。

<select id="queryUser2" resultType="User">
    <include refid="user_field"></include>
  <!-- where标签:
      1.补充where关键字
      2.识别where子句中如果以or开头,会将or去除。and也是如此
  -->
<where> <if test="username!=null"> username=#{username} </if> <if test="gender!=null"> or gender=#{gender} </if> </where> </select>

 

此时再去测试刚刚的那个问题:

Opening JDBC Connection
Created connection 1716093734.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@66498326]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user WHERE gender=?
==> Parameters: true(Boolean)
<==      Total: 1
User{id=2, username='shine_002', password='99999', gender=true, registTime=Fri Nov 27 15:04:09 CST 2020}

 

发现这里已经不报错了,而且也拼接出了正确的sql语句。

好了,至此。我们学习的第三个动态sql标签<where>,他解决的是SQL语句中where后面以and或者or开头的问题。

11.4 <set>

 <set>标签主要针对的是更新操作。原本我们的更新操作是这样的。

<update id="updateUser" parameterType="User">
    update t_user
    set username = #{username},password=#{password},gender=#{gender},regist_time=#(registTime)
    where id=#{id}
</update>

 

就是set后面的每一个字段我们都要设置更新。我们测试一下,比如我们这里的password、gender和registTime传入是null。

@Test
public void test7() {
    Integer updateRows = mapper.updateUser(new User(2, "aaa", null, null, null));
    MyBatisUtil.commit();
    System.out.println(updateRows);
}

 

此时数据库更新后:

 

 但是实际上我们的想法是根据这个id,只更新起username字段,其它字段不更新。那么怎么办?用这个sql肯定不行,因此我们再写一个方法和sql。

Dao:

Integer updateUserByName(User user);

 

SQL语句:

<update id="updateUser" parameterType="User">
    update t_user
    set username = #{username},password=#{password},gender=#{gender},regist_time=#{registTime}
    where id=#{id}
</update>

 

测试:

@Test
public void test7() {
    Integer updateRows = mapper.updateUserByName(new User(4, "aaa", null, null, null));
    MyBatisUtil.commit();
    System.out.println(updateRows);
}

 

测试结果:

 

 我们这里只有username列变化了,其它的保持原样(即使我们传入的user其它属性是null,数据库也不会变为null)。

这里是解决了,但是如果我下面想更新密码呢?或者想更新姓名和密码呢?或者想更新姓名、密码、性别和注册时间呢?

此时我们很自然的想到了if语句(这里提一句,if语句是最基本的语句,大家看到我们在上面学习<where>标签时就是用的if,然后加<where>标签去除where查询条件前面可能出现的and或者or)。其实这里也一样,也是先用if判断,然后要用<set>标签去除某个可能多余的东西,大家接着看吧。

修改方法updateUser对应的sql:

原本是这样的:

<update id="updateUser" parameterType="User">
    update t_user
    set username = #{username},password=#{password},gender=#{gender},regist_time=#{registTime}
    where id=#{id}
</update>

 

由于,我们不一定修改所有的,因此要编程用if判断的(如果传入为空,sql语句里面就没有该字段,不修改),这里要注意test=""中间的名字是属性名哦,而且是取值,只是不用写#{}。

<update id="updateUser" parameterType="User">
    update t_user
    set
    <if test="username!=null">
        username=#{username},
    </if>
    <if test="password!=null">
        password=#{password},
    </if>
    <if test="gender!=null">
        gender=#{gender},
    </if>
    <if test="registTime!=null">
        regist_time=#{registTime}
    </if>
    where id=#{id}
</update>

 

这样我们就根据是否为空写出来了,你想如果传入的参数password为null,则更新时没有这个字段;如果username和password为空,则更新时不更新这两个字段。

测试:

@Test
public void test7() {
    Integer updateRows = mapper.updateUser(new User(2, null, "1234", false, new Date()));
    MyBatisUtil.commit();
    System.out.println(updateRows);
}

 

更新前数据:

 

 更新后数据:

 

 我们发现这里,username保持不变,其它都更新了。

我们再看下控制台日志拼接的sql语句:没毛病,就是根据我们的if判断创建的sql语句。

==>  Preparing: update t_user set password=?, gender=?, regist_time=? where id=?

 

这就结束了吗?我们的<set>标签哪里去了,怎么用,还没讲啊!请看,如果此时我的测试数据是这样的,就是registTime属性为null,会是什么结果?或者所有都是null呢(当然我们是根据id传值的,不会都为空,但是我想前段传的时候他就是没有传值呢,怎么办?还不清楚。因此我们是不是要在业务中判断一下。)?

@Test
public void test7() {
    Integer updateRows = mapper.updateUser(new User(2, "你好", "null", null, null));
    MyBatisUtil.commit();
    System.out.println(updateRows);
}

 

测试失败:

==>  Preparing: update t_user set username=?, password=?, where id=?

 

显然,如果我们的registTime为空,则拼出来的sql语句中set的最后就会有一个,。这个符号会导致错误。怎么办?我们就要加入<set>标签。注意这个标签的作用,会自动添加一个set,如果以,结尾,会去除该符号。

<update id="updateUser" parameterType="User">
    update t_user
  <!--
    1.补充set
    2.自动将set子句的最后的,去除
   -->
<set> <if test="username!=null"> username=#{username}, </if> <if test="password!=null"> password=#{password}, </if> <if test="gender!=null"> gender=#{gender}, </if> <if test="registTime!=null"> regist_time=#{registTime} </if> </set> where id=#{id} </update>

 

此时再次测试:

 

 测试通过。

我们总结一下<set>标签,这个标签是用在更新语句中的,其同<where>标签一样,是配合<if>标签使用的。如果<if>标签用了后可能导致sql中的set后面多,这个符号,则用这个标签去除。

11.5 <trim>

下面学习<trim>标签,这个标签非常有意思,学了之后,就可以替换掉set和where标签。

用<trim>替换掉<where>标签

<select id="queryUser2" resultType="User">
    <include refid="user_field"></include>
    <!--<where>
        <if test="username!=null">
            username=#{username}
        </if>
        <if test="gender!=null">
            or gender=#{gender}
        </if>
    </where>-->
    <trim prefix="where" prefixOverrides="or|and">
        <if test="username!=null">
            username=#{username}
        </if>
        <if test="gender!=null">
            or gender=#{gender}
        </if>
    </trim>
</select>

 

这个标签很明白,就是在前面加where标签,如果条件开始部分有and或者or就去除。这不就是<where>标签的作用吗?

测试通过,因此略。

下面用<trim>替换<set>标签。

<update id="updateUser" parameterType="User">
    update t_user
    <!--<set>
        <if test="username!=null">
            username=#{username},
        </if>
        <if test="password!=null">
            password=#{password},
        </if>
        <if test="gender!=null">
            gender=#{gender},
        </if>
        <if test="registTime!=null">
            regist_time=#{registTime}
        </if>
    </set>-->
    <trim prefix="set" suffixOverrides=",">
        <if test="username!=null">
            username=#{username},
        </if>
        <if test="password!=null">
            password=#{password},
        </if>
        <if test="gender!=null">
            gender=#{gender},
        </if>
        <if test="registTime!=null">
            regist_time=#{registTime}
        </if>
    </trim>
    where id=#{id}
</update>

 

测试略。

至此<trim>标签已经学完了。其实非常简单,就是用于替换<where>和<set>标签。

11.6 <foreach>

 <foreach>标签是我们学习动态SQL的最重要的一个标签。这个标签对批量操作做一些支持,比如批量删除,批量更新。

我们先以批量删除为例:批量删除id,即传入的是一个List<Integer> ids;

DAO:

Integer deleteManyUser(List<Integer> ids);

 

SQL语句:

大致应该是这样的:这里的paramterType="list",我这里重点说一下。其有几种写法:1、list;2、List;3、java.util.List;4、整个parameterType都不要写,Mybatis会自动识别。

对于第三种写全名,就不说了;第四种省略也不说了,因为MyBatis会自动识别。但是对于1和2,我想说一下,我们一定是没有给这个List类起别名的,但是为什么可以只写个类名,而不是全名(3这种情况)也能通过呢?首先我告诉你,我不知道,但是我想这是MyBatis的设置吧。大家暂时先记住这几种写法都可以,我测试了。

此外,还有很重要的一点,我们知道,如果是返回结果是List,比如List<User>,那么我们在写resultType的时候写的是User,即泛型。而这里参数是不写泛型的,就是写的List集合。

<delete id="deleteManyUser" parameterType="list">
    delete from t_user where id in (x,x,x,x);
</delete>

 

这里显然是要遍历传入的List即可:运用<foreach>标签后的操作如下:这里的<foreach>标签非常好理解,就是遍历参数List<Integer> ids;属性collection取值有list,array等,代表的就是集合类型;open是说拼出来的语句整个前面要加的东西,显然这里是左括号,close是说拼出来的语句整个后面要加的东西,显然这里是右括号;item是只从集合中取出的每个变量的取名,方便我们下边使用;separator是每个变量之间用什么分割。

<delete id="deleteManyUser" parameterType="list">
    delete from t_user where id in
    <foreach collection="list" open="(" close=")" item="id" separator=",">
        #{id}
    </foreach>
</delete>

 

测试:

@Before
public void init() {
    mapper = MyBatisUtil.getMapper(UserDAO.class);
}

@Test
public void test8() {
    List<Integer> ids = new ArrayList<>();
    ids.add(2);
    ids.add(3);
    mapper.deleteManyUser(ids);
    MyBatisUtil.commit();
}

 

发现数据库这两个id字段已经删除。

控制台拼接sql为:

==>  Preparing: delete from t_user where id in ( ? , ? )

 

下面我们再做一个批量的增加:给你一个用户List集合,即List<User> users,我们添加到数据库中:

DAO:

Integer insertManyUser(List<User> users);

 

SQL语句:

大致如下:

<insert id="insertManyUser" parameterType="list">
    insert into t_user values(null,x,x,x,x,x),(null,x,x,x,x,x),(null,x,x,x,x,x)
</insert>

 

显然就是我们将给个对象的每个属性取出来,用<foreach>标签:

<insert id="insertManyUser" parameterType="list">
    insert into t_user values
    <foreach collection="list" open="" close="" separator="," item="user">
        (null,#{user.username},#{user.password},#{user.gender},#{user.registTime})
    </foreach>
</insert>

 

测试:

@Test
public void test9() {
    List<User> users = Arrays.asList(new User(null,"张三","123",true,new Date()),
                                    new User(null,"李四","456",false,new Date())
            );
    mapper.insertManyUser(users);
    MyBatisUtil.commit();
}

 

查看数据库:

 

 注意:以后我们的项目如果需要插入大量数据,一定要批量的插入,这比一行一行的插入效率高的多。

十二、缓存(Cache)【重点】

缓存只对查询语句有作用!!!! 

下面我们要讲MyBatis的缓存,这是非常重要的一个章节,为什么重要呢?增删改查这四个操作中,最耗时,最影响效率的是哪个?就是查询。因为查询要从拥有大量数据的表格中个根据条件筛选出来一系列数据,这需要大量磁盘IO操作(数据在我们本地磁盘上存放)。

不仅是每次查询慢,而且用户的查询需求也是最多的。比如淘宝,你是买的多,还是查的多。逛一周淘宝,啥也没买,就是查询。

因此这四种操作,查询最耗时,而且这个动作又被频繁触发。

因此性能平均在查询这里体现还是比较严重的,也就是说如果我们能够将查询操作的性能有所提升,那么整个系统的性能都会有大幅的提示。

我们这里学的缓存,就是提升查询的很有力的手段,因此在当下开发中,缓存是我们必备的一个组件。

 

 当然,缓存也不是万能的,缓存使用的前提是:频繁查询,但是很少改动的数据。如果你查询不频繁,不用说,你还要啥缓存;如果你查询很频繁,但是改动也很频繁。那你缓存了有什么意义,马上就又变化了。因此缓存的前提就是数据很少修改,查询很频繁。

一节缓存,和二级缓存。区别是缓存的位置不一样,一个是在sqlsession中,一个是在sqlsessionfactory中。缓存位置不一样,数据的有效时间也就不一样。数据的缓存价值也会有所不同。在这里边我们重点讲解,也是重点使用的是二级缓存,他的使用价值更高。但是一级缓存使用价值没有那么高,但是使用起来非常简单,我们也可以学习一下。

12.1 一级缓存

 我们先演示一级缓存,他的存储范围是sqlsession,也就是同一个sqlsession的话,就默认开启一级缓存。

测试:

@Test
public void test() {
    SqlSession sqlSession = MyBatisUtil.openSession();
    UserDAO mapper = sqlSession.getMapper(UserDAO.class);
    List<User> users = mapper.queryUsers();
    users.stream().forEach(System.out::println);
    System.out.println("=========");
    List<User> users1 = mapper.queryUsers();
    users.stream().forEach(System.out::println);
}

 

结果:

Opening JDBC Connection
Created connection 1437941060.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@55b53d44]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7
User{id=4, username='aaa', password='00000', gender=false, registTime=Fri Nov 27 15:23:40 CST 2020}
User{id=10000, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:25:45 CST 2020}
User{id=10001, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:37:12 CST 2020}
User{id=10002, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:38:47 CST 2020}
User{id=10003, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:45:34 CST 2020}
User{id=10004, username='张三', password='123', gender=true, registTime=Thu Dec 03 15:10:43 CST 2020}
User{id=10005, username='李四', password='456', gender=false, registTime=Thu Dec 03 15:10:43 CST 2020}
=========
User{id=4, username='aaa', password='00000', gender=false, registTime=Fri Nov 27 15:23:40 CST 2020}
User{id=10000, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:25:45 CST 2020}
User{id=10001, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 15:37:12 CST 2020}
User{id=10002, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:38:47 CST 2020}
User{id=10003, username='shine_000', password='00000', gender=false, registTime=Fri Nov 27 16:45:34 CST 2020}
User{id=10004, username='张三', password='123', gender=true, registTime=Thu Dec 03 15:10:43 CST 2020}
User{id=10005, username='李四', password='456', gender=false, registTime=Thu Dec 03 15:10:43 CST 2020}

Process finished with exit code 0

 

我们发现上面是两次查询,但是只执行了一次sql语句。这就自动用到了一级缓存。即同一个sqlSession下,查询相同的东西,第二次就不去数据库了,直接从缓存中拿。这就减少了网络IO和本地IO操作。当然我们这里没有网络IO,因为我们数据库是在本地,但是是有磁盘IO的。

下面我们再演示一下:如果是两个不同的sqlSession,查询相同的内容:是否会有缓存(当然是没有的了)。

我们需要修改一下MyBAtisUtil,因为我们的Util中的sqlSession是基于线程绑定的,即同一个线程则sqlSession相同。即:下面的是同一个sqlSession。

@Test
public void test() {
    SqlSession sqlSession = MyBatisUtil.openSession();
    UserDAO mapper = sqlSession.getMapper(UserDAO.class);
    SqlSession sqlSession1 = MyBatisUtil.openSession();
}

 

修改util,只是在里面增加了一个getSession方法。

package com.qf.util;

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 java.io.IOException;
import java.io.InputStream;

/*
* 1.加载配置
* 2.创建SqlSessionFactory
* 3.创建Session
* 4.事务管理
* 5. Mapper获取
* */
public class MyBatisUtil {
    private static SqlSessionFactory sqlSessionFactory;
    // 创建ThreadLocal绑定当前线程中的SqlSession对象
    private static final ThreadLocal<SqlSession> t1 = new ThreadLocal<SqlSession>();
    static { //加载配置信息,并构建session工厂
        // 1.加载配置文件
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSession openSession() {
        SqlSession sqlSession = t1.get();
        if (sqlSession == null) {
            sqlSession = sqlSessionFactory.openSession();
            t1.set(sqlSession);
        }
        return sqlSession;
    }
    
    public static SqlSession getSession() {
        return sqlSessionFactory.openSession();
    }

    public static void closeSession() {
        SqlSession sqlSession = t1.get();
        sqlSession.close();
    }

    public static void commit() {
        SqlSession sqlSession = openSession();
        sqlSession.commit();
        closeSession();
    }

    public static void rollback() {
        SqlSession sqlSession = openSession();
        sqlSession.rollback();
        closeSession();
    }

    public static <T> T getMapper(Class<T> mapper) {
        SqlSession sqlSession = openSession();
        return sqlSession.getMapper(mapper);
    }
}

 

测试:

package com.qf;
import com.qf.dao.UserDAO;
import com.qf.entity.User;
import com.qf.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class MyBatisTest {
    @Test
    public void test() {
        SqlSession sqlSession = MyBatisUtil.openSession();
        UserDAO mapper = sqlSession.getMapper(UserDAO.class);
        System.out.println("===================================================");
        List<User> users = mapper.queryUsers();

        System.out.println("===================================================");
        SqlSession sqlSession1 = MyBatisUtil.getSession();
        UserDAO mapper1 = sqlSession1.getMapper(UserDAO.class);
        List<User> users1 = mapper1.queryUsers();
    }
}

 

结果:我们发现此时执行了两条sql语句:

===================================================
Opening JDBC Connection
Created connection 1437941060.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@55b53d44]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7
===================================================
Opening JDBC Connection
Created connection 1306834002.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4de4b452]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7

 

上面我们说了一级缓存不实用,为什么不实用?

我们在同一个sqlsession中可以用一级缓存,但是你想你会同一个问题查两次吗?不会,比如这里查询user,你第二次还想查,就直接用第一次查的结果就好了,哪还会想着再查询一下。

即我们已经查询出了users,那么这届用就好了,干嘛要再查一次。

12.2 二级缓存

 SqlSessionFactory级别的缓存,同一个SqlSessionFactory构建的SqlSession发起的多次同构查询会将数据保存在二级缓冲中。

  • 注意:在sqlSession.commit()或者sqlSession.close()之后生效。

此外,我们的项目,一个就是一个项目一个SqlSessionFactory。这里我把最原始的我们学习MyBatis的操作步骤拿过来。

package com.qf.test;
import com.qf.dao.UserDao;
import com.qf.entity.User;
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 java.io.IOException;
import java.io.InputStream;
public class TestMyBatis {
    public static void main(String[] args) throws IOException {
        // MyBatis API
        // 1. 加载配置文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 2. 构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //3. 通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 4.通过SqlSession获得DAO实现类的对象
        UserDao mapper = sqlSession.getMapper(UserDao.class); //获取UserDao对应的实现类的对象

        // 5. 测试查询方法
        User user = mapper.queryUserById(1);
        User user1 = mapper.queryUserById(2);

        System.out.println(user);
        System.out.println(user1);
    }
}

 

我们看到这里其实SqlSessionFactory是通过MyBatis的核心配置文件build出来的,换句话说,我们一个项目可能会有多个SqlSessionFactory。其实不然,我们后面学习就会用到Spring框架,SpringCloud(集成了Spring)。我们不会手动创建SqlSessionFactory。而是自动创建的,我们只需要注入DAO接口即可调用,这都是后话了,现在我们记住我们的一个项目一般只有一个SqlSessionFactory实例即可。

12.2.1 开启全局缓存

 <setting>是MyBatis中极为重要的调整设置,他们会改变MyBatis的运行行为,其他详细配置可参考官方文档。

我们这里开启全局缓存非常简单:我把完整的MyBatis核心配置文件列举如下:我们这里开启只需要在核心配置文件中设置<settings>标签即可;请注意,这个是默认开启的,因此你完全可以不写这个。

<?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?, settings?, typeAliases?, typeHandlers?, 
    objectFactory?, objectWrapperFactory?, reflectorFactory?, 
    plugins?, environments?, databaseIdProvider?, mappers?)-->

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

    <settings>
        <setting name="cacheEnabled" value="true"/> <!--mybatis-config.xml中开启全局缓存(默认开启)-->
    </settings>

    <typeAliases>
        <package name="com.qf.entity" />
    </typeAliases>
    <environments default="shine_config">
        <environment id="shine_config">
            <transactionManager type="JDBC"/>
            <dataSource type="org.apache.ibatis.datasource.pooled.PooledDataSourceFactory">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/qf/dao/UserDAOMapper.xml"></mapper>
    </mappers>
</configuration>

 

此外,需要注意的是:上面的标签是有顺序的,我们点击<configration>标签可以进去看到。顺序不能写错。顺序如上面红色字体标注。

12.2.2 指定Mapper缓存

 这一步非常简单,但是不能省略(上一步可以省略)。这一步是在Mapper文件中添加的,而不是核心配置文件。他的作用是,拥有这个标签的Mapper文件中的所有select查询语句都要用到进入二级缓存。这是什么意思,因为二级缓存默认是开启的(如果我们不写,第一步默然开启全局缓存)。但是并不是说你就能用了,只有某个Mapper文件有这个<cache>标签才会进入二级缓存。也就是说这个标签相当于二级缓存的入场券,写了之后,该Mapper文件下的查询才会使用。

这里我们在我们的UserDAOMapper.xml文件中添加该标签:其与<select>,<update><sql>等标签同级。

<!--二级缓存默认开启的,但并不是所有的查询结果,都会进入二级缓存-->
<cache />

 测试:这里我们每次都会关闭SqlSession,你先不要管为什么(因为我们之前就没有操作过),后面我们讲。

package com.qf;
import com.qf.dao.UserDAO;
import com.qf.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyBatisTest {
    @Test
    public void test() {
        // 通过相同的SqlSessionFactory获取多个SqlSession
        SqlSession sqlSession1 = MyBatisUtil.getSession();
        SqlSession sqlSession2 = MyBatisUtil.getSession();
        SqlSession sqlSession3 = MyBatisUtil.getSession();
        UserDAO mapper1 = sqlSession1.getMapper(UserDAO.class);
        UserDAO mapper2 = sqlSession2.getMapper(UserDAO.class);
        UserDAO mapper3 = sqlSession3.getMapper(UserDAO.class);
        mapper1.queryUsers();
        sqlSession1.close();
        System.out.println("===============================");
        mapper2.queryUsers();
        sqlSession2.close();
        System.out.println("===============================");
        mapper3.queryUsers();
        sqlSession3.close();
    }
}

 

运行结果:我们发现这里报错了,是说我们的实体类没有序列化,这说明我们使用二级缓存需要序列号实体类。

修改后:再次运行:

Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Created connection 278240974.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Returned connection 278240974 to pool.
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.5
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.6666666666666666

Process finished with exit code 0

 

我们发现只执行了一条SQL语句。而且上面还有缓存命中率的日志。这是什么意思呢?我们总共查询了三次,第一次缓存中没有该数据,因此命中率是0;第二次查询,缓存中已经有了,因此从缓存中获得,2次中其中一次数据来自缓存,因此是0.5;第三次查询,缓存中也有了数据,因此从缓存获得,三次查询两次来自缓存,因此缓存命中率是66.6%。

我们上面提到,每次我们用一个SqlSession查询后,都会主动关闭这个SqlSession,这是我们使用二级缓存必须的操作。因为只有我们主动关闭了SqlSession之后,这里的查询数据才会存入到二级缓存中。我们现在测试下不关闭:

package com.qf;
import com.qf.dao.UserDAO;
import com.qf.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyBatisTest {
    @Test
    public void test() {
        // 通过相同的SqlSessionFactory获取多个SqlSession
        SqlSession sqlSession1 = MyBatisUtil.getSession();
        SqlSession sqlSession2 = MyBatisUtil.getSession();
        SqlSession sqlSession3 = MyBatisUtil.getSession();
        UserDAO mapper1 = sqlSession1.getMapper(UserDAO.class);
        UserDAO mapper2 = sqlSession2.getMapper(UserDAO.class);
        UserDAO mapper3 = sqlSession3.getMapper(UserDAO.class);
        mapper1.queryUsers();
        //sqlSession1.close();
        System.out.println("===============================");
        mapper2.queryUsers();
        //sqlSession2.close();
        System.out.println("===============================");
        mapper3.queryUsers();
        //sqlSession3.close();
    }
}

 

结果:

Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Created connection 278240974.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Created connection 2119891622.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e5afaa6]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Created connection 1473771722.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@57d7f8ca]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7

 三次缓存命中率都是0。因为没有进入缓存中。

12.2.3 缓存清空并重新缓存

 

package com.qf;
import com.mysql.cj.Session;
import com.qf.dao.UserDAO;
import com.qf.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyBatisTest {
    @Test
    public void test() {
        // 通过相同的SqlSessionFactory获取多个SqlSession
        SqlSession sqlSession1 = MyBatisUtil.getSession();
        SqlSession sqlSession2 = MyBatisUtil.getSession();
        SqlSession sqlSession3 = MyBatisUtil.getSession();
        UserDAO mapper1 = sqlSession1.getMapper(UserDAO.class);
        UserDAO mapper2 = sqlSession2.getMapper(UserDAO.class);
        UserDAO mapper3 = sqlSession3.getMapper(UserDAO.class);
        mapper1.queryUsers();
        sqlSession1.close(); // 二级缓存生效

        // 修改,修改相关的缓存,会被移除
        SqlSession sqlSession4 = MyBatisUtil.getSession();
        UserDAO mapper4 = sqlSession4.getMapper(UserDAO.class);
        mapper4.deleteUser(10000);
        sqlSession4.commit();
        sqlSession4.close();

        System.out.println("===============================");
        mapper2.queryUsers();
        sqlSession2.close();
        System.out.println("===============================");
        mapper3.queryUsers();
        sqlSession3.close();
    }
}

 

测试结果:

Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Created connection 278240974.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 7
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Returned connection 278240974 to pool.
Opening JDBC Connection
Checked out connection 278240974 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
==>  Preparing: delete from t_user where id = ?
==> Parameters: 10000(Integer)
<==    Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Returned connection 278240974 to pool.
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Checked out connection 278240974 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user
==> Parameters: 
<==      Total: 6
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10959ece]
Returned connection 278240974 to pool.
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.3333333333333333

Process finished with exit code 0

 

我们发现只有最后一次缓存命中了,因此查询三次,1次命中的命中率为0.3333.

我们想第一次查询,我们关闭了sqlsession,因此二级缓存中有了数据;但是我们接下来删除了一条数据,那么相关的内容发生了变化,因此二级缓存清空;第二次查询又保存,第三次查询缓存命中。

我们这里说的就该相关数据二级缓存会改变,那么哪些算修改?增删改都算,那么哪些算相关数据?只要是同一个Mapper下的增删改都算是操作相关数据。

我现在有一个以为,我第一次查询的是编号10002的数据,第二次修改,第三次再查询10002,那么会缓存命中吗?还是说缓存修改了,无法命中?第三次查询1001呢?

之所以有这样的意思是因为我们查询的User和删除的并不是同一条,而上面查询的是所有,而删除任意一条肯定会影响到;我们测试下:

对应SQL:

<select id="queryUser" resultType="User">
    <include refid="user_field"></include>
    where
    <if test="id!=null">
        id=#{id}
    </if>
    <if test="username!=null">
        username=#{username}
    </if>
</select>

 

测试结果:

Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Created connection 574268151.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user where id=?
==> Parameters: 10002(Integer)
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Returned connection 574268151 to pool.
Opening JDBC Connection
Checked out connection 574268151 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
==>  Preparing: delete from t_user where id = ?
==> Parameters: 10001(Integer)
<==    Updates: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Returned connection 574268151 to pool.
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.0
Opening JDBC Connection
Checked out connection 574268151 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
==>  Preparing: select id,username,password,gender,regist_time registTime from t_user where id=?
==> Parameters: 10002(Integer)
<==      Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@223aa2f7]
Returned connection 574268151 to pool.
===============================
Cache Hit Ratio [com.qf.dao.UserDAO]: 0.3333333333333333

Process finished with exit code 0

 

发现缓存还是清空了,也就是说只要我们的数据发生了改变,不管是否影响到查询的数据,只要是在同一个Mapper下,那么二级缓存就会清空。

十三、Druid连接池

为什么要引入连接池?
首先在Web开发中,如果使用 JDBC连接数据库,那么每次访问请求都必须建立连接——打开数据库——存取数据库——关闭连接等一系列步骤。但是我们知道数据库的连接打开不仅费时,而 且消耗比较多的系统资源。如果进行数据库操作的次数比较少,那么还不至于有多大的影响,但是假如频繁的进行数据库操作,那么系统的性能将会受到很大影响。
其 次,是造成数据库连接泄漏。数据库连接泄漏这个词是第一次听说,指的是如果在某次使用或者某段程序中没有正确地关闭Connection、 Statement和ResultSet资源,那么每次执行都会留下一些没有关闭的连接,这些连接失去了引用而不能得到重新使用,因此就造成了数据库连接 的泄漏。数据库连接的资源是宝贵而且是有限的,如果在某段使用频率很高的代码中出现这种泄漏,那么数据库连接资源将被耗尽,影响系统的正常运转。
为了解决上述问题,因此就引入了数据库连接池技术。用一句话概括数据库连接池技术那就是负责分配、管理和释放数据库连接。

13.1 概念

Druid是阿里巴巴开源平台删管道一个项目,整个项目由数据库连接池、插件框架和SQL解析器组成。该项目主要是为了扩展JDBC的一些限制,可以让程序员实现一些特殊的需求,比如向秘钥服务请求凭证、统计SQL信息、SQL性能收集、SQL注入检查、SQL翻译等,程序员可以通过定制来实现自己需要的功能。 

13.2 不同连接池对比

 测试执行申请归还连接1,000,000(一百万)次总耗时性能对比。

13.2.1 测试环境

环境 版本
OS OS X 10.8.2
CPU Intel i7 2GHz 4 Core
JVM Java Version 1.7.0_05

13.2.2 基准测试结果对比

 

13.2.3 测试结论

  •  Druid是性能最好的数据库连接池,tomcat-jdbc和druid性能接近。
  • Proxool在激烈并发时会抛出异常,不适用。
  • C3P0和Proxool都相当慢,影响sql执行效率。
  • BoneCP性能并不优越,采用LinkedTrancsferQueue并没有能够获得性能提升。
  • 除了bonecp,其他的在JDK7上跑得比JDK6上快。
  • jboss-datasource虽然稳定,但性能很糟糕。

13.3 配置pom.xml

 如何使用该数据库连接池,非常简单,只需要引入依赖;创建DruidDataSourceFactory;修改MyBatis核心配置文件这三步。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.23</version>
</dependency>

 

13.4 创建DruidDataSourceFactory

 MyDruidSourceFactory并集成PooledDataSourceFactory,并替换数据源:

package com.qf.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.datasource.pooled.PooledDataSourceFactory;

public class MyDruidDataSourceFactory extends PooledDataSourceFactory {
    public MyDruidDataSourceFactory() {
        this.dataSource = new DruidDataSource(); //替换数据源
    }
}

 

这是固定的写法,就是自己创建一个类,然后继承PooledDataSourceFactory(注意:这个类是MyBatis的)。然后里面创建构造方法,构造方法中new DruidDataSource(),并将其赋值给this.dataSource,请注意这个this.dataSource是父类PooledDataSourceFactory中的变量。

13.5 修改mybatis-config.xml

mybatis-config.xml中连接池相关配置

<environments default="shine_config">
    <environment id="shine_config">
        <transactionManager type="JDBC"/>
      <!-- 连接池 --> <dataSource type="com.qf.datasource.MyDruidDataSourceFactory"> <!-- 数据源工厂 --> <property name="driverClass" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments>

 

注意:<property name="属性名"/>属性名必须与com.alibaba.druid.pool.DruidAbstractDataSource中一致。

此外,要注意,这里的driver已经改名为driverClass,而url改名为jdbcUrl。

测试:没啥测的,只要任何test方法正确运行即可。看了下7控制台,没有发现和该数据库相关的具体信息。

Opening JDBC Connection
{dataSource-1} inited
PooledDataSource forcefully closed/removed all connections.

 

十四、PageHelper

14.1 概念

 PageHelper是适用于MyBatis框架的一个分页插件,使用方式极为便捷,支持任何复杂的单表、多表分页查询操作。

14.2 访问与下载

 官网地址:https://pagehelper.github.io/

下载地址:https://github.com/pagehelper/Mybatis-PageHelper

14.3 开发步骤

 PageHelper中提供了多个分页操作的静态方法入口。

14.3.1 引入依赖

 pom.xml中引入PageHelper依赖。

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.4</version>
</dependency>

 

14.3.2 配置MyBatis-config.xml

 在MyBatis-config.xml中添加<plugins>。这里一定要注意标签的位置,我们前面已经说过了<configuration>下面的标签是有顺序的。

 

 

14.3.3 PageHelper应用方式

 

14.4 PageInfo对象

posted @ 2020-11-26 11:03  峡谷挨打记  阅读(866)  评论(0编辑  收藏  举报