MyBatis

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_account SET user_name = ‘张三’ , age = age + 1 WHERE id = 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 注解

@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 注解

@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 注解

@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;  
}