ListForm 属于 DevKit 中的一个模块。

在中后台系统的开发中,我们经常会遇到需要展示列表数据的场景,这些列表数据需要支持排序、筛选、分页等功能。为了应对复杂的查询逻辑,开发人员通常需要编写大量重复的代码,涉及过滤条件、排序规则、分页控制、关联实体查询等。而通过设计一个灵活的 ListForm,自动生成 buildQuery 方法的实现细节,可以极大地提高开发效率,减少冗余代码的编写。

什么是 ListForm?

ListForm 是一种用于处理数据查询请求的表单类。它主要用于接收查询参数,例如过滤条件、排序规则、分页信息等。通过 ListForm,开发人员可以清晰地定义查询的输入参数,而不必关心查询的实现细节。

ListForm 中,可以包含以下几个关键要素:

  • 过滤条件:支持基于字段的等值查询、范围查询、集合查询等。
  • 排序规则:允许对多个字段进行排序,支持升序和降序。
  • 分页控制:通过指定页码和每页大小来控制查询的结果集。
  • 关联实体查询:支持查询与主实体关联的子实体信息。
  • 复杂查询封装:例如关键词搜索、特定条件匹配等。

ListForm 有以下几个特点:

  • 自动构建查询:根据声明的表单参数和注解自动生成具体的 buildQuery 逻辑。
  • 类型安全:生成的代码是类型安全的,避免了手动拼接 SQL 语句查询,减少了运行时错误。
  • 条件受控:通过定义控制查询条件的可见性,避免了不合理的查询方式和组合。

一个示例

下面是一个简单的 ListForm 定义示例:

@Getter
@Setter
@ListForm
public class PlatformEmployeeListForm extends ListFormBase<PlatformEmployeeQuery> {

    /* 只可根据 ID 排序,不可基于 ID 查询 */
    @Sortable
    Void id;

    /* 下面 3 个默认 EQ 查询 */
    String phoneNumber;
    Boolean admin;
    Boolean locked;

    /* 下面 3 个显式 @Filter 指定过滤类型和对应字段 */
    @Filter(value = FilterType.IN, field = "phoneNumber")
    String[] phoneNumbers;

    @Filter(value = FilterType.GTE, field = "createdAt")
    LocalDateTime startCreatedAt;

    @Filter(value = FilterType.LTE, field = "createdAt")
    LocalDateTime endCreatedAt;

    /* 查询出关联实体信息 */
    @Wrap
    Leader leader;

    @Data
    public static class Leader {

        /* 只可根据 ID 排序,不可基于 ID 查询 */
        @Sortable
        Void id;

        /* 下面 2 个默认 EQ 查询 */
        Boolean admin;
        String phoneNumber;

    }

    /* 需要单个参数控制查询多个字段时 */
    @Filter(FilterType.NONE)
    String keyword;

    /* 员工或员工上级的手机号或姓名匹配关键词 */
    PlatformEmployeeQuery.P matchKeyword() {
        if (keyword == null) {
            return null;
        }
        var $ = $platformEmployee;
        return $.phoneNumber.eq(keyword)
                .or($.name.contains(keyword))
                .or($.$leader.phoneNumber.eq(keyword))
                .or($.$leader.name.contains(keyword));
    }

    /* 固定匹配管理员 */
    PlatformEmployeeQuery.P matchAdmin() {
        return $platformEmployee.admin.eq(true);
    }

    /* 当没有指定排序参数时,默认按 ID 降序排列 */
    PlatformEmployeeQuery.Sort[] defaultSort() {
        return new PlatformEmployeeQuery.Sort[]{
                $platformEmployee.id.desc()
        };
    }

}

在 Controller 中的使用示例:

@GetMapping("/list")
public PlatformEmployeeListResponse list(PlatformEmployeeListForm form) {
    Page<PlatformEmployeeWrapper> page = platformEmployeeService.page(form.buildQuery());
    return PlatformEmployeeListResponse.map(page);
}

在编译后,会为 ListForm 生成一个对应的子类,实现了关键的 buildQuery 方法。生成的内容如下:

public class PlatformEmployeeListForm$Generated extends PlatformEmployeeListForm {
  @Override
  public PlatformEmployeeQuery buildQuery() {
    var $ = PlatformEmployeeQuery.$platformEmployee;
    return PlatformEmployeeQuery.builder()
        .wraps($.$leader)
        .filter(buildFilters())
        .sorts(buildSorts())
        .build(Limit.page(getPage(), getSize()));
  }

  private PlatformEmployeeQuery.P[] buildFilters() {
    var $ = PlatformEmployeeQuery.$platformEmployee;
    List<PlatformEmployeeQuery.P> predicates = new ArrayList<>();
    addPredicateIfPresent(phoneNumber, $.phoneNumber::eq, predicates);
    addPredicateIfPresent(admin, $.admin::eq, predicates);
    addPredicateIfPresent(locked, $.locked::eq, predicates);
    addPredicateIfPresent(phoneNumbers, $.phoneNumber::in, predicates);
    addPredicateIfPresent(startCreatedAt, $.createdAt::gte, predicates);
    addPredicateIfPresent(endCreatedAt, $.createdAt::lte, predicates);
    Optional.ofNullable(leader).ifPresent(leader -> {
      addPredicateIfPresent(leader.admin, $.$leader.admin::eq, predicates);
      addPredicateIfPresent(leader.phoneNumber, $.$leader.phoneNumber::eq, predicates);
    });
    Optional.ofNullable(matchKeyword()).ifPresent(predicates::add);
    Optional.ofNullable(matchAdmin()).ifPresent(predicates::add);
    return predicates.toArray(PlatformEmployeeQuery.P[]::new);
  }

  private PlatformEmployeeQuery.Sort[] buildSorts() {
    var $ = PlatformEmployeeQuery.$platformEmployee;
    var sorts = getSortCriteriaStream()
        .map(sortCriteria -> {
            boolean isDesc = sortCriteria.startsWith("-");
            String field = isDesc ? sortCriteria.substring(1) : sortCriteria;
            return switch (field) {
                case "leader.id" -> isDesc ? $.$leader.id.desc() : $.$leader.id.asc();
                case "id" -> isDesc ? $.id.desc() : $.id.asc();
                default -> null;
            };
        })
        .filter(Objects::nonNull)
        .toArray(PlatformEmployeeQuery.Sort[]::new);
    return sorts.length > 0 ? sorts : defaultSort();
  }
}