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

MyBatis 学习


1. JDBC

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

下面是一个简单的从数据库中获取对应 id user 的实例:

@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 = "123456";
    // 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);
    }
}

JDBC 整体规范架构

JDBC 运行流程

1.1 JdbcTemplate

JdbcTemplate 是 Spring 对 jdbc 操作数据库进行的封装,使得开发者可以直接在 Java 文件中编写 SQL,无需配置 xml 文件。

JdbcTemplate 结合模板方法和回调机制,动态实现组件之间的跳转,让 JDBC 代码以一个简洁的方式进行调用。

回调示意图

1.1.1 DataSource

DataSource 在 JDBC 规范中代表的是一种数据源,核心作用是获取数据库连接对象 Connection。在 JDBC 规范中,实际可以直接通过 DriverManager 获取 Connection。

为了提高性能,通常会建立一个中间层,该中间层将 DriverManager 生成的 Connection 存放到连接池中,然后从池中获取 Connection,可以认为,DataSource 就是这样一个中间层。

CommonDataSource 类层结构图

DataSource 接口同时还继承了一个 Wrapper 接口。从接口的命名上看,可以判断该接口应该起到一种包装器的作用,事实上,由于很多数据库供应商提供了超越标准 JDBC API 的扩展功能,所以,Wrapper 接口可以把一个由第三方供应商提供的、非 JDBC 标准的接口包装成标准接口。

在 JDBC 规范中,除了 DataSource 之外,Connection、Statement、ResultSet 等核心对象也都继承了这个接口。

继承了一个 Wrapper 接口

上面的代码可以优化为:

// 创建池化的数据源
PooledDataSource dataSource = new PooledDataSource ();
// 设置 MySQL Driver
dataSource.setDriver ("com.mysql.jdbc.Driver");
// 设置数据库 URL、用户名和密码
dataSource.setUrl ("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 获取连接
Connection connection = dataSource.getConnection();
// 执行查询
PreparedStatement statement = connection.prepareStatement ("select * from user");
// 获取查询结果进行处理
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {}
// 关闭资源
statement.close();
resultSet.close();
connection.close();

1.1.2 Connection

可以把 Connection 理解为一种会话(Session)机制。Connection 代表一个数据库连接,负责完成与数据库之间的通信。所有 SQL 的执行都是在某个特定 Connection 环境中进行的,同时它还提供了一组重载方法,分别用于创建 Statement 和 PreparedStatement。另一方面,Connection 也涉及事务相关的操作,为了实现分片操作,ShardingSphere 同样也实现了定制化的 Connection 类 ShardingConnection。

1.1.3 Statement

JDBC 规范中的 Statement 存在两种类型,一种是普通的 Statement,一种是支持预编译的 PreparedStatement。所谓预编译,是指数据库的编译器会对 SQL 语句提前编译,然后将预编译的结果缓存到数据库中,这样下次执行时就可以替换参数并直接使用编译过的语句,从而提高 SQL 的执行效率。

如果需要查询数据库中的数据,只需要调用 Statement 或 PreparedStatement 对象的 executeQuery 方法即可,该方法以 SQL 语句作为参数,执行完后返回一个 JDBC 的 ResultSet 对象。

Statement 或 PreparedStatement 中提供了一大批执行 SQL 更新和查询的重载方法。在 ShardingSphere 中,同样也提供了 ShardingStatement 和 ShardingPreparedStatement 这两个支持分片操作的 Statement 对象。

1.1.4 ResultSet

一旦通过 Statement 或 PreparedStatement 执行了 SQL 语句并获得了 ResultSet 对象后,那么就可以通过调用 Resulset 对象中的 next() 方法遍历整个结果集。如果 next() 方法返回为 true,就意味结果集中存在数据,则可以调用 ResultSet 对象的一系列 getXXX() 方法来取得对应的结果值。

对于分库分表操作而言,因为涉及从多个数据库或数据表中获取目标数据,势必需要对获取的结果进行归并。因此,ShardingSphere 中也提供了分片环境下的 ShardingResultSet 对象。

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
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台输出日志

之后在 idea 中的数据库一栏,进行数据库的连接就可以方便的管理和处理 SQL 语句了当然,可以提前设置 idea 的数据库为 MySQL,这样在写 xml 文件的时候就会有代码提示了,最好也装一下 idea 中的自带的 MyBatis 辅助插件。

SQL 日志打印

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 可以用来替代任意实体类,当数据比较复杂时,需要使用临时传参时可以考虑使用,可以考虑使用。在 SpringBoot 中,也可以通过 DTO、VO 等规范,规范类返回。

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 {
    @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>

2.5 动态 SQL

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

2.5.1 if

某些 SQL 语句提供可选的参数输入,可以使用 if 标签。下面的语句实现了 title 和 author 参数的可选搜索。

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

2.5.2 where

上面的 SQL 仍在存在一个隐藏的问题,就是拼接的问题。将 XML 文件改写成这种情况时,会有两种情况出现问题。一是没有任何参数传入,SQL 语句变为 SELECT * FROM BLOG WHERE。或者只进行第二参数的匹配,SELECT * FROM BLOG WHERE AND title like ‘someTitle’。这俩情况都会触发错误。

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

在这种情况可以使用 where 标签,where 元素只会在子元素返回任何内容的情况下才插入“WHERE”子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除(MyBatis 的标签无法在 idea 的数据库会话测试中触发,需要注意)。

<select id="findAccount" resultType="sast.freshcup.entity.Account">
    select *
    from account
    <where>
        <if test="uid != null">
            uid = #{uid}
        </if>
        <if test="`role` != null">
            AND `role` = #{role}
        </if>
    </where>
</select>

如果 where 的功能还需要再细化,可以使用 trim 定制 where 功能。prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。where 等价为:

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

无法在会话中使用

set 标签会动态在行首插入 SET 关键字,并删除额外的逗号。

<!--使用 set 配合 update 例子-->
<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

可以用 trim 实现同样的效果:

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

2.5.3 choose

choose 类似于 Java 中的 switch,可以将选择多个参数其中一个继续传入,通过 otherwise 传入无参默认值。

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

2.5.4 foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
    SELECT *
    FROM POST P
    <where>
        <foreach item="item" index="index" collection="list"
                 open="ID in (" separator="," close=")" nullable="true">
            #{item}
        </foreach>
    </where>
</select>

2.6 实现分页

MyBatis 常规实现分页通常有三种方式:

  1. 直接在 Select 语句上增加数据库提供的分页关键字,然后在应用程序里面传递当前页,以及每页展示条数即可;
  2. 使用 Mybatis 提供的 RowBounds 对象,实现内存级别分页;
  3. 基于 Mybatis 里面的 Interceptor 拦截器,在 select 语句执行之前动态拼接分页关键字(插件(PageHelper)及(MyBaits-Plus、tkmybatis)框架实现这些插件本质上也是使用 Mybatis 的拦截器来实现的)。

2.6.1 物理分页

<select id="findByPager" resultType="com.xxx.mybatis.domain.User">
	select * from xx_user limit #{page},#{size}
</select>

在传参到 mapper 时手动计算好 size 的大小。

public Pager<User> findByPager(int page,int size){
	Map<String, Object> params = new HashMap<String, Object>();
	params.put("page", (page-1)*size);
	params.put("size", size);
	Pager<User> pager = new Pager<User>();
	List<User> list = userDao.findByPager(params);
	pager.setRows(list);
	pager.setTotal(userDao.count());
	return pager;
}

2.6.2 Rowbounds

Mybatis 的逻辑分页比较简单,简单来说就是取出所有满足条件的数据,然后舍弃掉前面 offset 条数据,然后再取剩下的数据的 limit 条。在接口中传入 RowBounds,SQL 会自动实现分页功能。Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。

//接口方法
public List<Honor> getHonorList(HashMap<String, Object> maps,RowBounds rowBounds);

//调用方法
RowBounds rowBounds = new RowBounds(offset, page.getPageSize()); 
// offset起始行 
// limit是当前页显示多少条数据

RowBounds  rowBounds = new RowBounds(2, 2);
List<Honor> honors = studentMapper.getHonorList(maps,rowBounds);

2.6.3 Interceptor

分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 SQL,然后重写 SQL,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

举例:select * from student,拦截 SQL 后重写为:select t.* from (select * from student)t limit 0,10

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 中,也是满足了这个需求,并且很多事情都直接让框架自动执行。甚至有一个代码生成器可以自动生成需要的代码。(虽然那个东西十分难用,样例好像都是错的)

如果追求自动代码生成,可以在 idea 中使用插件进行自定义配置。

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

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-core</artifactId>
    <version>3.5.1</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 自定义使用的方法

通常更推荐使用新版本的 LambadaQueryWrapper,lambda 形式的条件构造器可以不用手动指定数据库字段,直接使用 lambda 表达式中的引用形式。(mybatis-plus-core 的依赖项中才存在 LambadaQueryWrapper)

@Service("classroomService")
public class ClassroomServiceImpl implements ClassroomService {
    private final AccountMapper accountMapper;

    public ClassroomServiceImpl(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }
    
    public Account getAccountByUsername(String username) {
        LambdaQueryWrapper<Account> queryWrapper = new LambdaQueryWrapper<>();
        /*
        queryWrapper.eq(Account::getUsername, username);
        QueryWrapper<Account> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(Account::getUsername, username);
        本身 QueryWrapper 也可以用 lambda() 方法进行连接
        wrapper.eq("username", username);
        */
        return accountMapper.selectOne(queryWrapper);
    }
}

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 或者其他类推的,指定在特定命令时进行填入(自动填入需要的类型需要和 handler 保持一致)。

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        // 进行强制填充
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

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 分页

MybatisPlusInterceptor 会拦截 Executor 类中的 query 和 update 方法,在执行前自动处理分页、逻辑删除、填充字段、乐观锁等操作。在使用自带的分页功能之前,需要引入插件的配置:

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 本质都是通过 MyBatis interceptor 实现分页
        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 类型映射

根据 JDBC 4.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 和之后新增字符就不能兼容。

6. 自动生成代码

在 idea 中,可以使用 easy code 这个插件自动生成对应的 controller、entity 等等。

可以根据多种情况进行自定义设计,配置也支持 MyBatis/MyBatis-Plus 这样的情况。

使用 idea 连接数据库选中需要生成的表

// 以下是自动配置的个人模板,源模板结合 MyBatis/MyBatis-Plus
// controller 配置
##导入宏定义
$!{define.vm}

##设置表后缀(宏定义)
#setTableSuffix("Controller")

##保存文件(宏定义)
#save("/controller", "Controller.java")

##包路径(宏定义)
#setPackageSuffix("controller")

##定义服务名
#set($serviceName = $!tool.append($!tool.firstLowerCase($!tableInfo.name), "Service"))

##定义实体对象名
#set($entityName = $!tool.firstLowerCase($!tableInfo.name))

$!autoImport
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

##表注释(宏定义)
#tableComment("表控制层")
@RestController
@Slf4j
@RequestMapping("$!tool.firstLowerCase($!tableInfo.name)")
public class $!{tableName} {

}

// dao xml 配置
##引入mybatis支持
$!{mybatisSupport.vm}

##设置保存名称与保存位置
$!callback.setFileName($tool.append($!{tableInfo.name}, "Dao.xml"))
$!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mapper"))

##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
    #set($pk = $tableInfo.pkColumn.get(0))
#end

<?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="$!{tableInfo.savePackageName}.dao.$!{tableInfo.name}Dao">

  
</mapper>
// dao 层配置
##导入宏定义
$!{define.vm}

##设置表后缀(宏定义)
#setTableSuffix("Dao")

##保存文件(宏定义)
#save("/dao", "Dao.java")

##包路径(宏定义)
#setPackageSuffix("dao")

$!autoImport
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import $!{tableInfo.savePackageName}.entity.$!tableInfo.name;
import org.springframework.stereotype.Repository;

##表注释(宏定义)
#tableComment("表数据库访问层")
@Repository
public interface $!{tableName} extends BaseMapper<$!tableInfo.name> {

}

// entity 配置
##导入宏定义
$!{define.vm}

##保存文件(宏定义)
#save("/entity", ".java")

##包路径(宏定义)
#setPackageSuffix("entity")

##自动导入包(全局变量)
$!autoImport
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

##表注释(宏定义)
#tableComment("表实体类")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class $!{tableInfo.name} implements Serializable {
// serialVersionUID 是自动装配的识别 ID
// 目的是在产生冲突时通过 ID 进行区分
private static final long serialVersionUID = $!tool.serial();

#foreach($column in $tableInfo.fullColumn)
    #if(${column.comment})//${column.comment}#end

    private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end

}

// service 配置
##导入宏定义
$!{define.vm}

##设置表后缀(宏定义)
#setTableSuffix("Service")

##保存文件(宏定义)
#save("/service", "Service.java")

##包路径(宏定义)
#setPackageSuffix("service")

$!autoImport
import $!{tableInfo.savePackageName}.entity.$!tableInfo.name;

##表注释(宏定义)
#tableComment("表服务接口")
public interface $!{tableName} {

}

// ServiceImpl 配置
##导入宏定义
$!{define.vm}

##设置表后缀(宏定义)
#setTableSuffix("ServiceImpl")

##保存文件(宏定义)
#save("/service/impl", "ServiceImpl.java")

##包路径(宏定义)
#setPackageSuffix("service.impl")

$!autoImport
import org.springframework.stereotype.Service;
import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service;

##表注释(宏定义)
#tableComment("表服务实现类")
@Service("$!tool.firstLowerCase($tableInfo.name)Service")
public class $!{tableName} implements $!{tableInfo.name}Service {

}

7. ShardingSphere

由于数据量逐渐增多,在数据库设计时候考虑垂直分库和垂直分表。不要马上考虑做水平切分,首先考虑缓存处理,读写分离,使用索引等等方式,如果这些方式不能根本解决问题了,再考虑做水平分库和水平分表。

分库分表问题:跨节点连接查询问题(分页、排序)、多数据源管理问题。

除了分库分表的问题,ShardingSphere 也提供了读写分离的解决方式(最终原理是应用 MySQL 中的 binlog)。具体内容已经在数据库八股中有详细提及。

7.1 Sharding(分片)

无论是分库还是分表,都是把数据划分成不同的数据片,并存储在不同的目标对象中。

具体的分片方式涉及实现分库分表的不同解决方案。

7.1.1 客户端分片

客户端分片,相当于在数据库的客户端就实现了分片规则。将分片处理的工作进行前置,客户端管理和维护着所有的分片逻辑,并决定每次 SQL 执行所对应的目标数据库和数据表。

应用层分片

最为简单的方式就是应用层分片,在应用程序中直接维护着分片规则和分片逻辑。

在应用程序中直接维护着分片规则和分片逻辑

通常会将分片规则的处理逻辑打包成一个公共 JAR 包,其他业务开发人员只需要在代码工程中引入这个 JAR 包即可。针对这种方案,因为没有独立的服务器组件,所以也不需要专门维护某一个具体的中间件。

直接在业务代码中嵌入分片组件的方法也有明显的缺点:

  • 一方面,由于分片逻辑侵入到了业务代码中,业务开发人员在理解业务的基础上还需要掌握分片规则的处理方式,增加了开发和维护成本;
  • 另一方面,一旦出现问题,也只能依赖业务开发人员通过分析代码来找到原因,而无法把这部分工作抽离出来让专门的中间件团队进行完成。

重写 JDBC 协议

在 JDBC 协议层面嵌入分片规则。这样,业务开发人员还是使用与 JDBC 规范完全兼容的一套 API 来操作数据库,但这套 API 的背后却自动完成了分片操作,从而实现了对业务代码的零侵入:

重写 JDBC 协议

ShardingSphere JDBC 是重写 JDBC 规范以实现客户端分片的典型实现框架。

7.1.2 代理服务器分片

采用了代理机制,在应用层和数据库层之间添加一个代理层。有了代理层之后,就可以把分片规则集中维护在这个代理层中,并对外提供与 JDBC 兼容的 API 给到应用层。这样,应用层的业务开发人员就不用关心具体的分片规则,而只需要完成业务逻辑的实现:

代理服务器分片结构

解放了业务开发人员对分片规则的管理工作,而缺点就是添加了一层代理层,所以天生具有代理机制所带来的一些问题,比方说因为新增了一层网络传输对性能所产生的影响。在 ShardingSphere 3.X 版本中,也添加了 Sharding-Proxy 模块来实现代理服务器分片。

7.2 核心功能

ShardingSphere 在设计上采用了微内核(MicroKernel)架构模式,来确保系统具有高度可扩展性。微内核架构包含两部分组件,即内核系统和插件。使用微内核架构对系统进行升级,要做的只是用新插件替换旧插件,而不需要改变整个系统架构:

微内核架构模式

在 ShardingSphere 中,抽象了一大批插件接口,包含用实现 SQL 解析的 SQLParserEntry、用于实现配置中心的 ConfigCenter、用于数据脱敏的 ShardingEncryptor,以及用于数据库治理的注册中心接口 RegistryCenter 等。开发人员完全可以根据自己的需要,基于这些插件定义来提供定制化实现,并动态加载到 ShardingSphere 运行时环境中。

分布式事务支持

7.3 SPI & API

SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。

  • API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用;
  • SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

7.3.1 简单 SPI

public interface UploadCDN {     
    void upload(String url);
}

public class QiyiCDN implements UploadCDN {  //上传爱奇艺cdn
    @Override
    public void upload(String url) {
        System.out.println("upload to qiyi cdn");
    }
}

public class ChinaNetCDN implements UploadCDN { // 上传网宿cdn
    @Override
    public void upload(String url) {
        System.out.println("upload to chinaNet cdn");
    }
}

public static void main(String[] args) {
    ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class);
    for (UploadCDN u : uploadCDN) {
        u.upload("filePath");
    }
}

创建目录,指定 SPI

调用流程

7.3.2 dubbo SPI

dubbo 作为一个高度可扩展的 rpc 框架,也依赖于 Java 的 SPI,并且 dubbo 对 Java 原生的 SPI 机制作出了一定的扩展,使得其功能更加强大。

从上面的 Java SPI 的原理中可以了解到,Java 的 SPI 机制有着如下的弊端:

  • 只能遍历所有的实现,并全部实例化;
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们;
  • 扩展如果依赖其他的扩展,做不到自动注入和装配;;
  • 扩展很难和其他的框架集成,比如扩展里面依赖了一个 Spring Bean,原生的 Java SPI 不支持。

dubbo 的 SPI 有如下几个概念:

  • 扩展点:一个接口;
  • 扩展:扩展(接口)的实现;
  • 扩展自适应实例:其实就是一个 Extension 的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。dubbo 会根据接口中的参数,自动地决定选择哪个实现;
  • @SPI:该注解作用于扩展点的接口上,表明该接口是一个扩展点;
  • @Adaptive:@Adaptive 注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo 在为扩展点生成自适应实例时,如果方法有 @Adaptive注解,会为该方法生成对应的代码。

7.4 ShardingSphere JDBC

是轻量级的 Java 框架,是增强版的 JDBC 驱动主要目的是简化对分库分表之后数据相关操作。

使用 ShardingSphere JDBC 进行分表分库的过程

在本地数据库中可以使用自增主键来完成主键生成。在分片场景下,需要考虑主键在各个数据库中的全局唯一性。ShardingSphere 同样提供了分布式主键的实现机制,默认采用的是 SnowFlake(雪花)算法。

添加需要的依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.1.2</version>
</dependency>

7.4.1 水平分表分库

SpringBoot 默认连接池为 hikari,com.zaxxer.hikari.HikariDataSource。

通过 properties 对分表规则进行分配。添加课程 id 是偶数把数据添加 course_1,如果奇数添加到 course_2(官方文档中使用 properties 举例,这里使用 yaml 进行简化)。针对 use_id,偶数放入 course_db_1,奇数放入 course_db_2

spring:
  main:
    allow-bean-definition-overriding: true

  shardingsphere:
    datasource:
      names: m1,m2,m0,
      # m1 库
      m1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://127.0.0.1:3306/course_db_1?serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true
        username: root
        password: 123456
      # m2 库
      m2:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://127.0.0.1:3306/course_db_2?serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true
        username: root
        password: 123456
      # m0 库
      m0:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://127.0.0.1:3306/course_db?serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true
        username: root
        password: 123456
    rules:
      sharding:
        # 如果遇到多表联查的情况,ShardingSphere 可以防止出现笛卡尔积的情况
        binding-tables[0]: course,user
        binding-tables[1]: course
        binding-tables[2]: user

        # 绑定公共表
        broadcast-tables[0]: t_udict
        # 设置表规则
        tables:
          # course 表规则
          # 自定义表规则,名称需要跟表一致
          course:
            # 配置表的分布情况
            # 数据源名
            actual-data-nodes: m$->{1..2}.course_$->{1..2}
            # 配置分表策略
            table-strategy:
              # 分片列名称与自动分片算法名称
              # 用于单分片键的标准分片场景
              standard:
                sharding-column: cid
                sharding-algorithm-name: course-inline
            key-generate-strategy:
              column: cid
              key-generator-name: snowflake
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: database-inline
            # audit-strategy: 添加审计
          user:
            actual-data-nodes: m$->{0}.user
            key-generate-strategy:
              column: user_id
              key-generator-name: snowflake
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: user-inline
          t_udict:
            key-generate-strategy:
              column: dict_id
              key-generator-name: snowflake

        # 配置默认配置
        default-table-strategy:
          standard:
            sharding-column: cid
            sharding-algorithm-name: default-inline
        default-key-generate-strategy:
          column: dict_id
          key-generator-name: snowflake

        # 为 key-generator-name 配置算法类型
        sharding-algorithms:
          default-inline:
            type: INLINE
            props:
              algorithm-expression: m$->{user_id % 2 + 1}
          course-inline:
            type: INLINE
            props:
              algorithm-expression: course_$->{cid % 2 + 1}
          database-inline:
            type: INLINE
            props:
              algorithm-expression: m$->{user_id % 2 + 1}
          user-inline:
            type: INLINE
            props:
              algorithm-expression: user

        key-generators:
          snowflake:
            type: SNOWFLAKE
    # 显示 SQL 日志
    props:
      sql-show: true

ShardingSphere 已经在开发文档中写了详细的说明,具体可以查询文档(文档的例子说明不够具体)。

如果启动或者测试时有 Data Source Empty 等其他错误,多半是配置文件有问题,一定要保证名称和数据库/表的名称对应一致。

在使用公共表和专库专表的时候需要指定 @TableName

雪花算法

雪花算法一般用来实现全局唯一的业务主键,解决分库分表之后主键 id 的唯一性问题。

实现全局唯一也可以通过 UUID、Redis 原子递增、数据库全局表的自增 id 等等。

由一个 64 位的 long 类型数字组成,分为四个部分:

  • 第一部分,用 1 个 bit 表示符号位,一般情况下是 0 ;
  • 第二部分,用 41 个bit 来表示时间戳,使用系统时间的毫秒数;
  • 第三部分,用 10 个 bit 来记录工作机器 id,这样就可以保证在多个服务器上生成的 id 的唯一性。如果存在跨机房部署,我们还可以把它分成两个 5 bit,前面 5 个 bit 可以表示机房 id,后面 5 个 bit 可以表示机器 id;
  • 第四个部分,用 12 个 bit 表示序列号,表示一个递增序列,用来记录同毫秒内产生的不同 id。

生成示意图

广播表

广播表表示每个数据源都会存在的表,表结构及其数据在每个数据库都保存一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

广播具有以下特性:

  1. 插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性;
  2. 查询操作,只从一个节点获取;
  3. 可以跟任何一个表进行 JOIN 操作。

7.4.2 读写分离

读写分离基于

读写分离原理

7.4.3 重写 JDBC

在 ShardingSphere 中,实现与 JDBC 规范兼容性的基本策略就是采用了设计模式中的适配器模式(Adapter Pattern)。适配器模式通常被用作连接两个不兼容接口之间的桥梁,涉及为某一个接口加入独立的或不兼容的功能。

ShardingSphere 的分片引擎中提供了一系列 ShardingJdbcObject 来支持分片操作,包括 ShardingDataSource、ShardingConnection、ShardingStatement、ShardingPreparedStament 等。

参考文章

  1. MyBatis 的执行流程详解
  2. MyBatis 3 官方文档
  3. Mybatis 源码级教学
  4. 预编译语句介绍,以 MySQL 为例
  5. MyBatis-Plus 官方文档
  6. Java 8日期与数据库日期的映射关系
  7. SpringBoot 中使用 info 日志级别打印 MyBatis SQL 语句
  8. MyBatis 的两种分页方式 RowBounds 和 PageHelper
  9. MyBaits-Plus 解决关键字冲突
  10. Mybatis-plus 自动填充不生效或自动填充数据为 null 原因及解决方案
  11. 全面了解 MySQL 中 utf8 和 utf8mb4 的区别
  12. ShardingSphere 核心讲解
  13. Java SPI 详解
  14. I try to use custom sharding · Issue #18927
  15. ShardingSphere 学习

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