MyBatis-Flex
MyBatis-Flex 是一个优雅的 MyBatis 增强框架,它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何数据库,其内置的 QueryWrapper 帮助我们极大的减少了 SQL 编写的工作的同时,减少出错的可能性。 https://mybatis-flex.com/zh/intro/what-is-mybatisflex.html
QueryWrapper
https://mybatis-flex.com/zh/base/querywrapper.html
//构造 QueryWrapper
QueryWrapper query = new QueryWrapper();
query.where(ACCOUNT.ID.ge(100));
//通过 query 查询数据列表
accountMapper.selectListByQuery(query);
// 这里的都是 数据库 列名
OrgDeviceInfoNotify notifyEndity = orgDeviceInfoNotifyDao
.selectOneByQuery(QueryWrapper.create().eq("notify_tuple_key", notifyTupleKey));在 QueryWrapper 的条件构建中,如果传入 null 值,则自动忽略该条件。
在 MyBatis-Flex 的 BaseMapper 中,提供了如下的功能用于查询数据库的数据:
selectOneById(id):根据主键查询数据。selectOneByEntityId(entity):根据实体主键查询数据,便于对复合主键实体类的查询。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):根据条件查询数据总量。
代码实例, 封装注解查询
实例, 它还支持类似JPA的api 方式
import com.mybatisflex.core.constant.SqlConsts;
import com.mybatisflex.core.query.Brackets;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryWrapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.data.domain.Sort;
/**
* @author yangfh
* @date 2023-11-07 16:51
**/
public class QueryHelpler {
public static <T> List<T> getOrPutIfNeed(Map<String, List<T>> queryGroupMap , String key){
List<T> ret = queryGroupMap.get(key);
if(ret == null){
ret = new ArrayList<>();
queryGroupMap.put(key, ret);
}
return ret;
}
public static Map<String, List<Group>> getQueryGroupMap(BeanDesc desc, Object instance) throws IllegalAccessException {
Map<String, List<Group>> queryGroupMap = new HashMap<>();
Collection<PropDesc> propDescs = desc.getProps();
for (Iterator<PropDesc> iterator = propDescs.iterator(); iterator.hasNext(); ) {
PropDesc next = iterator.next();
Field field = next.getField();
field.setAccessible(true);
QueryField q = field.getAnnotation(QueryField.class);
if(q != null){
Object val = field.get(instance);
if (ObjectUtil.isNull(val) && q.type()!= Type.NULL_ABLE) {
continue;
}
List<Group> condList = getOrPutIfNeed(queryGroupMap, q.groupName());
condList.add(new Group(q.groupName(), q.conn(),q.order(),q , next));
}
}
return queryGroupMap;
}
public static QueryWrapper getPredicate(QueryCandidate criteria,
Iterable<JoinOn> joinTables, Class<?> listDtoClass) {
QueryWrapper root = new QueryWrapper();
String outConnect = preDeclarationQuery(root,
criteria, joinTables, listDtoClass);
appendPredicate(root, criteria, joinTables, outConnect);
return root;
}
public static QueryWrapper setSort(QueryWrapper root, Sort sort) {
if (sort.isSorted()){
sort.stream().forEach(order->{
String property = order.getProperty();
Sort.Direction direction = order.getDirection();
if(direction.isAscending()){
root.orderBy(property, true);
}else{
root.orderBy(property,false);
}
});
}
return root;
}
public static QueryWrapper setSort(QueryWrapper root, Sort sort, Class<?> listDtoClass) {
if (sort.isSorted()){
Map<String, String> selectMapper = getSelectMapper(listDtoClass);
sort.stream().forEach(order->{
String property = order.getProperty();
String columnName = selectMapper.get(property);
Sort.Direction direction = order.getDirection();
if(direction.isAscending()){
root.orderBy(columnName, true);
}else{
root.orderBy(columnName,false);
}
});
}
return root;
}
public static QueryWrapper appendJoin(QueryWrapper root,String mainTable, Iterable<JoinOn> joinTables) {
root.from(mainTable);
for (Iterator<JoinOn> iterator = joinTables.iterator(); iterator.hasNext(); ) {
JoinOn next = iterator.next();
String joinType = next.getJoinType();
List<QueryCondition> on = next.getOn();
//TODO 多条件
if (SqlConsts.LEFT_JOIN.equals(joinType)){
root.leftJoin(next.getJoinTable()).on(on.get(0));
}else if (SqlConsts.RIGHT_JOIN.equals(joinType)){
root.rightJoin(next.getJoinTable()).on(on.get(0));
}else{
root.join(next.getJoinTable()).on(on.get(0));
}
}
return root;
}
public static QueryWrapper appendSelect(QueryWrapper root, Class<?> listDtoClass) {
Map<String, String> selectMapper = getSelectMapper(listDtoClass);
selectMapper.forEach((key,val)->{
QueryColumn queryColumn = new QueryColumn(val);
queryColumn.setAlias(key);
root.select( queryColumn);
});
return root;
}
/**
*
* key 是 实体属性名
* value 是 数据库字段名
*/
private static Map<String, String> getSelectMapper(Class<?> listDtoClass) {
final Map<String, String> map = new HashMap<>();
final BeanDesc desc = BeanUtil.getBeanDesc(listDtoClass);
Collection<PropDesc> props = desc.getProps();
for (Iterator<PropDesc> iterator = props.iterator(); iterator.hasNext(); ) {
PropDesc next = iterator.next();
Field field = next.getField();
ResultField selectField = field.getAnnotation(ResultField.class);
if(null != selectField){
String path = selectField.path();
String as = next.getFieldName();
map.put(as, path);
}
}
return map;
}
static String preDeclarationQuery(QueryWrapper root,
QueryCandidate criteria, Iterable<JoinOn> joinTables, Class<?> listDtoClass){
Class<?> queryClass = criteria.getClass();
QueryClass queryDto = (QueryClass) queryClass.getAnnotation(QueryClass.class);
if(null == queryDto){
throw new RuntimeException(criteria.getClass()+", 无查询注解声明");
}
String mainTable = queryDto.mainTable();
appendJoin(root, mainTable, joinTables);
appendSelect(root, listDtoClass);
String outConnect = queryDto.groupConn();
return outConnect;
}
/**
* 实现多分组条件
*/
public static QueryWrapper appendPredicate(QueryWrapper root,QueryCandidate query, Iterable<JoinOn> joinTables,
String outConnect) {
//它这个有缓存
final BeanDesc desc = BeanUtil.getBeanDesc(query.getClass());
Map<String, List<Group>> queryGroupMap = null;
try {
queryGroupMap = getQueryGroupMap (desc, query);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Set<String> keys = queryGroupMap.keySet();
//外部
for (Iterator<String> iterator = keys.iterator(); iterator.hasNext(); ) {
String key = iterator.next();
List<Group> condSlist = queryGroupMap.get(key)
.stream()
.sorted((s, other)->{
return s.getOrder()-other.getOrder();
}).collect(Collectors.toList());
//小组
QueryCondition groupPredicate = null;
for (Iterator<Group> queryIterator = condSlist.iterator(); queryIterator.hasNext(); ) {
Group next = queryIterator.next();
QueryField q = next.getQuery();
Field field = next.getDesc().getField();
String path = q.path();
path = isBlank(path) ? field.getName() : path;
String conn = next.getConn();
Object val = null;
try {
val = field.get(query);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
if (ObjectUtil.isNull(val) && q.type()!= Type.NULL_ABLE) {
continue;
}
QueryCondition predicate = parseCondition(path, q, val);
if (predicate == null) {
continue;
}
//小小组内条件
if(groupPredicate == null) {
groupPredicate = predicate;
}else if(SqlConsts.AND.equals(conn)){
groupPredicate.and(predicate);
}else {
groupPredicate.or(predicate);
}
}
//小组 无条件
if (groupPredicate == null) {
continue;
}
//最外部, 各小组的连接
if(SqlConsts.AND.equals(outConnect)){
root.and(new Brackets(groupPredicate) );
}else {
root.or(new Brackets(groupPredicate) );
}
}
return root;
}
// public static QueryWrapper parseGroup(QueryWrapper root, QueryWrapper another) {
//
// }
//
public static QueryCondition parseCondition(String path, QueryField query, Object value) {
QueryField.Type type = query.type();
if(type == Type.NULL_EXC) {
if(ObjectUtil.isNull(value)) {
return null;
}else {
type = query.nullType();
return parseExpression(path, type, value);
}
}
return parseExpression(path, type, value);
}
//创建单个表达式 条件
private static QueryCondition parseExpression(String path, Type type, Object value) {
QueryColumn queryColumn = new QueryColumn(path);
switch (type) {
case EQUAL:{
return QueryCondition.create(queryColumn, SqlConsts.EQUALS, value);
}
case NOT_EQUAL:
return QueryCondition.create(queryColumn, SqlConsts.NOT_EQUALS, value);
case GREATER_THAN:
return QueryCondition.create(queryColumn, SqlConsts.GE, value);
case GREATER_THAN_NQ:
return QueryCondition.create(queryColumn, SqlConsts.GT, value);
case LESS_THAN:
return QueryCondition.create(queryColumn, SqlConsts.LE, value);
case LESS_THAN_NQ:
return QueryCondition.create(queryColumn, SqlConsts.LT, value);
case NULL_ABLE:
if(ObjectUtil.isNull(value)) {
return QueryCondition.create(queryColumn, SqlConsts.IS_NULL);
}else {
return QueryCondition.create(queryColumn, SqlConsts.IS_NOT_NULL);
}
case INNER_LIKE:
return QueryCondition.create(queryColumn, SqlConsts.LIKE, "%" + value.toString() + "%");
case LEFT_LIKE:
return QueryCondition.create(queryColumn, SqlConsts.LIKE, "%" + value.toString() );
case RIGHT_LIKE:
return QueryCondition.create(queryColumn, SqlConsts.LIKE, value.toString() + "%");
case IN:
if (CollUtil.isNotEmpty((Collection)value)) {
return QueryCondition.create(queryColumn, SqlConsts.IN, value);
}
case NOT_IN:
if (CollUtil.isNotEmpty((Collection)value)) {
return QueryCondition.create(queryColumn, SqlConsts.NOT_IN, value);
}
default:
return null;
}
}
public static boolean isBlank(final CharSequence cs) {
int strLen;
if (cs == null || (strLen = cs.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (Character.isWhitespace(cs.charAt(i)) == false) {
return false;
}
}
return true;
}
}
final com.mybatisflex.core.paginate.Page<T> page = PageUtils.pageableToFlex( pageable);
QueryWrapper root = QueryHelpler.getPredicate(criteria, joinTables, listDtoClass);
QueryHelpler.setSort( root, pageable.getSort(), listDtoClass);
Optional<GenericDaoMapper<E>> daoMapperFound = getDAOMapper();
GenericDaoMapper<E> daoMapper = daoMapperFound.get();
pageResult = daoMapper.paginateAs(page, root, listDtoClass)
链式操作
https://mybatis-flex.com/zh/base/chain.html
在 MyBatis-Flex 中,内置了 QueryChain.java 、 UpdateChain.java 以及 DbChain.java 用于对数据进行链式查询操作和链式操作(修改和删除)。
QueryChain:链式查询 UpdateChain:链式更新 DbChain:链式调用 Db + Row
List<Article> articles = QueryChain.of(mapper)
.select(ARTICLE.ALL_COLUMNS)
.from(ARTICLE)
.where(ARTICLE.ID.ge(100))
.list();
QueryChain 的方法
- one():获取一条数据
- list():获取多条数据
- page():分页查询
- obj():当 SQL 查询只返回 1 列数据的时候,且只有 1 条数据时,可以使用此方法
- objList():当 SQL 查询只返回 1 列数据的时候,可以使用此方法
- count():查询数据条数
- exists():是否存在,判断 count 是否大于 0
update
@Test
public void testUpdateChain1() {
UpdateChain.of(Account.class)
.set(Account::getUserName, "张三")
.setRaw(Account::getAge, "age + 1")
.where(Account::getId).eq(1)
.update();
}UPDATE
tb_accountSETuser_name= ‘张三’ ,age= age + 1 WHEREid= 1
one()
- one():获取一条数据
- oneAs(asType):查询数据,并直接转换为 vo、dto 等
- oneWithRelations:查询一条数据及其关联数据
- oneWithRelationsAs:查询一条数据及其关联数据,并直接转换为 vo、dto 等
- oneOpt:返回 Optional 类型,获取一条数据
- oneAsOpt(asType):返回 Optional 类型,查询数据,并直接转换为 vo、dto 等
- oneWithRelationsOpt:返回 Optional 类型,查询一条数据及其关联数据
- oneWithRelationsAsOpt:返回 Optional 类型,查询一条数据及其关联数据,并直接转换为 vo、dto 等
list()
- list():查询数据列表
- listWithRelations():查询数据列表及其关联数据
- listAs():查询数据列表,并直接转换为 vo、dto 等
- listWithRelationsAs():查询数据列表,及其关联数据,并直接转换为 vo、dto 等
page()
- page(page):分页查询数据列表
- pageAs(page):分页查询数据列表,并直接转换为 vo、dto 等
注解
实体注解
@Table 注解
public @interface Table {
/**
* 显式指定表名称
*/
String value();
/**
* 数据库的 schema(模式)
*/
String schema() default "";
/**
* 默认为 驼峰属性 转换为 下划线字段
*/
boolean camelToUnderline() default true;
/**
* 默认使用哪个数据源,若系统找不到该指定的数据源时,默认使用第一个数据源
*/
String dataSource() default "";
/**
* 监听 entity 的 insert 行为
*/
Class<? extends InsertListener> onInsert() default NoneListener.class;
/**
* 监听 entity 的 update 行为
*/
Class<? extends UpdateListener> onUpdate() default NoneListener.class;
/**
* 监听 entity 的查询数据的 set 行为,用户主动 set 不会触发
*/
Class<? extends SetListener> onSet() default NoneListener.class;
/**
* 在某些场景下,我们需要手动编写 Mapper,可以通过这个注解来关闭 APT 的 Mapper 生成
*/
boolean mapperGenerateEnable() default true;
}@Id 注解
public @interface Id {
/**
* ID 生成策略,默认为 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;
}
自增的方式
public enum KeyType {
/**
* 自增的方式
*/
Auto,
/**
* 通过执行数据库 sql 生成
* 例如:select SEQ_USER_ID.nextval as id from dual
*/
Sequence,
/**
* 通过 IKeyGenerator 生成器生成
*/
Generator,
/**
* 其他方式,比如在代码层用户手动设置
*/
None,
}@Column 注解
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
*/
boolean isLogicDelete() default false;
/**
* 是否为乐观锁字段,若是乐观锁字段的话,数据更新的时候会去检测当前版本号,若更新成功的话会设置当前版本号 +1
* 只能用于数值的字段
*/
boolean version() default false;
/**
* 配置的 jdbcType
*/
JdbcType jdbcType() default JdbcType.UNDEFINED;
/**
* 自定义 TypeHandler
*/
Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;
}关联关系注解
https://mybatis-flex.com/zh/base/relations-query.html https://mybatis-flex.com/zh/base/querywrapper.html
在 MyBatis-Flex 中,我们内置了 3 种方案,帮助用户进行关联查询,比如 一对多、一对一、多对一、多对多等场景
在 MyBatis-Flex 中,提供了 4 个 Relations 注解,他们分别是:
- RelationOneToOne:用于一对一的场景
- RelationOneToMany:用于一对多的场景
- RelationManyToOne:用于多对一的场景
- RelationManyToMany:用于多对多的场景
添加了以上配置的实体类,在通过 BaseMapper 的方法查询数据时,需要调用 select***WithRelations() 方法,Relations 注解才能生效。 否则 MyBatis-Flex 自动忽略 Relations 注解。
一对一 @RelationOneToOne
假设有一个账户,账户有身份证,账户和身份证的关系是一对一的关系,代码如下所示:
Account.java :
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationOneToOne(selfField = "id", targetField = "accountId")
private IDCard idCard;
//getter setter
}IDCard.java :
@Table(value = "tb_idcard")
public class IDCard implements Serializable {
private Long accountId;
private String cardNo;
private String content;
//getter setter
}@RelationOneToOne 配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
PS: 若 selfField 是主键,且当前表只有 1 个主键时,可以不填写。因此,以上的配置可以简化为
@RelationOneToOne(targetField = "accountId")
List<Account> accounts = accountMapper.selectAllWithRelations();
System.out.println(accounts);其执行的 SQL 如下:
SELECT `id`, `user_name`, `age` FROM `tb_account`
SELECT `account_id`, `card_no`, `content` FROM `tb_idcard`
WHERE account_id IN (1, 2, 3, 4, 5)在以上的
@RelationOneToOne注解中,若IDCard.java是 VO、DTO 等,而不是一个带有@Table注解的 Entity 类, 则需要在@RelationOneToOne配置上targetTable用于指定查询的表名。
一对多 @RelationOneToMany
假设一个账户有很多本书籍,一本书只能归属一个账户所有;账户和书籍的关系是一对多的关系,代码如下:
Account.java :
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
@RelationOneToMany(selfField = "id", targetField = "accountId")
private List<Book> books;
//getter setter
}Book.java :
@Table(value = "tb_book")
public class Book implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private Long accountId;
private String title;
//getter setter
}@RelationOneToMany 配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
多对一 @RelationManyToOne
假设一个账户有很多本书籍,一本书只能归属一个账户所有;账户和书籍的关系是一对多的关系,书籍和账户的关系为多对一的关系,代码如下:
Account.java:
public class Account implements Serializable {
@Id(keyType = KeyType.Auto)
private Long id;
private String userName;
}Book.java 多对一的配置:
@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;
//getter setter
}@RelationManyToOne 配置描述:
- selfField 当前实体类的属性
- targetField 目标对象的关系实体类的属性
PS: 若 targetField 目标对象的是主键,且目标对象的表只有 1 个主键时,可以不填写。因此,以上的配置可以简化为
@RelationManyToOne(selfField = "accountId")
多对多 @RelationManyToMany
- 方案1:
- 方案2:Field Query
- 方案3:Join Query
执行原生SQL(Db)
https://mybatis-flex.com/zh/base/db-row.html
Db + Row 工具类,提供了在 Entity 实体类之外的数据库操作能力。使用 Db + Row 时,无需对数据库表进行映射, Row 是一个 HashMap 的子类,相当于一个通用的 Entity。
//使用原生 SQL 插入数据
String sql = "insert into tb_account(id,name) value (?, ?)";
Db.insertBySql(sql,1,"michael");
//使用 Row 插入数据
Row account = new Row();
account.set("id",100);
account.set(ACCOUNT.USER_NAME,"Michael");
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);
//查询所有大于 18 岁用户的Id和用户名对应的Map
Map map = Db.selectFirstAndSecondColumnsAsMap("select id,user_name from tb_account where age >?",18);
//分页查询:每页 10 条数据,查询第 3 页的年龄大于 18 的用户
QueryWrapper query=QueryWrapper.create()
.where(ACCOUNT.AGE.ge(18));
Page<Row> rowPage=Db.paginate("tb_account",3,10,query);事务模版方法
https://mybatis-flex.com/zh/core/tx.html
MyBatis-Flex 提供了一个名为 Db.tx() 的方法^1.0.6,用于进行事务管理,若使用 Spring 框架的场景下,也可使用 @Transactional 注解进行事务管理。
Db.tx() 方法定义如下:
boolean tx(Supplier<Boolean> supplier);
boolean tx(Supplier<Boolean> supplier, Propagation propagation);
<T> T txWithResult(Supplier<T> supplier);
<T> T txWithResult(Supplier<T> supplier, Propagation propagation);方法:
- tx:返回结果为 Boolean,返回
null或者false或者 抛出异常,事务回滚 - txWithResult:返回结果由
Supplier参数决定,只有抛出异常时,事务回滚
参数:
- supplier:要执行的内容(代码)
- propagation:事务传播属性
Db.tx(() -> {
//进行事务操作
boolean success = Db.tx(() -> {
//另一个事务的操作
return true;
});
return true;
});若 tx() 方法抛出异常,或者返回 false,或者返回 null,则回滚事务。只有正常返回 true 的时候,进行事务提交。
数据权限
https://mybatis-flex.com/zh/core/data-permission.html
方式1:使用自定义数据方言 IDialect
在自定义方言中,重写 forSelectByQuery 方法,这个方法是用于构建返回根据 QueryWrapper 查询的方法, 以下是示例代码:
public class MyPermissionDialect extends CommonsDialectImpl {
@Override
public String forSelectByQuery(QueryWrapper queryWrapper) {
//获取当前用户信息,为 queryWrapper 添加额外的条件
queryWrapper.and("...");
return super.buildSelectSql(queryWrapper);
}
}方式2:重写 IService 的查询方法
在一般的应用中,查询是通过 Service 进行的,MyBatis-Flex 提供了 IService 接口及其默认的 ServiceImpl 实现类。
我们可以通过构建自己的 IServiceImpl 来实现这一种需求,例如:
public class MyServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
@Autowired
protected M mapper;
@Override
public BaseMapper<T> getMapper() {
return mapper;
}
@Override
public List<T> list(QueryWrapper query) {
//获取当前用户信息,为 queryWrapper 添加额外的条件
return IService.super.list(query);
}
}当然,在 IService 中,除了 list 方法以外,还有其他的查询方法,可能也需要复写。
字段权限
字段权限,指的是在一张表中设计了许多字段,但是不同的用户(或者角色)查询,返回的字段结果是不一致的。 比如:tb_account 表中,有 user_name 和 password 字段,但是 password 字段只允许用户本人查询, 或者超级管理员查询,这种场景下,我们会用到 字段权限 的功能。
在 @Table() 注解中,有一个配置名为 onSet,用于设置这张表的 设置 监听
这里的 设置 监听指的是: 当我们使用 sql 、调用某个方法去查询数据,得到的数据内容映射到 entity 实体,mybatis 通过 setter 方法去设置 entity 的值时的监听。
以下是示例:
step 1: 为实体类编写一个 set 监听器(SetListener)
public class AccountOnSetListener implements SetListener {
@Override
public Object onSet(Object entity, String property, Object value) {
if (property.equals("password")){
//去查询当前用户的权限
boolean hasPasswordPermission = getPermission();
//若没有权限,则把数据库查询到的 password 内容修改为 null
if (!hasPasswordPermission){
value = null;
}
}
return value;
}
}step 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;
//getter setter
}with SpringBoot 配置项
SpringBoot 配置文件(application.yml 等);Solon 配置文件(app.yml 等)。主要是用于对 MyBatis 原生以及 MyBatis-Flex 的 FlexGlobalConfig 进行配置。
mybatis-flex:
#......
datasource:
#......
configuration:
#......
global-config:
#......
admin-config:
#......
seata-config:
#......datasource
- 类型:
Map<String, Map<String, String>> - 默认值:
null
MyBatis-Flex 多数据源配置,参考 [多数据源配置](../core/multi-datasource.md#更多的 Spring 或 Solon Yaml 配置支持)。
config-location
- 类型:
String - 默认值:
null
MyBatis 配置文件位置,如果有单独的 MyBatis 配置,需要将其路径配置到 configLocation 中。MyBatis Configuration 的具体内容请参考 MyBatis 官方文档。
mapper-locations
- 类型:
String[] - 默认值:
["classpath*:/mapper/**/*.xml"]
MyBatis Mapper 所对应的 XML 文件位置,如果在 Mapper 中有自定义的方法(XML 中有自定义的实现),需要进行该配置,指定 Mapper 所对应的 XML 文件位置。
type-aliases-package
- 类型:
String - 默认值:
null
MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名,注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名(即 XML 中调用的时候不用包含包名)。
type-aliases-super-type
- 类型:
Class<?> - 默认值:
null
该配置请和 typeAliasesPackage 一起使用,如果配置了该属性,则仅仅会扫描路径下以该类作为父类的域对象。
type-handlers-package
- 类型:
String - 默认值:
null
TypeHandler 扫描路径,如果配置了该属性,SqlSessionFactoryBean 会把该包下面的类注册为对应的 TypeHandler 处理器。
check-config-location
- 类型:
boolean - 默认值:
false
启动时检查是否存在 MyBatis XML 文件,默认不检查。
executor-type
- 类型:
ExecutorType - 默认值:
simple
通过该属性可指定 MyBatis 的执行器,MyBatis 的执行器总共有三种:
ExecutorType.SIMPLE:该执行器类型不做特殊的事情,为每个语句的执行创建一个新的预处理语句(PreparedStatement)。ExecutorType.REUSE:该执行器类型会复用预处理语句(PreparedStatement)。ExecutorType.BATCH:该执行器类型会批量执行所有的更新语句。
defaults-scripting-language-driver
- 类型:
Class<? extends LanguageDriver> - 默认值:
null
指定默认的脚本语言驱动器。
configuration-properties
- 类型:
Properties - 默认值:
null
指定外部化 MyBatis Properties 配置,通过该配置可以抽离配置,实现不同环境的配置部署。
MyBatisFlex 定制 (MyBatisFlexCustomizer)
MyBatisFlexCustomizer 是 MyBatis-Flex 为了方便 SpringBoot 或 Solon 用户对 MyBatis-Flex 进行初始化而产生的接口。
通过在 @Configuration 去实现 MyBatisFlexCustomizer 接口,我们可以对 MyBatis-Flex 进行一系列的初始化配置。这些配置可能包含如下的内容:
见这个类 com.mybatisflex.core.FlexGlobalConfig
- 1、FlexGlobalConfig 的全局配置
- 2、自定义主键生成器
- 3、多租户配置
- 4、动态表名配置
- 5、逻辑删除处理器配置
- 6、自定义脱敏规则
- 7、SQL 审计配置
- 8、SQL 打印配置
- 9、数据源解密器配置
- 10、自定义数据方言配置
- 11、…
/**
* @author yangfh
* @date 2023-11-01 10:26
**/@Configuration
public class MybatisFlexConfig implements MyBatisFlexCustomizer {
private static final Logger logger = LoggerFactory.getLogger(MybatisFlexConfig.class);
public MybatisFlexConfig() {
}
@Override
public void customize(FlexGlobalConfig globalConfig) {
// 打印审计日志
AuditManager.setAuditEnable(true);
AuditManager.setMessageCollector(auditMessage ->
logger.info("mybatis-flex audit: {} , {}ms", auditMessage.getFullSql() //打印完整的sql语句 包括参数,
, auditMessage.getElapsedTime())
);
// org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
}
}源码调试分析
解析列名的逻辑?
主要负责解析sql的对象是 com.mybatisflex.core.dialect.impl.CommonsDialectImpl
com.mybatisflex.core.dialect.impl.CommonsDialectImpl#buildSelectColumnSql
private void buildSelectColumnSql(StringBuilder sqlBuilder, List<QueryTable> queryTables, List<QueryColumn> selectColumns, String hint) {
sqlBuilder.append(SELECT);
sqlBuilder.append(forHint(hint));
if (selectColumns == null || selectColumns.isEmpty()) {
sqlBuilder.append(ASTERISK);
} else {
int index = 0;
for (QueryColumn selectColumn : selectColumns) {
String selectColumnSql = CPI.toSelectSql(selectColumn, queryTables, this);
sqlBuilder.append(selectColumnSql);
if (index != selectColumns.size() - 1) {
sqlBuilder.append(DELIMITER);
}
index++;
}
}
}
public static String toSelectSql(QueryColumn queryColumn, List<QueryTable> queryTables, IDialect dialect) {
return queryColumn.toSelectSql(queryTables, dialect);
}com.mybatisflex.core.query.QueryColumn#toSelectSql
protected String toSelectSql(List<QueryTable> queryTables, IDialect dialect) {
return toConditionSql(queryTables, dialect) + WrapperUtil.buildColumnAlias(alias, dialect);
}
最终是在 QueryColumn 本身中构建列名的
com.mybatisflex.core.query.QueryColumn#getSelectTable
QueryTable getSelectTable(List<QueryTable> queryTables, QueryTable selfTable) {
// 未查询任何表,或查询表仅有一个
// 可以省略表的引用,直接使用列名
// SELECT 1
// SELECT id FROM tb_user
if (queryTables == null || queryTables.isEmpty()) {
return null;
}
QueryTable consideredTable = queryTables.get(0);
// 列未指定表名,仅以字符串的形式输入列名
// 以查询表中的第一个表为主
// SELECT tb_user.id FROM tb_user
if (selfTable == null) {
return consideredTable;
}
// 当前表有别名,以别名为主
// SELECT u.id FROM tb_user u
if (StringUtil.hasText(selfTable.alias)) {
return selfTable;
}
// 当前表没有别名,查询表只有一个
// 如果两个表是一样的则可以忽略表的引用
// 兼容子查询时,子查询的查询表和父查询没有合并的问题
if (queryTables.size() == 1 && Objects.equals(selfTable.name, consideredTable.name)) {
return null;
}
consideredTable = selfTable;
// 当前表存在且没有别名
ListIterator<QueryTable> it = queryTables.listIterator(queryTables.size());
while (it.hasPrevious()) {
QueryTable queryTable = it.previous();
if (Objects.equals(queryTable.name, selfTable.name)) {
if (StringUtil.noText(queryTable.alias)) {
// 因为当前表没有别名,所以表名相同有没有别名,一定是这个表
return queryTable;
} else {
// 只是表名相同,但是查询表有别名,当前表没有别名
// 考虑这个表,但是继续寻找,是否有未设置别名的表
consideredTable = queryTable;
}
}
}
return consideredTable;
}分页查询转换的逻辑?
@SafeVarargs
public static <T, R> Page<R> doPaginate(
BaseMapper<T> mapper,
Page<R> page,
QueryWrapper queryWrapper,
Class<R> asType,
boolean withRelations,
Consumer<FieldQueryBuilder<R>>... consumers
) {
Long limitRows = CPI.getLimitRows(queryWrapper);
Long limitOffset = CPI.getLimitOffset(queryWrapper);
try {
// 只有 totalRow 小于 0 的时候才会去查询总量
// 这样方便用户做总数缓存,而非每次都要去查询总量
// 一般的分页场景中,只有第一页的时候有必要去查询总量,第二页以后是不需要的
if (page.getTotalRow() < 0) {
QueryWrapper countQueryWrapper;
if (page.needOptimizeCountQuery()) {
countQueryWrapper = MapperUtil.optimizeCountQueryWrapper(queryWrapper);
} else {
countQueryWrapper = MapperUtil.rawCountQueryWrapper(queryWrapper);
}
// optimize: 在 count 之前先去掉 limit 参数,避免 count 查询错误
CPI.setLimitRows(countQueryWrapper, null);
CPI.setLimitOffset(countQueryWrapper, null);
page.setTotalRow(mapper.selectCountByQuery(countQueryWrapper));
}
if (!page.hasRecords()) {
if (withRelations) {
RelationManager.clearConfigIfNecessary();
}
return page;
}
queryWrapper.limit(page.offset(), page.getPageSize());
List<R> records;
if (asType != null) {
records = mapper.selectListByQueryAs(queryWrapper, asType);
} else {
// noinspection unchecked
records = (List<R>) mapper.selectListByQuery(queryWrapper);
}
if (withRelations) {
queryRelations(mapper, records);
}
queryFields(mapper, records, consumers);
page.setRecords(records);
return page;
} finally {
// 将之前设置的 limit 清除掉
// 保险起见把重置代码放到 finally 代码块中
CPI.setLimitRows(queryWrapper, limitRows);
CPI.setLimitOffset(queryWrapper, limitOffset);
}
}com.mybatisflex.core.BaseMapper#selectObjectListByQueryAs
default <R> List<R> selectObjectListByQueryAs(QueryWrapper queryWrapper, Class<R> asType) {
List<Object> queryResults = selectObjectListByQuery(queryWrapper);
if (queryResults == null || queryResults.isEmpty()) {
return Collections.emptyList();
}
List<R> results = new ArrayList<>(queryResults.size());
for (Object queryResult : queryResults) {
results.add((R) ConvertUtil.convert(queryResult, asType));
}
return results;
}