嘘~ 正在从服务器偷取页面 . . .

SpringBoot MyBatis


1. JDBC

JDBC(Java DataBase Connection),Java 原生对。然而 JDBC 太麻烦了,因为你需要写很多连接数据库、关闭数据库等等方法,进行数据库的交互。最致命的是,你的逻辑代码里会混有 SQL 语句,这不是我们想要看到的。

@Test
public void testConnection1() throws Exception{
    //1.数据库连接的4个基本要素:
    String url = "jdbc:mysql://localhost:3306/ssm" + 
        "?characterEncoding=utf8&serverTimezone=Asia/Shanghai";
    String username = "root";
    String password = "root";
    //8.0之后名字改了 com.mysql.cj.jdbc.Driver
    String driverName = "com.mysql.cj.jdbc.Driver";
    //2.实例化Driver
    Class clazz = Class.forName(driverName);
    Driver driver = (Driver) clazz.newInstance();
    //3.注册驱动
    DriverManager.registerDriver(driver);
    //4.获取连接
    Connection conn = DriverManager.getConnection(url, username, password);
    PreparedStatement preparedStatement =
        conn.prepareStatement("select * from user where id = ?");
    preparedStatement.setInt(1,1);
    ResultSet resultSet = preparedStatement.executeQuery();
    // 处理结果集
    while (resultSet.next()){
        User user = new User();
        user.setId(resultSet.getInt("id"));
        user.setUsername(resultSet.getString("username"));
        user.setPassword(resultSet.getString("password"));
        System.out.println(user);
    }
}

2. MyBatis

2.1 持久化层

持久化是将程序数据在持久状态和瞬时状态间转换的机制。通俗地讲,就是瞬时数据(比如内存中的数据,是不能永久保存的)持久化为持久数据(比如持久化至数据库中,能够长久保存)。

程序产生地数据首先都是在内存中,程序在运行时说的持久化通常是将内存的数据存储在硬盘中。

2.2 ORM

ORM,即 Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去和复杂的 SQL 语句打交道,只需简单的操作对象的属性和方法。

  • JPA(Java Persistence API)是 Java 持久化规范,是 ORM 框架的标准,主流 ORM 框架都实现了这个标准;
  • Hibernate:全自动的框架,强大、复杂、笨重、学习成本较高,不够灵活,实现了 JPA 规范。Java Persistence API(Java 持久层 API);
  • MyBatis:半自动的框架(懂数据库的人 才能操作) 必须要自己写 SQL,不是依照的 JPA 规范实现的。

2.3 源码配置

MyBatis 基本配置如下:

MyBatis 的一个基本流程

MyBatis 架构如下:

MyBatis 架构

MyBatis 连接数据库有两种:

  1. 直接使用注释。在 mapper 的方法上添加特定方法进行操作,这样的操作有几个弊端。一是如果接口方法躲起来,在后续的管理方面并不是很好;二是这样写由于没有 xml 的错误提示,很容易写错,且不方便进行后续的修改;三是,如果需要写一些比较的操作,像是连表查询,注释就不能写复杂的 SQL 逻辑了;
  2. 在 resource 文件中创建一个 mapper 文件,用来储存对应 mapper 的 xml 文件。这里需要在开头配置一段 MyBatis 的配置,可以直接在文档上进行复制。可以在 idea 中下载对应的插件,提供代码提示,并且对应字段会有高亮显示。
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="......">
<!--namespace 中放着对应 mapper 文件-->
</mapper>

2.3.1 配置数据库连接

在这之前,我们需要在 SpringBoot 中添加能连接数据库连接的依赖,然后在 yml 中进行配置。

url 表示数据库的地址和具体配置,useUnicode 防止乱码问题 characterEncoding 数据库中使用的编码格式(大小写不敏感)serverTimezone 是对时区的设置,在高版本的 SQL 中,时区需要进行特殊的配置(CST可视为美国、澳大利亚、古巴或中国的标准时间)useSSL 在高版本的 SQL 中是需要设置的,如果不设置有时候会出现对应的报错

driver 表示数据库对应的驱动

spring:
  datasource:
    url: jdbc:mysql://pipe.sast.codes:7336/atsast?useUnicode=true&characterEncoding=UTF-8&serverTimezone=CST&useSSL=false&allowPublicKeyRetrieval=true
    username: atsast
    password: sast_forever
    driver-class-name: com.mysql.cj.jdbc.Driver

2.3.2 配置 MyBatis

type-aliases-package:匹配实体类所在的文件夹

mapper-locations:指定 mapper 文件的 xml 所在地

mybatis:
  type-aliases-package: com.sast.atSast.model
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

之后在 idea 中的数据库一栏,进行数据库的连接就可以方便的管理和处理 SQL 语句了

当然,可以提前设置 idea 的数据库为 MySQL,这样在写 xml 文件的时候就会有代码提示了,最好也装一下 idea 中的自带的 MyBatis 辅助插件

2.4 简单示例

Mapper 层代码:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
import sast.freshcup.entity.AccountContestManager;

import java.util.List;

@Repository
//这里的 extends 是 MyBatis-Plus 中的内容,之后会有说明
public interface AccountContestManagerMapper extends BaseMapper<AccountContestManager> {

    List<Long> getUidsByContestId(Long ContestId);

}

对应 xml 配置文件代码:

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


    <select id="getUidsByContestId" resultType="java.lang.Long">
        select uid
        from account_contest_manager
        where contest_id = #{contestId}
    </select>

</mapper>
  • resultType:指定返回类型;
  • parameterType:指定参数类型;
  • id:指定映射方法的名字。

2.4.1 占位符

#{}作为占位符使用,MyBatis 会将其替换成?,可以有效防止 SQL 注入。

${}直接进行字符串替换,但是对于字符串替换,可能会出现注入情况,不推荐使用。

2.4.2 使用 map

Map 可以用来替代任意实体类,当数据比较复杂时,可以考虑使用。

SQL 代码:

<select id="getUsersByParams" parameterType="java.util.HashMapmap">
	select id,username,password from user where username = #{name}
</select>

测试代码:

@Test
public void findByParams() {
    UserMapper mapper = session.getMapper(UserMapper.class);
    Map<String,String> map = new HashMap<>();
    map.put("name", "磊磊哥");
    List<User> users = mapper.getUsersByParams(map);
    for (User user: users){
        System.out.println(user.getUsername());
    }
}

2.4.3 注释开发

Mapper 代码示例:

public interface AdminMapper {
    
    /**
    * 保存管理员
    * @param admin
    * @return
    */
    @Insert("insert into admin (username,password) values (#{username},#{password})")
    int saveAdmin(Admin admin);
    
}

但是某种程度上来说,这样的代码样式必定会带来可读性和可维护性的降低。

2.4.4 @Param

对于映射关系,在 Mapper 层中,如果字段和参数的名称不一致,需要使用到@Param进行修饰。

默认是一一对应的关系。

初次之外,如果参数使用对象,也需要使用@Param进行修饰。

int insertUser(@Param("id") int id, @Param("username") String name, @Param("password") String pws);

mapper 层参数使用对象示例:

@Repository
public interface UserDao {
    StudentInfo findInfoByForm(@Param("formVO") FormVO formVO);
}

对应 xml 文件:

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


    <select id="findInfoByForm"
            resultType="form.covid.maxtune.pojo.vo.StudentInfo"
            parameterType="form.covid.maxtune.pojo.vo.FormVO">
        SELECT
            c.`name` collegeName,
            m.`name` majorName,
            cl.`name` className
        FROM
            college c
                INNER JOIN major m ON c.id = m.college_id
                INNER JOIN class cl ON m.id = cl.major_id
        WHERE
            college_id = #{formVO.collegeId};
    </select>
</mapper>

3. 简单了解 JPA

SpringBoot JPA 是 Spring 基于 ORM 框架、Jpa 规范的基础上封装的一套 Jpa 应用框架,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展。学习并使用 Spring Data Jpa 可以极大提高开发效率,最终要的是 JPA 让程序员解脱了 mapper 的操作。

在使用之前,我们需要让 mapper 接口继承 JpaRepository,之后这个 mapper 的对象就可以直接使用默认定义的方法了。最神奇的地方在于,JPA 可以通过在持久化层中的方法名称生成一些简单的 SQL(在 MyBatis 的 mapper xml 配置中,因为 idea SQL ”方言“的设置也会根据方法名称进行部分代码的自动补全,但这是完全借助于 idea 自带的智能提示),JPA 会根据方法中和 SQL 中相似的关键字名称进行匹配。

@Test
public void testBaseQuery() throws Exception {
	User user = new User();
	userRepository.findAll();
	userRepository.findOne(1l);
	userRepository.save(user);
	userRepository.delete(user);
	userRepository.count();
	userRepository.exists(1l);
	// ...
}

3.1 复杂 SQL 逻辑的实现

对于一些需要分页需求的接口中,JPA 自带的方法中,可以传入一个 Pageable 类的对象。Pageable 是 Spring 封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则。可以通过传入页数,分页数目,排序三个参数进行对象的实例化,之后传入方法中。

Spring Data 绝大部分的 SQL 都可以根据方法名定义的方式来实现,但是由于某些原因我们想使用自定义的 SQL 来查询,Spring Data 也是完美支持的。在 SQL 的查询方法上面使用 @Query 注解,如涉及到删除和修改在需要加上 @Modifying 。也可以根据需要添加 @Transactional 对事物的支持,查询超时的设置等(在传入参数时,通过 ?1 表示第一个参数,其他位置的参数依次类推)。

最关注的多表问题,主要解决方法又有两个:

  1. 利用 Hibernate 的级联查询来实现;
  2. 创建一个结果集的接口来接收连表查询后的结果(主要还是使用这种方式)。

4. MyBatis-Plus

MyBatis-Plus 并非 SpringBoot 官方依赖,而是一群“同人”创建的“DLC”。也就是说,就算把依赖替换成 MyBatis-Plus,原来的写法依然是可以的,只不过多了一些更加简单的方法。在 JPA 框架中,程序员是可以直接使用它定义的默认方法的,在 MyBatis-Plus 中,也是满足了这个需求,并且很多事情都直接让框架自动执行。甚至有一个代码生成器可以自动生成需要的代码。(虽然那个东西十分难用,样例好像都是错的)

<!--导入依赖包-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3.3</version>
</dependency>

4.1 基本 CRUD

在 mapper 和 service 层中,直接调用对应的接口就可以直接使用内置的方法。在 mapper 层中,需要类调用 BaseMapper 接口,后面跟上对应的实体类的类型,之后在自动装配的 mapper 对象中,就可以直接调用 MyBatis-Plus 内置的各种方法了。接下来举几个查的方法,其他的类似,一些复杂的下面的条件构造器会说。

selectList:里面参数表示条件构造器,如果没有直接写 null,表示返回这个表格中所有的数据,返回值是一个 List 列表;

selectByMap:构造一个 map 表示过滤的内容,可以简单代替条件构造器,不过不是很推荐;

selectById:通过主键的值进行查询(一般的主键都设置为 id);

selectBatchIds:通过一个 id 的列表进行查询。

剩下来的很多方法都是类似的,但是主要是要注意在 update 中的一个方法。

updateById 这里的参数是实体类,而不是 id 的值。

4.2 条件构造器

通常的 SQL 语句总是需要过滤语句像是 where 或者是 Like,在 MyBatis-Plus 中,我们使用条件构造器来代替这些

首选要实例化一个 QueryWrapper 的对象,然后可以调用里面的方法,通过定义方法的链式编程来代替 SQL 语句。当然,如果你对能实现的复杂逻辑的范围不满意,还可以直接在里面使用 xml 自定义使用的方法

4.3 乐观锁

当要更新一条记录的时候,希望这条记录没有被别人更新

乐观锁总体上实现了防止多个线程同时更改一个数据的情况,在一个线程修改完之后,另一个线程检查到乐观锁时,如果数据不同,会报错并取消更改(就好像一开始的 version 是1,更改之后会默认 +1)

实现乐观锁的前提,需要现在字段上添加 @Version 注释,再添加文档中需要的配置文件

@Configuration
@MapperScan("com.sast.atsast.mapper")
public class MyBatis_PlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

乐观锁实现方式:

  • 取出记录时,获取当前version
  • 更新时,带上这个version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果version不对,就更新失败

4.4 自动填入

有些东西,我们希望数据库更新或者更改时能够自动添加某些数据(一般都是创建时间,或者是更新时间这种)。

在配置好对应的 handler 文件之后,在需要使用自动填入的字段上加入一个 @TableField 的注释,如果在里面填入参数,可以使用 fill = FieldFill.INSERT 或者其他类推的,指定在特定命令时进行填入。

因为文档样例错误的问题,这个功能暂时无法使用。

@Slf4j
@Component
//不知道为什么官方文档的方法没有办法正常使用
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        setFieldValByName("createTime", new Date(), metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        setFieldValByName("updateTime", new Date(), metaObject);
    }
}

4.5 逻辑删除

逻辑删除说的通俗一点,就是我们所谓的假删,像是腾讯的消息记录,即使你撤回或者删除,在后台都是可以看到的(相当于只是放在“回收站中”,并没有真正的删除,像是原本的 git 命令删除也只是假删,需要使用 git gc 进行垃圾回收才能完全去除记录)。

在项目中,通常也是有这种功能,一般都是设置一个 enable 字段,1表示显示,0表示删除。在 SQL 语句中,查找时加上 where eable = 1 显示未被假删的数据。在 MyBatis-Plus 中,这种事情可以自动的完成。在直接文档中的配置文件之后(可以根据要求自行定义),在相关的字段中添加注释 @TableLogic 就表示这是一个逻辑字段了。如果我设置0表示删除,1表示显示。使用内置的功能时,SpringBoot 便只会返回 enable 为1的字段(相当于自动调用了之前所说的命令);同理,在执行删除方法时,其实是在内部执行了一个 update 语法,将 enable 从1改为0。

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

4.6 分页

在使用自带的分页功能之前,需要引入插件的配置:

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        paginationInnerInterceptor.setOverflow(true);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
    
}

实际使用效果如下:

/**
* @param pageNum
* @param pageSize
* @return
* @Description: 如果没有输入比赛id就获取所有管理员信息,否则只获取对应比赛管理员
*/
public Map<String, Object> getAllAdmin(Integer pageNum, Integer pageSize) {
    Page<AccountVO> data = accountVOMapper.selectPage(
        new Page<>(pageNum, pageSize),
        //第几页(从1k),每页的数目
        new LambdaQueryWrapper<AccountVO>().eq(AccountVO::getRole, 1)
        //引入 LambdaQueryWrapper 作为条件筛选
    );
    return getResultMap(data.getRecords(), data.getTotal(), pageNum, pageSize);
}

5. 踩过的坑

5.1 SQL 关键字冲突

在 SQL 语句中,存在非常大关键字和字段名冲突的问题,为了解决这个问题,我们需要使用`` `进行加注。如果使用了关键字作为字段名甚至是表明,我们需要手动添加。

示例:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("`read`")
//read 是关键字,需要添加 @TableName 注解
public class Read implements Serializable {

    private static final long serialVersionUID = 657404720805159402L;

    @TableId(type = IdType.AUTO)
    private Integer id;

    @TableField("`user_id`")
    //如果字段和关键词冲突,需要使用 @TableField 进行注解
    private Long userId;

    private Long bookId;

    private LocalDateTime lendTime;

    @TableLogic
    private Byte enable;

    public Read(Long userId, Long bookId, LocalDateTime lendTime) {
        this.userId = userId;
        this.bookId = bookId;
        this.lendTime = lendTime;
    }
}

总结:如果对于难以解决的 You have an error in your SQL syntax(SQL 语句出错),可以尝试去 navicat 确认问题(navicat 会自动对冲突字添加引号)。

5.2 MySQL 类型映射

根据JDBC4.2的规范,Java日期类型和数据库日期类型关系如下:

Java 日期 数据库日期
java.sql.Date DATE
java.sql.Time TIME
java.sql.Timestamp TIMESTAMP
java.util.Calendar TIMESTAMP
java.util.Date TIMESTAMP
java.time.LocalDate DATE
java.time.LocalTime TIME
java.time.LocalDateTime TIMESTAMP
java.time.OffsetTime TIME_WITH_TIMEZONE
java.time.OffsetDatetime TIMESTAMP_WITH_TIMEZONE

5.3 MySQL 编码

MySQL在5.5.3之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode

老版本 MySQL 支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。三个字节的 UTF-8 最大能编码的 Unicode 字符是 0xffff,也就是 Unicode 中的基本多文种平面。所以 emoji 和之后新增字符就不能兼容。

参考文章

  1. MyBatis的执行流程详解
  2. MyBatis 3 官方文档
  3. Mybatis源码级教学
  4. 预编译语句介绍,以 MySQL 为例
  5. MyBatis-Plus 官方文档
  6. Java 8日期与数据库日期的映射关系
  7. MyBaits-Plus 解决关键字冲突
  8. 全面了解 MySQL 中 utf8 和 utf8mb4 的区别

文章作者: 陈鑫扬
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈鑫扬 !
评论
  目录