MyBatis-Flex是一个优雅的MyBatis增强框架,它非常轻量、同时拥有极高的性能与灵活性。我们可以使用MyBatis-Flex链接任何数据库,其内置的QueryWrapper帮助我们极大的减少了SQL编写的工作的同时,减少出错的可能性。
1、轻量
除了MyBatis,没有任何第三方依赖、没有任何拦截器,其原理是通过
SqlProvider
的方式实现的。同时,在执行的过程中,没有任何的Sql解析(Parse)。这带来的几个好处:高性能,代码易于跟踪调试,可把控性高。
2、灵活
支持Entity的增删改查、以及分页查询的同时,MyBatis-Flex提供了Db + Row工具,可以无需实体类对数据库进行增删改查以及分页查询。与此同时,MyBatis-Flex 内置的QueryWrapper可以轻易的实现多表查询、连接查询、子查询等。
3、强大
支持任意关系型数据库,还可以通过方言持续扩展,同时支持复合主键、逻辑删除、乐观锁配置、数据脱敏、数据审计、数据填充等功能。
示例采用IDEA作为开发工具,maven构建工程,使用Java语言开发,数据库使用MySQL,选择2.7.15版本的Spring Boot框架(低版本会出现TransactionManager
接口不存在的情况),选择的mysql驱动包是mysql-connector-j
(避免出现HikariDataSource
异常问题)。
1、创建maven工程flex
,引入相关的依赖
<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.0modelVersion>
<groupId>org.qcx.mybatisgroupId>
<artifactId>flexartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.15version>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.mybatis-flexgroupId>
<artifactId>mybatis-flex-spring-boot-starterartifactId>
<version>1.6.5version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
2、定义表结构并初始化数据
create table if not exists `tb_account`
(
`id` integer primary key auto_increment,
`user_name` varchar(100),
`age` integer,
`birthday` datetime
);
insert into tb_account(id, user_name, age, birthday)
values (1, '张三', 20, '2023-01-11'),
(2, '李四', 22, '2023-09-16');
3、对Spring Boot项目进行配置
(1)新建配置文件application.yml
,添加数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: root
(2)新建启动类Application.java
package org.qcx.mybatis.flex;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
//扫描Mapper文件夹
@MapperScan(basePackages = "org.qcx.mybatis.flex.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4、编写实体类Account.java
和Mapper接口AccountMapper.java
package org.qcx.mybatis.flex.entity;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import java.util.Date;
@Data
@Table("tb_account") //设置实体类与表名的映射关系
public class Account {
@Id(keyType = KeyType.Auto) //标识主键为自增
private Long id;
private String userName;
private Integer age;
private Date birthday;
}
package org.qcx.mybatis.flex.mapper;
import com.mybatisflex.core.BaseMapper;
import org.qcx.mybatis.flex.entity.Account;
public interface AccountMapper extends BaseMapper<Account> {
}
5、添加测试类ApplicationTest.java
进行功能测试,MyBatis-Flex使用了APT(Annotation Processing Tool)技术,在项目编译的时候,会自动根据Entity类定义的字段生成“ACCOUNT”类以及Entity对应的Mapper类,通过开发工具构建工具或者执行mvn clean package
都可以自动生成。
package org.qcx.mybatis.flex;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.qcx.mybatis.flex.entity.Account;
import org.qcx.mybatis.flex.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.qcx.mybatis.flex.entity.table.AccountTableDef.ACCOUNT;
@SpringBootTest
public class ApplicationTest {
@Autowired
private AccountMapper accountMapper;
@Test //Test注解的包不是org.junit
public void contextLoads() {
QueryWrapper queryWrapper = QueryWrapper.create()
.select()
.where(ACCOUNT.AGE.eq("20"));
Account account = accountMapper.selectOneByQuery(queryWrapper);
System.out.println(account);
}
}
Account(id=1, userName=张三, age=20, birthday=Wed Jan 11 00:00:00 CST 2023)
MyBatis-Flex主要是和MyBatis-Plus
和Fluent-MyBatis
对比。MyBatis-Plus是老牌的MyBatis增强框架,Fluent-MyBatis是阿里云开发的MyBatis增强框架。
功能或特点 | MyBatis-Flex | MyBatis-Plus | Fluent-MyBatis |
---|---|---|---|
对 entity 的基本增删改查 | ✅ | ✅ | ✅ |
分页查询 | ✅ | ✅ | ✅ |
分页查询之总量缓存 | ✅ | ✅ | ❌ |
分页查询无 SQL 解析设计(更轻量,及更高性能) | ✅ | ❌ | ✅ |
多表查询: from 多张表 | ✅ | ❌ | ❌ |
多表查询: left join、inner join 等等 | ✅ | ❌ | ✅ |
多表查询: union,union all | ✅ | ❌ | ✅ |
单主键配置 | ✅ | ✅ | ✅ |
多种 id 生成策略 | ✅ | ✅ | ✅ |
支持多主键、复合主键 | ✅ | ❌ | ❌ |
字段的 typeHandler 配置 | ✅ | ✅ | ✅ |
除了 MyBatis,无其他第三方依赖(更轻量) | ✅ | ❌ | ❌ |
QueryWrapper 是否支持在微服务项目下进行 RPC 传输 | ✅ | ❌ | 未知 |
逻辑删除 | ✅ | ✅ | ✅ |
乐观锁 | ✅ | ✅ | ✅ |
SQL 审计 | ✅ | ❌ | ❌ |
数据填充 | ✅ | ✅ | ✅ |
数据脱敏 | ✅ | ✔️ (收费) | ❌ |
字段权限 | ✅ | ✔️ (收费) | ❌ |
字段加密 | ✅ | ✔️ (收费) | ❌ |
字典回写 | ✅ | ✔️ (收费) | ❌ |
Db + Row | ✅ | ❌ | ❌ |
Entity 监听 | ✅ | ❌ | ❌ |
多数据源支持 | ✅ | 借助其他框架或收费 | ❌ |
多数据源是否支持 Spring 的事务管理,比如 @Transactional 和 TransactionTemplate 等 |
✅ | ❌ | ❌ |
多数据源是否支持 “非Spring” 项目 | ✅ | ❌ | ❌ |
多租户 | ✅ | ✅ | ❌ |
动态表名 | ✅ | ✅ | ❌ |
动态 Schema | ✅ | ❌ | ❌ |
MyBatis-Flex:
QueryWrapper query = QueryWrapper.create()
//条件为空时自动忽略
.where(EMPLOYEE.LAST_NAME.like(searchWord))
.and(EMPLOYEE.GENDER.eq(1))
.and(EMPLOYEE.AGE.gt(24));
List<Employee> employees = employeeMapper.selectListByQuery(query);
MyBatis-Plus:
QueryWrapper<Employee> queryWrapper = Wrappers.query()
.like(searchWord != null, "last_name", searchWord)
.eq("gender", 1)
.gt("age", 24);
List<Employee> employees = employeeMapper.selectList(queryWrapper);
或者Lambda写法:
LambdaQueryWrapper<Employee> queryWrapper = Wrappers.<Employee>lambdaQuery()
.like(StringUtils.isNotEmpty(searchWord), Employee::getUserName,"B")
.eq(Employee::getGender, 1)
.gt(Employee::getAge, 24);
List<Employee> employees = employeeMapper.selectList(queryWrapper);
Mybatis-Flex:
QueryWrapper query = QueryWrapper.create()
.select(
ACCOUNT.ID,
ACCOUNT.USER_NAME,
max(ACCOUNT.BIRTHDAY),
avg(ACCOUNT.SEX).as("sex_avg")
);
List<Employee> employees = employeeMapper.selectListByQuery(query);
MyBatis-Plus:
QueryWrapper<Employee> queryWrapper = Wrappers.query()
.select(
"id",
"user_name",
"max(birthday)",
"avg(birthday) as sex_avg"
);
List<Employee> employees = employeeMapper.selectList(queryWrapper);
假设我们需要构建SQL查询,在SQL中添加括号。
SELECT * FROM tb_account
WHERE id >= 100
AND (sex = 1 OR sex = 2)
OR (age IN (18,19,20) AND user_name LIKE "%michael%" )
MyBatis-Flex:
QueryWrapper query = QueryWrapper.create()
.where(ACCOUNT.ID.ge(100))
.and(ACCOUNT.SEX.eq(1).or(ACCOUNT.SEX.eq(2)))
.or(ACCOUNT.AGE.in(18, 19, 20).and(ACCOUNT.USER_NAME.like("michael")));
MyBatis-Plus:
QueryWrapper<Employee> query = Wrappers.query()
.ge("id", 100)
.and(i -> i.eq("sex", 1).or(x -> x.eq("sex", 2)))
.or(i -> i.in("age", 18, 19, 20).like("user_name", "michael"));
// or lambda
LambdaQueryWrapper<Employee> query = Wrappers.<Employee>lambdaQuery()
.ge(Employee::getId, 100)
.and(i -> i.eq(Employee::getSex, 1).or(x -> x.eq(Employee::getSex, 2)))
.or(i -> i.in(Employee::getAge, 18, 19, 20).like(Employee::getUserName, "michael"));
MyBatis-Flex:
QueryWrapper query = QueryWrapper.create()
.select().from(ACCOUNT)
.leftJoin(ARTICLE).on(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID))
.where(ACCOUNT.AGE.ge(10));
List<Account> accounts = mapper.selectListByQuery(query);
假设查询如下SQL:
SELECT a.id, a.user_name, b.id AS articleId, b.title
FROM tb_account AS a, tb_article AS b
WHERE a.id = b.account_id
MyBatis-Flex:
QueryWrapper query = new QueryWrapper()
.select(
ACCOUNT.ID
, ACCOUNT.USER_NAME
, ARTICLE.ID.as("articleId")
, ARTICLE.TITLE)
.from(ACCOUNT.as("a"), ARTICLE.as("b"))
.where(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID));
假设一个实体类Account中,需要更新userName
为Tom,age
为20,birthday
为null。
update tb_account
set user_name = "michael", age = 18, birthday = null
where id = 100
MyBatis-Flex:
Account account = UpdateEntity.of(Account.class);
account.setId(100); //设置主键
account.setUserName("michael");
account.setAge(18);
account.setBirthday(null);
accountMapper.update(account);
MyBatis-Plus:
UpdateWrapper<Account> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", 100);
updateWrapper.set("user_name", "michael");
updateWrapper.set("age", 18);
updateWrapper.set("birthday", null);
accountMapper.update(null, updateWrapper);
MyBatis-Flex:
QueryWrapper queryWrapper = new QueryWrapper()
.where(FLEX_ACCOUNT.ID.ge(100));
mapper.paginate(page, pageSize, 20000, queryWrapper);
MyBatis-Plus:
LambdaQueryWrapper<PlusAccount> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(PlusAccount::getId, 100);
queryWrapper.eq(PlusAccount::getEmail, "[email protected]");
Page<PlusAccount> p = Page.of(page, pageSize, 20000, false);
mapper.selectPage(p, queryWrapper);
使用MyBatis-Flex增强框架不会影响原有的MyBatis的任何功能。
@select
等原生注解MyBatis提供了@Insert
、@Delete
、@Update
、@Select
注解,用于对Mapper的方法进行配置,用于原生编写原生 SQL 进行增删改查。
package org.qcx.mybatis.flex.mapper;
import com.mybatisflex.core.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.qcx.mybatis.flex.entity.Account;
public interface AccountMapper extends BaseMapper<Account> {
@Select("select * from tb_account where id=#{id}")
Account selectById(@Param("id") Object id);
}
使用xml方法需要在application.yml
中配置xml加载路径。
mybatis-flex:
mapper-locations:
- classpath*:/mapper/*.xml
mapper:
public interface AccountMapper extends BaseMapper<Account> {
Account selectByName(@Param("name") String name);
}
xml:
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.qcx.mybatis.flex.mapper.AccountMapper">
<select id="selectByName" resultType="org.qcx.mybatis.flex.entity.Account">
select * from `tb_account` where `user_name` = #{name}
select>
mapper>
XML分页在MyBatis-Flex的v1.5.5版本开始提供的XML分页解决方案,方便使用XML对数据进行分页。
QueryWrapper qw = QueryWrapper.create()
.where(Account::getAge).eq(18)
.and(Account::getId).ge(0);
//selectByName是
Page<Account> accountPage = ccountMapper
.xmlPaginate("selectByName", Page.of(1, 10), qw);
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.qcx.mybatis.flex.mapper.AccountMapper">
<select id="selectByName" resultType="org.qcx.mybatis.flex.entity.Account">
select * from `tb_account` ${qwSql} limit ${pageOffset}, ${pageSize}
select>
<select id="selectByName_COUNT" resultType="long">
select count(*) from `tb_account` ${qwSql}
select>
mapper>
QueryWrapper qw = QueryWrapper.create()
.where(Account::getAge).eq(18)
.and(Account::getId).ge(0);
Map<String, Object> otherParams = new HashMap<>();
otherParams.put("otherName", "tom");
Page<Account> accountPage = ccountMapper
.xmlPaginate("selectByName", Page.of(1, 10), qw, otherParams);
我们可以在XML中直接使用自定义参数:
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.qcx.mybatis.flex.mapper.AccountMapper">
<select id="selectByName" resultType="org.qcx.mybatis.flex.entity.Account">
select * from `tb_account` ${qwSql}
and user_name = #{otherName}
limit ${pageOffset}, ${pageSize}
select>
<select id="selectByName_COUNT" resultType="long">
select count(*) from `tb_account` ${qwSql}
and user_name = #{otherName}
select>
mapper>
MyBatis-Flex支持的数据库类型如下表,还可以通过自定义方言的方式,添加更多的数据库支持。
数据库 | 描述 |
---|---|
mysql | MySQL 数据库 |
mariadb | MariaDB 数据库 |
oracle | Oracle11g 及以下数据库 |
oracle12c | Oracle12c 及以上数据库 |
db2 | DB2 数据库 |
H2 | H2 数据库 |
hsql | HSQL 数据库 |
sqlite | SQLite 数据库 |
postgresql | PostgreSQL 数据库 |
sqlserver2005 | SQLServer2005 数据库 |
sqlserver | SQLServer 数据库 |
dm | 达梦数据库 |
xugu | 虚谷数据库 |
kingbasees | 人大金仓数据库 |
phoenix | Phoenix HBase 数据库 |
gauss | Gauss 数据库 |
clickhouse | ClickHouse 数据库 |
gbase | 南大通用(华库)数据库 |
gbase-8s | 南大通用数据库 GBase 8s |
oscar | 神通数据库 |
sybase | Sybase ASE 数据库 |
OceanBase | OceanBase 数据库 |
Firebird | Firebird 数据库 |
derby | Derby 数据库 |
highgo | 瀚高数据库 |
cubrid | CUBRID 数据库 |
goldilocks | GOLDILOCKS 数据库 |
csiidb | CSIIDB 数据库 |
hana | SAP_HANA 数据库 |
impala | Impala 数据库 |
vertica | Vertica 数据库 |
xcloud | 行云数据库 |
redshift | 亚马逊 redshift 数据库 |
openGauss | 华为 openGauss 数据库 |
TDengine | TDengine 数据库 |
informix | Informix 数据库 |
greenplum | Greenplum 数据库 |
uxdb | 优炫数据库 |
Doris | Doris数据库 |
Hive SQL | Hive 数据库 |
lealone | Lealone 数据库 |
sinodb | 星瑞格数据库 |
在某些场景下,需要实现自己的SQL生成逻辑时,可以通过实现自己的方言达到这个目的。
1、编写自己的方言类,实现IDialect
接口;
2、通过DialectFactory.registerDialect()
方法注册自己的方言。
MyBatis-Flex内置了一个BaseMapper
接口,实现了基本的增删改查功能以及分页查询功能。
BaseMapper
接口提供了insert和insertBatch方法用于新增数据;
insert(entity)
:插入实体类数据,不忽略 null
值。insertSelective(entity)
:插入实体类数据,但是忽略 null
的数据,只对有值的内容进行插入。这样的好处是数据库已经配置了一些默认值,这些默认值才会生效。insert(entity, ignoreNulls)
:插入实体类数据。insertWithPk(entity)
:插入带有主键的实体类,不忽略 null
值。insertSelectiveWithPk(entity)
:插入带有主键的实体类,忽略 null
值。insertWithPk(entity, ignoreNulls)
:带有主键的插入,此时实体类不会经过主键生成器生成主键。insertBatch(entities)
:批量插入实体类数据,只会根据第一条数据来构建插入的字段内容。insertBatch(entities, size)
:批量插入实体类数据,按 size 切分。insertOrUpdate(entity)
:插入或者更新,若主键有值,则更新,若没有主键值,则插入,插入或者更新都不会忽略 null
值。insertOrUpdateSelective(entity)
:插入或者更新,若主键有值,则更新,若没有主键值,则插入,插入或者更新都会忽略 null
值。insertOrUpdate(entity, ignoreNulls)
:插入或者更新,若主键有值,则更新,若没有主键值,则插入。在某些场景下,我们希望新增数据时,数据字段内容是数据库的某个函数或者SQL片段生成的内容,而不是手动设置的内容。属于v1.5.8版本后新增内容。例如:
INSERT INTO `tb_account`(`user_name`, `birthday`)
VALUES (?, now())
@Test
void testInsertWithRaw() {
Account account = new Account();
account.setUserName("Jack");
Account newAccount = UpdateWrapper.of(account)
.setRaw(Account::getBirthday, "now()").toEntity();
accountMapper.insert(newAccount);
}
BaseMapper
接口还提供了deleteById、deleteBatchByIds、deleteByMap、deleteByQuery 方法用于删除数据;
deleteById(id)
:根据主键删除数据。如果是多个主键的情况下,需要传入数组,例如:new Integer[]{100,101}
。deleteBatchByIds(ids)
:根据多个主键批量删除数据。deleteBatchByIds(ids, size)
:根据多个主键批量删除数据。deleteByMap(whereConditions)
:根据 Map 构建的条件来删除数据。deleteByCondition(whereConditions)
:根据查询条件来删除数据。deleteByQuery(queryWrapper)
:根据查询条件来删除数据。QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.where(ACCOUNT.ID.ge(100));
accountMapper.deleteByQuery(queryWrapper);
accountMapper.deleteByCondition(ACCOUNT.ID.ge(100));
BaseMapper
接口提供了update、updateByMap、updateByQuery 方法用于更新数据;
update(entity)
:根据主键来更新数据,若实体类属性数据为 null
,该属性不会更新到数据库。update(entity, ignoreNulls)
:根据主键来更新数据到数据库。updateByMap(entity, whereConditions)
:根据 Map 构建的条件来更新数据。updateByMap(entity, ignoreNulls, whereConditions)
:根据 Map 构建的条件来更新数据。updateByCondition(entity, whereConditions)
:根据查询条件来更新数据。updateByCondition(entity, ignoreNulls, whereConditions)
:根据查询条件来更新数据。updateByQuery(entity, queryWrapper)
:根据查询条件来更新数据。updateByQuery(entity, ignoreNulls, queryWrapper)
:根据查询条件来更新数据。updateNumberAddByQuery(fieldName, value, queryWrapper)
:执行类似 update table set field = field + 1 where ...
的场景。updateNumberAddByQuery(column, value, queryWrapper)
:执行类似 update table set field = field + 1 where ...
的场景。updateNumberAddByQuery(fn, value, queryWrapper)
:执行类似 update table set field = field + 1 where ...
的场景。在很多情况下我们只希望更新部分字段,而更新的字段中一些为空,一些不为空,此时需要用到UpdateEntity
工具类。
//把id为100的数据的user_name更新为空,age更新为10
Account account = UpdateEntity.of(Account.class, 100);
account.setUserName(null);
account.setAge(10);
accountMapper.update(account);
//更新为数据库计算的数据
UpdateWrapper wrapper = UpdateWrapper.of(account);
wrapper.setRaw("age", "age+1");
accountMapper.update(account);
//高级用法
UpdateWrapper wrapper = UpdateWrapper.of(account);
wrapper.set(ACCOUNT.AGE, ACCOUNT.AGE.add(1));
accountMapper.update(account);
//高级用法
UpdateWrapper wrapper = UpdateWrapper.of(account);
wrapper.set(ACCOUNT.AGE, select().from(...));
accountMapper.update(account);
UpdateChain是一个对UpdateEntity
、UpdateWrapper
等进行封装的一个工具类,用于链式操作。
@Test
public void testUpdateChain() {
UpdateChain.of(Account.class)
.set(Account::getUserName, "张三")
.setRaw(Account::getAge, "age + 1")
.where(Account::getId).eq(1)
.update();
}
UPDATE `tb_account` SET `user_name` = '张三' , `age` = age + 1
WHERE `id` = 1
set()
和setRaw()
的区别set方法用于设置参数数据;setRaw方法用于设置SQL拼接数据,传入不恰当的参数可能会造成SQL注入的危险。
setRaw经常使用的场景:
UpdateChain.of(Account.class)
.setRaw(Account::getMoney, "money+10")
.where(Account::getId).eq(1)
.update();
UPDATE `tb_account` SET `money` = money + 100
WHERE `id` = 1
UpdateChain.of(Account.class)
.setRaw(Account::getUserName, "UPPER(user_name)")
.where(Account::getId).eq(1)
.update();
在 MyBatis-Flex 的 BaseMapper
中,提供了如下的功能用于查询数据库的数据:
selectOneById(id)
:根据主键查询数据。selectOneByMap(whereConditions)
:根据 Map 构建的条件来查询数据。selectOneByCondition(whereConditions)
:根据查询条件查询数据。selectOneByQuery(queryWrapper)
:根据查询条件来查询 1 条数据。selectOneByQueryAs(queryWrapper, asType)
:根据查询条件来查询 1 条数据。selectOneWithRelationsByMap(whereConditions)
:根据 Map 构建的条件来查询 1 条数据。selectOneWithRelationsByCondition(whereConditions)
:根据查询条件查询 1 条数据。selectOneWithRelationsByQuery(queryWrapper)
:根据查询条件来查询 1 条数据。selectOneWithRelationsByQueryAs(queryWrapper, asType)
:根据查询条件来查询 1 条数据。selectListByIds(ids)
:根据多个主键来查询多条数据。selectListByMap(whereConditions)
:根据 Map 来构建查询条件,查询多条数据。selectListByMap(whereConditions, count)
:根据 Map 来构建查询条件,查询多条数据。selectListByCondition(whereConditions)
:根据查询条件查询多条数据。selectListByCondition(whereConditions, count)
:根据查询条件查询多条数据。selectListByQuery(queryWrapper)
:根据查询条件查询数据列表。selectListByQuery(queryWrapper, consumers)
:根据查询条件查询数据列表。selectCursorByQuery(queryWrapper)
:根据查询条件查询游标数据,该方法必须在事务中才能正常使用,非事务下无法获取数据。selectRowsByQuery(queryWrapper)
:根据查询条件查询 Row 数据。selectListByQueryAs(queryWrapper, asType)
:根据查询条件查询数据列表,要求返回的数据为 asType。这种场景一般用在 left join 时,有多出了实体类本身的字段内容,可以转换为 dto、vo 等场景。selectListByQueryAs(queryWrapper, asType, consumers)
:根据查询条件查询数据列表,要求返回的数据为 asType 类型。selectListWithRelationsByQuery(queryWrapper)
:查询实体类及其 Relation 注解字段。selectListWithRelationsByQueryAs(queryWrapper, asType)
:查询实体类及其 Relation 注解字段。selectListWithRelationsByQueryAs(queryWrapper, asType, consumers)
:查询实体类及其 Relation 注解字段。selectAll()
:查询全部数据。selectAllWithRelations()
:查询全部数据,及其 Relation 字段内容。selectObjectByQuery(queryWrapper)
:查询第一列返回的数据,QueryWrapper 执行的结果应该只有 1 列,例如:QueryWrapper.create().select(ACCOUNT.id).where(...);
selectObjectByQueryAs(queryWrapper, asType)
:查询第一列返回的数据,QueryWrapper 执行的结果应该只有 1 列,例如:QueryWrapper.create().select(ACCOUNT.id).where(...);
selectObjectListByQuery(queryWrapper)
:查询第一列返回的数据集合,QueryWrapper 执行的结果应该只有 1 列,例如:QueryWrapper.create().select(ACCOUNT.id).where(...);
selectObjectListByQueryAs(queryWrapper, asType)
:查询第一列返回的数据集合,QueryWrapper 执行的结果应该只有 1 列,例如:QueryWrapper.create().select(ACCOUNT.id).where(...);
selectCountByQuery(queryWrapper)
:查询数据量。selectCountByCondition(whereConditions)
:根据条件查询数据总量。select…As使用注意事项:
假设项目中有 User.java
的 Entity 类以及 UserVo.java
两个类。而 User.java
的代码如下
public class User {
@Column(typeHandler=xxxHandler.class)
private String attr1;
//getter setter
}
User.java
的 attr1
属性配置了 typeHandler
,当我们通过 userMapper.select...As(UserVo.class)
查询得到 UserVo
的时候, 也同样需要在 UserVo
的 attr1
属性中也配置上 @Column(typeHandler=xxxHandler.class)
,两者才能得到相同的结果。
我们对大量数据进行处理时,为防止方法内存泄漏情况,应该使用游标(Cursor)方式进行数据查询并处理数据。 在 BaseMapper
中,存在如下的游标查询方法:
Cursor<T> selectCursorByQuery(QueryWrapper queryWrapper);
使用方法如下,数据库并不是把所有的数据一次性返回给应用,而是每循环一次才会去数据库里拿数据,同时在循环中,我们可以随时终止数据读取,需要保证方法在事务中进行才能确保未与数据库断开连接。
Db.tx(() -> {
Cursort<Account> accounts = accountMapper.selectCursorByQuery(query);
for (Account account:accounts) {
System.out.println(account);
}
return true;
});
游标查询功能应用场景包括:数据查询并写入到缓存,Excel导出等。
List<Row> selectRowsByQuery(QueryWrapper queryWrapper);
@Test
void testQueryMap() {
QueryWrapper queryWrapper = QueryWrapper.create()
.select(ACCOUNT.USER_NAME, ACCOUNT.AGE, ACCOUNT.BIRTHDAY);
List<Row> rows = accountMapper.selectRowsByQuery(queryWrapper);
System.out.println(rows);
}
[{user_name=张三, age=20, birthday=2023-01-11T00:00}, {user_name=李四, age=22, birthday=2023-09-16T00:00}, {user_name=Jack, birthday=2023-09-18T10:04:22}]
Relations注解查询指的是用于查询带有注解@RelationOneToOne
,@RelationOneToMany
,@RelationManyToOne
,@RelationManyToMany
的查询。
selectOneWithRelationsByMap(whereConditions)
:根据 Map 构建的条件来查询 1 条数据。selectOneWithRelationsByCondition(whereConditions)
:根据查询条件查询 1 条数据。selectOneWithRelationsByQuery(queryWrapper)
:根据查询条件来查询 1 条数据。selectOneWithRelationsByQueryAs(queryWrapper, asType)
:根据查询条件来查询 1 条数据。selectListWithRelationsByQuery(queryWrapper)
:查询实体类及其 Relation 注解字段。selectListWithRelationsByQueryAs(queryWrapper, asType)
:查询实体类及其 Relation 注解字段。selectListWithRelationsByQueryAs(queryWrapper, asType, consumers)
:查询实体类及其 Relation 注解字段。selectAllWithRelations()
:查询全部数据,及其 Relation 字段内容。paginateWithRelations(pageNumber, pageSize, queryWrapper)
:分页查询,及其 Relation 字段内容。paginateWithRelations(pageNumber, pageSize, whereConditions)
:分页查询,及其 Relation 字段内容。paginateWithRelations(pageNumber, pageSize, totalRow, queryWrapper)
:分页查询,及其 Relation 字段内容。paginateWithRelations(pageNumber, pageSize, totalRow, whereConditions)
:分页查询,及其 Relation 字段内容。paginateWithRelations(page, queryWrapper)
:分页查询,及其 Relation 字段内容。paginateWithRelations(page, queryWrapper, consumers)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(pageNumber, pageSize, queryWrapper, asType)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(pageNumber, pageSize, totalRow, queryWrapper, asType)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(page, queryWrapper, asType)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(page, queryWrapper, asType, consumers)
:分页查询,及其 Relation 字段内容。在BaseMapper
中提供了selectOneByQueryAs
、selectListByQueryAs
、paginateAs
等方法用于处理关联查询的场景。
假设有 tb_account
用户表和 tb_article
文章表,他们的字段分别如下:
CREATE TABLE IF NOT EXISTS `tb_account`
(
`id` INTEGER PRIMARY KEY auto_increment,
`user_name` VARCHAR(100),
`age` Integer,
`birthday` DATETIME
);
CREATE TABLE IF NOT EXISTS `tb_article`
(
`id` INTEGER PRIMARY KEY auto_increment,
`account_id` Integer,
`title` VARCHAR(100),
`content` text
);
insert into tb_article(account_id, title, content) values(1, '测试', '测试');
1、定义Article
类,添加表的字段映射。
package org.qcx.mybatis.flex.entity;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;
@Data
@Table("tb_article")
public class Article {
@Id(keyType = KeyType.Auto)
private Long id;
private Long accountId;
private String title;
private String content;
}
2、定义ArticleDTO
类,添加表的字段映射。
package org.qcx.mybatis.flex.entity;
import lombok.Data;
import java.util.Date;
@Data
public class ArticleDTO {
private Long id;
private Long accountId;
private String title;
private String content;
private String userName;
private int age;
private Date birthday;
}
3、编写方法测试
package org.qcx.mybatis.flex;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.row.Row;
import com.mybatisflex.core.update.UpdateWrapper;
import org.junit.jupiter.api.Test;
import org.qcx.mybatis.flex.entity.Account;
import org.qcx.mybatis.flex.entity.ArticleDTO;
import org.qcx.mybatis.flex.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.qcx.mybatis.flex.entity.table.AccountTableDef.ACCOUNT;
import static org.qcx.mybatis.flex.entity.table.ArticleTableDef.ARTICLE;
@SpringBootTest
public class ApplicationTest {
@Autowired
private AccountMapper accountMapper;
@Test
void testLeftJoin() {
QueryWrapper queryWrapper = QueryWrapper.create()
.select(ARTICLE.ALL_COLUMNS)
.select(ACCOUNT.USER_NAME, ACCOUNT.AGE, ACCOUNT.BIRTHDAY)
.from(ARTICLE)
.leftJoin(ACCOUNT).on(ARTICLE.ACCOUNT_ID.eq(ACCOUNT.ID))
.where(ACCOUNT.ID.ge(0));
List<ArticleDTO> results = accountMapper
.selectListByQueryAs(queryWrapper, ArticleDTO.class);
System.out.println(results);
}
}
[ArticleDTO(id=1, accountId=1, title=测试, content=测试, userName=张三, age=20, birthday=Wed Jan 11 00:00:00 CST 2023)]
假设ArticleDTO
定义的属性和SQL查询的字段不一致时,那么QueryWrapper
需要添加as
。
package org.qcx.mybatis.flex.entity;
import lombok.Data;
import java.util.Date;
@Data
public class ArticleDTO {
private Long id;
private Long accountId;
private String title;
private String content;
private String authorName;
private int authorAge;
private Date birthday;
}
QueryWrapper query = QueryWrapper.create()
.select(ARTICLE.ALL_COLUMNS)
.select(ACCOUNT.USER_NAME.as(ArticleDTO::getAuthorName)
,ACCOUNT.AGE.as(ArticleDTO::getAuthorAge)
,ACCOUNT.BIRTHDAY)
.from(ARTICLE)
.leftJoin(ACCOUNT).on(ARTICLE.ACCOUNT_ID.eq(ACCOUNT.ID))
.where(ACCOUNT.ID.ge(0));
List<ArticleDTO> results = accountMapper
.selectListByQueryAs(queryWrapper, ArticleDTO.class);
System.out.println(results);
1、 定义ArticleDTO
类,在ArticleDTO
定义Account
实体类属性。
package org.qcx.mybatis.flex.entity;
import lombok.Data;
import java.util.Date;
@Data
public class ArticleDTO {
private Long id;
private Long accountId;
private String title;
private String content;
private Account account;
}
2、使用QueryWrapper
构建左连接查询。
QueryWrapper query = QueryWrapper.create()
.select(ARTICLE.ALL_COLUMNS)
.select(ACCOUNT.USER_NAME, ACCOUNT.AGE, ACCOUNT.BIRTHDAY)
.from(ARTICLE)
.leftJoin(ACCOUNT).on(ARTICLE.ACCOUNT_ID.eq(ACCOUNT.ID))
.where(ACCOUNT.ID.ge(0));
List<ArticleDTO> results = accountMapper
.selectListByQueryAs(queryWrapper, ArticleDTO.class);
System.out.println(results);
在MyBatis-Flex的BaseMapper
中提供了如下的分页查询功能:
paginate(pageNumber, pageSize, queryWrapper)
:分页查询。paginateWithRelations(pageNumber, pageSize, queryWrapper)
:分页查询,及其 Relation 字段内容。paginate(pageNumber, pageSize, whereConditions)
:分页查询。paginateWithRelations(pageNumber, pageSize, whereConditions)
:分页查询,及其 Relation 字段内容。paginate(pageNumber, pageSize, totalRow, queryWrapper)
:分页查询。paginateWithRelations(pageNumber, pageSize, totalRow, queryWrapper)
:分页查询,及其 Relation 字段内容。paginate(pageNumber, pageSize, totalRow, whereConditions)
:分页查询。paginateWithRelations(pageNumber, pageSize, totalRow, whereConditions)
:分页查询,及其 Relation 字段内容。paginate(page, queryWrapper)
:分页查询。paginate(page, queryWrapper, consumers)
:分页查询。paginateWithRelations(page, queryWrapper)
:分页查询,及其 Relation 字段内容。paginateWithRelations(page, queryWrapper, consumers)
:分页查询,及其 Relation 字段内容。paginateAs(pageNumber, pageSize, queryWrapper, asType)
:分页查询。paginateAs(pageNumber, pageSize, totalRow, queryWrapper, asType)
:分页查询。paginateAs(page, queryWrapper, asType)
:分页查询。paginateAs(page, queryWrapper, asType, consumers)
:分页查询。paginateWithRelationsAs(pageNumber, pageSize, queryWrapper, asType)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(pageNumber, pageSize, totalRow, queryWrapper, asType)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(page, queryWrapper, asType)
:分页查询,及其 Relation 字段内容。paginateWithRelationsAs(page, queryWrapper, asType, consumers)
:分页查询,及其 Relation 字段内容。totalRow非必须值,如果传入该值,就不再去查询总数据量,提高程序的查询效率。
在MyBatis-Flex中内置了自动映射功能,查询数据时从数据结果集绑定到实体类变得极其简单易用。
-- 账号表tb_account
create table if not exist tb_account (
id integer auto_increment,
user_name varchar(100),
age integer
);
-- 图书表tb_book
create table if not exists tb_book (
id integer auto_increment,
account_id integer,
title varchar(100),
content text
);
-- 角色表tb_role
crete table if not exists tb_role (
id integer auto_increment,
name varchar(100)
);
-- 账户角色多对多映射表tb_role_mapping
create table if not exists tb_role_mapping (
account_id integer,
role_id integer
);
基础映射指的是定义的实体类和表结构是一一对应的关系,例如Account.java
与表tb_account
是字段和属性是一一对应关系的。此时可以通过AccountMapper
方法直接查询,或者通过链式查询。
@Table(value = "tb_account")
@Data
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
private String username;
private int age;
}
QueryWrapper qw = new QueryWrapper();
qw.select(ACCOUNT.ALL_COLUMNS)
.where(ACCOUNT.ID.ge(100));
List<Account> accounts = accountMapper.selectListByQuery(qw);
QueryChain.of(accountMapper).select(ACCOUNT.ALL_COLUMNS)
.where(ACCOUNT.ID.ge(100)).list();
假设在Account.java
中多定义了一些其他属性,查询的时候可以通过as
进行映射关联。
@Table(value = "tb_account")
@Data
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
private String username;
private int age;
private int maxAge;
private int avgAge;
}
QueryChain.of(accountMapper)
.select(
ACCOUNT.ALL_COLUMNS,
max(ACCOUNT.AGE).as("maxAge"),
avg(ACCOUNT.AGE).as("avgAge")
).where(ACCOUNT.ID.ge(100))
.groupBy(ACCOUNT.AGE)
.list();
QueryChain.of(accountMapper)
.select(
ACCOUNT.ALL_COLUMNS,
max(ACCOUNT.AGE).as("max_age"),
avg(ACCOUNT.AGE).as("avg_age")
).where(ACCOUNT.ID.ge(100))
.groupBy(ACCOUNT.AGE)
.list();
QueryChain.of(accountMapper)
.select(
ACCOUNT.ALL_COLUMNS,
max(ACCOUNT.AGE).as(Account::getMaxAge),
avg(ACCOUNT.AGE).as(Account::getAvgAge)
).where(ACCOUNT.ID.ge(100))
.groupBy(ACCOUNT.AGE)
.list();
select tb_account.*
, max(tb_account.age) as maxAge
, avg(tb_account.age) as avgAge
where tb_account.id >= 100
group by tb_account.age
定义一个BookVo.java
,其中包含图书的基本信息,也包含图书归属的用户信息。
public class BookVo {
//图书的基本字段
private Long id;
private Long accountId;
private String title;
private String content;
//用户表的字段
private String userName;
private int userAge;
}
List<BookVo> bookVos = QueryChain.of(bookMapper)
.select(
BOOK.ALL_COLUMNS,
ACCOUNT.USER_NAME,
ACCOUNT.AGE.as("userAge")
).from(BOOK)
.leftJoin(ACCOUNT).on(BOOK.ACCOUNT_ID.eq(ACCOUNT.ID))
.where(ACCOUNT.ID.ge(100))
.listAs(BookVo.java);
定义AccountVO.java
:
public class AccountVO {
private Long id;
private String userName;
private int age;
//账户拥有的 图书列表
private List<Book> books;
}
List<AccountVO> bookVos = QueryChain.of(accountMapper)
.select()
.from(ACCOUNT)
.leftJoin(BOOK).on(ACCOUNT.ID.eq(BOOK.ACCOUNT_ID))
.where(ACCOUNT.ID.ge(100))
.listAs(AccountVO.java);
//指定查询参数
List<AccountVO> bookVos = QueryChain.of(accountMapper)
.select(
ACCOUNT.ID,
ACCOUNT.USER_NAME,
ACCOUNT.AGE,
BOOK.TITLE,
BOOK.CONTENT,
)
.from(ACCOUNT)
.leftJoin(BOOK).on(ACCOUNT.ID.eq(BOOK.ACCOUNT_ID))
.where(ACCOUNT.ID.ge(100))
.listAs(AccountVO.java);
在很多类型嵌套的场景下,可能会出现字段名定义重复的情况。账户和图书都定义了id
和name
。
public class AccountVO {
private Long id;
private String name;
private int age;
//账户拥有的 图书列表
private List<Book> book;
}
public class Book {
private Long id;
private Long accountId;
private String name;
}
List<AccountVO> bookVos = QueryChain.of(accountMapper)
.select(
ACCOUNT.ID,
ACCOUNT.NAME,
ACCOUNT.AGE,
BOOK.ID,
BOOK.NAME,
)
.from(ACCOUNT)
.leftJoin(BOOK).on(ACCOUNT.ID.eq(BOOK.ACCOUNT_ID))
.where(ACCOUNT.ID.ge(100))
.listAs(AccountVO.java);
在MyBatis-Flex中内置了3种方案实现关联查询。
@RelationOneToOne
注解用于一对一的场景,@RelationOneToMany
注解用于一对多的场景,@RelationManyToOne
注解用于多对一的场景,@RelationManyToMany
注解用于多对多的场景。添加了相关配置的实体类,通过BaseMapper
的方法查询数据时,需要调用select***WithRelations()
方法,Relations注解才能生效。
比如账户Account.java
和身份证IDCard.java
是一对一的关系。
@Data
@Table("tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
//指定当前实体类的属性和目标对象的关联实体类的属性,若selfField是主键,且只有一个可以不写
@RelationOneToOne(selfField = "id", targetField = "accountId")
private IDCard idCard;
//如果IDCard.java不是带有@Table注解的类,需要指定targetTable
//@RelationOneToOne(selfField = "id", targetField = "accountId",
// targetTable = "tb_idcard")
//如果关联关系是通过中间表的方式进行的需要添加配置
//@RelationOneToOne(
// joinTable = "tb_idcard_mapping"
// ,joinSelfColumn = "account_id"
// ,joinTargetColumn = "idcard_id"
// ,selfField = "id"
// ,targetField = "accountId"
//)
}
@Data
@Table("tb_idcard")
public class IDCard implements Serializable {
private Long accountId;
private String cardNo;
private String content;
}
List<Account> accounts = accountMapper.selectAllWithRelations();
System.out.println(accounts);
比如一个账户Account.java
有很多书Book.java
。
@Data
@Table("tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationOneToMany(selfField = "id", targetField = "accountId")
private List<Book> books;
//如果是Map则需要指定mapKeyField
//@RelationOneToMany(selfField = "id", targetField = "accountId",
// mapKeyField = "id")
//private Map books;
}
@Data
@Table("tb_book")
public class Book implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private Long accountId;
private String title;
}
比如很多书Book.java
属于同一个人Account.java
。
@Data
@Table("tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
}
@Data
@Table(value = "tb_book")
public class Book implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private Long accountId;
private String title;
@RelationManyToOne(selfField = "accountId", targetField = "id")
private Account account;
}
比如账户Account.java
和角色Role.java
的关系,需要通过中间表完成。
CREATE TABLE `tb_role_mapping`
(
`account_id` INTEGER ,
`role_id` INTEGER
);
@Data
@Table("tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationManyToMany(
joinTable = "tb_role_mapping", // 中间表
selfField = "id", joinSelfColumn = "account_id",
targetField = "id", joinTargetColumn = "role_id"
)
private List<Role> roles;
}
@Data
@Table(value = "tb_role")
public class Role implements Serializable {
private Long id;
private String name;
}
RelationOneToOne
、RelationOneToMany
、RelationManyToOne
、RelationManyToMany
新增属性valueField
。注解其他属性配置使用不变,当配置了此属性时,只提取目标对象关系实体类的该属性。
假设有账户类UserVO.java
,每个账户有唯一对应的id_number
列在表tb_id_card
中。一个账户可以有多个角色,一个角色也可以分配给多个账户,通过中间表tb_user_role
进行映射。
@Data
@Table("tb_user")
public class UserVO {
@Id
private Integer userId;
private String userName;
private String password;
@RelationOneToOne(
selfField = "userId",
targetTable = "tb_id_card",
targetField = "id",
valueField = "idNumber"
)
//字段名可以是其他属性名,不一定是目标对象的字段名
private String idNumberCustomFieldName;
@RelationManyToMany(
selfField = "userId",
targetField = "tb_role",
targetField = "roleId",
valueField = "roleName",
joinTable = "tb_user_role",
joinSelfColumn = "user_id",
joinTargetColumn = "role_id"
)
private List<String> roleNameList;
}
List userVOS = userMapper.selectListWithRelationsByQueryAs(QueryWrapper.create(), UserVO.class);
System.out.println(JSON.toJSONString(userVOS));
比如多级菜单,定义菜单表如下:
create table tb_menu (
id integer auto_increment,
parent_id integer,
name varchar(100)
);
@Data
@Table("tb_menu")
public class Menu implements Serializable {
private Long id;
private Long parentId;
private String name;
@RelationManyToOne(selfField = "parentId", targetField = "id")
private Menu parent;
@RelationOneToMany(selfField = "id", targetField = "parentId")
private List<Menu> children;
}
查询顶级菜单,默认的递归查询深度为3个层级。
QueryWrapper qw = QueryWrapper.create();
qw.where(MENU.PARENT_ID.eq(0));
List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);
System.out.println(JSON.toJSONString(menus));
SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE `parent_id` = 0
SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE id = 0
SELECT `id`, `parent_id`, `name` FROM `tb_menu` WHERE parent_id IN (1, 2, 3)
//设置递归查询深度为10层
QueryWrapper qw = QueryWrapper.create();
qw.where(MENU.PARENT_ID.eq(0));
//只在当前第一次查询有效,查询后会清除设置
RelationManager.setMaxDepth(10);
List<Menu> menus = menuMapper.selectListWithRelationsByQuery(qw);
在很多场景下,一个类中可能会有多个@RelationXXX
注解配置的属性。
@Data
@Table(value = "tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationOneToOne(targetField = "accountId")
private IDCard idCard;
@RelationOneToMany(targetField = "accountId")
private List<Book> books;
@RelationManyToMany(
joinTable = "tb_role_mapping",
joinSelfColumn = "account_id",
joinTargetColumn = "role_id"
)
private List<Role> roles;
}
默认情况下通过BaseMapper
的withRelation方法查询时会查询Account所有带有@RelationXXX
注解的属性。但是可能在个别业务中不需要那么多关联数据,比如只需要查询roles
,而忽略掉idCard
books
,此时代码如下:
//只在当前第一次查询有效,查询后会清除设置,会影响其所有嵌套的Relations配置
RelationManager.addIgnoreRelations("idCard", "books");
List<Account> accounts = accountMapper.selectAllWithRelations();
@Data
@Table(value = "tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationOneToOne(targetField = "accountId")
private IDCard idCard;
@RelationOneToMany(targetField = "accountId")
private List<Book> books;
@RelationManyToMany(
joinTable = "tb_role_mapping",
joinSelfColumn = "account_id",
joinTargetColumn = "role_id"
)
private List<Role> roles;
}
如果只想查询books
和roles
字段,而忽略其他字段参考可以如下配置。Account代码无论如何变动都不会影响到原来的业务。忽略配置优先级更高。
RelationManager.addQueryRelations("books", "roles");
List<Account> accounts = accountMapper.selectAllWithRelations();
在一对多和多对多场景下,除了通过关联字段查询结果外,还可以添加一些额外的条件,此时可以通过添加extraCondition
配置来满足这种场景。
@Data
@Table(value = "tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationManyToMany(
joinTable = "tb_role_mapping",
joinSelfColumn = "account_id",
joinTargetColumn = "role_id",
extraCondition = "(name like '%2%' or id > 1)"
)
private List<Role> roles;
}
SELECT `id`, `name` FROM `tb_role`
WHERE id IN (1, 2, 3) AND (name like '%2%' or id > 1)
如果extraCondition
配置的条件里,需要通过外部传入参数,参考如下配置:
@Data
@Table(value = "tb_account")
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationManyToMany(
joinTable = "tb_role_mapping",
joinSelfColumn = "account_id",
joinTargetColumn = "role_id",
//:name和:id相当于占位符,接收外部参数
extraCondition = "(name like :name or id > :id)"
)
private List<Role> roles;
}
RelationManager.addExtraConditionParam("name", "%myName%");
RelationManager.addExtraConditionParam("id", 100);
List<Account> accounts = accountMapper.selectAllWithRelations();
System.out.println(JSON.toJSONString(accounts));
以文章与分类的多对多的关系为例。
public class Article {
private Long id;
private String title;
private String content;
//文章归属分类
private List categories'
}
QueryWrapper queryWrapper = QueryWrapper.create()
.select().from(ARTICLE)
.where(ARTICLE.id.ge(100));
List<Article> articles = mapper.selectListByQuery(queryWrapper,
fieldQueryBuilder -> fieldQueryBuilder
.field(Article::getCategories)
.queryWrapper(article -> QueryWrapper.create()
.select().from(CATEGORY)
.where(CATEGORY.id.in(
select("category_id").from("article_category_mapping")
.where("article_id = ?", article.getId())
))
));
其原理是:MyBatis-Flex 的内部逻辑是先查询出 Article
的数据,然后再根据 Article
构建出新的 SQL,查询分类,并赋值给 Article.categories
属性, 假设 Article
有 10 条数据,那么最终会进行 11 次数据库查询。
select * from tb_article where id >= 100;
-- 以上 SQL 得到结果后,再执行查询分类的 SQL,如下:
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 100);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 101);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 102);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 103);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 104);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 105);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 106);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 107);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 108);
select * from tb_category where id in
(select category_id from article_category_mapping where article_id = 109);
在构建子查询时,只需要明白为哪个字段通过什么样的SQL查询就可以了。
List<Article> articles = mapper.selectListByQuery(query
, fieldQueryBuilder -> fieldQueryBuilder
.field(...) // 为哪个字段查询的?
.queryWrapper(...) // 通过什么样的 SQL 查询的?
);
Join Query是通过QueryWrapper构建Left Join
等方式进行查询,原理是MyBatis-Flex自动构建了MyBatis的
,只需要关注SQL构建即可。
以用户和角色的多对多关系为例:
@Table("sys_user")
public class User {
@Id
private Integer userId;
private String userName;
}
@Table("sys_role")
public class Role {
@Id
private Integer roleId;
private String roleKey;
private String roleName;
}
public class UserVO {
private String userId;
private String userName;
private List<Role> roleList;
}
现在需要查询所有用户以及用户对应的角色信息,并通过UserVO对象返回。
QueryWrapper queryWrapper = QueryWrapper.create()
.select(USER.USER_ID, USER.USER_NAME, ROLE.ALL_COLUMNS)
.from(USER.as("u"))
.leftJoin(USER_ROLE).as("ur").on(USER_ROLE.USER_ID.eq(USER.USER_ID))
.leftJoin(ROLE).as("r").on(USER_ROLE.ROLE_ID.eq(ROLE.ROLE_ID));
List<UserVO> userVOS = userMapper.selectListByQueryAs(queryWrapper, UserVO.class);
userVOS.forEach(System.err::println);
SELECT `u`.`user_id`,
`u`.`user_name`,
`r`.*
FROM `sys_user` AS `u`
LEFT JOIN `sys_user_role` AS `ur` ON `ur`.`user_id` = `u`.`user_id`
LEFT JOIN `sys_role` AS `r` ON `ur`.`role_id` = `r`.`role_id`;
BaseMapper.insertBatch
方法在小批量数据执行插入时候,效率非常高,但是当数据列表过多时,其生成的SQL可能会非常大,这个大的SQL在传输和执行的时候会变得很慢了。
List<Account> accounts = ...
mapper.insertBatch(accounts);
insert into tb_account(id,nickname, .....) values
(100,"miachel100", ....),
(101,"miachel101", ....),
(102,"miachel102", ....),
(103,"miachel103", ....),
(104,"miachel104", ....),
(105,"miachel105", ....);
Db.executeBatch
方法Db.executeBatch
方法可以执行批量的插入、修改和删除操作,通过JDBC的Statement.executeBatch()
进行批量执行,这个在大批量数据执行时效率高。
List<Account> accounts = ...
//写法1
Db.executeBatch(accounts.size(), 1000, AccountMapper.class, (mapper, index) -> {
Account account = accounts.get(index);
mapper.insert(account);
});
//写法2
Db.executeBatch(accounts, 1000, AccountMapper.class, (mapper, account) -> {
mapper.insert(account);
});
IService
中有很多批量操作的方法也是通过Db.executeBatch
进行封装的,可以通过其扩展出自己的批量操作方法。
//批量忽略null的插入示例
public boolean saveBatchSelective(Collection<Account> entities) {
int[] result = Db.executeBatch(entities, 1000, AccountMapper.class,
BaseMapper::insertSelective);
return SqlUtil.toBool(result);
}
Db.updateBatch
方法Db.updateBatch
方法也可以执行insert
、delete
、update
等任何SQL,类似Spring的jdbcTemplate.batchUpdate()
方法。
List<Account> accounts = new ArrayList<>();
String sql = "insert into tb_account(user_name, age, birthday) " +
"values (?, ?, ?)";
Db.updateBatch(sql, new BatchArgsSetter() {
@Override
public int getBatchSize() {
return accounts.size();
}
@Override
public Object[] getSqlArgs(int index) {
Account account = accounts.get(index);
Object[] args = new Object[3];
args[0] = account.getUserName();
args[1] = account.getAge();
args[2] = new Date();
return args;
}
});
Db.updateEntitiesBatch
方法Db.updateEntitiesBatch
方法用于批量根据id更新entity,是对Db.executeBatch
的封装。
List<Account> accounts = new ArrayList<>();
Db.updateEntitiesBatch(accounts, 1000);
在MyBatis-Flex中内置了QueryChain.java
、UpdateChain.java
以及DbChain.java
用于对数据进行链式操作。QueryChain
用于链式查询,UpdateChain
用于链式更新,DbChain
用于链式调用Db + Row
。
以查询文章列表为例。
@SpringBootTest
class ArticleServiceTest {
@Autowired
ArticleService articleService;
@Test
void testChain() {
List<Article> articles = articleService.queryChain()
.select(ARTICLE.ALL_COLUMNS)
.from(ARTICLE)
.where(ARTICLE.ID.ge(100))
.list();
}
}
如果不是在Service中,也可以通过QueryChain.of(mapper)
方法创建一个QueryChain
实例。
List<Article> articles = QueryChain.of(mapper)
.select(ARTICLE.ALL_COLUMNS)
.from(ARTICLE)
.where(ARTICLE.ID.ge(100))
.list();
假设我们需要更新Account
的userName
为张三,年龄加1。
@Test
public void testUpdateChain1() {
UpdateChain.of(Account.class)
.set(Account::getUserName, "张三")
.setRaw(Account::getAge, "age + 1")
.where(Account::getId).eq(1)
.update();
}
one()
系列方法list()
系列方法page()
系列方法obj()
系列方法objList()
系列方法Db + Row工具类提供了在Entity实体类之外的数据库操作能力。使用Db + Row时不需要对数据库表进行映射,Row是HashMap的子类。
//使用原生SQL插入数据
String sql = "insert into tb_account(id, name) value(?, ?)";
Db.insertBySql(sql, 1, "Lucy");
//使用Row插入数据
Row account = new Row();
account.set("id", 100);
account.set(ACCOUNT.USER_NAME, "Lucy");
Db.insert("tb_account", account);
//根据主键查询数据
Row row = Db.selectOneById("tb_account", "id", 1);
//Row可以直接转换为Entity实体类
Account account = row.toEntity(Account.class);
//查询所有大于18岁的用户
String listSql = "select * from tb_account where age > ?";
List<Row> rows = Db.selectListBySql(listSql, 18);
//分页查询
QueryWrapper query = QueryWrapper.create()
.where(ACCOUNT.AGE.ge(18));
Page<Row> rowPage = Db.paginate("tb_account", 3, 10, query);
使用DbChain
后无需将QueryWrapper
与Row
的构建分离,直接即可操作。
//新增Row构建
DbChain.table("tb_account")
.set(RowKey.AUTO)
.set("user_name", "王五")
.set("age", 18)
.set("birthday", new Date())
.save();
//查询QueryWrapper构建
DbChain.table("tb_account")
.select("id", "user_name", "age", "birthday")
.where("age > ?", 18)
.list()
.forEach(System.out::println);
Row.toEntity(Entity.class)
方法用于把Row转换为entity实体类。通过这个方法可以吧Entity里的@Column()
配置的列名和Row里面的key自动关联。
Row row=Db.selectOneBySql("select * from ....");
Account entity = row.toEntity(Account.class);
在进行join关联查询时,返回的结果如果出现重复字段,Row会自动添加上字段序号。
CREATE TABLE IF NOT EXISTS `tb_account`
(
`id` INTEGER PRIMARY KEY auto_increment,
`user_name` VARCHAR(100),
`age` Integer,
`is_delete` Integer
);
CREATE TABLE IF NOT EXISTS `tb_article`
(
`id` INTEGER PRIMARY KEY auto_increment,
`account_id` Integer,
`title` VARCHAR(100),
`content` text,
`is_delete` Integer
);
INSERT INTO tb_account
VALUES (1, '张三' ,18, 0),
(2, '王麻子叔叔' ,19, 0);
INSERT INTO tb_article
VALUES (1, 1,'标题1', '内容1',0),
(2, 2,'标题2', '内容2',0);
QueryWrapper query = new QueryWrapper();
query.select().from(ACCOUNT).leftJoin(ARTICLE)
.on(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID));
List<Row> rows = Db.selectListByQuery(query);
我们在进行toEntity数据转换时,需要加上序号,例如:
List<Account> accounts = RowUtil.toEntityList(rows, Account.class);
System.out.println(accounts);
//添加上序号 1
List<Article> articles = RowUtil.toEntityList(rows, Article.class, 1);
System.out.println(articles);
Row.toObject(Other.class)
和Row.toEntity(Entity.class)
相似,不同点是通过去查找Other.class
的setter
方法去匹配Row的key进行赋值的。当我们通过SQL查询得到Row的时候,Row里面的key为userName
、UserName
、USERNAME
等都能自动适配到Other.userName
属性。这个方法常用于把Row直接转换为VO的场景。我们可以通过RowUtil.registerMapping(clazz, columnSetterMapping)
去让更多的字段名称和属性进行匹配。
public class Other {
private String id;
private String userName;
}
Row row = Db.selectOneBySql("select * from ...");
//如果出现了left join等情况需要添加序号
Other other = row.toObject(Other.class);
Row row = Db.selectOneBySql("select * from ...");
Map result = row.toCamelKeysMap();
//Row字段转换为下划线风格
Map reuslt = row.toUnderlineKeysMap();
//RowKey.ID_AUTO ID自增
//RowKey.ID_UUID UUID
Row row = Row.ofKey(RowKey.ID_AUTO);
row.set(ACCOUNT.USER_NAME, "Jessy");
Db.insert("tb_account", row);
//自定义Row主键生成方式
RowKey myRowKey = RowKey.of("id", KeyType.Generator, "uuid", true);
Row row = Row.ofKey(myRowKey);
RowUtil
工具类用于帮助用户快速的把Row
或者List
转换为VO的工具类。
RowUtil.toObject(row, objectClass)
RowUtil.toObjectList(rows, objectClass)
RowUtil.toEntity(row, entityClass)
RowUtil.toEntityList(rows, entityClass)
RowUtil.registerMapping(clazz, columnSetterMapping)
用于注册数据库 字段
名称和 Class 属性的映射关系。RowUtil.printPretty(rows)
打印 Row
或者 List
数据到控制台,一般用户调试。在Active Record模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,还知道如何从数据库中读取数据。在MyBatis-Flex中实现Active Record功能十分简单,只需让Entity类继承Model即可。
使用Active Record功能时,项目中必须注入对应实体类的BaseMapper对象。
@Table("tb_account") //标记实体类对应的数据表
@Accessors(chain = true) //开启return this,这样既可以被序列化也可以链式调用
@Data(staticConstructor = "create") //创建一个create()静态方法用于链式调用
public class Account extends Model<Account> {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
private Integer age;
private Date birthday;
}
@RestController
@RequestMapping("/account")
public class AcconutController {
@PostMapping("save")
public boolean save(@RequestMapping Account account) {
return account.save();
}
}
Model
提供了save
方法来保存数据。
Account.create()
.setUserName("张三")
.setAge(18)
.setBirthday(new Date())
.save();
Model
提供了remove
方法来删除数据。
//根据主键删除
Account.create().setId(1L).removeById();
//根据条件删除
Account.create().where(Account::getId).eq(1L).remove();
Model
提供了update
方法更新数据,调用该方法前需要填充数据。
//根据主键更新
Account.create().setId(1L).setAge(100).updateById();
//根据条件更新
Account.create().setAge(100).where(Account::getId).eq(1L).update();
//查询一条数据
Account.create().where(Account::getId).eq(1L).one();
//查询多条数据
Account.create().where(Account::getAge).ge(18).list();
//查询单列数据
Account.create().select(Account::getUserName).where(Account::getAge).ge(18).objList();
//查询分页数据
Account.create().where(Account::getAge).ge(18).page(Page.of(1, 10));
Model
提供了三种方式实现多表关联查询,例如用户与角色的关系。
User.create()
.select(USER.ALL_COLUMNS,ROLE.ALL_COLUMNS)
.leftJoin(USER_ROLE).as("ur").on(USER_ROLE.USER_ID.eq(USER.USER_ID))
.leftJoin(ROLE).as("r").on(USER_ROLE.ROLE_ID.eq(ROLE.ROLE_ID))
.where(USER.USER_ID.eq(1))
.one();
User.create()
.where(USER.USER_ID.eq(1))
.withRelations() // 使用 Relations Query 的方式进行关联查询。
.maxDepth(3) // 设置父子关系查询中,默认的递归查询深度。
.ignoreRelations("orderList") // 忽略查询部分 Relations 注解标记的属性。
.extraConditionParam("id", 100) // 添加额外的 Relations 查询条件。
.one();
User.create()
.where(USER.USER_ID.eq(1))
.withFields() // 使用 Fields Query 的方式进行关联查询。
.fieldMapping(User::getRoleList, user -> // 设置属性对应的 QueryWrapper 查询。
QueryWrapper.create()
.select()
.from(ROLE)
.where(ROLE.ROLE_ID.in(
QueryWrapper.create()
.select(USER_ROLE.ROLE_ID)
.from(USER_ROLE)
.where(USER_ROLE.USER_ID.eq(user.getUserId()))
)))
.one();
有时候,在操作完数据库后还需要继续使用实体类的内容,此时返回值为布尔类型的方法就不满足需求了,应该使用xxxOpt
方法,操作数据库成功后返回Optional.of(this)
,执行失败返回Optional.empty()
,就可以进行链式调用了。
// 插入成功之后返回主键信息
Account.create().setUserName("张三").setAge(18).setBirthday(new Date())
.saveOpt().orElseThrow(RuntimeException::new) // 保存失败抛出异常
.getId();
MyBatis-Flex 提供了一个名为 IService
的接口,及其默认实现类 ServiceImpl
,用于简化在 Service 层重复定义 Mapper 层的方法。
public interface IAccountService implements IService<Account> {
List<Account> customMethod();
}
@Component
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account>
implements IAccountService {
@Override
public List<Account> customMethod() {
//返回id >= 100的数据
return list(ACCOUNT.ID.ge(100));
}
}
IService
的接口提供了 save、saveOrUpdate、saveBatch 方法用来保存数据:
IService
的接口提供了 remove、removeById、removeByIds、removeByMap 方法,用于删除数据:
QueryWrapper
构建的条件来删除数据。QueryCondition
构建的条件来删除数据。Map<字段名,值>
组成的条件删除数据,字段名和值的关系为相等的关系;同时,防止 “不小心” 全表删除数据,Map 的值不允许为 null 或者空数据。IService
的接口提供了 update、updateById、updateBatch 方法,用于更新数据:
Map<字段名,值>
组成的条件更新数据,实体类可以没有主键(如果有也会被忽略),实体类的 null 属性,会自动被忽略。QueryWrapper
构建的条件更新数据,实体类可以没有主键(如果有也会被忽略),实体类的 null 属性,会自动被忽略。QueryCondition
构建的条件更新数据,实体类可以没有主键(如果有也会被忽略),实体类的 null 属性,会自动被忽略。IService
的接口提供了 getById、getByIdOpt、getOne、getOneOpt、getOneAs、getOneAsOpt 方法,用于查询一条数据:
Optional
返回。QueryWrapper
构建的条件查询一条数据。QueryCondition
构建的条件查询一条数据。QueryWrapper
构建的条件查询一条数据,并封装为 Optional
返回。QueryCondition
构建的条件查询一条数据,并封装为 Optional
返回。QueryWrapper
构建的条件查询一条数据,并通过 asType 进行接收。QueryWrapper
构建的条件查询一条数据,并通过 asType 进行接收,封装为 Optional
返回。IService
的接口提供了 list、listAs、listByIds、listByMap 方法,用于查询多条数据:
QueryCondition
或 QueryWrapper
构建的条件查询多条数据。QueryCondition
构建的条件查询多条数据。QueryWrapper
构建的条件查询多条数据,并通过 asType 进行接收。Map<字段名,值>
组成的条件查询多条数据。IService
的接口提供了 exists、count 方法,用于查询数据数量;
QueryWrapper
构建的条件查询数据数量。QueryCondition
构建的条件查询数据数量。QueryWrapper
构建的条件判断数据是否存在。QueryCondition
构建的条件判断数据是否存在。IService
的接口提供了 page、pageAs 方法,用于分页查询数据:
QueryWrapper
构建的条件分页查询数据。QueryCondition
构建的条件分页查询数据。QueryWrapper
构建的条件分页查询数据,并通过 asType 进行接收。BaseMapper
接口。QueryWrapper
类。SpringBoot配置文件主要是针对MyBatis原生以及MyBatis-Flex的FlexGlobalConfig
的配置。
mybatis-flex:
datasource: # 多数据源配置
config-location: # MyBatis配置文件路径,如果有单独的MyBatis配置,需要添加
mapper-locations: # MyBatis Mapper所对应的XML文件位置
type-aliases-package:
# MyBatis别名包扫描路径,通过该属性可以给包中的类注册别名,
#注册后在Mapper对应的XML文件中可以直接使用类名,而不是用全限定类名
type-aliases-super-type:
# 需要和typeAliasesPackage一起使用,配置了该属性仅仅会扫描路径下以该类作为父类的域对象
type-handlers-package:
# TypeHandler扫描路径,配置了该属性SqlSessionFactoryBean会把该包下面的类注册
#为对应的TypeHandler处理器
check-config-location: # 启动时检查是否存在MyBatis XML文件,默认不检查
executor-type: # 指定MyBatis的执行器: SIMPLE、REUSE、BATCH
defaults-scripting-language-driver: # 指定默认的脚本语言驱动器
configuration-properties: # 指定外部化配置,可以抽离配置,实现不同环境的配置部署
configuration: # 对MyBatis原生支持的配置
global-config:
print-banner: # 默认在控制台打印MyBatis-Flex的LOGO和版本号
key-config: # 全局的ID生成策略配置,当@Id未配置或者配置为None时使用全局配置
normal-value-of-logic-delete: # 逻辑删除数据存在标记值,默认0
deleted-value-of-logic-delete: # 逻辑删除数据删除标记值,默认1
logic-delete-column: # 默认的逻辑删除字段,默认del_flag
tenant-column: # 默认的多租户字段,默认值tenant_id
version-column: # 默认的乐观锁字段,默认值version
admin-config: # 可以吧当前应用执行的SQL发送到MyBatis-Flex-Admin审计
enable: # 是否启用 MyBatis-Flex-Admin 连接,默认不启用
endpoint: # MyBatis-Flex-Admin连接端点
secret-key: # MyBatis-Flex-Admin连接密钥
seata-config:
enable: # 是否启用Seata代理数据源
seata-mode: # 使用Seata AT模式代理数据源
MyBatisFlexCustomizer
是 MyBatis-Flex 为了方便 SpringBoot
用户对 MyBatis-Flex 进行初始化而产生的接口。
通过在 @Configuration
去实现 MyBatisFlexCustomizer
接口,我们可以对 MyBatis-Flex 进行一系列的初始化配置。
QueryWrapper是用于构造SQL的强有力的工具,也是MyBatis-Flex的亮点和特色,可以被序列化通过RPC进行传输。
package com.mybatisflex.annotation;
import java.lang.annotation.*;
/**
* 数据库表信息注解。
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
public @interface Table {
/**
* 显式指定表名称,配置指定实体类与表名的映射关系
*/
String value();
/**
* 数据库的 schema(模式)。
*/
String schema() default "";
/**
* 默认为 驼峰属性 转换为 下划线字段。
*/
boolean camelToUnderline() default true;
/**
* 默认使用哪个数据源,若系统找不到该指定的数据源时,默认使用第一个数据源。
*/
String dataSource() default "";
/**
* 监听 entity 的 insert 行为,可以在实体类被新增时做一些前置操作
* onInsert指定实现InsertListener接口的实现类
*/
Class<? extends InsertListener>[] onInsert() default {};
/**
* 监听 entity 的 update 行为。
*/
Class<? extends UpdateListener>[] onUpdate() default {};
/**
* 监听 entity 的查询数据的 set 行为,用户主动 set 不会触发。
* 可用于字段权限、字典回写、一对多及一对一查询、字段加密、字段脱敏
*/
Class<? extends SetListener>[] onSet() default {};
/**
* 在某些场景下,我们需要手动编写 Mapper,可以通过这个注解来关闭 APT 的 Mapper 生成。
*/
boolean mapperGenerateEnable() default true;
}
@Id
注解是用来表示主键的,多主键就是在Entity类中有多个@Id注解标识。
package com.mybatisflex.annotation;
import java.lang.annotation.*;
/**
* 数据库表中的列信息注解。
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Id {
/**
* ID 生成策略,默认为 {@link KeyType#None}。
* AUTO:自增的方式
* Sequence: 通过执行数据库SQL生成
* Generator: 通过IKeyGenerator生成器生成
* None:其他方式,比如在代码层用户手动设置
* @return 生成策略
*/
KeyType keyType() default KeyType.None;
/**
* 若 keyType 类型是 sequence, value 则代表的是
* sequence 序列的 sql 内容。
* 例如:select SEQ_USER_ID.nextval as id from dual
*
*
若 keyType 是 Generator,value 则代表的是使用的那个 keyGenerator 的名称。
*/
String value() default "";
/**
* sequence 序列执行顺序。
*
*
是在 entity 数据插入之前执行,还是之后执行,之后执行的一般是数据主动生成的 id。
*
* @return 执行之前还是之后
*/
boolean before() default true;
}
内置的三种主键生成器都定义在KeyGenerators
类中。
UUIDKeyGenerator
生成 UUID 作为数据库主键。SnowFlakeIDKeyGenerator
)生成数据库主键。自定义主键生成器步骤
IKeyGenerator
接口;public class UUIDKeyGenerator implements IKeyGenerator {
@Override
public Object generate(Object entity, String keyColumn) {
return UUID.randomUUID().toString().replace("-", "");
}
}
UUIDKeyGenerator
KeyGeneratorFactory.register("myUUID", new UUIDKeyGenerator());
@Data
@Table("tb_account")
public class Account {
@Id(keyType = KeyType.Generator, value = "myUUID")
private String otherId;
}
@Table("tb_account")
public class Account {
@Id(keyType=KeyType.Sequence, value="select SEQ_USER_ID.nextval as id from dual")
private Long id;
}
FlexGlobalConfig.KeyConfig keyConfig = new FlexGlobalConfig.KeyConfig();
keyConfig.setKeyType(KeyType.Sequence);
keyConfig.setValue("select SEQ_USER_ID.nextval as id from dual")
keyConfig.setBefore(true);
FlexGlobalConfig.getDefaultConfig().setKeyConfig(keyConfig);
@Table("tb_account")
public class Account {
@Id
private Long id;
}
package com.mybatisflex.annotation;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.UnknownTypeHandler;
import java.lang.annotation.*;
/**
* 数据库表中的列信息注解。
*
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Column {
/**
* 字段名称
*/
String value() default "";
/**
* 是否忽略该字段,可能只是业务字段,而非数据库对应字段。
*/
boolean ignore() default false;
/**
* insert 的时候默认值,这个值会直接被拼接到 sql 而不通过参数设置。
*/
String onInsertValue() default "";
/**
* update 的时候自动赋值,这个值会直接被拼接到 sql 而不通过参数设置。
*/
String onUpdateValue() default "";
/**
* 是否是大字段,大字段 APT 不会生成到 DEFAULT_COLUMNS 里。
*/
boolean isLarge() default false;
/**
* 是否是逻辑删除字段,一张表中只能存在 1 一个逻辑删除字段。
*
*
逻辑删除的字段,被删除时,会设置为 1,正常状态为 0,可以通过 FlexGlobalConfig 配置来修改 1 和 0 为其他值。
*/
boolean isLogicDelete() default false;
/**
* 是否为乐观锁字段。
*
*
若是乐观锁字段的话,数据更新的时候会去检测当前版本号,若更新成功的话会设置当前版本号 +1
* 只能用于数值的字段。
*/
boolean version() default false;
/**
* 是否是租户 ID。
*/
boolean tenantId() default false;
/**
* 配置的 jdbcType。
*/
JdbcType jdbcType() default JdbcType.UNDEFINED;
/**
* 自定义 TypeHandler。
*/
Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;
}
逻辑删除指的是在删除数据的时候,并非真正的去删除,而是将表中列所对应的状态字段(status)做修改操作, 实际上并未删除目标数据。
@Table("tb_account")
public class Account {
@Column(isLogicDelete = true)
private Boolean isDelete;
}
accountMapper.deleteById(1);
update tb_account set is_delete = 1 where id = ? and is_delete = 0
当tb_account的数据被逻辑删除时,通过selectOneById去查找数据时会查询不到数据。
默认逻辑删除的值是0和1,可以通过FlexGlobalConfig修改。
FlexGlobalConfig globalConfig = FlexGlobalConfig.getDefaultConfig();
//设置数据库正常时的值
globalConfig.setNormalValueOfLogicDelete("...");
//设置数据已被删除时的值
globalConfig.setDeletedValueOfLogicDelete("...");
LogicDeleteManager.execWithoutLogicDelete(() -> accountMapper.deleteById(1));
处理器名称 | 对应字段类型 | 数据正常时的值 | 数据被删除时的值 |
---|---|---|---|
IntegerLogicDeleteProcessor | integer | 0 | 1 |
BooleanLogicDeleteProcessor | tinyint | false | true |
DateTimeLogicDeleteProcessor | datetime | null | 被删除时间 |
TimeStampLogicDeleteProcessor | bigint | 0 | 被删除时的时间戳 |
PrimaryKeyLogicDeleteProcessor | 该条数据的主键类型 | null | 该条数据的主键值 |
//通过LogicDeleteManager设置
LogicDeleteManager.setProcessor(new DateTimeLogicDeleteProcessor());
通过LogicDeleteManager设置一个实现了LogicDeleteProcessor接口的处理器。
方法1是重写IService的removeById方法;
方法2是自定义逻辑删除处理功能。
@Configuration
public class MyConfiguration {
@Bean
public LogicDeleteProcessor logicDeleteProcessor(){
return new DateTimeLogicDeleteProcessor();
}
}
FlexGlobalConfig.getDefaultConfig().setLogicDeleteColumn("del_flag");
用于当有多个用户(或者多场景)去同时修改同一条数据的时候,只允许有一个修改成功。
使用一个字段,用于记录数据的版本,当修改数据的时候,会去检测当前版本是否是正在修改的版本,同时修改成功后会把 版本号 + 1。
@Table("tb_account")
public class Account {
//同一张表只能有一个字段被@Column(version = true)修饰
//Account在插入数据时,如果version未设置值,那么会自动被MyBatis-Flex设置为0
@Column(version = true)
private Long version;
}
FlexGlobalConfig.getDefaultConfig().setVersionColumn("version");
数据填充指的是,当 Entity 数据被插入 或者 更新的时候,会为字段进行一些默认的数据设置。这个非常有用,比如当某个 entity 被插入时候 会设置一些数据插入的时间、数据插入的用户 id,多租户的场景下设置当前租户信息等等。
@Table
注解的 onInsert
主要是在 Java 应用层面进行数据设置,而 @Column
注解的 onInsertValue
则是在数据库层面进行数据设置。
随着《网络安全法》的颁布施行,对个人隐私数据的保护已经上升到法律层面。 数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形, 实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供使用, 如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
MyBatis-Flex 提供了 @ColumnMask()
注解,以及内置的9种脱敏规则,帮助开发者方便的进行数据脱敏。
@Table("tb_account")
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
@ColumnMask(Masks.CHINESE_NAME)
private String userName;
}
1、通过MaskManager
注册新的脱敏规则:
MaskManager.registerMaskProcessor("自定义规则名称",
data -> { return data; })
2、使用自定义的脱敏规则
@Table("tb_account")
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
@ColumnMask("自定义规则名称")
private String userName;
}
MaskManager 提供了 execWithoutMask
、skipMask
、restoreMask
三个方法来处理这种场景。推荐使用execWithoutMask
方法,该方法使用了模版方法设计模式,保障跳过脱敏处理并执行相关逻辑后自动恢复脱敏处理。
MyBatis-Flex是一个MyBatis增强框架,可以使用MyBatis提供的二级缓存来作为数据缓存。但是它的缺点有很多,比如不适用于分布式环境,可以使用Spring Cache
模块来处理数据缓存。
以SpringBoot项目作为例子实现MyBatis-Flex项目将缓存数据存入Redis组件中。
1、引入spring-boot-starter-cache
和spring-boot-starter-data-redis
模块
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
2、设置Redis连接信息,全是默认额可以跳过这步
spring:
redis:
port: 6379
host: localhost
3、启用Spring Cache缓存
@EnableCaching
@Configuration
public class CacheConfig {
}
4、将ServiceImpl默认实现类转换为CacheableServiceImpl实现类
public interface AccountService extends IService<Account> {
}
@Service
public class AccountServiceImpl extends CacheableServiceImpl<AccountMapper, Account> implements AccountService {
}
5、最后即可使用Spring Cache的相关注解实现数据缓存到Redis中
@Service
@CacheConfig(cacheNames = "account") //设置统一的缓存名称
public class AccountServiceImpl extends CacheableServiceImpl<AccountMapper, Account> implements AccountService {
@Override
@CacheEvict(allEntries = true)
public boolean remove(QueryWrapper query) {
return super.remove(query);
}
@Override
@CacheEvict(key = "#id")
public boolean removeById(Serializable id) {
return super.removeById(id);
}
@Override
@CacheEvict(allEntries = true)
public boolean removeByIds(Collection<? extends Serializable> ids) {
return super.removeByIds(ids);
}
// 根据查询条件更新时,实体类主键可能为 null。
@Override
@CacheEvict(allEntries = true)
public boolean update(Account entity, QueryWrapper query) {
return super.update(entity, query);
}
@Override
@CacheEvict(key = "#entity.id")
public boolean updateById(Account entity, boolean ignoreNulls) {
return super.updateById(entity, ignoreNulls);
}
@Override
@CacheEvict(allEntries = true)
public boolean updateBatch(Collection<Account> entities, int batchSize) {
return super.updateBatch(entities, batchSize);
}
@Override
@Cacheable(key = "#id") //根据主键缓存数据
public Account getById(Serializable id) {
return super.getById(id);
}
@Override
@Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
public Account getOne(QueryWrapper query) {
return super.getOne(query);
}
@Override
@Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
public <R> R getOneAs(QueryWrapper query, Class<R> asType) {
return super.getOneAs(query, asType);
}
@Override
//根据方法名加查询SQL语句缓存结果数据,加上方法名是为了避免不同的方法使用一样的QueryWrapper
@Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
public List<Account> list(QueryWrapper query) {
return super.list(query);
}
@Override
@Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
public <R> List<R> listAs(QueryWrapper query, Class<R> asType) {
return super.listAs(query, asType);
}
// 无法通过注解进行缓存操作
@Override
@Deprecated
public List<Account> listByIds(Collection<? extends Serializable> ids) {
return super.listByIds(ids);
}
@Override
@Cacheable(key = "#root.methodName + ':' + #query.toSQL()")
public long count(QueryWrapper query) {
return super.count(query);
}
@Override
@Cacheable(key = "#root.methodName + ':' + #page.getPageSize() + ':'
+ #page.getPageNumber() + ':' + #query.toSQL()")
public <R> Page<R> pageAs(Page<R> page, QueryWrapper query, Class<R> asType) {
return super.pageAs(page, query, asType);
}
}
如果有非常多的缓存实现类,并且需要使用全部的缓存方法,可以使用代码生成器
辅助生成。
// 使用代码风格 1 生成
globalConfig.setServiceImplGenerateEnable(true);
globalConfig.setServiceImplSuperClass(CacheableServiceImpl.class);
globalConfig.setServiceImplCacheExample(true);
// 或者使用代码风格 2 生成
globleConfig.enableServiceImpl()
.setSuperClass(CacheableServiceImpl.class)
.setCacheExample(true);
SQL 审计是一项非常重要的工作,是企业数据安全体系的重要组成部分,通过 SQL 审计功能为数据库请求进行全程记录,为事后追溯溯源提供了一手的信息,同时可以通过可以对恶意访问及时警告管理员,为防护策略优化提供数据支撑。同时提供 SQL 访问日志长期存储,满足等保合规要求。
默认SQL审计功能是关闭的,默认情况下审计消息只会输出到控制台。
AuditManager.setAuditEnable(true);
Sql Audit: {platform=‘mybatis-flex’, module=‘null’, url=‘null’, user=‘null’, userIp=‘null’, hostIp=‘192.168.3.24’, query=‘SELECT * FROM
tb_account
WHEREid
= ?’, queryParams=[1], queryTime=1679991024523, elapsedTime=1}
消息包含了以下内容:
MyBatis-Flex内置了一个MessageFactory
的接口,只需要实现该接口,并为AduitManager
配置新的MessageFactory
就可以了。
public class MyMessageFactory implements MessageFactory {
@Override
public AuditMessage create() {
AuditMessage message = new AuditMessage();
// 设置 message 的基础内容,包括 platform、module、url、user、userIp、hostIp 内容
// 剩下的query、queryParams、queryCount、queryTime、elapsedTime为mybatis-flex设置
return message;
}
}
为AuditManager
配置新写的MyMessageFactory
:
MessageFactory creator = new MessageFactory();
AuditManager.setMessageFactory(creator);
MessageReporter
负责把 Mybaits-Flex 收集的 SQL 审计日志发送到指定位置,在 MyBatis-Flex 中只内置两个 MessageReporter
,他们分别是:
ConsoleMessageReporter
用于把 SQL 审计日志发送到控制台。HttpMessageReporter
用于把 SQL 审计日志发动到指定服务器。//reporter为自定义的实现了MessageReporter接口的实例
AuditManager.setMessageReporter(reporter);
MyBatis-Flex 内置了两个 Collector,他们分别是:
使用ConsoleMessageCollector
实时输出SQL日志。
MessageCollector collector = new ConsoleMessageCollector();
AuditManager.setMessageCollector(collector);
AuditManager.setAuditEnable(true);
1、内置方案
MyBatis-Flex内置的SQL打印分析功能是使用SQL审计模块完成的。
AuditManager.setAuditEnable(true);
MessageCollector collector = new ConsoleMessageCollector();
AuditManager.setMessageCollector(collector);
每次执行SQL请求,控制台将输出:
Flex exec sql took 2 ms >>> SELECT * FROM
tb_account
WHEREid
= 1
或者在Spring工程中将SQL打印到日志中,可以通过配置日志级别控制是否输出SQL,通过配置日志头控制SQL输出目的地。
import com.mybatisflex.core.audit.AuditManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisFlexConfiguration {
private static final Logger logger = LoggerFactory
.getLogger("mybatis-flex-sql");
public MyBatisFlexConfiguration() {
AuditManager.setAuditEnable(true);
AuditManager.setMessageCollector(auditMessage ->
logger.info("{},{}ms", auditMessage.getFullSql,
auditMessage.getElapsedTime()));
}
}
2、MyBatis自带方案
bootstrap.setLogImpl()
方法指定输出日志DataSource dataSource = ...;
MyBatisFlexBootstrap bootstrap = MyBatisFlexBootstrap.getInstance()
.setDataSource(dataSource)
.setLogImpl(StdOutImpl.class)
.addMapper(AccountMapper.class)
.start();
ConfigurationCustomizer
来为configuration
配置LogImpl
:@Configuration
public class MyConfigurationCustomizer implements ConfigurationCustomizer {
@Override
public void customize(FlexConfiguration configuration) {
configuration.setLogImpl(StdOutImpl.class);
}
}
3、p6spy方案,把数据源配置为p6spy数据源,使用p6spy的SQL输出功能进行SQL打印。
MyBaits-Flex 内置了功能完善的多数据源支持,不需要借助第三方插件或者依赖,开箱即用,包括支持druid
、hikaricp
、dbcp2
、beecp
在内的任何数据源。
mybatis-flex:
datasource:
ds1: # 用户自定义数据源
url: jdbc:mysql://127.0.0.1:3306/db
username: root
password: 123456
ds2:
url: jdbc:mysql://127.0.0.1:3306/db2
username: root
password: 123456
在没有Spring框架的情况下:
DataSource dataSource1 = new HikariDataSource();
dataSource1.setJdbcUrl(....)
DataSource dataSource2 = new HikariDataSource();
dataSource2.setJdbcUrl(....)
DataSource dataSource3 = new HikariDataSource();
dataSource3.setJdbcUrl(....)
MybatisFlexBootstrap.getInstance()
.setDataSource("ds1", dataSource1)
.addDataSource("ds2", dataSource2)
.addDataSource("ds3", dataSource3)
.start();
1、默认使用第一个配置的数据源
List<Row> rows = Db.selectAll("tb_account");
System.out.println(rows);
通过编码的方式切换到数据源ds2
:
try {
DataSourceKey.use("ds2");
List<Row> rows = Db.selectAll("tb_account");
System.out.println(rows);
} finally {
DataSourceKey.clear();
}
List<Row> rows = DataSourceKey.use("ds2", () -> Db.selectAll("tb_account"));
2、数据源切换
DataSourceKey.use
方法编码;@UseDataSource("dataSourceName")
在Mapper类上,添加注解用于指定使用那个数据源;@UseDataSource("dataSourceName")
在Mapper方法上,添加注解用于指定使用那个数据源;@Table(dataSource="dataSourceName")
在Entity类上添加注解,该 Entity 的增删改查请求默认使用该数据源。在SpringBoot项目上,
@UseDataSource("dataSourceName")
也可以用于在Controller或者Service上;非Spring项目需要参考MultiDataSourceAutoConfiguration
进行配置后使用。
@UseDataSource("ds2")
public interface AccountMapper extends BaseMapper {
List<Account> myMethod();
}
3、更多的Spring YAML配置支持
在上述配置中,ds1和ds2并未指定使用那个数据连接池,MyBatis-Flex会自动探测当前项目依赖那个连接池。目前支持了 druid
、hikaricp
、dbcp2
、beecp
的自动探测,如果项目中使用的不是这 4 种类型只有,需要添加 type
配置。
mybatis-flex:
datasource:
ds1:
type: com.your.datasource.type1
url: jdbc:mysql://127.0.0.1:3306/db
username: root
password: 123456
ds2:
type: com.your.datasource.type2
url: jdbc:mysql://127.0.0.1:3306/db2
username: root
password: 123456
4、动态添加新的数据源
在多租户等某些场景下,可能会用到动态的添加新的数据源。通过 FlexGlobalConfig 去获取 FlexDataSource 时,需等待应用完全启动成功后,才能正常获取 FlexDataSource, 否则将会得到 null 值。
FlexDataSource flexDataSource = FlexGlobalConfig.getDefaultConfig().getDataSource();
HikariDataSource newDataSource = new HikariDataSource();
flexDataSource.addDataSource("newKey", newDataSource);
Spring用户可以通过ApplicationListener
去监听ContextRefreshedEvent
事件,然后再去添加新的数据源。
public class DataSourceInitListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
FlexDataSource dataSource = FlexGlobalConfig.getDefaultConfig()
.getDataSource();
dataSource.addDataSource("....", new DruidDataSource("..."));
}
}
5、多数据源负载均衡
在进行数据查询的时候,随机使用一个数据源。 这是的在高并发的场景下,起到负载的效果。
假设多数据源配置如下:
mybatis-flex:
datasource:
ds1:
type: druid
url: jdbc:mysql://127.0.0.1:3306/db
username: root
password: 123456
asyncInit: true
ds2:
type: com.your.datasource.type2
url: jdbc:mysql://127.0.0.1:3306/db2
username: root
password: 123456
other:
type: com.your.datasource.type2
url: jdbc:mysql://127.0.0.1:3306/db2
username: root
password: 123456
如果想负载均衡使用多个数据源配置如下:
try{
//使用ds开头的任意一个数据源
DataSourceKey.use("ds*");
List<Row> rows = Db.selectAll("tb_account");
System.out.println(rows);
}finally{
DataSourceKey.clear();
}
数据源加密指的是我们对数据库的链接信息进行加密,目前 MyBatis-Flex 支持加密的内容有数据源 URL、用户名、用户密码。通过数据源加密功能,我们可以有效的保证数据库安全,减少数据盗用风险。
1、MyBatis-Flex 支持任何类型的加密方式。 在 MyBatis-Flex 中内置了一个名为 DataSourceDecipher
的接口,其作用是去解密用户配置的加密内容。
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/flex_test");
dataSource.setUsername("root123"); // 真实的账号应该是 root
dataSource.setPassword("123456---0000"); // 真实的密码应该是 123456
//配置数据源解密器:DataSourceDecipher
//需要在MyBatis-Flex初始化之前进行
DataSourceManager.setDecipher(new DataSourceDecipher() {
@Override
public String decrypt(DataSourceProperty property, String value) {
//解密用户名,通过编码支持任意加密方式的解密
if (property == DataSourceProperty.USERNAME) {
return value.substring(0, 4);
}
//解密密码
else if (property == DataSourceProperty.PASSWORD) {
return value.substring(0, 6);
}
return value;
}
});
MybatisFlexBootstrap.getInstance()
.setDataSource(dataSource)
.addMapper(TenantAccountMapper.class)
.start();
List<Row> rowList = Db.selectAll("tb_account");
RowUtil.printPretty(rowList);
2、SpringBoot支持
@Configuration
public class MyConfiguration {
@Bean
public DataSourceDeciper deciper() {
DataSourceDecipher decipher = new ...;
return decipher;
}
}
动态表名是指用户对数据进行增删改查时,传入表名能够根据上下文动态修改当前的表。在多租户情况下,不同租户拥有不同的表;分库分表时减轻数据压力。
在启动时通过TableManager.setDynamicTableProcessor()
配置动态表名处理器即可。对数据库进行增删改查,都会调用DynamicTableProcessor.process
方法,获得最新的表名进行SQL构建操作。
TableManager.setDynamicTableProcessor(new DynamicTableProcessor() {
@Override
public String process(String tableName) {
return tableName + "_01";
}
});
在某些情况下,临时修改映射关系,可以通过如下配置,此时当前线程不再通过DynamicTableProcessor
获取。
try {
TableManager.setHintTableMapping("tb_account", "tb_account_01");
} finally {
TableManager.clear();
}
1、动态Schema
动态Schema和动态表名相似,通过 TableManager.setDynamicSchemaProcessor()
配置动态 Schema 处理器 DynamicSchemaProcessor
即可。动态Schema的配置,只对使用了注解@Table(schema="xxx")
的Entity有效。
2、SpringBoot支持
@Configuration
public class MyConfiguration {
@Bean
public DynamicTableProcessor dynamicTableProcessor(){
DynamicTableProcessor processor = new ....;
return processor;
}
@Bean
public DynamicSchemaProcessor dynamicSchemaProcessor(){
DynamicSchemaProcessor processor = new ....;
return processor;
}
}
MyBatis-Flex提供了一个Db.tx()
方法进行事务管理,如果使用Spring框架,可以使用@Transactional
注解进行事务管理。
//返回结果为Boolean,返回null或者false或者抛出异常,事务回滚
boolean tx(Supplier<Boolean> supplier);
//suplier是要执行的内容,propagation是事务传播属性
boolean tx(Supplier<Boolean> supplier, Propagation propagation);
//返回结果由Supplier参数决定,只有抛出异常时事务回滚
<T> T txWithResult(Supplier<T> supplier);
<T> T txWithResult(Supplier<T> supplier, Propagation propagation);
Db.tx(() -> {
//事务操作
return true;
});
支持无限极嵌套,默认情况下,嵌套事务直接的关系是:REQUIRED
(若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务)。
Db.tx(() -> {
//进行事务操作
boolean success = Db.tx(() -> {
//另一个事务操作
return true;
});
return true;
});
@Transactional
MyBatis-Flex 已支持 Spring 框架的 @Transactional
,在使用 SpringBoot 的情况下,可以直接使用 @Transactional
进行事务管理。 同理使用 Spring 的 TransactionTemplate
进行事务管理也是没问题的。非SpringBoot项目,参考FlexTransactionAutoConfiguration
进行事务配置。
在多数据源的情况下,所有数据源的数据库请求(Connection)会执行相同的 commit
或者 rollback
,MyBatis-Flex 只保证了程序端的原子操作, 但并不能保证多个数据源之间的原子操作。
@Transactional
public void doSomething(){
try{
DataSourceKey.use("ds1");
Db.updateBySql("update ....");
}finally{
DataSourceKey.clear()
}
try{
DataSourceKey.use("ds2");
Db.updateBySql("update ...");
}finally{
DataSourceKey.clear()
}
//抛出异常
int x = 1/0;
}
在上例执行了两次Db.updateBySql(...)
,它们是两个不同数据源,都在同一个事务中,因此当抛出异常时,它们都会进行回滚。
在某些场景下父子线程会同时访问父线程进行传递值进行切换数据源的场景。
public static void main(String[]args){
//线程1
//进行数据库操作读取 ds1
//切换数据源2
DataSourceKey.use("ds2");
new Thread(() -> {
//查询数据源 ds2
//实际在线程2并不是ds2而是ds1
}).start();
}
此类场景可以使用跨越线程池的ThreadLocal,比如阿里的transmittable-thread-local
。
<dependency>
<groupId>com.alibabagroupId>
<artifactId>transmittable-thread-localartifactId>
<version>2.14.2version>
dependency>
数据权限指不同的用户通过某个方法查询时候得到的是不同的数据结果集。常见的数据权限有获取全部数据、仅获取本人创建的数据、获取当前用户的部门数据、获取部门级以下部门的数据、获取某个地区的数据等等。通过当前用户的部门、角色、权限等信息,查询时添加特定的条件。
1、使用自动以数据方言IDialect
在自定义方言中,重写forSelectByQuery
方法,用来构建返回根据QueryWrapper
查询的方法。
public class MyPermissionDialect extends CommonsDialectImpl {
@Override
public String forSelectByQuery(QueryWrapper queryWrapper) {
//获取当前用户信息,为 queryWrapper 添加额外的条件
queryWrapper.and("...");
return super.buildSelectSql(queryWrapper);
}
}
如果使用的是Oracle数据库,需要继承OracleDialect
类。在项目启动时需要进行注册。
DialectFactory.registerDialect(DbType.MYSQL, new MyPermissionDialect());
重写IDialect
后所有的查询都添加了条件,可以通过CPI获取QueryWrapper查询了那些表,然后可以设置不添加条件。
List<QueryTable> tables = CPI.getQueryTables(queryWrapper);
2、重写IService的查询方法
在一般应用中查询是通过Service进行的,可以构建自己的IServiceImpl来实现这种需求。
字段权限指在一张表中设计了许多字段,不同用户查询返回的字段结果是不一致的。在@Table
注解中,有一个配置onSet
用来设置这张表的设置监听。当我们使用 sql 、调用某个方法去查询数据,得到的数据内容映射到 entity 实体,mybatis 通过 setter 方法去设置 entity 的值时的监听。
1、为实体类编写一个set监听器
public class AccountOnSetListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Ojbect value) {
if (property.equals("password")) {
//查询当前用户的权限
boolean hasPasswordPermission = getPermission();
//如果没有权限则把数据库查询到的password修改为null
if (!hasPasswordPermission) {
value = null;
}
}
return value;
}
}
2、为实体类配置onSet监听
@Table(value = "tb_account", onSet = AccountOnSetListener.class)
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
private String password;
}
字段加密指的是数据库在存入了明文内容,当我们查询时返回内容为加密内容。
1、编写一个set监听器
public class AccountOnSetListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Object value) {
if (value != null){
//对字段内容进行加密
value = encrypt(value);
}
return value;
}
}
2、为实体类配置监听
@Table(value = "tb_account", onSet = AccountOnSetListener.class)
public class Account {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
private String password;
}
字典回写指的是在一个实体类中,可能会有很多业务字段,当我们发现有某个数据库字段赋值后,主动去为业务赋值。编写一个set监听器,为实体类配置监听。
public class AccountOnSetListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Object value) {
Account account = (Account) entity;
if (property.equals("sex") && value != null){
switch (value){
case 0:
account.setSexString('女');
break;
case 1:
account.setSexString('男');
break;
default:
account.setSexString('未知');
}
}
return value;
}
}
在某些场景下,我们希望在Entity中定义的属性是一个枚举类,而不是基本类型。MyBatis内置了EnumTypeHandler处理器,通过处理后,数据库保存的是TypeEnum对应的属性名称。
@Table(value = "tb_account")
public class Account{
@Id(keyType = KeyType.Auto)
private Long id;
private TypeEnum typeEnum;
}
如果希望保存到数据库的是TypeEnum枚举的某个属性值,需要使用@EnumValue
注解。
public enum TypeEnum {
TYPE1(1, "类型1"),
TYPE2(2, "类型2"),
TYPE3(3, "类型3"),
;
@EnumValue
private int code;
private String desc;
TypeEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
多租户简单来说是指一个单独的实例可以为多个用户(或组织)服务。多租户技术要求所有用户共用同一个数据中心,但能提供多个客户端相同甚至可定制化的服务,并且仍然可以保障客户的数据隔离。多租户的数据隔离有许多种方案,但最为常见的是以列进行隔离的方式。MyBatis-Flex 内置的正是通过指定的列(租户ID tenant_id
)进行隔离的方案。
1、通过@Column(tenantId = true)
标识租户列;
2、为TenantManager
配置TenantFactory
(生产租户ID)。
@Table("tb_article")
public class Article {
@Id(keyType = KeyType.Auto)
private Long id;
@Column(tenantId = true)
private Long tenantId;
}
TenantManager.setTenantFactory(new TenantFactory() {
@Override
public Object[] getTenantIds() {
//通过这里返回当前租户 ID
return new Object[]{100};
}
});
@Configuration
public class MyConfiguration {
@Bean
public TenantFactory tenantFactory(){
TenantFactory tenantFactory = new ....;
return tenantFactory;
}
}
在某些场景下,增删改查时需要忽略租户条件,可以使用TenantManager的withoutTenantCondition
、ignoreTenantCondition
、restoreTenantCondition
三个方法。
TenantAccountMapper mapper = ...;
List<TenantAccount> tenantAccounts = TenantManager.withoutTenantCondition(mapper::selectAll);
System.out.println(tenantAccounts);
FlexGloalConfig.getDefaultConfig().setTenantColumn("tenant_id");
MyBatis-Flex 的读写分离功能是基于多数据源功能实现的。原理是让主数据库处理事务性操作,比如增删改,而从数据库处理查询操作。在 MyBatis-Flex 中,提供了一种基于 Mapper 方法的读写分离策略。
自定义DataSourceShardingStrategy
:
public class MyStrategy implements DataSourceShardingStrategy {
//currentDataSourceKey:当前由用户端已配置的 key
//mapper:当前的mapper对象
//mapperMethod: 当前的mapper方法
//methodArgs:当前的 mapper 方法的参数内容
public String doSharding(String currentDataSourceKey
, Object mapper, Method mapperMethod, Object[] methodArgs){
//返回新的数据源 key
return "newDataSourceKey";
}
}
自定义好数据源分片策略后,项目启动时配置自定义的分片策略:
DataSourceManager.setDataSourceShardingStrategy(new MyStrategy());
在模块mybatis-flex-codegen
中,提供了可以通过数据库表,生成 Entity 类和 Mapper 类的功能。当我们把数据库表设计完成 后可以使用其快速生成 Entity 和 Mapper 的 java 类。
先添加maven依赖:
<dependency>
<groupId>com.mybatis-flexgroupId>
<artifactId>mybatis-flex-codegenartifactId>
<version>1.6.6version>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
<version>4.0.3version>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
<version>8.0.32version>
dependency>
然后编写一个带有main方法的类:
public class Codegen {
public static void main(String[] args) {
//配置数据源
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/your-database?characterEncoding=utf-8");
dataSource.setUsername("root");
dataSource.setPassword("******");
//创建配置内容,两种风格都可以。
GlobalConfig globalConfig = createGlobalConfigUseStyle1();
//GlobalConfig globalConfig = createGlobalConfigUseStyle2();
//通过 datasource 和 globalConfig 创建代码生成器
Generator generator = new Generator(dataSource, globalConfig);
//生成代码
generator.generate();
}
public static GlobalConfig createGlobalConfigUseStyle1() {
//创建配置内容
GlobalConfig globalConfig = new GlobalConfig();
//设置根包
globalConfig.setBasePackage("com.test");
//设置表前缀和只生成哪些表
globalConfig.setTablePrefix("tb_");
globalConfig.setGenerateTable("tb_account", "tb_account_session");
//设置生成 entity 并启用 Lombok
globalConfig.setEntityGenerateEnable(true);
globalConfig.setEntityWithLombok(true);
//设置生成 mapper
globalConfig.setMapperGenerateEnable(true);
//可以单独配置某个列
ColumnConfig columnConfig = new ColumnConfig();
columnConfig.setColumnName("tenant_id");
columnConfig.setLarge(true);
columnConfig.setVersion(true);
globalConfig.setColumnConfig("tb_account", columnConfig);
return globalConfig;
}
public static GlobalConfig createGlobalConfigUseStyle2() {
//创建配置内容
GlobalConfig globalConfig = new GlobalConfig();
//设置根包
globalConfig.getPackageConfig()
.setBasePackage("com.test");
//设置表前缀和只生成哪些表,setGenerateTable 未配置时,生成所有表
globalConfig.getStrategyConfig()
.setTablePrefix("tb_")
.setGenerateTable("tb_account", "tb_account_session");
//设置生成 entity 并启用 Lombok
globalConfig.enableEntity()
.setWithLombok(true);
//设置生成 mapper
globalConfig.enableMapper();
//可以单独配置某个列
ColumnConfig columnConfig = new ColumnConfig();
columnConfig.setColumnName("tenant_id");
columnConfig.setLarge(true);
columnConfig.setVersion(true);
globalConfig.getStrategyConfig()
.setColumnConfig("tb_account", columnConfig);
return globalConfig;
}
}
由于 MyBatis-Flex 的 APT 功能会自动帮我们生成了 Mapper 的 Java 类,如果我们在代码生成器中选择生成 Mapper, 则建议把 APT 的 Mapper 生成功能给关闭掉,否则系统中会存在两份一样功能的 Mapper。
1、在代码生成器中,支持如下8中类型的产物生成:
启用或关闭某种类型产物的生成。
// 开启 Entity 的生成
globalConfig.enableEntity();
// 关闭 Entity 的生成
globalConfig.disableEntity();