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);
}
}
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 就是这样一个中间层。
DataSource 接口同时还继承了一个 Wrapper 接口。从接口的命名上看,可以判断该接口应该起到一种包装器的作用,事实上,由于很多数据库供应商提供了超越标准 JDBC API 的扩展功能,所以,Wrapper 接口可以把一个由第三方供应商提供的、非 JDBC 标准的接口包装成标准接口。
在 JDBC 规范中,除了 DataSource 之外,Connection、Statement、ResultSet 等核心对象也都继承了这个接口。
上面的代码可以优化为:
// 创建池化的数据源
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 连接数据库有两种:
- 直接使用注释。在 mapper 的方法上添加特定方法进行操作,这样的操作有几个弊端。一是如果接口方法躲起来,在后续的管理方面并不是很好;二是这样写由于没有 xml 的错误提示,很容易写错,且不方便进行后续的修改;三是,如果需要写一些比较的操作,像是连表查询,注释就不能写复杂的 SQL 逻辑了;
- 在 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 辅助插件。
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 常规实现分页通常有三种方式:
- 直接在 Select 语句上增加数据库提供的分页关键字,然后在应用程序里面传递当前页,以及每页展示条数即可;
- 使用 Mybatis 提供的 RowBounds 对象,实现内存级别分页;
- 基于 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
表示第一个参数,其他位置的参数依次类推)。
最关注的多表问题,主要解决方法又有两个:
- 利用 Hibernate 的级联查询来实现;
- 创建一个结果集的接口来接收连表查询后的结果(主要还是使用这种方式)。
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 这样的情况。
// 以下是自动配置的个人模板,源模板结合 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 的背后却自动完成了分片操作,从而实现了对业务代码的零侵入:
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");
}
}
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 同样提供了分布式主键的实现机制,默认采用的是 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。
广播表
广播表表示每个数据源都会存在的表,表结构及其数据在每个数据库都保存一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
广播具有以下特性:
- 插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性;
- 查询操作,只从一个节点获取;
- 可以跟任何一个表进行 JOIN 操作。
7.4.2 读写分离
读写分离基于
7.4.3 重写 JDBC
在 ShardingSphere 中,实现与 JDBC 规范兼容性的基本策略就是采用了设计模式中的适配器模式(Adapter Pattern)。适配器模式通常被用作连接两个不兼容接口之间的桥梁,涉及为某一个接口加入独立的或不兼容的功能。
ShardingSphere 的分片引擎中提供了一系列 ShardingJdbcObject 来支持分片操作,包括 ShardingDataSource、ShardingConnection、ShardingStatement、ShardingPreparedStament 等。
参考文章
- MyBatis 的执行流程详解
- MyBatis 3 官方文档
- Mybatis 源码级教学
- 预编译语句介绍,以 MySQL 为例
- MyBatis-Plus 官方文档
- Java 8日期与数据库日期的映射关系
- SpringBoot 中使用 info 日志级别打印 MyBatis SQL 语句
- MyBatis 的两种分页方式 RowBounds 和 PageHelper
- MyBaits-Plus 解决关键字冲突
- Mybatis-plus 自动填充不生效或自动填充数据为 null 原因及解决方案
- 全面了解 MySQL 中 utf8 和 utf8mb4 的区别
- ShardingSphere 核心讲解
- Java SPI 详解
- I try to use custom sharding · Issue #18927
- ShardingSphere 学习