Fluent Mybatis 入门介绍二

3,364 阅读5分钟

上篇FluentMybatis入门介绍一,介绍了框架提供的Mapper方法,其中有几个重要的方法用到了IQuery和IUpdate对象。 这2个对象是FluentMybatis实现复杂和动态sql的构造类,通过这2个对象fluent mybatis可以不用写具体的xml文件,直接通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一。

Fluent Mybatis, 原生Mybatis, Mybatis Plus三者功能对比

Fluent Mybatis文档&示例

下面这篇接着介绍如何通过IQuery和IUpdate定义强大的动态SQL语句。

先构造一个业务场景

  • 表结构 假如有学生成绩表结构如下:
create table `student_score`
(
    id           bigint auto_increment comment '主键ID' primary key,
    student_id   bigint            not null comment '学号',
    gender_man   tinyint default 0 not null comment '性别, 0:女; 1:男',
    school_term  int               null comment '学期',
    subject      varchar(30)       null comment '学科',
    score        int               null comment '成绩',
    gmt_create   datetime          not null comment '记录创建时间',
    gmt_modified datetime          not null comment '记录最后修改时间',
    is_deleted   tinyint default 0 not null comment '逻辑删除标识'
) engine = InnoDB default charset=utf8;
  • 需求 现在有需求: 统计2000年到2019年, 三门学科('英语', '数学', '语文')分数按学期,学科统计最低分,最高分和平均分,统计结果按学期和学科排序 实现这个需求的SQL语句如下
select school_term, subject, count(score), min(score), max(score), avg(score)
from student_score
where school_term between 2000 and 2019
  and subject in ('英语', '数学', '语文')
  and is_deleted = 0
group by school_term, subject
order by school_term, subject

现在我们通过FluentMybatis来进行具体实现

  1. 在StudentScoreDao类上定义接口
@Data
public class ScoreStatistics {
    private int schoolTerm;
    private String subject;
    private long count;
    private Integer minScore;
    private Integer maxScore;
    private BigDecimal avgScore;
}
public interface StudentScoreDao extends IBaseDao<StudentScoreEntity> {
    /**
     * 统计从fromYear到endYear年间学科subjects的统计数据
     *
     * @param fromYear 统计年份区间开始
     * @param endYear  统计年份区间结尾
     * @param subjects 统计的学科列表
     * @return 统计数据
     */
    List<ScoreStatistics> statistics(int fromYear, int endYear, String[] subjects);
}
  1. 在StudentScoreDaoImpl上实现业务逻辑
@Repository
public class StudentScoreDaoImpl extends StudentScoreBaseDao implements StudentScoreDao {
    @Override
    public List<ScoreStatistics> statistics(int fromSchoolTerm, int endSchoolTerm, String[] subjects) {
        return super.listPoJos(ScoreStatistics.class, super.query()
            .select.schoolTerm().subject()
            .count("count")
            .min.score("min_score")
            .max.score("max_score")
            .avg.score("avg_score")
            .end()
            .where.isDeleted().isFalse()
            .and.schoolTerm().between(fromSchoolTerm, endSchoolTerm)
            .and.subject().in(subjects)
            .end()
            .groupBy.schoolTerm().subject().end()
            .orderBy.schoolTerm().asc().subject().asc().end()
        );
    }
}

DaoImpl实现中,除了根据条件返回统计结果,还讲结果按照下划线转驼峰的规则自动转换为ScoreStatistics对象返回。

  1. 写个测试验证下
@RunWith(SpringRunner.class)
@SpringBootTest(classes = QuickStartApplication.class)
public class StudentScoreDaoImplTest {
    @Autowired
    private StudentScoreDao dao;

    @Test
    public void statistics() {
        List<ScoreStatistics> list = dao.statistics(2000, 2019, new String[]{"语文", "数学", "英语"});
        System.out.println(list);
    }
}
  1. 查看控制台输出结果
DEBUG - ==>  Preparing: SELECT school_term, subject, count(*) AS count, MIN(score) AS min_score, MAX(score) AS max_score, AVG(score) AS avg_score 
    FROM student_score 
    WHERE is_deleted = ? 
    AND school_term BETWEEN ? AND ? 
    AND subject IN (?, ?, ?) 
    GROUP BY school_term, subject 
    ORDER BY school_term ASC, subject ASC  
DEBUG - ==> Parameters: false(Boolean), 2000(Integer), 2019(Integer), 语文(String), 数学(String), 英语(String) 
DEBUG - <==      Total: 30 
[ScoreStatistics(schoolTerm=2000, subject=数学, count=17, minScore=1, maxScore=93, avgScore=36.0588), 
 ...
 ScoreStatistics(schoolTerm=2009, subject=语文, count=24, minScore=3, maxScore=100, avgScore=51.2500)]

上面通过例子大致预览了FluentMybatis动态构造查询的逻辑,下面我们具体讲解

where条件构造

一般where条件构造

where条件设置语法形式如下, 以where开头,以end()方法结束 在内置变量where后面,可以自动列出可以设置的表字段(方法), 在字段后可以紧跟着比较符和比较值。

    .where
    .字段().条件(条件参数)
    .end()

eq: 等于 = ?

  • 相等条件设置示例
@Test
public void column_eq_value() {
    UserQuery query = new UserQuery()
        .where.userName().eq("darui.wu")
        .and.eMail().eq("darui.wu@163.com").end();
    mapper.findOne(query);
}
  • 使用带条件判断方法: eq(Object value, Predicate when)

当预言when为真时, 才会执行 eq(value)判断

示例:

    /**
     * 根据条件查询用户列表
     * 如果设置了积分, 加上积分条件
     * 如果设置了状态, 加上状态条件
     *
     * @return
     */
    @Override
    public List<UserEntity> findByBirthdayAndBonusPoints(Date birthday, Long points, String status) {
        UserQuery query = super.query()
            .where.birthday().eq(birthday)
            .and.bonusPoints().ge(points, If::notNull)
            .and.status().eq(status, If::notBlank).end();
        return mapper.listEntity(query);
    }

上面的示例,相当于下面常见的java条件设置

    public List<UserEntity> findByBirthdayAndBonusPoints(Date birthday, Long points, String status) {
        UserQuery query = super.query();
        query.where.birthday().eq(birthday);
        if (points != null) {
            query.where.bonusPoints().ge(points);
        }
        if (status != null && !status.trim().isEmpty()) {
            query.where.status().eq(status).end();
        }
        return mapper.listEntity(query);
    }

或者,使用mybatis的条件设置

<select id="findByBirthdayAndBonusPoints" parameterType="java.util.Map" resultMap="UserEntity">
    SELECT ... FROM user
    WHERE birthday = #{birthday}
    <if test="bonus_points != null ">
        AND bonus_points = #{bonusPoints}
    </if>
    <if test="status != null and status != '' ">
        AND status = #{status}
    </if>
</select>

使用fluent mybatis的条件语句可以达到上面java判断和xml判断的同样效果,同时,如果birthday如果传入一个空串,还会进行判空,抛出异常, 防止传参错误,导致的非预期条件查询。

ne: 不等于 <> ?

@Test
public void ne() {
    UserQuery query = new UserQuery()
        .where
        .age().ne(34)
        .end();
    mapper.count(query);
}

gt: 大于 > ?

@Test
public void gt() {
    UserQuery query = new UserQuery()
        .where.age().gt(34).end();
    mapper.count(query);
}

ge: 大于等于 >= ?

@Test
public void ge() {
    UserQuery query = new UserQuery()
        .where.age().ge(34).end();
    mapper.count(query);
}

lt: 小于 < ?

@Test
public void lt() {
    UserQuery query = new UserQuery()
        .where.age().lt(34).end();
    mapper.count(query);
}

le: 小于等于 <= ?

@Test
public void le() {
    UserQuery query = new UserQuery()
        .where.age().le(34).end();
    mapper.count(query);
}

between: BETWEEN ? AND ?

@Test
public void between() {
    UserQuery query = new UserQuery()
        .where.age().between(23, 40).end();
    mapper.count(query);
}

notBetween: NOT BETWEEN ? AND ?

@Test
public void not_between() {
    UserQuery query = new UserQuery()
        .where.age().notBetween(23, 40).end();
    mapper.count(query);
}

like: LIKE ?, ? 为 "%value%"

@Test
void like() {
    mapper.listEntity(new UserQuery()
        .where
        .userName().like("abc") // like '%abc%'
        .end()
    );
}

notLike: NOT LIKE ?, ? 为 "%value%"

@Test
void not_like() {
    mapper.listEntity(new UserQuery()
        .where
        .userName().notLike("abc") // not like '%abc%'
        .end()
    );
}

likeLeft: LIKE ?, ? 为 "value%"

likeRight: LIKE ?, ? 为 "%value"

isNull: column IS NULL

@Test
void isNull() {
    mapper.listEntity(new UserQuery()
        .where.age().isNull().end()
    );
}

isNotNull: column IS NOT NULL

@Test
void isNotNull() {
    mapper.listEntity(new UserQuery()
        .where.age().isNotNull().end()
    );
}

in: 在...之中 IN (?, ..., ?)

@Test
public void in_collection() {
    UserQuery query = new UserQuery()
        .where
        .age().in(Arrays.asList(34, 35))
        .end();
    mapper.count(query);
}

@Test
public void in_array() {
    UserQuery query = new UserQuery()
        .where
        .age().in(new int[]{34, 35})
        .end();
    mapper.count(query);
}

notIn: 不在...之中 NOT IN (?, ..., ?)

嵌套条件构造

FluentMybatis还支持嵌套条件的构造

in (select 子查询)

  • 嵌套查询表和主查询表一样的场景

.column().in( query-> {对query设置条件})

只需要在in里面引用一个lambda表达式,lambda表达式入参是一个同名的Query。对这个入参可以设置where参数。

@DisplayName("嵌套查询和主查询的表是同一个")
@Test
void test_in_same_table_query() {
    UserQuery query = new UserQuery()
        .where.id().in(q -> q.selectId()
            .where.id().eq(3L).end())
        .and.userName().like("user")
        .and.age().gt(23).end();

    List list = mapper.listEntity(query);
}

上面逻辑执行下面SQL语句

SELECT id, gmt_create, gmt_modified, is_deleted, account, age, avatar, birthday, bonus_points, e_mail, password, phone, status, user_name 
FROM user WHERE id IN (SELECT id FROM user WHERE id = ?) 
AND user_name LIKE ? 
AND age > ?
  • 嵌套查询表是另外表的场景 .column().in(new XyzQuery(){对query设置条件})

如果嵌套查询的不是同表一张表,需要在in方法里面显式声明一下Query对象的class类型, 后面用法同方法一。

@DisplayName("嵌套查询和主查询的表是不同")
@Test
void test_in_difference_table_query() {
    UserQuery query = new UserQuery()
        .selectId()
        .where.addressId().in(new ReceivingAddressQuery().selectId()
            .where.id().in(new int[]{1, 2}).end())
        .end();
    mapper.listEntity(query);
}

执行sql语句

SELECT id FROM user 
WHERE address_id IN (SELECT id 
    FROM receiving_address WHERE id IN (?, ?)
)
  • not in嵌套查询: 使用方法同 in 嵌套查询

exists (select子查询)

  • 嵌套查询表和主查询表一样的场景 Exists查询不需要指定字段,直接在query where中可以引用exists方法。
  1. exists( query-> {对query设置条件}) 如果exists查询的表和主查询一致,直接在lambada表达式中使用同类型query参数即可,参数用法同in方法。

  2. exists(new XyzQuery{对query设置条件}) 如果exists查询的表和主查询不一致,在exists方法第一个参数指定query类型,第二个参数同方法1。

具体示例

@DisplayName("EXISTS查询")
@Test
void test_exists_query() {
    UserQuery query = new UserQuery()
        .where.exists(new ReceivingAddressQuery()
            .where.detailAddress().like("杭州")
            .and.id().apply(" = user.address_id").end())
        .end();
    mapper.listEntity(query);
}

执行sql语句

SELECT id, gmt_create, gmt_modified, is_deleted, account, address_id, age, avatar, birthday, bonus_points, e_mail, password, phone, status, user_name 
FROM user 
WHERE EXISTS (SELECT * FROM receiving_address
    WHERE detail_address LIKE ?
    AND id = user.address_id
)

group by条件构造

groupBy条件设置语法形式如下, 以groupBy开头,以end()方法结束

    .groupBy.column1().column2()
    .end()

示例1:

@Test
public void test_groupBy() throws Exception {
    UserQuery query = new UserQuery()
        .selectId()
        .where.id().eq(24L).end()
        .groupBy.userName().age().end();
    mapper.listEntity(query);
}

执行sql语句

SELECT id FROM t_user WHERE id = ? GROUP BY user_name, age

having条件

    .groupBy.column1().column2().end().end()
    .having
    .聚合函数1.column1().条件(条件值)
    .聚合函数2.column2().条件(条件值)
    .end()

这里的条件和where部分类似, 支持eq, ge, gt等,示例

    .where.id().eq(24L).end()
    .groupBy.id().end()
    .having.sum.age().between(2, 10)
    .and.count.id().gt(2)
    .and.avg.age().in(new int[]{2, 3})
    .and.min.age().gt(10)
    .and.max.age().lt(20)
    .end();

对应sql语句

GROUP BY id 
HAVING SUM(age) BETWEEN ? AND ? 
AND COUNT(id) > ? 
AND AVG(age) IN (?, ?) 
AND MIN(age) > ? 
AND MAX(age) < ?

order by条件构造

orderBy条件设置语法形式如下, 以orderBy开头,以end()方法结束

    .orderBy
    .字段().asc()
    .字段().desc()
    .end()

示例代码1

@Test
public void test_orderBy() throws Exception {
    UserQuery query = new UserQuery()
        .selectId()
        .where.id().eq(24L).end()
        .orderBy.id().asc().age().desc().end();
    mapper.listEntity(query);
}

对应sql语句

SELECT id FROM t_user WHERE id = ? ORDER BY id ASC, age DESC

设置update值

语法:以set开头,更新对应的字段值,以end()结束

    .set.字段1().is(设置值)
    .set.字段2().is(设置值)
    .set.字段3().is(设置值)
    .end()
    .where.条件设置.end()  

示例代码

@Test
void test_update() {
    mapper.updateBy(new UserUpdate()
        .set.age().is(34).end()
        .where.id().eq(2).end()
    );
}

对应sql语句

UPDATE t_user SET gmt_modified = now(), age = ? WHERE id = ?

说明: 示例显式指定字段age的更新值, 但gmt_modified字段设置了默认更新值now(), 所以在执行语句时同时更新了gmt_modified字段 控制台log输出

...UserMapper.updateBy - ==>  Preparing: UPDATE t_user SET gmt_modified = now(), age = ? WHERE id = ? 
...UserMapper.updateBy - ==> Parameters: 34(Integer), 2(Integer)

Fluent Mybatis文档&示例

FluentMybatis文章序列