跳转至

第四章 数据查询-类型安全与灵活性

4.0 引言:数据查询的类型安全与灵活性平衡

在现代企业应用开发中,数据查询是连接业务逻辑与持久化存储的关键桥梁。无论是简单的信息展示,还是复杂的数据分析,都离不开高效、可靠的查询操作。随着应用复杂度的不断提升和数据量的持续增长,我们对查询能力的要求也在不断提高:既要保证查询的正确性和类型安全,又要具备足够的灵活性来应对各种业务场景。

本章将深入探讨Jimmer框架如何通过其独特的设计理念和创新技术,为我们提供一种全新的数据查询范式——在保证类型安全的同时,提供不亚于原生SQL的灵活性和表达力。

4.0.1 数据查询在现代应用中的核心地位

数据查询不仅仅是简单的CRUD操作,它是连接用户交互与数据存储的关键纽带,直接影响着应用的核心竞争力。在当代企业应用中,数据查询面临着前所未有的挑战:

复杂性持续提升

现代企业应用的查询需求远超传统的简单数据获取:

// 一个典型的现代企业应用查询场景
public List<OrderSummaryDTO> findOrdersWithComplexCriteria(OrderQueryCriteria criteria) {
    // 1. 动态的查询条件(可能有10+个过滤条件)
    // 2. 关联多个实体(订单、客户、商品、支付记录等)
    // 3. 复杂的统计计算(订单总额、商品数量、折扣金额等)
    // 4. 权限控制过滤
    // 5. 数据分页与排序
    // 6. 可能的全文搜索需求
    // ...
}

业务变化速度加快

数字化转型要求系统能够快速响应业务变化:

  • 营销活动可能在几小时内改变查询逻辑
  • 新的法规合规要求可能引入额外的数据过滤条件
  • 用户体验优化可能需要调整数据聚合方式

数据规模与性能压力

随着业务增长,查询性能面临双重压力:

  • 数据量级从GB级向TB级甚至PB级跃升
  • 用户对响应时间的容忍度持续降低(从秒级到毫秒级)
  • 移动端和小程序应用对API响应速度要求更高

开发效率与质量要求并存

在快节奏的开发环境中:

  • 需要减少查询开发的样板代码
  • 必须在编译期捕获潜在错误,而非运行时崩溃
  • 代码必须具备良好的可读性和可维护性

这些挑战相互交织,使得数据查询不再是简单的技术问题,而是关乎应用成功与否的战略决策。在这个背景下,选择合适的查询方案变得至关重要。

4.0.2 传统查询方案的局限性

面对现代应用的复杂查询需求,传统的查询方案都面临着不同程度的局限性。让我们分析几种主流方案的优缺点:

原生SQL:灵活但不安全

原生SQL具备无与伦比的灵活性和表达能力,但在企业应用开发中却面临严峻挑战:

// 原生SQL示例
String sql = "SELECT o.id, o.order_number, c.name AS customer_name, " +
             "SUM(i.quantity * i.price) AS total_amount " +
             "FROM orders o " +
             "JOIN customers c ON o.customer_id = c.id " +
             "JOIN order_items i ON o.id = i.order_id " +
             "WHERE o.status = ? AND o.created_time > ? " +
             "GROUP BY o.id, o.order_number, c.name";

List<Map<String, Object>> results = jdbcTemplate.queryForList(
    sql, OrderStatus.PAID.name(), LocalDateTime.now().minusDays(30)
);

存在的问题: - 类型不安全:SQL字符串中的错误只能在运行时发现 - 对象-关系阻抗不匹配:查询结果需要手动映射到领域对象 - SQL注入风险:特别是在动态拼接SQL时 - 数据库移植困难:不同数据库方言差异明显 - IDE支持有限:重构和导航功能无法覆盖SQL字符串

JPA/Hibernate Criteria API:安全但表达能力受限

JPA Criteria API尝试提供类型安全的查询能力,但使用体验和表达能力受到很大限制:

// JPA Criteria API示例
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<Order> order = query.from(Order.class);
Join<Order, Customer> customer = order.join("customer");
Join<Order, OrderItem> items = order.join("items");

query.multiselect(
    order.get("id"),
    order.get("orderNumber"),
    customer.get("name").alias("customerName"),
    cb.sum(cb.prod(items.get("quantity"), items.get("price"))).alias("totalAmount")
);

query.where(
    cb.equal(order.get("status"), OrderStatus.PAID),
    cb.greaterThan(order.get("createdTime"), LocalDateTime.now().minusDays(30))
);

query.groupBy(order.get("id"), order.get("orderNumber"), customer.get("name"));

List<Tuple> results = entityManager.createQuery(query).getResultList();

存在的问题: - 冗长且复杂:代码量显著增加,可读性下降 - 学习曲线陡峭:API设计不够直观 - 功能覆盖不全:一些高级SQL特性难以表达 - 动态查询笨重:条件组合逻辑繁琐 - 性能开销:自动生成的SQL有时效率不佳

MyBatis动态SQL:灵活但开发效率低

MyBatis提供了XML配置的动态SQL能力,表达能力强但开发体验不佳:

<!-- MyBatis XML示例 -->
<select id="findOrdersWithCriteria" resultMap="orderSummaryMap">
    SELECT o.id, o.order_number, c.name AS customer_name,
           SUM(i.quantity * i.price) AS total_amount
    FROM orders o
    JOIN customers c ON o.customer_id = c.id
    JOIN order_items i ON o.id = i.order_id
    <where>
        <if test="status != null">
            o.status = #{status}
        </if>
        <if test="startDate != null">
            AND o.created_time > #{startDate}
        </if>
        <!-- 其他条件 -->
    </where>
    GROUP BY o.id, o.order_number, c.name
</select>

存在的问题: - 类型不安全:XML中的错误只能在运行时发现 - 代码分离:Java代码与SQL分离,增加了维护难度 - 重构支持弱:IDE难以识别XML中的引用关系 - 学习成本:需要掌握特定的XML语法 - 调试困难:动态生成的SQL不易追踪

这些传统方案各有优缺点,但都无法同时满足类型安全与灵活性的需求。开发团队常常面临艰难的选择:要么牺牲类型安全换取灵活性,要么接受有限的表达能力以获得类型安全。

4.0.3 Jimmer查询的设计理念

Jimmer查询API的设计目标是一个看似不可能的任务:同时实现类型安全与SQL级别的灵活性。这种平衡是通过一系列创新的设计理念实现的:

1. 类型安全至上

Jimmer坚持"如果代码能够编译,那么查询就应该能够正确执行"的原则:

// Jimmer类型安全查询示例
BookTable book = Tables.BOOK_TABLE;
List<Book> javaBooks = sqlClient.createQuery(book)
    .where(book.name().like("%Java%"))
    .where(book.price().between(
        new BigDecimal("50"), 
        new BigDecimal("200")
    ))
    .select(book)
    .execute();

在这个示例中: - 每个属性路径都是类型安全的(如book.name()返回String类型的表达式) - 操作符匹配属性类型(如字符串可以使用like,数值可以使用between) - 参数类型与属性类型严格匹配 - 返回类型由select子句决定,清晰明确

2. 流畅的API设计

Jimmer的查询API追求直观、易读且符合SQL思维模式的设计:

// Jimmer流畅API示例
OrderTable order = Tables.ORDER_TABLE;
CustomerTable customer = Tables.CUSTOMER_TABLE;
OrderItemTable item = Tables.ORDER_ITEM_TABLE;

List<OrderView> orders = sqlClient.createQuery(order)
    .leftJoin(customer, customer.id().eq(order.customerId()))
    .leftJoin(item, item.orderId().eq(order.id()))
    .where(order.status().eq(OrderStatus.PAID))
    .where(order.createdTime().gt(LocalDateTime.now().minusDays(30)))
    .groupBy(order.id())
    .select(
        order.fetch(
            OrderFetcher.$.orderNumber()
                .customer(CustomerFetcher.$.name())
                .items(OrderItemFetcher.$.price().quantity())
        )
    )
    .execute();

流畅API的优势: - 链式调用结构清晰,接近自然语言表达 - 方法名与SQL关键字对应,降低学习成本 - 代码组织方式与SQL语句结构一致 - IDE提供全面的自动完成支持

3. 动态查询的一等公民支持

Jimmer将动态查询需求提升为设计核心,而非事后添加的功能:

// Jimmer动态查询示例
public List<Book> searchBooks(BookQuery query) {
    BookTable book = Tables.BOOK_TABLE;
    return sqlClient.createQuery(book)
        .whereIf(
            query.getName() != null && !query.getName().isEmpty(),
            () -> book.name().like("%" + query.getName() + "%")
        )
        .whereIf(
            query.getMinPrice() != null && query.getMaxPrice() != null,
            () -> book.price().between(query.getMinPrice(), query.getMaxPrice())
        )
        .whereIf(
            query.getCategoryId() != null,
            () -> book.category().id().eq(query.getCategoryId())
        )
        .orderBy(book.price().desc())
        .select(book)
        .execute();
}

动态查询设计优势: - 条件判断与SQL逻辑无缝集成 - 避免了条件嵌套带来的代码可读性下降 - 生成的SQL仅包含实际需要的条件 - 保持了类型安全,同时提供了灵活性

4. 对象图与关联查询的无缝集成

Jimmer创新性地将实体关联与SQL查询结合,提供了对象图的精确控制:

// Jimmer对象图查询示例
BookTable book = Tables.BOOK_TABLE;
List<Book> booksWithAuthorAndStore = sqlClient.createQuery(book)
    .where(book.price().ge(new BigDecimal("100")))
    .select(
        book.fetch(
            BookFetcher.$.name()
                .price()
                .author(AuthorFetcher.$.name().gender())
                .store(StoreFetcher.$.name().address())
        )
    )
    .execute();

对象图设计的优势: - 精确控制返回的对象结构,避免过度获取 - 关联自动转换为高效的JOIN操作 - N+1问题自动规避 - 查询结果直接映射为完整对象图,无需手动组装

5. 数据库无关性与优化并重

Jimmer平衡了跨数据库兼容性与性能优化需求:

// 数据库优化示例
BookTable book = Tables.BOOK_TABLE;
Page<Book> pagedBooks = sqlClient.createQuery(book)
    .where(book.category().eq("编程"))
    .orderBy(book.publishTime().desc())
    .select(book)
    .fetchPage(0, 20);

这个简单的分页查询会根据不同数据库自动优化: - MySQL可能使用LIMIT ? OFFSET ? - Oracle可能使用ROWNUM或窗口函数 - SQL Server可能使用OFFSET ? ROWS FETCH NEXT ? ROWS ONLY

同时,Jimmer还支持各数据库的特殊功能,如MySQL的全文搜索、PostgreSQL的JSONB操作等。

这些设计理念相互配合,使Jimmer查询API成为兼具类型安全和灵活性的现代查询解决方案,为开发者提供了前所未有的查询开发体验。

4.0.4 本章内容导航

本章将系统性地探索Jimmer的查询能力,从基础概念到高级应用,帮助读者全面掌握这一强大工具。以下是本章的学习路线:

基础入门 (4.1-4.2节) - 类型安全查询的基本原理与优势 - 基础查询操作与条件表达式 - 简单到复杂的条件组合 - 查询结果类型转换与映射

进阶应用 (4.3-4.4节) - 关联查询与对象图控制 - 数据排序、分页与分组 - 聚合函数与高级分组 - 窗口函数的应用

实战技巧 (4.5-4.6节) - 动态查询构建与应用 - 子查询与复杂SQL功能 - 查询性能优化最佳实践

通过这一章的学习,你将能够使用Jimmer构建从简单到复杂的各类查询,充分发挥类型安全与灵活性并存的优势,显著提升开发效率和代码质量。让我们开始这段探索Jimmer查询世界的旅程!

4.1 类型安全查询基础

在企业应用开发中,查询是最常见的数据操作。无论是简单的条件过滤还是复杂的关联查询,数据查询的质量直接影响着应用的性能和可靠性。而在这个领域,Jimmer的类型安全查询API提供了一种独特的解决方案,它在保持强类型检查的同时,提供了灵活且强大的查询能力。

本节将详细探讨Jimmer类型安全查询的基础知识,通过实际代码示例,帮助你理解和掌握这一强大功能。我们将以一个电子书店应用为例,展示如何使用Jimmer查询API高效检索和过滤图书数据。

4.1.1 类型安全查询的优势与意义

传统的SQL查询方式通常存在以下问题:

// 字符串拼接SQL - 容易出现SQL注入风险
String sql = "SELECT * FROM t_book WHERE category = '" + category + "'";

// 预编译SQL - 字段名称错误无法在编译期检测
String sql = "SELECT * FROM t_book WHERE categry = ?"; // 注意这里categry拼写错误

// ORM框架属性映射 - 属性名修改后查询可能失效
Criteria criteria = session.createCriteria(Book.class);
criteria.add(Restrictions.eq("categoy", "编程语言")); // 属性名称错误

这些问题直到运行时才能被发现,这类"隐蔽错误"是最危险的,它们可能通过基本测试,却在特定条件下引发生产问题,甚至可能导致生产环境的严重bug。而Jimmer的类型安全查询从根本上解决了这些问题,让我们通过一个实际测试案例来看看这些优势:

@Test
@DisplayName("测试类型安全查询的编译时安全保障")
void testTypeSafeQuerySafety() {
    // 使用类型安全的表达式API
    BookTable book = BookTable.$;

    // 编译时类型安全的查询构建
    List<Book> programmingBooks = sqlClient.createQuery(book)
        .where(book.category().eq("编程语言")) // 属性名自动补全,编译时检查
        .where(book.price().ge(new BigDecimal("50"))) // 类型安全,不能用字符串比较数值
        .orderBy(book.publishTime().desc()) // 排序字段类型安全
        .select(book) // 返回类型安全
        .execute();

    // 验证查询结果
    assertThat(programmingBooks).isNotEmpty();
    assertThat(programmingBooks).allMatch(b -> b.category().equals("编程语言"));
}
  1. 编译时错误检查:如果实体属性名称拼写错误或者类型不匹配,在编译时就会报错
  2. IDE智能提示:开发过程中IDE可以提供属性名和方法的自动补全
  3. 重构友好:当实体属性名修改时,查询代码也会被同步检查

与其他ORM框架相比,Jimmer的类型安全程度更高:

特性 原生JDBC JPA/Hibernate MyBatis Jimmer
SQL语法错误检查 运行时 运行时 部分在运行时 编译时
属性名称错误检查 部分在运行时 部分在运行时 编译时
属性类型匹配检查 部分在运行时 编译时
关联路径检查 部分在运行时 编译时
表达式类型安全 部分支持 完全支持

4.1.2 Jimmer查询API的整体架构

为了实现类型安全查询,Jimmer设计了精巧的API架构。让我们通过下面的测试用例来探索这一架构:

@Test
@DisplayName("测试查询API的架构设计")
void testQueryApiArchitecture() {
    BookTable book = BookTable.$;

    // 1. 演示查询阶段的类型安全
    // 每个阶段返回的是特定类型的对象,确保链式调用的类型安全
    var configurationStage = sqlClient.createQuery(book);
    var whereStage = configurationStage.where(book.category().eq("编程语言"));
    var selectStage = whereStage.select(book);
    List<Book> books = selectStage.execute();

    // 2. 验证结果
    assertThat(books).isNotEmpty();
    assertThat(books).allMatch(b -> b.category().equals("编程语言"));
}

这个测试拆分了查询的构建过程,展示了Jimmer查询API的阶段性设计。实际上,Jimmer查询API分为以下几个核心阶段:

  1. 查询创建阶段JSqlClient.createQuery()方法创建查询对象
  2. 条件配置阶段:设置where、orderBy、groupBy等条件
  3. 结果映射阶段:通过select方法指定返回的数据类型和形状
  4. 查询执行阶段:调用execute()、fetchOne()等方法执行查询并返回结果

这种阶段性设计确保了API的类型安全和使用直观性。例如,在没有指定select之前,你无法执行查询;在指定了groupBy之后,你必须使用聚合函数或分组字段进行选择。

graph TD A[JSqlClient.createQuery] -->|返回TypedRootQuery| B[条件配置] B -->|where| B B -->|orderBy| B B -->|groupBy| B B -->|select| C[结果映射] C -->|execute| D[查询执行] C -->|fetchOne| D C -->|fetchPage| D style A fill:#015467,stroke:#634f7d,color:#ffffff style B fill:#634f7d,stroke:#015467,color:#ffffff style C fill:#47a1ad,stroke:#015467 style D fill:#e7cd79,stroke:#015467

与此同时,Jimmer生成的元数据类(如BookTable)提供了类型安全的属性访问和表达式构建能力。这些元数据类是在编译时根据实体接口定义自动生成的,确保了类型安全。

4.1.3 基础查询操作入门

掌握了Jimmer查询API的整体架构后,让我们逐步学习基础查询操作,这些操作构成了日常开发的基础。我们将基于TypeSafeQueryTest.java中的测试案例进行讲解:

  • 查询所有记录

最简单的查询操作是获取实体的所有记录(或符合特定条件的所有记录):

@Test
@DisplayName("测试查询所有记录")
void testFindAll() {
    // 查询所有测试图书
    BookTable table = BookTable.$;
    List<Book> books = sqlClient.createQuery(table)
        .where(table.id().like(testPrefix + "-%"))
        .select(table)
        .execute();

    // 验证结果
    assertThat(books).isNotEmpty();
    assertThat(books).hasSize(3);
    assertThat(books).allMatch(book -> book.id().startsWith(testPrefix));
}

在这个测试中,我们使用了createQuery()方法创建查询,指定了简单的过滤条件,并通过select(table)选择返回完整的实体对象。

值得注意的是,Jimmer不鼓励无条件地查询所有记录,因为这在生产环境中可能导致性能问题。相反,它鼓励开发者始终指定一些过滤条件。在测试代码中,我们使用testPrefix确保只查询测试数据。

  • 根据ID查询单条记录

根据主键查询单条记录是最常见的查询操作之一。Jimmer提供了专门的API来优化这种场景:

@Test
@DisplayName("测试根据ID查询单条记录")
void testFindById() {
    // 根据ID查询图书
    String bookId = testPrefix + "-book-1";
    Book book = sqlClient.findById(Book.class, bookId);

    // 验证结果
    assertThat(book).isNotNull();
    assertThat(book.id()).isEqualTo(bookId);
    assertThat(book.name()).contains("Java编程思想");
}

findById方法是一个高度优化的快捷方式,它内部会自动应用主键索引并处理缓存。当你需要按ID查询单个实体时,应该始终使用这个方法而不是构建一个完整的查询。

Jimmer还提供了findByIds方法用于批量查询多个ID,这在处理关联数据时特别有用。

  • 简单条件查询

在实际应用中,我们常常需要根据一个或多个条件过滤数据:

@Test
@DisplayName("测试简单条件查询")
void testSimpleConditionQuery() {
    // 按价格区间查询图书
    BookTable table = BookTable.$;
    List<Book> books = sqlClient.createQuery(table)
        .where(table.id().like(testPrefix + "-%"))
        .where(table.price().between(
            new BigDecimal("50"), 
            new BigDecimal("100")
        ))
        .select(table)
        .execute();

    // 验证结果
    assertThat(books).hasSize(2);

    // 验证每本书的价格都在指定区间内
    assertThat(books).allMatch(book -> 
        book.price().compareTo(new BigDecimal("50")) >= 0 &&
        book.price().compareTo(new BigDecimal("100")) <= 0
    );
}

Jimmer提供了丰富的条件操作符,如: - 相等比较:eqne - 大小比较:gtgeltle - 范围比较:betweenin - 字符串操作:likestartsWithendsWith - 空值检查:isNullisNotNull - 集合检查:isEmptyisNotEmpty

所有这些操作符都是类型安全的,编译器会确保你只能对相应类型的属性使用适当的操作符,例如,你不能对数值类型使用like操作符。

同时,可以通过多次调用where方法添加多个条件,这些条件默认用AND逻辑连接。如果需要更复杂的逻辑组合,可以使用下一节将介绍的复杂条件构建机制。

此外,Jimmer还支持添加排序条件:

@Test
@DisplayName("测试排序操作")
void testOrderBy() {
    // 按价格降序查询所有测试图书
    BookTable table = BookTable.$;
    List<Book> books = sqlClient.createQuery(table)
        .where(table.id().like(testPrefix + "-%"))
        .orderBy(table.price().desc())
        .select(table)
        .execute();

    // 验证顺序是否正确(价格降序)
    for (int i = 0; i < books.size() - 1; i++) {
        assertThat(books.get(i).price().compareTo(books.get(i + 1).price())).isGreaterThanOrEqualTo(0);
    }
}

排序可以指定升序(asc)或降序(desc),也可以组合多个排序条件,Jimmer会按照它们的声明顺序应用这些排序。

4.1.4 查询结果的类型映射

在实际应用中,我们经常需要选择性地获取特定字段,而不是整个实体对象。Jimmer的查询API提供了强大的结果映射能力:

@Test
@DisplayName("测试查询结果的映射转换")
void testResultMapping() {
    BookTable book = BookTable.$;

    // 1. 只选择ID字段
    List<String> bookIds = sqlClient.createQuery(book)
        .where(book.id().like(testPrefix + "-%"))
        .select(book.id())
        .execute();

    assertThat(bookIds).hasSize(3);
    assertThat(bookIds).allMatch(id -> id.startsWith(testPrefix));

    // 2. 选择多个字段到简单DTO
    List<Book> books = sqlClient.createQuery(book)
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 手动映射到DTO
    List<BookView> views = books.stream()
        .map(b -> new BookView(b.id(), b.name(), b.price()))
        .toList();

    // 验证结果
    assertThat(views).hasSize(3);
    assertThat(views).allMatch(v -> 
        v.id() != null && 
        v.name() != null && 
        v.price() != null
    );
}

这个测试案例展示了两种结果映射方式:

  1. 单字段投影:通过select(book.id())只选择ID字段,返回List
  2. 手动DTO映射:先查询实体,然后手动映射到DTO对象

在实际应用中,Jimmer还支持更多高级的结果映射方式,例如:

// 直接映射到元组
List<Tuple2<String, BigDecimal>> idAndPrices = sqlClient.createQuery(book)
    .where(book.id().like(testPrefix + "-%"))
    .select(book.id(), book.price())
    .execute();

// 使用TypedArray映射到多个字段
List<Object[]> rows = sqlClient.createQuery(book)
    .where(book.id().like(testPrefix + "-%"))
    .select(
        book.id(),
        book.name(),
        book.price()
    )
    .execute();

// 动态投影到自定义DTO
class BookDto {
    private String id;
    private String name;
    private BigDecimal price;
    // 构造函数和getter/setter
}

List<BookDto> dtos = sqlClient.createQuery(book)
    .where(book.id().like(testPrefix + "-%"))
    .select(book.id(), book.name(), book.price())
    .map((id, name, price) -> {
        BookDto dto = new BookDto();
        dto.setId(id);
        dto.setName(name);
        dto.setPrice(price);
        return dto;
    })
    .execute();

这种灵活的结果映射能力使Jimmer能够适应各种业务场景的需求,既可以减少不必要的数据传输,又能保持类型安全。

4.1.5 异常处理与空值安全

在实际应用中,处理查询不到数据的情况是必不可少的。Jimmer提供了优雅的异常处理和空值安全机制:

@Test
@DisplayName("测试异常处理")
void testNullSafety() {
    // 查询不存在的记录
    String nonExistentId = testPrefix + "-non-existent";
    Book book = sqlClient.findById(Book.class, nonExistentId);

    // 验证结果为null
    assertThat(book).isNull();
}

默认情况下,findById方法在找不到记录时会返回null,而不是抛出异常。这种行为使得调用代码可以简洁地处理不存在的情况。

同时,Jimmer还提供了更多控制异常和空值的方法:

// 使用orElse提供默认值
Book bookOrDefault = sqlClient.findById(Book.class, nonExistentId)
    .orElse(() -> createDefaultBook());

// 使用orElseThrow在找不到时抛出异常
Book bookOrThrow = sqlClient.findById(Book.class, nonExistentId)
    .orElseThrow(() -> new BookNotFoundException(nonExistentId));

// 使用fetchOne获取单条记录(期望最多一条记录)
Book singleBook = sqlClient.createQuery(book)
    .where(book.id().eq(bookId))
    .select(book)
    .fetchOne();

// 使用fetchOptional获取Optional包装的结果
Optional<Book> optionalBook = sqlClient.createQuery(book)
    .where(book.id().eq(bookId))
    .select(book)
    .fetchOptional();

这些方法为不同的业务场景提供了灵活的选择,使得代码更加简洁和健壮。

  • 深入理解:Jimmer查询机制的原理

为了更好地理解和应用Jimmer的查询功能,让我们简要探讨其底层实现原理。

Jimmer的类型安全查询建立在以下技术基础之上:

  1. 编译时代码生成:Jimmer在编译时根据实体接口定义生成元数据类(如BookTable),这些类包含了完整的类型信息。

  2. DSL转译系统:类型安全的DSL表达式最终会被转译为底层的SQL语句。这个过程是完全类型安全的,任何类型错误或语法错误都会在编译阶段被捕获。

  3. 表达式树构建:查询条件实际上构建了一棵表达式树,Jimmer会根据数据库方言将这棵树转换为最终的SQL语句。

  4. 结果映射系统:查询结果会根据映射规则自动转换为相应的Java对象,确保类型安全。

正是这些技术基础保证了Jimmer查询的类型安全和高效性能。

小结与实践建议

在本节中,我们深入探讨了Jimmer的类型安全查询基础。通过实际代码示例,我们学习了如何使用Jimmer的查询API进行各种查询操作,从简单的条件过滤到复杂的逻辑组合,从基本的结果映射到高级的异常处理。

基于我们的探索,以下是使用Jimmer查询API的最佳实践建议:

  1. 充分利用类型安全:尽可能使用Jimmer的类型安全API,避免使用字符串表示属性名称。

  2. 合理拆分复杂查询:对于复杂查询,考虑创建可重用的条件表达式,提高代码可读性和可维护性。

  3. 选择合适的结果映射方式:根据业务需求选择合适的结果映射方式,既能减少数据传输,又能保持代码的简洁性。

  4. 处理好空值情况:使用Jimmer提供的空值处理机制,确保代码健壮性,避免空指针异常。

  5. 遵循性能最佳实践

  6. 只查询需要的字段,避免不必要的数据加载
  7. 添加适当的查询条件,避免全表扫描
  8. 对大结果集使用分页查询
  9. 合理利用缓存机制

在下一节中,我们将在此基础上,进一步探索Jimmer的高级条件查询功能,学习如何构建更复杂的查询表达式,处理更复杂的业务场景。

4.2 高级条件查询:灵活性与类型安全的完美融合

在现代企业应用中,数据查询的需求远不止于简单的等值匹配。用户可能需要按价格区间筛选商品,按名称模糊搜索文档,或者组合多种复杂条件进行高级过滤。这些场景对查询系统提出了更高的要求——既要保证足够的灵活性以满足各种复杂条件,又要维持类型安全以避免运行时错误。

Jimmer的高级条件查询功能正是为解决这一挑战而设计。通过巧妙的API设计,Jimmer成功实现了在保持完全类型安全的同时,提供不亚于原生SQL的表达能力。本节将深入探讨Jimmer高级条件查询的各项特性,并通过实际的业务场景展示其强大能力。

4.2.1 比较操作符的使用

数据查询的基础是比较操作——通过比较数据字段与期望值的关系来过滤记录。Jimmer提供了全面的比较操作符集合,支持各种数据类型的精确匹配与范围筛选。

业务场景:电子书店的多维度筛选

想象一个在线电子书店,用户可能需要: - 查找特定类别的图书 - 筛选特定价格区间内的图书 - 搜索书名包含特定关键词的图书 - 查找有或没有详细描述的图书

传统的方案往往需要手写SQL或使用笨重的查询构建器,代码冗长且容易出错:

// 传统查询构建方案中的问题
String sql = "SELECT * FROM books WHERE 1=1 ";
List<Object> params = new ArrayList<>();

if (category != null) {
    sql += "AND category = ? ";
    params.add(category);
}

if (minPrice != null && maxPrice != null) {
    sql += "AND price BETWEEN ? AND ? ";
    params.add(minPrice);
    params.add(maxPrice);
}

if (keyword != null) {
    sql += "AND name LIKE ? ";
    params.add("%" + keyword + "%"); // 潜在的SQL注入风险
}

if (hasDescription) {
    sql += "AND description IS NOT NULL ";
} else {
    sql += "AND description IS NULL ";
}

// 执行查询...这里省略

这种方式存在多个问题: - 类型不安全,参数类型错误只能在运行时发现 - SQL拼接容易引入语法错误 - 存在SQL注入风险 - 不同数据库方言处理不一致

测试用例:验证Jimmer比较操作符的能力

让我们通过测试来验证Jimmer如何优雅地解决上述问题:

@Test
@DisplayName("测试等值比较与不等比较")
void testEqualityComparisons() {
    BookTable book = BookTable.$;

    // 1. 等值比较 (=)
    List<Book> javaBooks = sqlClient.createQuery(book)
        .where(book.category().eq("编程语言"))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(javaBooks).hasSize(3);
    assertThat(javaBooks).allMatch(b -> b.category().equals("编程语言"));

    // 2. 不等比较 (!=)
    List<Book> nonJavaBooks = sqlClient.createQuery(book)
        .where(book.category().ne("编程语言"))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(nonJavaBooks).hasSize(3);
    assertThat(nonJavaBooks).noneMatch(b -> b.category().equals("编程语言"));

    // 3. NULL值比较
    List<Book> booksWithDescription = sqlClient.createQuery(book)
        .where(book.description().isNotNull())
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    List<Book> booksWithoutDescription = sqlClient.createQuery(book)
        .where(book.description().isNull())
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(booksWithDescription.size() + booksWithoutDescription.size()).isEqualTo(6);
}

这个测试验证了Jimmer的基本比较操作,包括等值比较(eq)、不等比较(ne)和NULL值处理(isNull/isNotNull)。每个操作都是完全类型安全的,编译期即可发现类型不匹配问题。

范围比较:超越简单的等值判断

对于数值类型(如商品价格、评分)、日期类型(如发布时间、截止日期)等,范围比较尤为重要。Jimmer提供了完整的范围比较操作符集合,包括大于(gt)、大于等于(ge)、小于(lt)、小于等于(le)以及范围(between)等。

@Test
@DisplayName("测试范围比较操作符")
void testRangeComparisons() {
    BookTable book = BookTable.$;

    // 1. 大于比较 (>)
    List<Book> expensiveBooks = sqlClient.createQuery(book)
        .where(book.price().gt(new BigDecimal("100")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(expensiveBooks).isNotEmpty();
    assertThat(expensiveBooks).allMatch(b -> b.price().compareTo(new BigDecimal("100")) > 0);

    // 2. 大于等于比较 (>=)
    List<Book> notCheapBooks = sqlClient.createQuery(book)
        .where(book.price().ge(new BigDecimal("100")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(notCheapBooks).isNotEmpty();
    assertThat(notCheapBooks).allMatch(b -> b.price().compareTo(new BigDecimal("100")) >= 0);

    // 3. 小于比较 (<)
    List<Book> cheapBooks = sqlClient.createQuery(book)
        .where(book.price().lt(new BigDecimal("80")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(cheapBooks).isNotEmpty();
    assertThat(cheapBooks).allMatch(b -> b.price().compareTo(new BigDecimal("80")) < 0);

    // 4. 小于等于比较 (<=)
    List<Book> affordableBooks = sqlClient.createQuery(book)
        .where(book.price().le(new BigDecimal("80")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(affordableBooks).isNotEmpty();
    assertThat(affordableBooks).allMatch(b -> b.price().compareTo(new BigDecimal("80")) <= 0);

    // 5. 范围比较 (BETWEEN)
    List<Book> midRangeBooks = sqlClient.createQuery(book)
        .where(book.price().between(new BigDecimal("80"), new BigDecimal("120")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(midRangeBooks).isNotEmpty();
    assertThat(midRangeBooks).allMatch(b -> 
        b.price().compareTo(new BigDecimal("80")) >= 0 && 
        b.price().compareTo(new BigDecimal("120")) <= 0
    );
}

通过这个测试,我们可以看到Jimmer对范围比较的支持非常全面,而且完全类型安全。例如,price()方法返回的是数值类型表达式,因此只能调用数值比较操作符,编译器会阻止错误的类型比较。

字符串模糊匹配:搜索功能的基础

对于字符串类型,除了基本的等值比较外,模糊匹配是最常用的功能之一。无论是搜索框还是筛选功能,字符串模糊匹配都是不可或缺的。Jimmer提供了功能丰富的字符串匹配能力:

@Test
@DisplayName("测试字符串模糊匹配")
void testStringPatternMatching() {
    BookTable book = BookTable.$;

    // 1. 简单的LIKE匹配
    List<Book> javaBooks = sqlClient.createQuery(book)
        .where(book.name().like("%Java%"))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(javaBooks).isNotEmpty();
    assertThat(javaBooks).allMatch(b -> b.name().contains("Java"));

    // 2. 以特定字符串开始
    List<Book> booksStartingWithE = sqlClient.createQuery(book)
        .where(book.name().like("E%"))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(booksStartingWithE).isNotEmpty();
    assertThat(booksStartingWithE).allMatch(b -> b.name().startsWith("E"));

    // 3. 以特定字符串结束
    List<Book> booksEndingWithA = sqlClient.createQuery(book)
        .where(book.name().like("%想"))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果 (应该包含Java编程思想)
    assertThat(booksEndingWithA).isNotEmpty();
    assertThat(booksEndingWithA).allMatch(b -> b.name().endsWith("想"));

    // 4. 使用不同的LIKE模式
    List<Book> booksWithPattern = sqlClient.createQuery(book)
        .where(book.name().like("SQL%", LikeMode.EXACT)) // 精确匹配模式
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(booksWithPattern).hasSize(1);
    assertThat(booksWithPattern.get(0).name()).startsWith("SQL");
}

在这个测试中,我们展示了Jimmer对字符串模糊匹配的强大支持:

  1. 标准的SQL LIKE模式:可以使用%_通配符
  2. 使用like方法配合不同参数模式:
  3. 包含匹配:like("%Java%")
  4. 前缀匹配:like("E%")
  5. 后缀匹配:like("%想")
  6. 支持自定义匹配模式:LikeMode.EXACT

通过这些灵活的模糊匹配方式,Jimmer可以轻松实现各种搜索功能,从简单的关键词搜索到复杂的模式匹配均可覆盖。

比较操作符的智能类型匹配

Jimmer比较操作符的一个关键设计是智能的类型匹配。不同数据类型提供不同的比较操作符:

  • 数值类型:支持全部比较操作符(eqnegtgeltlebetween等)
  • 字符串类型:除了基本比较外,还支持likeilike(不区分大小写)等特殊操作符
  • 日期时间类型:支持全部比较操作符,便于日期范围查询
  • 布尔类型:主要支持eqne
  • 枚举类型:自动转换为底层存储值进行比较
  • 可空类型:额外支持isNullisNotNull操作符

这种设计保证了API的自然性和安全性——你只能对特定类型使用合适的操作符,错误的使用将导致编译错误,而不是运行时异常。

graph TD A[属性类型] --> B{数值类型?} A --> C{字符串类型?} A --> D{时间类型?} A --> E{可空类型?} B --> B1[支持: eq, ne, gt, ge, lt, le, between] C --> C1[支持: eq, ne, like, ilike] D --> D1[支持: eq, ne, gt, ge, lt, le, between] E --> E1[额外支持: isNull, isNotNull] style B1 fill:#47a1ad,stroke:#015467 style C1 fill:#47a1ad,stroke:#015467 style D1 fill:#47a1ad,stroke:#015467 style E1 fill:#47a1ad,stroke:#015467

Jimmer不仅提供了全面的比较操作符支持,还通过类型安全设计避免了常见的错误。开发者可以使用自然、流畅的API编写各种复杂的比较条件,而无需担心类型不匹配或SQL语法问题。在实际应用中,这些比较操作符可以组合使用,构建出更加复杂的查询条件。

4.2.2 逻辑运算符组合

在实际业务场景中,单一的比较条件往往不足以满足复杂的查询需求。例如,我们可能需要:

  • 查找名称包含"Java""Python"的书籍
  • 查找价格低于50评分高于4.5的书籍
  • 查找不是编程类别但名称包含"编程"的书籍

这些查询需要通过逻辑运算符将多个条件组合起来。Jimmer提供了全面的逻辑运算符支持,使开发者能够构建任意复杂度的查询条件。

业务场景:电商平台的商品搜索

考虑一个电商平台的高级搜索功能,用户可能需要组合多种条件进行复杂搜索:

  • 价格区间品牌筛选的组合
  • 多个关键词的关系搜索
  • 排除特定商品类型的条件

传统实现方式通常依赖字符串拼接或复杂的查询构建器,代码冗长且易出错:

// 传统方案中的条件组合
StringBuilder sql = new StringBuilder("SELECT * FROM products WHERE 1=1 ");
List<Object> params = new ArrayList<>();

// AND条件
if (minPrice != null && maxPrice != null) {
    sql.append("AND (price >= ? AND price <= ?) ");
    params.add(minPrice);
    params.add(maxPrice);
}

// OR条件
if (keywords != null && !keywords.isEmpty()) {
    sql.append("AND (");
    for (int i = 0; i < keywords.size(); i++) {
        if (i > 0) sql.append(" OR ");
        sql.append("name LIKE ?");
        params.add("%" + keywords.get(i) + "%");
    }
    sql.append(") ");
}

// NOT条件
if (excludedCategories != null && !excludedCategories.isEmpty()) {
    sql.append("AND category_id NOT IN (");
    for (int i = 0; i < excludedCategories.size(); i++) {
        if (i > 0) sql.append(", ");
        sql.append("?");
        params.add(excludedCategories.get(i));
    }
    sql.append(") ");
}

// 执行查询...

这种实现方式不仅冗长、难以阅读,还容易引入错误(例如忘记添加括号导致逻辑错误)。Jimmer通过其强大的逻辑运算符API解决了这些问题。

AND逻辑:多条件同时满足

在大多数查询中,AND逻辑是最基本的条件组合方式——要求记录同时满足多个条件。Jimmer提供了多种方式实现AND逻辑:

@Test
@DisplayName("测试逻辑运算符 - AND")
void testLogicalAnd() {
    BookTable book = BookTable.$;

    // 使用AND组合多个条件
    List<Book> javaAndExpensive = sqlClient.createQuery(book)
        .where(
            book.name().like("%Java%").and(
                book.price().gt(new BigDecimal("85"))
            )
        )
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(javaAndExpensive).isNotEmpty();
    assertThat(javaAndExpensive).allMatch(b -> 
        b.name().contains("Java") && 
        b.price().compareTo(new BigDecimal("85")) > 0
    );

    // 使用链式调用方式组合AND条件
    List<Book> javaAndExpensiveAlt = sqlClient.createQuery(book)
        .where(book.name().like("%Java%"))
        .where(book.price().gt(new BigDecimal("85")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证两种方式的结果一致
    assertThat(javaAndExpensive.size()).isEqualTo(javaAndExpensiveAlt.size());
}

从测试中可以看出,Jimmer提供了两种等效的方式表达AND逻辑:

  1. 使用.and()方法显式连接条件:condition1.and(condition2)
  2. 使用链式.where()调用隐式表达AND关系:.where(condition1).where(condition2)

第二种方式更加简洁,也是更常用的模式,特别适合条件较多的场景。但第一种方式在需要控制条件优先级或与其他逻辑运算符混合使用时更为灵活。

OR逻辑:满足任一条件

OR逻辑用于表达"满足任一条件即可"的查询需求,例如搜索多个关键词。Jimmer同样提供了简洁的API:

@Test
@DisplayName("测试逻辑运算符 - OR")
void testLogicalOr() {
    BookTable book = BookTable.$;

    // 使用OR组合多个条件
    List<Book> javaOrDatabase = sqlClient.createQuery(book)
        .where(
            Predicate.or(
                book.name().like("%Java%"),
                book.name().like("%SQL%")
            )
        )
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(javaOrDatabase).isNotEmpty();
    assertThat(javaOrDatabase).allMatch(b -> 
        b.name().contains("Java") || b.name().contains("SQL")
    );

    // 使用Predicate.or方法的另一种形式
    List<Book> javaOrDatabaseAlt = sqlClient.createQuery(book)
        .where(book.name().like("%Java%").or(book.name().like("%SQL%")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证两种方式的结果一致
    assertThat(javaOrDatabase.size()).isEqualTo(javaOrDatabaseAlt.size());
}

Jimmer提供了两种方式表达OR逻辑:

  1. 使用静态方法Predicate.or(condition1, condition2, ...)
  2. 使用实例方法condition1.or(condition2)

这两种方式在功能上完全等价,可以根据代码风格和可读性选择。第一种方式在处理三个或更多条件时更为清晰,而第二种方式在只有两个条件时更加简洁。

NOT逻辑:条件取反

NOT逻辑用于表达"不满足某条件"的查询需求,例如排除特定类别的商品。Jimmer提供了直观的API:

@Test
@DisplayName("测试逻辑运算符 - NOT")
void testLogicalNot() {
    BookTable book = BookTable.$;

    // 使用NOT反转条件
    List<Book> notJavaBooks = sqlClient.createQuery(book)
        .where(book.name().like("%Java%").not())
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(notJavaBooks).isNotEmpty();
    assertThat(notJavaBooks).noneMatch(b -> b.name().contains("Java"));

    // 使用另一种NOT形式
    List<Book> notJavaBooksAlt = sqlClient.createQuery(book)
        .where(Predicate.not(book.name().like("%Java%")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证两种方式的结果一致
    assertThat(notJavaBooks.size()).isEqualTo(notJavaBooksAlt.size());
}

Jimmer同样提供了两种方式表达NOT逻辑:

  1. 使用实例方法:condition.not()
  2. 使用静态方法:Predicate.not(condition)

这两种方式功能上完全等价,可以根据个人偏好和代码上下文选择使用。

逻辑运算符的优先级控制

在复杂查询中,逻辑运算符的优先级至关重要。例如,A AND B OR CA AND (B OR C)的含义完全不同。Jimmer通过显式的方法调用和括号嵌套确保逻辑优先级的正确表达:

// 错误理解:(name包含Java AND 价格>100) OR 类别是编程语言
// 正确理解:name包含Java AND (价格>100 OR 类别是编程语言)
Predicate wrongCondition = book.name().like("%Java%").and(book.price().gt(new BigDecimal("100")))
                            .or(book.category().eq("编程语言"));

// 正确表达第一种逻辑
Predicate correctCondition1 = Predicate.or(
    book.name().like("%Java%").and(book.price().gt(new BigDecimal("100"))),
    book.category().eq("编程语言")
);

// 正确表达第二种逻辑
Predicate correctCondition2 = book.name().like("%Java%").and(
    Predicate.or(
        book.price().gt(new BigDecimal("100")),
        book.category().eq("编程语言")
    )
);

通过显式的嵌套结构,Jimmer确保了逻辑运算的优先级完全符合开发者的预期,避免了模糊不清的隐式优先级规则。

逻辑运算的组合能力

Jimmer的逻辑运算符可以任意组合,构建出任何复杂度的查询条件。以下是一个复杂条件的示例:

graph TD A[最终条件] -->|AND| B["name LIKE '%Java%' OR price > 100"] A -->|AND| C["category = '编程语言' OR name LIKE '%SQL%'"] B -->|OR| B1["name LIKE '%Java%'"] B -->|OR| B2["price > 100"] C -->|OR| C1["category = '编程语言'"] C -->|OR| C2["name LIKE '%SQL%'"] style A fill:#015467,stroke:#634f7d,color:#ffffff style B fill:#47a1ad,stroke:#015467 style C fill:#47a1ad,stroke:#015467

这种复杂条件在Jimmer中可以清晰地表达:

@Test
@DisplayName("测试复杂条件组合")
void testComplexConditionCombination() {
    BookTable book = BookTable.$;

    // 构建复杂的查询条件: (name包含Java OR 价格>100) AND (category是编程语言 OR name包含SQL)
    Predicate condition = Predicate.and(
        Predicate.or(
            book.name().like("%Java%"),
            book.price().gt(new BigDecimal("100"))
        ),
        Predicate.or(
            book.category().eq("编程语言"),
            book.name().like("%SQL%")
        )
    );

    // 添加测试数据过滤条件
    condition = condition.and(book.id().like(testPrefix + "-%"));

    // 执行查询
    List<Book> books = sqlClient.createQuery(book)
        .where(condition)
        .select(book)
        .execute();

    // 验证结果
    assertThat(books).isNotEmpty();
    assertThat(books).allMatch(b -> 
        (b.name().contains("Java") || b.price().compareTo(new BigDecimal("100")) > 0) &&
        (b.category().equals("编程语言") || b.name().contains("SQL"))
    );
}

通过这种嵌套组合的方式,Jimmer可以表达任意复杂的查询条件,同时保持代码的可读性和类型安全性。

小结

Jimmer的逻辑运算符设计具有以下优势:

  1. 完整的逻辑运算支持:包括AND、OR、NOT以及它们的任意组合
  2. 多种表达式语法:支持链式调用和静态方法两种风格,适应不同场景
  3. 显式的优先级控制:通过方法嵌套明确表达逻辑优先级,避免歧义
  4. 类型安全保证:所有条件组合在编译期验证类型正确性
  5. 代码可读性:逻辑结构清晰,接近自然语言表达

在下一节中,我们将探讨如何使用Jimmer处理集合相关的条件查询,进一步扩展查询能力。

4.2.3 集合相关条件

在实际业务场景中,我们经常需要处理与集合相关的查询条件,例如:

  • 查找特定分类中的商品(分类ID在给定集合中)
  • 查找具有多个标签的文章(文章ID存在于多个标签关联表中)
  • 排除某些状态的订单(订单状态不在特定集合中)

这类查询需要使用IN、EXISTS等集合相关条件。Jimmer为这类查询提供了类型安全且强大的支持。

业务场景:电商平台的商品筛选

在电商平台中,用户通常需要按照多个维度筛选商品:

  • 多品牌筛选:查找属于某几个品牌的商品
  • 多分类联合:查找属于某几个分类的商品
  • 多标签过滤:查找具有特定标签组合的商品

传统实现方式通常依赖字符串拼接和参数列表,容易出错且难以维护:

// 传统IN查询实现
StringBuilder sql = new StringBuilder("SELECT * FROM products WHERE 1=1 ");
List<Object> params = new ArrayList<>();

if (brands != null && !brands.isEmpty()) {
    sql.append("AND brand_id IN (");
    for (int i = 0; i < brands.size(); i++) {
        if (i > 0) sql.append(", ");
        sql.append("?");
        params.add(brands.get(i));
    }
    sql.append(") ");
}

// 执行查询...省略

这种方式不仅代码冗长,还容易出现参数数量不匹配的错误。Jimmer通过其强大的集合条件API解决了这些问题。

IN查询:多值匹配

IN查询是最基本的集合条件,用于检查某个字段的值是否在给定集合中。Jimmer提供了简洁的IN查询支持:

@Test
@DisplayName("测试IN查询")
void testInQuery() {
    BookTable book = BookTable.$;

    // 使用IN查询多个可能的值
    List<Book> specificCategories = sqlClient.createQuery(book)
        .where(book.category().in(Arrays.asList("编程语言", "框架技术")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(specificCategories).isNotEmpty();
    assertThat(specificCategories).allMatch(b -> 
        b.category().equals("编程语言") || b.category().equals("框架技术")
    );

    // 使用NOT IN排除特定值
    List<Book> excludeCategories = sqlClient.createQuery(book)
        .where(book.category().notIn(Arrays.asList("编程语言", "框架技术")))
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute();

    // 验证结果
    assertThat(excludeCategories).isNotEmpty();
    assertThat(excludeCategories).noneMatch(b -> 
        b.category().equals("编程语言") || b.category().equals("框架技术")
    );
}

从测试中可以看出,Jimmer提供了两种IN查询方式:

  1. in(Collection<?>):字段值在给定集合中
  2. notIn(Collection<?>):字段值不在给定集合中

这两种方法直接接收Java集合类型,无需手动构建参数列表和SQL片段,大大简化了代码编写。同时,Jimmer完全保证了类型安全——集合元素类型必须与字段类型兼容,否则将在编译期报错。

空集合处理

一个常见的陷阱是处理空集合的IN查询。例如,当用户没有选择任何筛选条件时,我们通常希望返回所有记录,而不是空结果。Jimmer对此有特殊处理:

// 当categories为空集合时
List<String> categories = Collections.emptyList();
Predicate condition = book.category().in(categories);

// Jimmer会将其优化为TRUE常量
// 等价于SQL: WHERE 1=1

这种行为避免了空集合导致的意外查询结果,符合大多数业务场景的预期。如果需要不同的行为,可以通过条件检查显式处理:

Predicate condition = null;
if (categories != null && !categories.isEmpty()) {
    condition = book.category().in(categories);
} else {
    // 自定义空集合行为
    condition = Predicate.constant(false); // 返回空结果
    // 或者
    condition = null; // 忽略此条件
}

EXISTS查询:子查询条件

除了简单的IN查询外,更复杂的场景可能需要使用EXISTS子查询。例如,查找至少有一个五星评价的商品。Jimmer同样提供了类型安全的EXISTS查询支持:

// EXISTS查询示例
BookTable book = BookTable.$;
ReviewTable review = ReviewTable.$;

List<Book> booksWithHighRating = sqlClient.createQuery(book)
    .where(
        sqlClient.createSubQuery(review)
            .where(review.bookId().eq(book.id()))
            .where(review.rating().eq(5))
            .exists()
    )
    .select(book)
    .execute();

在这个例子中,我们使用子查询检查每本书是否有五星评价。Jimmer将生成类似以下的SQL:

SELECT * FROM t_book b
WHERE EXISTS (
    SELECT 1 FROM t_review r 
    WHERE r.book_id = b.id AND r.rating = 5
)

Jimmer的子查询API完全保持了类型安全: - 表之间的关联条件(review.bookId().eq(book.id()))在编译期验证类型匹配 - 条件表达式(review.rating().eq(5))同样类型安全 - 子查询构建器与主查询分离,但共享同一上下文

这种强类型的子查询支持极大地减少了错误,同时保持了SQL的表达力。

IN子查询:集合子查询

IN查询也可以与子查询结合,例如,查找有特定类型评论的所有书籍:

// IN子查询示例
BookTable book = BookTable.$;
ReviewTable review = ReviewTable.$;

List<Book> booksWithCriticalReviews = sqlClient.createQuery(book)
    .where(
        book.id().in(
            sqlClient.createSubQuery(review)
                .where(review.type().eq(ReviewType.CRITICAL))
                .select(review.bookId())
        )
    )
    .select(book)
    .execute();

这段代码生成的SQL类似于:

SELECT * FROM t_book b
WHERE b.id IN (
    SELECT r.book_id FROM t_review r 
    WHERE r.type = 'CRITICAL'
)

通过组合IN查询和子查询,Jimmer可以表达复杂的数据选择逻辑,同时保持代码的可读性和类型安全性。

集合函数:ANY和ALL

对于更高级的集合条件,Jimmer还支持ANY和ALL函数,用于表达"任意满足"和"全部满足"的条件:

// ANY示例:价格高于任何参考价格
BookTable book = BookTable.$;
List<BigDecimal> referenceValues = Arrays.asList(
    new BigDecimal("50"), 
    new BigDecimal("100"),
    new BigDecimal("150")
);

List<Book> booksWithHigherPrice = sqlClient.createQuery(book)
    .where(book.price().gt().any(referenceValues))
    .select(book)
    .execute();

// ALL示例:价格高于所有参考价格
List<Book> veryExpensiveBooks = sqlClient.createQuery(book)
    .where(book.price().gt().all(referenceValues))
    .select(book)
    .execute();

这些函数为复杂的集合比较提供了简洁的表达方式,避免了繁琐的条件组合。

小结

Jimmer的集合相关条件查询功能具有以下优势:

  1. 完整的集合操作支持:包括IN、NOT IN、EXISTS、NOT EXISTS等各种集合操作
  2. 简洁的API:直接接收Java集合,无需手动构建参数列表
  3. 类型安全保证:集合元素类型必须与字段类型兼容,编译期验证
  4. 智能的空集合处理:避免空集合导致的意外查询结果
  5. 子查询支持:类型安全的子查询构建,支持复杂的数据选择逻辑

集合条件查询与前面介绍的比较操作符和逻辑运算符相结合,可以构建出功能强大的查询表达式,满足各种复杂业务场景的需求。

4.2.4 条件组合与复杂表达式

随着应用复杂度的增加,我们经常需要构建非常复杂的查询条件。例如,一个高级搜索页面可能允许用户同时按多个维度过滤数据,这些条件之间具有复杂的逻辑关系。Jimmer通过其灵活的条件组合API,使构建复杂查询变得简单而直观。

业务场景:电商平台的高级搜索

想象一个电商平台的高级搜索功能,用户可以同时指定多种筛选条件:

  • 按多个价格区间筛选(例如,0-50元,100-200元)
  • 按品牌和类别组合筛选
  • 按评分和销量进行复合排序
  • 按关键词在多个字段中搜索(名称、描述、规格等)

这种复杂的搜索需求需要构建深度嵌套的条件表达式,如果使用传统方法,代码将变得非常复杂且难以维护。

构建复杂条件表达式

Jimmer允许我们通过直观的API构建任意复杂度的条件表达式。让我们通过一个例子来展示这种能力:

@Test
@DisplayName("测试复杂条件组合")
void testComplexConditionCombination() {
    BookTable book = BookTable.$;

    // 构建复杂的查询条件: (name包含Java OR 价格>100) AND (category是编程语言 OR name包含SQL)
    Predicate condition = Predicate.and(
        Predicate.or(
            book.name().like("%Java%"),
            book.price().gt(new BigDecimal("100"))
        ),
        Predicate.or(
            book.category().eq("编程语言"),
            book.name().like("%SQL%")
        )
    );

    // 添加测试数据过滤条件
    condition = condition.and(book.id().like(testPrefix + "-%"));

    // 执行查询
    List<Book> books = sqlClient.createQuery(book)
        .where(condition)
        .select(book)
        .execute();

    // 验证结果
    assertThat(books).isNotEmpty();
    assertThat(books).allMatch(b -> 
        (b.name().contains("Java") || b.price().compareTo(new BigDecimal("100")) > 0) &&
        (b.category().equals("编程语言") || b.name().contains("SQL"))
    );
}

这个测试展示了如何构建一个结构化的复杂条件表达式。关键特点包括:

  1. 嵌套结构:条件可以任意嵌套,形成树状结构
  2. 混合逻辑:可以自由混合AND、OR和NOT逻辑
  3. 表达式重用:条件表达式可以保存在变量中并进一步组合
  4. 增量构建:可以通过.and()等方法增量添加新条件

条件的动态构建

在实际应用中,查询条件往往需要根据用户输入动态构建。例如,用户可能只填写了部分搜索条件,或者根据用户角色显示不同范围的数据。Jimmer提供了强大的动态条件构建能力:

@Test
@DisplayName("测试动态条件构建")
void testDynamicConditionBuilding() {
    BookTable book = BookTable.$;

    // 模拟查询参数
    String namePattern = "Java";
    String category = "编程语言";
    BigDecimal minPrice = new BigDecimal("80");
    BigDecimal maxPrice = null; // 故意设置为null,测试动态条件

    // 动态构建查询条件
    List<Book> books = sqlClient.createQuery(book)
        .where(book.id().like(testPrefix + "-%"))
        .whereIf(namePattern != null && !namePattern.isEmpty(),
            () -> book.name().like("%" + namePattern + "%"))
        .whereIf(category != null && !category.isEmpty(),
            () -> book.category().eq(category))
        .whereIf(minPrice != null && maxPrice != null,
            () -> book.price().between(minPrice, maxPrice))
        .whereIf(minPrice != null && maxPrice == null,
            () -> book.price().ge(minPrice))
        .whereIf(maxPrice != null && minPrice == null,
            () -> book.price().le(maxPrice))
        .select(book)
        .execute();

    // 验证结果
    assertThat(books).isNotEmpty();
    assertThat(books).allMatch(b -> 
        b.id().startsWith(testPrefix) &&
        b.name().contains(namePattern) &&
        b.category().equals(category) &&
        b.price().compareTo(minPrice) >= 0
    );
}

这个测试展示了Jimmer的动态条件构建功能:

  1. 条件检查:使用whereIf方法根据条件决定是否添加查询条件
  2. 延迟求值:使用Lambda表达式延迟条件构建,避免空值问题
  3. 多条件组合:可以组合多个动态条件

这种方式避免了传统条件构建中的大量if-else语句,使代码更加简洁和可读。同时,由于使用了Lambda表达式和类型安全的API,错误在编译期就能被发现。

条件抽象与封装

对于复杂应用,我们通常需要复用查询条件或者封装特定的业务规则。Jimmer支持将条件抽象为可重用组件:

// 定义条件生成器
class BookConditions {
    public static Predicate isExpensive(BookTable table, BigDecimal threshold) {
        return table.price().ge(threshold);
    }

    public static Predicate containsKeyword(BookTable table, String keyword) {
        return Predicate.or(
            table.name().like("%" + keyword + "%"),
            table.description().like("%" + keyword + "%")
        );
    }

    public static Predicate isNewlyPublished(BookTable table, int withinDays) {
        LocalDateTime threshold = LocalDateTime.now().minusDays(withinDays);
        return table.publishTime().ge(threshold);
    }
}

// 使用条件生成器
BookTable book = BookTable.$;
Predicate condition = Predicate.and(
    BookConditions.isExpensive(book, new BigDecimal("100")),
    BookConditions.isNewlyPublished(book, 30)
);

通过这种方式,我们可以将常用的查询条件封装为可重用的方法,提高代码的可读性和可维护性。这些方法可以接受参数,实现更灵活的条件构建。

条件组合的安全性优势

与传统的字符串拼接或参数列表相比,Jimmer的条件组合方式具有显著的安全性优势:

  1. 类型安全:所有条件表达式都是类型安全的,错误在编译期发现
  2. 参数绑定安全:自动处理参数绑定,避免SQL注入风险
  3. 空值安全:通过Lambda表达式延迟求值,避免空值异常
  4. 优先级明确:表达式嵌套结构明确表示优先级,避免逻辑错误

这些安全特性使得开发者可以专注于业务逻辑,而不是处理低级的SQL拼接和错误处理。

4.2.5 查询片段复用与组合

在大型应用中,我们通常需要在多个地方复用相同的查询条件,或者将多个独立开发的条件组合在一起。Jimmer提供了强大的查询片段复用机制,使得条件的复用和组合变得简单高效。

业务场景:企业级应用中的条件复用

在企业级应用中,查询条件往往分散在不同的模块和组件中:

  • 权限系统提供数据访问控制条件
  • 多租户系统提供租户隔离条件
  • 业务模块提供特定的业务规则条件
  • UI组件提供用户交互产生的条件

这些条件需要在不同上下文中组合使用,如果每次都重新构建,将导致大量代码重复和维护困难。

函数式条件片段

Jimmer支持将条件片段定义为函数,便于复用和组合:

@Test
@DisplayName("测试条件片段复用")
void testConditionReuse() {
    BookTable book = BookTable.$;

    // 1. 定义可重用的条件片段
    Function<BigDecimal, Predicate> priceGreaterThan = price -> 
        book.price().gt(price);

    BiFunction<String, String, Predicate> categoryOrNameLike = (category, nameLike) ->
        book.category().eq(category).or(book.name().like("%" + nameLike + "%"));

    Predicate testDataFilter = book.id().like(testPrefix + "-%");

    // 2. 在不同查询中复用条件片段
    // 查询1: 价格>90 AND (类别是编程语言 OR 名称包含SQL)
    List<Book> query1Results = sqlClient.createQuery(book)
        .where(priceGreaterThan.apply(new BigDecimal("90")))
        .where(categoryOrNameLike.apply("编程语言", "SQL"))
        .where(testDataFilter)
        .select(book)
        .execute();

    // 查询2: 价格>120 AND (类别是框架技术 OR 名称包含Spring)
    List<Book> query2Results = sqlClient.createQuery(book)
        .where(priceGreaterThan.apply(new BigDecimal("120")))
        .where(categoryOrNameLike.apply("框架技术", "Spring"))
        .where(testDataFilter)
        .select(book)
        .execute();

    // 3. 验证结果
    assertThat(query1Results).allMatch(b -> 
        b.price().compareTo(new BigDecimal("90")) > 0 &&
        (b.category().equals("编程语言") || b.name().contains("SQL"))
    );

    assertThat(query2Results).allMatch(b -> 
        b.price().compareTo(new BigDecimal("120")) > 0 &&
        (b.category().equals("框架技术") || b.name().contains("Spring"))
    );
}

这个测试展示了如何使用函数式接口定义可重用的条件片段:

  1. 条件工厂函数:定义接受参数并返回Predicate的函数
  2. 条件参数化:通过函数参数调整条件行为
  3. 多次复用:在不同查询中复用相同的条件片段

这种方式使条件片段可以在不同上下文中灵活复用,同时保持类型安全。

条件片段的组合模式

对于更复杂的场景,我们可以使用组合模式构建条件库:

// 条件组合器
class BookConditionBuilder {
    private final BookTable table;
    private List<Predicate> conditions = new ArrayList<>();

    public BookConditionBuilder(BookTable table) {
        this.table = table;
    }

    public BookConditionBuilder withPriceRange(BigDecimal min, BigDecimal max) {
        if (min != null && max != null) {
            conditions.add(table.price().between(min, max));
        } else if (min != null) {
            conditions.add(table.price().ge(min));
        } else if (max != null) {
            conditions.add(table.price().le(max));
        }
        return this;
    }

    public BookConditionBuilder withCategory(String category) {
        if (category != null && !category.isEmpty()) {
            conditions.add(table.category().eq(category));
        }
        return this;
    }

    public BookConditionBuilder withKeyword(String keyword) {
        if (keyword != null && !keyword.isEmpty()) {
            conditions.add(table.name().like("%" + keyword + "%"));
        }
        return this;
    }

    public Predicate build() {
        if (conditions.isEmpty()) {
            return null;
        }

        Predicate result = conditions.get(0);
        for (int i = 1; i < conditions.size(); i++) {
            result = result.and(conditions.get(i));
        }
        return result;
    }
}

// 使用条件组合器
BookTable book = BookTable.$;
Predicate condition = new BookConditionBuilder(book)
    .withPriceRange(new BigDecimal("50"), new BigDecimal("100"))
    .withCategory("编程语言")
    .withKeyword("Java")
    .build();

List<Book> books = sqlClient.createQuery(book)
    .where(condition)
    .select(book)
    .execute();

这种组合模式使得条件构建更加直观和流畅,特别适合复杂的条件组合场景。同时,由于使用了Jimmer的类型安全API,整个构建过程仍然保持类型安全。

查询复用的最佳实践

基于Jimmer的查询片段复用能力,我们可以总结以下最佳实践:

  1. 分层条件组织
  2. 将通用条件封装在基础层(如权限、多租户等)
  3. 将业务规则条件封装在领域层
  4. 将UI交互条件处理在应用层

  5. 条件库模式

  6. 为每个实体类型创建专门的条件库类
  7. 将常用条件片段封装为静态方法
  8. 使用参数使条件具有灵活性

  9. 条件组合器模式

  10. 对于复杂查询场景,使用构建器模式
  11. 支持流畅的链式调用API
  12. 内部处理条件组合逻辑

通过这些实践,可以显著提高代码的可读性、可维护性和复用性,同时保持类型安全和性能优势。

  • 总结与展望

本节探讨了Jimmer高级条件查询的各个方面,包括比较操作符使用、逻辑运算符组合、集合相关条件,以及条件组合和复用。通过这些特性,Jimmer成功实现了类型安全与灵活性的平衡,为开发者提供了强大而直观的查询能力。

Jimmer的高级条件查询具有以下核心优势:

  1. 完全类型安全:所有查询操作在编译期验证类型正确性,避免运行时错误
  2. 丰富的表达能力:支持各种操作符、逻辑组合和集合条件,表达能力不亚于原生SQL
  3. 直观的API设计:流畅的接口设计使代码易于编写和阅读
  4. 强大的复用能力:支持条件片段的定义、参数化和组合,促进代码复用
  5. 动态查询支持:内置对动态条件的支持,简化条件构建逻辑

在下一节中,我们将探讨Jimmer的另一个强大特性:关联查询与数据图构建。这些特性将进一步扩展Jimmer的查询能力,使我们能够灵活地处理复杂的实体关系和数据加载需求。

4.3 关联查询

在实际业务开发中,实体之间的关联关系是数据建模的核心。无论是一对一、一对多还是多对多关系,我们都需要高效地查询和处理这些关联数据。传统ORM框架在处理关联查询时往往会遇到N+1查询问题、连接爆炸、性能下降等挑战。Jimmer通过其强大的关联查询机制,不仅解决了这些问题,还提供了灵活而高效的API来处理各种复杂的关联关系查询场景。

本节将深入探讨Jimmer的关联查询功能,包括基本关联查询、自动连接机制、动态Fetcher以及自定义连接条件等高级特性。通过实际案例,我们将展示Jimmer如何优雅地处理各种复杂的关联查询场景。

4.3.1 基本关联查询

在Jimmer中,关联查询是通过实体之间预先定义的关系自动处理的。以我们的电商示例为背景,一个Product(产品)属于一个Category(分类),同时一个产品可以有多个Tag(标签)。

实体关系定义回顾

首先让我们回顾一下实体之间的关系定义:

@Entity
public interface Product {
    @Id
    String id();

    String name();

    BigDecimal price();

    int stock();

    @ManyToOne
    Category category();

    @IdView("category")
    String categoryId();

    @ManyToMany
    List<Tag> tags();

    // 其他属性...
}

@Entity
public interface Category {
    @Id
    String id();

    String name();

    @OneToMany(mappedBy = "category")
    List<Product> products();

    // 其他属性...
}

@Entity
public interface Tag {
    @Id
    String id();

    String name();

    @ManyToMany(mappedBy = "tags")
    List<Product> products();

    // 其他属性...
}

简单的关联查询

在Jimmer中,我们可以通过导航属性轻松地进行关联查询。以下是一个简单的例子,查询所有属于"电子产品"分类的产品:

ProductTable table = ProductTable.$;
List<Product> products = sqlClient.createQuery(table)
    .where(table.category().name().eq("电子产品"))
    .select(table)
    .execute();

上面的代码非常直观:我们通过table.category().name()导航到关联的Category实体的name属性,然后对其应用条件。Jimmer会自动处理底层的SQL连接操作。

让我们来看一下实际的测试案例:

@Test
@DisplayName("测试基本关联查询")
void testBasicAssociationQuery() {
    ProductTable table = ProductTable.$;
    List<Product> products = sqlClient.createQuery(table)
        .where(table.category().name().eq("电子产品"))
        .where(table.id().like(testPrefix + "-%"))
        .select(table)
        .execute();

    // 验证结果
    assertThat(products).hasSize(2);
    assertThat(products).allMatch(p -> 
        p.name().contains("iPhone") || p.name().contains("MacBook")
    );
}

这段测试代码验证了我们可以通过关联查询正确获取所有属于"电子产品"分类的产品,即iPhone和MacBook。

4.3.2 Jimmer的自动连接机制

Jimmer的一个强大特性是其智能的自动连接机制。当我们编写查询时,无需显式指定JOIN语句,Jimmer会根据实体之间的关联关系自动添加必要的连接,并且还能够优化这些连接,避免不必要的表连接操作。

** 自动连接示例 **

考虑以下查询,我们想要找到价格大于1000元的电子产品:

ProductTable table = ProductTable.$;
List<Product> products = sqlClient.createQuery(table)
    .where(table.category().name().like("%产品"))
    .where(table.price().gt(new BigDecimal("1000")))
    .orderBy(table.category().name())
    .select(table)
    .execute();

在这个查询中,我们同时使用了产品的价格条件和分类名称条件,还按分类名称排序。Jimmer会自动添加必要的连接来满足这些条件和排序要求。

连接类型选择

Jimmer默认使用LEFT JOIN进行关联查询,以处理可能的空值情况。但在特定场景下,我们也可以显式指定连接类型:

@Test
@DisplayName("测试不同连接类型")
void testJoinTypes() {
    ProductTable product = ProductTable.$;
    CategoryTable category = CategoryTable.$;

    // 使用LEFT JOIN
    List<Product> productsWithLeftJoin = sqlClient.createQuery(product)
        .where(product.price().gt(new BigDecimal("1000")))
        .where(product.id().like(testPrefix + "-%"))
        .where(product.category(JoinType.LEFT).name().isNotNull())
        .select(product)
        .execute();

    assertThat(productsWithLeftJoin).hasSize(2);

    // 使用INNER JOIN
    List<Product> productsWithInnerJoin = sqlClient.createQuery(product)
        .where(product.price().gt(new BigDecimal("1000")))
        .where(product.id().like(testPrefix + "-%"))
        .where(product.category(JoinType.INNER).name().isNotNull())
        .select(product)
        .execute();

    assertThat(productsWithInnerJoin).hasSize(2);
    assertThat(productsWithInnerJoin).hasSameElementsAs(productsWithLeftJoin);
}

在这个例子中,由于所有产品都有对应的分类,两种连接方式的结果是一致的。但在数据可能存在空值的场景下,选择适当的连接类型非常重要。

4.3.3 使用Fetcher控制数据图形状

在处理复杂的关联关系时,我们经常需要控制查询结果中包含哪些关联数据。Jimmer的Fetcher是一个强大的工具,它允许我们精确地定义要获取的数据图的形状。

基本Fetcher使用

以下是一个使用Fetcher的例子,它不仅获取产品的基本信息,还包括关联的分类和标签数据:

// 构建Fetcher定义查询返回的数据结构
Fetcher<Product> fetcher = ProductFetcher.$
    .allScalarFields()
    .category(CategoryFetcher.$
        .allScalarFields()
    )
    .tags(TagFetcher.$
        .allScalarFields()
    );

// 使用Fetcher执行查询
List<Product> products = sqlClient.createQuery(ProductTable.$)
    .where(ProductTable.$.name().eq("iPhone 13"))
    .select(ProductTable.$.fetch(fetcher))
    .execute();

这个查询会返回名为"iPhone 13"的产品,同时包含其完整的分类信息和所有标签信息。

递归Fetcher

Fetcher的强大之处在于它可以定义任意深度和复杂度的数据图。下面是一个递归Fetcher的例子,从分类出发,获取其所有产品以及这些产品的标签:

@Test
@DisplayName("测试递归Fetcher")
void testRecursiveFetcher() {
    CategoryTable table = CategoryTable.$;

    // 构建递归Fetcher
    Fetcher<Category> fetcher = CategoryFetcher.$
        .allScalarFields()
        .products(ProductFetcher.$
            .allScalarFields()
            .tags(TagFetcher.$
                .allScalarFields()
            )
        );

    // 使用递归Fetcher查询
    List<Category> categories = sqlClient.createQuery(table)
        .where(table.name().eq("电子产品"))
        .select(table.fetch(fetcher))
        .execute();

    // 验证结果
    assertThat(categories).hasSize(1);
    Category category = categories.get(0);
    assertThat(category.products()).hasSize(2);

    // 验证每个产品的标签
    for (Product product : category.products()) {
        if (product.name().contains("iPhone")) {
            assertThat(product.tags()).hasSize(2);
            assertThat(product.tags().stream().map(Tag::name))
                .containsExactlyInAnyOrder("热销", "新品");
        } else if (product.name().contains("MacBook")) {
            assertThat(product.tags()).hasSize(1);
            assertThat(product.tags().stream().map(Tag::name))
                .containsExactly("热销");
        }
    }
}

这个例子展示了如何从分类出发,获取该分类下的所有产品以及每个产品的标签,构建了一个完整的数据图。

4.3.4 动态Fetcher与加载策略

在实际开发中,不同场景可能需要不同的数据加载策略。Jimmer允许我们根据实际需求动态构建Fetcher,实现精确的数据加载控制。

不同加载策略的对比

以下是三种不同加载策略的示例:

@Test
@DisplayName("测试不同加载策略的Fetcher")
void testDynamicFetcher() {
    ProductTable table = ProductTable.$;

    // 场景1: 加载所有关联数据
    Fetcher<Product> completeFetcher = ProductFetcher.$
        .allScalarFields()
        .category(CategoryFetcher.$
            .allScalarFields()
        )
        .tags(TagFetcher.$
            .allScalarFields()
        );

    // 场景2: 只加载分类,不加载标签
    Fetcher<Product> categoryOnlyFetcher = ProductFetcher.$
        .allScalarFields()
        .category(CategoryFetcher.$
            .allScalarFields()
        );

    // 场景3: 只加载标签,不加载分类
    Fetcher<Product> tagsOnlyFetcher = ProductFetcher.$
        .allScalarFields()
        .tags(TagFetcher.$
            .allScalarFields()
        );

    // 执行查询并验证结果
    List<Product> completeProducts = sqlClient.createQuery(table)
        .select(table.fetch(completeFetcher))
        .execute();

    List<Product> productsWithCategoryOnly = sqlClient.createQuery(table)
        .select(table.fetch(categoryOnlyFetcher))
        .execute();

    List<Product> productsWithTagsOnly = sqlClient.createQuery(table)
        .select(table.fetch(tagsOnlyFetcher))
        .execute();

    // 验证不同加载策略的结果
    assertThat(completeProducts).isNotEmpty();
    assertThat(productsWithCategoryOnly).isNotEmpty();
    assertThat(productsWithTagsOnly).isNotEmpty();
}

通过动态构建不同的Fetcher,我们可以精确控制查询返回的数据结构,避免加载不必要的数据,提高查询效率。

4.3.5 N+1问题的自动规避

N+1查询问题是ORM框架中常见的性能挑战。当查询一个集合实体及其关联实体时,可能会先执行一次查询获取主实体集合,然后针对每个主实体执行额外的查询获取关联实体,导致总共执行N+1次查询。

Jimmer的批量加载机制

Jimmer通过智能的批量加载机制自动规避N+1问题。当我们使用Fetcher查询关联数据时,Jimmer会自动优化为批量查询:

@Test
@DisplayName("测试N+1问题的自动规避")
void testNPlusOnePrevention() {
    ProductTable table = ProductTable.$;

    // 构建包含关联的Fetcher
    Fetcher<Product> fetcher = ProductFetcher.$
        .allScalarFields()
        .category(CategoryFetcher.$
            .allScalarFields()
        )
        .tags(TagFetcher.$
            .allScalarFields()
        );

    // 记录查询执行时间
    QueryResult<List<Product>> queryResult = measureQueryTime(() ->
        sqlClient.createQuery(table)
            .select(table.fetch(fetcher))
            .execute()
    );

    List<Product> products = queryResult.getResult();

    // 验证结果:所有关联数据都在有限次查询中加载完成
    assertThat(products).isNotEmpty();
    assertThat(products).allSatisfy(p -> {
        assertThat(p.category()).isNotNull();
        if (p.name().contains("iPhone") || p.name().contains("MacBook")) {
            assertThat(p.tags()).isNotEmpty();
        }
    });
}

在这个测试中,尽管我们查询了产品及其关联的分类和标签,但Jimmer会将其优化为有限次查询,避免N+1问题。实际生成的SQL通常是2~3条,而不是传统ORM中可能的N+1条。

4.3.6 关联查询最佳实践

在实际应用Jimmer的关联查询时,以下是一些建议的最佳实践:

1. 合理设计Fetcher

  • 只获取必要的关联数据,避免过度加载
  • 针对不同场景设计不同的Fetcher
  • 考虑将常用Fetcher定义为静态常量以便复用
// 定义常用的静态Fetcher
public static final Fetcher<Product> PRODUCT_WITH_BASIC_ASSOCIATIONS =
    ProductFetcher.$
        .allScalarFields()
        .category(CategoryFetcher.$.name())
        .tags(TagFetcher.$.name());

2. 利用子查询优化复杂条件

  • 对于复杂的关联过滤条件,考虑使用子查询
  • 子查询通常比复杂JOIN更高效,特别是在多对多关系中

3. 避免不必要的表连接

  • 使用@IdView属性可以直接访问外键而不触发连接
  • 合理使用Fetcher控制连接的生成

4. 监控和优化SQL

  • 在开发阶段启用SQL日志,监控生成的SQL
  • 对于复杂查询,分析执行计划并进行必要的优化
  • 考虑为频繁查询的场景添加适当的索引
// application.properties中启用SQL日志
spring.jpa.show-sql=true
jimmer.client.log-sql=true
jimmer.client.log-sql-pretty=true

4.3.7 小结

在本节中,我们深入探讨了Jimmer的关联查询功能。从基本的关联查询到复杂的自定义连接条件,Jimmer提供了强大而灵活的API来处理各种关联查询场景。特别是以下几点特性让Jimmer在处理关联查询时表现出色:

  1. 自动连接机制:无需手动编写JOIN语句,减少了代码量和出错可能
  2. 强大的Fetcher:精确控制数据图的形状,避免过度加载或数据不足
  3. N+1问题自动规避:智能的批量加载机制避免了传统ORM的性能瓶颈
  4. 灵活的子查询支持:处理复杂过滤条件的强大工具

通过这些特性,Jimmer让复杂的关联查询变得简单而高效,帮助开发者构建性能优异的数据访问层。

在下一节中,我们将探讨Jimmer的动态查询功能,了解如何基于运行时条件构建灵活的查询。

4.4 排序、分页与分组

在现代数据驱动的应用程序中,有效地组织和展示数据是用户体验的关键。无论是电子商务平台展示产品列表,还是管理系统呈现分析报表,都需要对数据进行排序、分页和分组等处理。本节将深入讲解Jimmer如何通过类型安全的API优雅地实现这些功能。

4.4.1 数据排序

排序是查询操作中最基本的需求之一。用户希望按照特定的字段和顺序来查看数据,例如按价格从低到高排列商品,或者按发布日期从新到旧排列文章。Jimmer提供了强大而灵活的排序API,使开发者能够轻松实现各种排序需求。

  • 单字段排序

最简单的排序情形是按照单一字段进行排序。在Jimmer中,这可以通过orderBy()方法结合字段表达式来实现:

ProductTable product = ProductTable.$;

// 按价格升序排序
List<Product> productsByPriceAsc = sqlClient.createQuery(product)
    .where(product.id().like(testPrefix + "-%"))
    .orderBy(product.price().asc())  // 使用asc()方法指定升序
    .select(product)
    .execute();

这段代码会生成如下SQL:

select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.PRICE,
    tb_1_.STOCK,
    tb_1_.ACTIVE,
    tb_1_.DESCRIPTION,
    tb_1_.CATEGORY_ID,
    tb_1_.CREATED_TIME,
    tb_1_.MODIFIED_TIME
from PRODUCT tb_1_
where
    tb_1_.ID like ?  /* 'prefix-%' */
order by
    tb_1_.PRICE asc

在这个例子中,我们使用asc()方法指定升序排序。如果需要降序排序,可以使用desc()方法:

// 按价格降序排序
List<Product> productsByPriceDesc = sqlClient.createQuery(product)
    .orderBy(product.price().desc())  // 使用desc()方法指定降序
    .select(product)
    .execute();
  • 多字段排序

在实际应用中,我们经常需要按多个字段排序。例如,在电商平台中,我们可能需要先按产品类别排序,再按价格排序,以便用户更容易找到所需的产品。

在Jimmer中,可以在orderBy()方法中指定多个排序字段:

// 先按分类ID升序,再按价格降序排序
List<Product> products = sqlClient.createQuery(product)
    .where(product.id().like(testPrefix + "-%"))
    .orderBy(
        product.categoryId().asc(),  // 第一排序字段:类别ID升序
        product.price().desc()       // 第二排序字段:价格降序
    )
    .select(product)
    .execute();

这段代码会生成如下SQL:

select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.PRICE,
    tb_1_.STOCK,
    tb_1_.ACTIVE,
    tb_1_.DESCRIPTION,
    tb_1_.CATEGORY_ID,
    tb_1_.CREATED_TIME,
    tb_1_.MODIFIED_TIME
from PRODUCT tb_1_
where
    tb_1_.ID like ?  /* 'prefix-%' */
order by
    tb_1_.CATEGORY_ID asc,
    tb_1_.PRICE desc

多字段排序的执行顺序是从左到右的,即先按第一个字段排序,当第一个字段值相同时,再按第二个字段排序,依此类推。

  • 动态排序

在实际开发中,排序条件往往是动态的,取决于用户的选择。Jimmer支持根据运行时条件构建排序语句:

List<Order> orders = new ArrayList<>();

if (sortByPrice) {
    orders.add(product.price().asc());
}

if (sortByName) {
    orders.add(product.name().asc());
}

// 应用动态排序条件
List<Product> dynamicSortedProducts = sqlClient.createQuery(product)
    .orderBy(orders)
    .select(product)
    .execute();

这种方式使得我们可以根据用户选择的排序条件动态构建查询,而无需编写大量的条件判断代码。

4.4.2 高效分页查询

在处理大量数据时,分页查询是提高性能和用户体验的重要技术。Jimmer提供了智能分页功能,能够自动生成分页所需的COUNT查询和数据查询。

  • 基本分页查询

在Jimmer中,使用fetchPage()方法可以轻松实现分页查询:

// 按价格升序排序,获取第1页,每页2条数据
Page<Product> page = sqlClient.createQuery(product)
    .where(product.id().like(testPrefix + "-%"))
    .orderBy(product.price().asc())
    .select(product)
    .fetchPage(0, 2);  // pageIndex=0(第一页), pageSize=2

注意fetchPage()方法的参数: - 第一个参数是页索引,从0开始计算(0表示第一页) - 第二个参数是每页记录数

执行上述代码时,Jimmer会自动生成两条SQL:

-- 计算总记录数的SQL
select count(tb_1_.ID)
from PRODUCT tb_1_
where tb_1_.ID like ?  /* 'prefix-%' */

-- 获取当前页数据的SQL
select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.PRICE,
    tb_1_.STOCK,
    tb_1_.ACTIVE,
    tb_1_.DESCRIPTION,
    tb_1_.CATEGORY_ID,
    tb_1_.CREATED_TIME,
    tb_1_.MODIFIED_TIME
from PRODUCT tb_1_
where
    tb_1_.ID like ?  /* 'prefix-%' */
order by
    tb_1_.PRICE asc
limit ? offset ?  /* limit 2 offset 0 */

fetchPage()方法返回一个Page<T>对象,包含以下核心信息: - 当前页的数据列表(getRows()) - 总记录数(getTotalRowCount()) - 总页数(getTotalPageCount()) - 当前页码 - 每页记录数

这些信息足以构建分页界面所需的所有元素。

  • 保持分页一致性的策略

在高并发环境下,分页数据可能在用户翻页过程中发生变化,导致某些记录被重复显示或被跳过。Jimmer提供了几种策略来处理这个问题:

  1. 基于计数器的传统分页(默认):即上面演示的方式,适用于数据变化不频繁的场景

  2. 基于键集的分页:适用于数据频繁变化的场景,通过记住上一页最后一条记录的关键值来确保分页一致性

// 基于键集的分页
KeyedPage<Product, BigDecimal> keyedPage = sqlClient.createQuery(product)
    .orderBy(product.price().asc())
    .select(product)
    .fetchKeyedPage(
        0,                // 页索引
        10,               // 页大小
        product.price()   // 分页键
    );

这种方式生成的SQL不使用OFFSET,而是使用键值比较,性能更好且数据一致性更强:

select
    ...
from PRODUCT tb_1_
where
    tb_1_.PRICE > ?  /* 上一页最后一条记录的价格 */
order by
    tb_1_.PRICE asc
limit ?  /* 10 */
  • 大数据量下的分页优化

处理大数据量分页时,COUNT查询可能变得很慢。Jimmer为此提供了多种优化技术:

  1. COUNT查询优化:Jimmer会自动分析查询,移除不必要的JOIN,提高COUNT查询性能
// 含关联表的查询
Page<Product> page = sqlClient.createQuery(product)
    .where(product.price().gt(new BigDecimal("1000")))
    // 下面的条件会导致JOIN,但COUNT查询会自动优化掉不必要的JOIN
    .where(product.category().name().eq("电子产品"))
    .select(product)
    .fetchPage(0, 10);
  1. 预估总数:对于超大数据量,可以使用估算总数的方式提高性能
// 使用估算总数的分页
Page<Product> page = sqlClient.createQuery(product)
    .select(product)
    // 设置预估总数为10000,避免执行COUNT查询
    .fetchPage(0, 20, ctx -> 10000);
  1. 懒加载总数:只有在用户真正需要知道总页数时才计算
// 使用懒加载总数的分页
LimitOffsetPage<Product> page = sqlClient.createQuery(product)
    .select(product)
    .fetchLimitOffsetPage(20, 0);

4.4.3 分组与聚合函数

数据分析是现代应用的重要功能,分组和聚合操作允许我们汇总和分析数据,从而获取有价值的业务洞察。Jimmer提供了强大的分组和聚合功能,使得复杂的数据分析变得简单。

  • 基本聚合函数

Jimmer支持标准的SQL聚合函数,包括COUNTSUMAVGMINMAX

// 计算所有产品的平均价格
BigDecimal avgPrice = sqlClient.createQuery(product)
    .select(product.price().avg())
    .fetchOne();

// 计算总库存
Integer totalStock = sqlClient.createQuery(product)
    .select(product.stock().sum())
    .fetchOne();

这些聚合函数可以与其他查询条件结合使用,例如:

// 计算电子产品类别的平均价格
BigDecimal electronicsAvgPrice = sqlClient.createQuery(product)
    .where(product.category().name().eq("电子产品"))
    .select(product.price().avg())
    .fetchOne();
  • GROUP BY的使用

使用Jimmer的groupBy()方法,我们可以按特定字段对数据进行分组:

// 按分类统计商品数量和库存总量
var summaries = sqlClient.createQuery(product)
    .where(product.id().like(testPrefix + "-%"))
    .groupBy(product.categoryId())
    .select(
        product.categoryId(),   // 分组字段
        product.count(),        // 统计记录数
        product.stock().sum()   // 累计库存
    )
    .execute();

上面的代码会生成如下SQL:

select
    tb_1_.CATEGORY_ID,
    count(tb_1_.ID),
    sum(tb_1_.STOCK)
from PRODUCT tb_1_
where
    tb_1_.ID like ?  /* 'prefix-%' */
group by
    tb_1_.CATEGORY_ID

执行结果包含每个类别的ID、产品数量和总库存,可以用于生成统计报表。

  • HAVING子句

当需要对分组结果进行进一步筛选时,可以使用having()方法:

// 查找平均价格超过1000的产品类别
var expensiveCategories = sqlClient.createQuery(product)
    .groupBy(product.categoryId())
    .having(product.price().avg().gt(new BigDecimal("1000")))
    .select(
        product.categoryId(),
        product.price().avg()
    )
    .execute();

生成的SQL包含HAVING子句:

select
    tb_1_.CATEGORY_ID,
    avg(tb_1_.PRICE)
from PRODUCT tb_1_
group by
    tb_1_.CATEGORY_ID
having
    avg(tb_1_.PRICE) > ?  /* 1000 */
  • 聚合结果的映射

Jimmer可以将聚合查询的结果映射到自定义对象中,方便后续处理:

// 定义映射类
public static class CategoryStatistics {
    private String categoryId;
    private long productCount;
    private int totalStock;

    // getter和setter方法
}

// 执行查询并映射结果
List<CategoryStatistics> statistics = sqlClient.createQuery(product)
    .groupBy(product.categoryId())
    .select(
        product.categoryId(),
        product.count(),
        product.stock().sum()
    )
    .execute((row, rowMetadata) -> {
        CategoryStatistics stats = new CategoryStatistics();
        stats.setCategoryId(row.get(product.categoryId()));
        stats.setProductCount(row.get(product.count()));
        stats.setTotalStock(row.get(product.stock().sum()));
        return stats;
    });

这种方式使得聚合数据更容易在业务代码中使用,同时保持了查询的类型安全性。

4.4.4 窗口函数的应用

窗口函数是SQL的高级特性,允许在不改变结果集行数的情况下执行聚合计算。Jimmer支持标准的SQL窗口函数,为复杂的数据分析提供了强大工具。

  • ROW_NUMBER的使用

ROW_NUMBER()是最常用的窗口函数之一,它为结果集中的每一行分配一个唯一的序号:

// 为每个类别的产品按价格分配排名
var productsWithRank = sqlClient.createQuery(product)
    .select(
        product,
        row -> row.getWindow()
            .partitionBy(product.categoryId())
            .orderBy(product.price().desc())
            .rowNumber()
            .as("rank")
    )
    .execute();

生成的SQL如下:

select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.PRICE,
    tb_1_.STOCK,
    tb_1_.CATEGORY_ID,
    row_number() over(
        partition by tb_1_.CATEGORY_ID
        order by tb_1_.PRICE desc
    ) as "rank"
from PRODUCT tb_1_
  • 分析函数

Jimmer还支持其他窗口函数,如RANK()DENSE_RANK()LEAD()LAG()等:

// 获取产品价格及其与类别平均价格的差异
var priceAnalysis = sqlClient.createQuery(product)
    .select(
        product.id(),
        product.name(),
        product.price(),
        row -> row.getWindow()
            .partitionBy(product.categoryId())
            .avg(product.price())
            .as("categoryAvgPrice")
    )
    .execute();

这个查询会返回每个产品的ID、名称和价格,以及该产品所属类别的平均价格,便于进行价格分析。

  • 小结与展望

本节我们详细探讨了Jimmer中的排序、分页和分组功能。这些功能构成了数据查询的重要组成部分,能够满足现代应用程序中复杂的数据展示和分析需求。Jimmer通过类型安全的API,使这些操作变得简单而直观,同时通过智能优化保证了查询的高效执行。

排序功能支持单字段和多字段排序,以及动态排序条件的构建,满足各种排序需求。分页查询功能提供了传统分页和键集分页两种模式,并通过多种优化策略解决了大数据量分页的性能挑战。分组与聚合功能则为数据分析提供了强大支持,窗口函数的应用更是拓展了复杂数据处理的可能性。

在下一节中,我们将探讨Jimmer的动态查询构建功能,这是处理复杂用户界面查询需求的强大工具。通过动态查询构建,我们可以根据用户输入动态生成查询条件,实现灵活而强大的搜索功能。

4.5 动态查询构建

在现代应用开发中,动态查询是一个不可或缺的功能。无论是复杂的搜索页面、筛选条件组合,还是根据用户权限动态调整查询范围,我们都需要能够根据运行时的条件灵活构建查询。然而,传统方案常常陷入困境:使用字符串拼接SQL不仅容易导致SQL注入风险,还缺乏类型安全保障;而硬编码的查询条件组合则会导致代码膨胀和维护困难。

Jimmer提供了一套优雅而强大的动态查询解决方案,它既保留了类型安全的特性,又提供了足够的灵活性。本节将深入探讨Jimmer的动态查询机制,并通过实际案例演示如何应对各种动态查询场景。

4.5.1 动态查询的常见场景

在深入技术细节之前,让我们先了解一下动态查询在实际业务中的常见应用场景:

  1. 搜索表单:用户可以输入多个可选的搜索条件,系统需要根据已填写的条件动态构建查询
  2. 高级筛选:允许用户通过各种筛选条件组合,如价格区间、评分范围、标签组合等进行精细化查询
  3. 权限过滤:根据用户权限动态添加数据访问限制条件
  4. 动态排序:根据用户选择的字段和排序方向动态调整结果排序
  5. 查询条件持久化:将查询条件保存下来,供用户稍后重新执行

以电商系统为例,一个典型的商品搜索页面可能需要支持以下查询条件:

  • 价格区间
  • 商品分类
  • 商品标签
  • 库存状态
  • 排序方式
  • 关键词搜索

用户可能只填写部分条件,系统需要智能地处理这些可选条件,构建出有效的查询。这正是动态查询的典型应用场景。

4.5.2 条件检查与安全构建模式

在传统应用中,处理可选查询条件的方式往往是通过条件判断和SQL拼接:

// 传统方式处理动态查询 - 不推荐
StringBuilder sql = new StringBuilder("SELECT * FROM product WHERE active = true");

if (minPrice != null) {
    sql.append(" AND price >= ?");
}
if (maxPrice != null) {
    sql.append(" AND price <= ?");
}
if (categoryId != null) {
    sql.append(" AND category_id = ?");
}
// ...设置参数...

这种方式不仅容易出错,还难以维护。Jimmer提供了更优雅的解决方案,让我们看看如何在Jimmer中构建动态查询。

  • whereIf方法

Jimmer的whereIf方法是处理动态条件的核心。它接收两个参数:一个布尔表达式和一个条件构建器。只有当布尔表达式为true时,条件构建器才会被执行并添加到查询中:

ProductTable product = ProductTable.$;

return sqlClient.createQuery(product)
    // 只有当minPrice不为null时,才添加此条件
    .whereIf(minPrice != null, () -> 
        product.price().ge(minPrice)
    )
    // 只有当maxPrice不为null时,才添加此条件
    .whereIf(maxPrice != null, () -> 
        product.price().le(maxPrice)
    )
    // 无条件添加的查询条件
    .where(product.active().eq(true))
    .select(product)
    .execute();

这种方式有几个明显的优势:

  1. 类型安全:所有条件都是通过类型安全的API构建,而非字符串拼接
  2. 懒执行:条件表达式仅在需要时执行,避免了不必要的对象创建
  3. 链式调用:保持了流畅的API风格,使代码易于阅读和维护
  4. 自动参数化:Jimmer自动处理参数绑定,防止SQL注入风险

4.5.3 查询构建器的链式调用

为了更好地组织动态查询代码,通常我们会创建专门的查询构建器或过滤器类。下面我们通过一个商品查询过滤器的例子来展示Jimmer动态查询的实际应用:

// 查询条件封装类
private static class ProductFilter {
    final BigDecimal minPrice;
    final BigDecimal maxPrice;
    final Integer minStock;
    final String categoryId;
    final List<String> tagIds;
    final String idPrefix;

    ProductFilter(BigDecimal minPrice, BigDecimal maxPrice, Integer minStock, 
                String categoryId, List<String> tagIds, String idPrefix) {
        this.minPrice = minPrice;
        this.maxPrice = maxPrice;
        this.minStock = minStock;
        this.categoryId = categoryId;
        this.tagIds = tagIds;
        this.idPrefix = idPrefix;
    }
}

有了这个过滤器类,我们就可以实现一个通用的查询执行方法:

private List<Product> executeProductQuery(ProductFilter filter) {
    ProductTable product = ProductTable.$;

    return sqlClient.createQuery(product)
        // 确保只查询指定前缀的数据
        .where(product.id().like(filter.idPrefix + "%"))
        // 价格区间条件
        .whereIf(filter.minPrice != null, () -> 
            product.price().ge(filter.minPrice)
        )
        .whereIf(filter.maxPrice != null, () -> 
            product.price().le(filter.maxPrice)
        )
        // 库存条件
        .whereIf(filter.minStock != null, () -> 
            product.stock().ge(filter.minStock)
        )
        // 分类条件
        .whereIf(filter.categoryId != null, () -> 
            product.category().id().eq(filter.categoryId)
        )
        // 标签条件 - 使用隐式子查询
        .whereIf(filter.tagIds != null && !filter.tagIds.isEmpty(), () -> 
            product.tags(tag -> tag.id().in(filter.tagIds))
        )
        // 只查询激活的商品
        .where(product.active().eq(true))
        // 排序
        .orderBy(product.price())
        .select(product)
        .execute();
}

这种方式将查询逻辑集中在一个地方,使得代码更加清晰和易于维护。业务层只需要构建适当的ProductFilter对象,而不需要关心具体的查询构建细节。

4.5.4 查询谓词的动态组合

在复杂的查询场景中,我们可能需要动态组合多个查询谓词(条件),例如根据用户选择的搜索模式调整查询逻辑。Jimmer允许我们灵活地构建和组合查询谓词:

@Test
@DisplayName("测试复杂的动态查询")
void testComplexDynamicQuery() {
    // 获取类别和标签ID
    String phoneCategory = testPrefix + "-category-手机";
    String newTag = testPrefix + "-tag-新品";
    String hotTag = testPrefix + "-tag-热卖";

    // 定义复杂查询条件
    ProductFilter filter = new ProductFilter(
        new BigDecimal("5000"),      // minPrice
        new BigDecimal("10000"),     // maxPrice
        null,                        // minStock
        phoneCategory,               // categoryId
        Arrays.asList(newTag, hotTag), // tagIds
        testPrefix                   // idPrefix
    );

    // 执行动态查询
    List<Product> products = executeProductQuery(filter);

    // 验证结果
    assertThat(products).hasSize(2)
        .extracting(Product::name)
        .containsExactlyInAnyOrder("iPhone 15", "iPhone 15 Pro");
}

在上面的例子中,我们组合了多个条件:价格区间、商品分类和标签。Jimmer会自动将这些条件以AND逻辑连接起来。

如果需要更复杂的逻辑组合,比如OR条件或嵌套条件,Jimmer也提供了相应的支持:

// OR条件示例
.where(
    Predicate.or(
        product.name().like("iPhone%"),
        product.name().like("iPad%")
    )
)

// 嵌套条件示例
.where(
    Predicate.and(
        product.price().gt(new BigDecimal("1000")),
        Predicate.or(
            product.stock().gt(10),
            product.preOrderCount().gt(0)
        )
    )
)

这种灵活性使Jimmer能够应对各种复杂的查询场景。

4.5.5 动态排序与字段映射

除了查询条件,排序也是常见的动态需求。Jimmer同样提供了类型安全的动态排序支持:

// 根据用户选择的排序字段和方向动态排序
public List<Product> findProductsWithSort(String sortField, boolean ascending) {
    ProductTable product = ProductTable.$;

    // 构建基本查询
    ConfigurableTypedRootQuery<Product> query = sqlClient
        .createQuery(product)
        .where(product.active().eq(true));

    // 动态添加排序
    if (sortField != null) {
        switch (sortField) {
            case "price":
                query.orderBy(product.price(), ascending ? OrderMode.ASC : OrderMode.DESC);
                break;
            case "name":
                query.orderBy(product.name(), ascending ? OrderMode.ASC : OrderMode.DESC);
                break;
            case "stock":
                query.orderBy(product.stock(), ascending ? OrderMode.ASC : OrderMode.DESC);
                break;
            default:
                // 默认按ID排序
                query.orderBy(product.id());
                break;
        }
    }

    return query.select(product).execute();
}

这种方式可以很容易地支持用户自定义排序,同时保持类型安全。

4.5.6 前端参数到查询条件的安全映射

在实际应用中,动态查询条件通常来自前端用户输入。安全地将这些输入映射到查询条件是一个重要的考量。以下是一个处理前端搜索参数的完整例子:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final JSqlClient sqlClient;

    @Autowired
    public ProductController(JSqlClient sqlClient) {
        this.sqlClient = sqlClient;
    }

    @GetMapping("/search")
    public List<ProductDTO> searchProducts(
            @RequestParam(required = false) BigDecimal minPrice,
            @RequestParam(required = false) BigDecimal maxPrice,
            @RequestParam(required = false) Integer minStock,
            @RequestParam(required = false) String categoryId,
            @RequestParam(required = false) List<String> tagIds,
            @RequestParam(defaultValue = "price") String sortBy,
            @RequestParam(defaultValue = "true") boolean ascending
    ) {
        ProductTable product = ProductTable.$;

        // 构建查询
        ConfigurableTypedRootQuery<Product> query = sqlClient
            .createQuery(product)
            .whereIf(minPrice != null, () -> product.price().ge(minPrice))
            .whereIf(maxPrice != null, () -> product.price().le(maxPrice))
            .whereIf(minStock != null, () -> product.stock().ge(minStock))
            .whereIf(categoryId != null, () -> product.category().id().eq(categoryId))
            .whereIf(tagIds != null && !tagIds.isEmpty(), () -> 
                product.tags(tag -> tag.id().in(tagIds))
            )
            .where(product.active().eq(true));

        // 添加排序
        switch (sortBy) {
            case "price":
                query.orderBy(product.price(), ascending ? OrderMode.ASC : OrderMode.DESC);
                break;
            case "name":
                query.orderBy(product.name(), ascending ? OrderMode.ASC : OrderMode.DESC);
                break;
            case "stock":
                query.orderBy(product.stock(), ascending ? OrderMode.ASC : OrderMode.DESC);
                break;
            default:
                query.orderBy(product.price());
                break;
        }

        // 定义返回字段 - 使用Fetcher控制
        Fetcher<Product> fetcher = ProductFetcher.$
            .allScalarFields()
            .category(CategoryFetcher.$.name())
            .tags(TagFetcher.$.allScalarFields());

        // 执行查询并转换为DTO
        return query
            .select(product.fetch(fetcher))
            .execute()
            .stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList());
    }

    private ProductDTO convertToDTO(Product product) {
        // 转换逻辑...
        return new ProductDTO(/* ... */);
    }
}

这种方式提供了一个完整的从前端参数到数据库查询的映射流程,保持了类型安全和代码的可维护性。

  • 测试与验证

为了验证我们的动态查询功能,我们可以编写详细的测试用例。以下是两个测试场景:

  1. 基本动态查询,使用价格区间和库存条件
  2. 复杂动态查询,组合价格区间、分类和标签条件
@Test
@DisplayName("测试基本的动态查询")
void testBasicDynamicQuery() {
    // 定义查询条件
    ProductFilter filter = new ProductFilter(
        new BigDecimal("7000"),  // minPrice
        new BigDecimal("9000"),  // maxPrice
        40,                      // minStock
        null,                   // categoryId
        null,                    // tagIds
        testPrefix               // idPrefix
    );

    // 执行动态查询
    List<Product> products = executeProductQuery(filter);

    // 验证结果
    assertThat(products).hasSize(2)
        .extracting(Product::name)
        .containsExactlyInAnyOrder("iPhone 15 Pro", "MacBook Air");
}

@Test
@DisplayName("测试复杂的动态查询")
void testComplexDynamicQuery() {
    // 获取类别和标签ID
    String phoneCategory = testPrefix + "-category-手机";
    String newTag = testPrefix + "-tag-新品";
    String hotTag = testPrefix + "-tag-热卖";

    // 定义复杂查询条件
    ProductFilter filter = new ProductFilter(
        new BigDecimal("5000"),      // minPrice
        new BigDecimal("10000"),     // maxPrice
        null,                        // minStock
        phoneCategory,               // categoryId
        Arrays.asList(newTag, hotTag), // tagIds
        testPrefix                   // idPrefix
    );

    // 执行动态查询
    List<Product> products = executeProductQuery(filter);

    // 验证结果
    assertThat(products).hasSize(2)
        .extracting(Product::name)
        .containsExactlyInAnyOrder("iPhone 15", "iPhone 15 Pro");
}

这些测试用例确保我们的动态查询机制能够正确处理不同的查询场景。每个测试都有明确的期望结果,可以验证查询逻辑的正确性。

  • 性能考量

动态查询虽然提供了极大的灵活性,但也需要注意性能问题。以下是一些提高动态查询性能的建议:

  1. 索引优化:确保常用的查询字段有适当的索引
  2. 分页处理:对于大数据集,始终使用分页查询
  3. 避免过度Join:仅在必要时加载关联数据
  4. 查询缓存:对于频繁执行的相同查询,考虑使用查询缓存
  5. 延迟加载:对于复杂对象图,考虑使用延迟加载策略

Jimmer的查询DSL不仅提供了类型安全和灵活性,还通过SQL优化和批处理加载等机制提供了良好的性能支持。

例如,多对多关联(如产品标签)的查询在Jimmer中会自动优化为批量加载,避免N+1查询问题:

// 使用隐式子查询查询标签
.whereIf(filter.tagIds != null && !filter.tagIds.isEmpty(), () -> 
    product.tags(tag -> tag.id().in(filter.tagIds))
)

这种查询会被转换为高效的SQL,避免了直接JOIN可能导致的笛卡尔积问题。

  • 小结与展望

本节中,我们深入探讨了Jimmer的动态查询机制,通过具体的商品搜索示例展示了如何构建类型安全且灵活的查询。Jimmer的whereIf方法和流畅的API风格使得动态查询变得简单而强大,可以轻松应对各种复杂的查询场景。

动态查询不仅仅是一个技术问题,更是现代应用用户体验的关键部分。通过Jimmer提供的强大查询能力,我们可以为用户提供精确、高效的数据筛选和检索功能,提升整体应用体验。

在下一节中,我们将进一步探索Jimmer的高级SQL功能,包括子查询、复杂连接和通用表表达式等,进一步扩展Jimmer的查询能力边界。通过掌握这些高级特性,我们将能够应对更复杂的数据查询挑战,构建更强大的企业级应用。

4.6 子查询与高级SQL功能

在处理复杂业务场景时,简单的单表查询和基本关联往往难以满足需求。此时,子查询和高级SQL功能成为解决复杂查询的关键工具。Jimmer提供了全面且类型安全的子查询支持,使开发者可以构建强大的嵌套查询,同时保持代码的可读性和可维护性。

本节将深入探讨Jimmer中子查询和高级SQL功能的实现,包括WHERE子句子查询、SELECT子句子查询、EXISTS子查询以及SQL集合操作等内容。我们将通过实际业务场景展示这些功能如何优雅地解决复杂查询需求。

4.6.1 子查询的基本概念与使用场景

什么是子查询

子查询是嵌套在另一个查询中的SQL查询,它可以出现在SQL语句的多个部分,包括WHERE子句、FROM子句和SELECT子句。子查询可以返回单个值、单列多行数据或多列多行数据,分别用于不同的查询场景。

常见使用场景

在实际业务开发中,子查询的典型应用场景包括:

  1. 比较操作:将某个值与子查询返回的值进行比较
  2. 查找价格高于平均价格的商品
  3. 查找销量超过品类平均销量的商品

  4. 存在性检查:验证是否存在符合特定条件的相关数据

  5. 查找拥有特定标签的商品
  6. 查找有过评价的商品

  7. 派生表:在FROM子句中使用子查询生成临时表

  8. 对复杂聚合结果进行二次查询
  9. 多表数据预处理后再进行关联

  10. 数据转换:在SELECT子句中使用子查询转换数据

  11. 计算每个商品与平均价格的差值
  12. 显示每个商品所占总销量的百分比

传统实现方式的挑战

在使用原生SQL或其他ORM框架时,实现子查询通常面临以下挑战:

// 传统JDBC方式实现子查询
String sql = "SELECT * FROM product " +
             "WHERE price > (SELECT AVG(price) FROM product)";

// 参数绑定复杂,类型安全性差
PreparedStatement stmt = connection.prepareStatement(sql);
ResultSet rs = stmt.executeQuery();
// 处理结果集...

这种方式存在的问题包括: - 字符串拼接容易出错 - 缺乏类型安全保障 - 参数绑定复杂 - 无法利用IDE的代码补全和错误检查

Jimmer通过流畅的API和完全类型安全的方式解决了这些问题。

4.6.2 在WHERE子句中使用子查询

基本语法

Jimmer提供了简洁而强大的API来创建WHERE子句中的子查询:

ProductTable product = ProductTable.$;

List<Product> expensiveProducts = sqlClient.createQuery(product)
    .where(product.price().gt(
        // 子查询:计算平均价格
        sqlClient.createSubQuery(product)
            .select(product.price().avg())
    ))
    .select(product)
    .execute();

在这个例子中,我们使用sqlClient.createSubQuery()创建了一个子查询,计算所有商品的平均价格,然后查找价格高于这个平均值的商品。

实际案例:查询价格高于平均价格的商品

让我们通过一个实际案例来展示WHERE子句子查询的应用:

@Test
@DisplayName("测试WHERE子句中的子查询")
void testSubqueryInWhereClause() {
    ProductTable product = ProductTable.$;

    // 查询价格高于所有商品平均价格的商品
    List<Product> products = sqlClient.createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .where(product.price().gt(
            // 子查询:计算所有商品的平均价格
            sqlClient.createSubQuery(product)
                .where(product.id().like(testPrefix + "-%"))
                .select(product.price().avg())
        ))
        .select(product)
        .execute();

    // 验证结果
    assertThat(products).isNotEmpty();

    // 查询所有商品的平均价格
    BigDecimal avgPrice = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .select(product.price().avg())
        .fetchOne();

    // 验证每个返回的商品价格都高于平均价格
    for (Product p : products) {
        assertThat(p.price()).isGreaterThan(avgPrice);
    }
}

在这个测试中,我们查询了价格高于平均价格的商品,并验证结果的正确性。这种方式比原生SQL更直观,同时保持了完全的类型安全性。

与普通条件的结合

子查询可以与其他条件自由组合,例如:

// 查询价格高于平均价格且库存充足的电子类商品
List<Product> products = sqlClient.createQuery(product)
    .where(
        product.category().name().eq("电子产品"),
        product.stock().gt(10),
        product.price().gt(
            sqlClient.createSubQuery(product)
                .select(product.price().avg())
        )
    )
    .select(product)
    .execute();

子查询条件可以与其他条件一起放在同一个where调用中,Jimmer会自动将它们以AND逻辑连接。

4.6.3 在SELECT子句中使用子查询

基本语法

子查询也可以出现在SELECT子句中,用于返回派生值:

ProductTable product = ProductTable.$;

List<Tuple2<Product, BigDecimal>> productsWithDiff = sqlClient.createQuery(product)
    .select(
        product,
        product.price().minus(
            sqlClient.createSubQuery(product)
                .select(product.price().avg())
        )
    )
    .execute();

这个查询返回了每个商品及其价格与平均价格的差值。

实际案例:计算商品价格与平均价格的差值

下面是一个完整的测试案例,展示了在SELECT子句中使用子查询:

@Test
@DisplayName("测试SELECT子句中的子查询")
void testSubqueryInSelectClause() {
    ProductTable product = ProductTable.$;

    // 查询所有商品及其价格与平均价格的差值
    List<Tuple2<Product, BigDecimal>> productWithPriceDiff = sqlClient.createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .select(
            product,
            product.price().minus(
                // 子查询:计算所有商品的平均价格
                sqlClient.createSubQuery(product)
                    .where(product.id().like(testPrefix + "-%"))
                    .select(
                        product.price().avg()
                    )
            )
        )
        .execute();

    // 验证结果
    assertThat(productWithPriceDiff).isNotEmpty();

    // 查询所有商品的平均价格
    BigDecimal avgPrice = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .select(product.price().avg())
        .fetchOne();

    // 验证每个返回的差价计算正确
    for (Tuple2<Product, BigDecimal> tuple : productWithPriceDiff) {
        Product p = tuple.get_1();
        BigDecimal priceDiff = tuple.get_2();

        // 验证差价计算正确
        BigDecimal expectedDiff = p.price().subtract(avgPrice);
        assertThat(priceDiff).isEqualByComparingTo(expectedDiff);
    }
}

在这个测试中,我们计算了每个商品价格与平均价格的差值,并验证结果的正确性。SELECT子句中的子查询使我们可以在单个查询中获取派生值,避免了多次查询和客户端计算。

类型安全的结果处理

Jimmer的一个显著优势是类型安全的结果处理。当子查询返回单个值时,这个值的类型会被正确推断:

// BigDecimal类型的差值
BigDecimal priceDiff = tuple.get_2();

这种类型安全性让开发者可以专注于业务逻辑,而不必担心类型转换和异常处理。

4.6.4 在FROM子句中使用子查询(派生表)

基本概念

派生表是FROM子句中的子查询,它生成一个临时表,可以像普通表一样进行查询和关联。派生表在处理复杂的数据预处理和多级聚合时特别有用。

Jimmer支持派生表操作,尽管其API与传统SQL的写法有所不同:

// 传统SQL中的派生表
/*
SELECT t.category_id, t.avg_price, c.name
FROM (
    SELECT category_id, AVG(price) as avg_price
    FROM product
    GROUP BY category_id
) t
JOIN category c ON t.category_id = c.id
*/

// Jimmer中的实现
CategoryTable category = CategoryTable.$;
ProductTable product = ProductTable.$;

var result = sqlClient.createQuery(category)
    .leftJoin(
        sqlClient.createSubQuery(product)
            .groupBy(product.categoryId())
            .select(
                product.categoryId().as("cid"),
                product.price().avg().as("avg_price")
            )
        .asTable("t"),
        (join, t) -> join.where(t.get("cid").eq(category.id()))
    )
    .select(
        category,
        t -> t.get("avg_price")
    )
    .execute();

Jimmer的派生表API需要使用.asTable()方法将子查询转换为一个可关联的表对象。

相关子查询与非相关子查询

子查询可以分为相关子查询和非相关子查询两种类型:

  1. 非相关子查询:不引用外部查询的表或列,可以独立执行
  2. 相关子查询:引用外部查询的表或列,执行依赖于外部查询的当前行

Jimmer支持这两种类型的子查询:

// 非相关子查询示例
@Test
@DisplayName("测试非相关子查询")
void testNonCorrelatedSubquery() {
    ProductTable product = ProductTable.$;

    // 查询价格高于平均价格的产品
    List<Product> highPriceProducts = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .where(
            product.price().gt(
                sqlClient.createSubQuery(product)
                    .where(product.id().like(testPrefix + "-%"))
                    .select(product.price().avg())
            )
        )
        .select(product)
        .execute();

    // 验证结果
    assertThat(highPriceProducts).isNotEmpty();

    // 手动验证每个产品价格都高于平均价格
    BigDecimal avgPrice = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .select(product.price().avg())
        .fetchOne();

    for (Product p : highPriceProducts) {
        assertThat(p.price()).isGreaterThan(avgPrice);
    }
}

4.6.5 相关子查询与非相关子查询

相关子查询实例

相关子查询引用外部查询的列,其执行依赖于外部查询的当前行:

@Test
@DisplayName("测试EXISTS子查询")
void testExistsSubquery() {
    ProductTable product = ProductTable.$;
    TagTable tag = TagTable.$;

    // 查找有任何标签的商品
    List<Product> productsWithTags = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .where(
            sqlClient.createSubQuery(tag)
                .where(tag.products(p -> p.id().eq(product.id())))
                .exists()
        )
        .select(product)
        .execute();

    // 验证结果 - 应该找到所有有标签的商品
    assertThat(productsWithTags).isNotEmpty();
    assertThat(productsWithTags).hasSize(6); // 所有商品都有标签
}

在这个例子中,子查询通过tag.products(p -> p.id().eq(product.id()))引用了外部查询的product.id(),这是一个典型的相关子查询。

存在性判断:EXISTS与NOT EXISTS

EXISTS和NOT EXISTS是相关子查询的常见用法,用于检查是否存在满足特定条件的记录:

// 查找没有评价的商品
List<Product> productsWithoutReviews = sqlClient
    .createQuery(product)
    .where(
        sqlClient.createSubQuery(review)
            .where(review.productId().eq(product.id()))
            .notExists()
    )
    .select(product)
    .execute();

Jimmer通过.exists().notExists()方法提供了简洁的EXISTS子查询支持。

性能考量

相关子查询在某些情况下可能会导致性能问题,特别是当外部查询返回大量行时。在这种情况下,Jimmer提供了几种优化方法:

  1. 转换为JOIN:有些相关子查询可以转换为等效的JOIN操作
  2. 使用IN代替EXISTS:在某些情况下,IN子查询可能比EXISTS更高效
  3. 批量处理:对于大数据集,考虑分批处理而非单次查询所有数据

4.6.6 UNION/INTERSECT/EXCEPT操作

SQL提供了几种集合操作符,用于组合多个查询的结果:

  1. UNION:合并两个查询的结果,去除重复记录
  2. UNION ALL:合并两个查询的结果,保留重复记录
  3. INTERSECT:返回两个查询结果的交集
  4. EXCEPT:返回第一个查询结果中排除第二个查询结果的部分

Jimmer提供了这些集合操作的支持:

@Test
@DisplayName("测试UNION操作")
void testUnionOperation() {
    ProductTable product = ProductTable.$;
    BookTable book = BookTable.$;

    // 使用UNION将商品和图书的名称合并查询
    List<String> itemNames = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .select(product.name())
        .union(
            sqlClient.createQuery(book)
                .where(book.id().like(testPrefix + "-%"))
                .select(book.name())
        )
        .execute();

    // 验证结果
    int productCount = sqlClient
        .createQuery(product)
        .where(product.id().like(testPrefix + "-%"))
        .select(product)
        .execute()
        .size();

    int bookCount = sqlClient
        .createQuery(book)
        .where(book.id().like(testPrefix + "-%"))
        .select(book)
        .execute()
        .size();

    // 应该包含所有的产品和图书名称
    assertThat(itemNames).hasSize(productCount + bookCount);
}

在这个例子中,我们使用UNION操作合并了商品名称和图书名称。这种方式特别适合需要跨实体类型查询的场景。

类型匹配要求

使用集合操作时,需要确保各个查询的SELECT子句结构相匹配:

  1. 选择的列数需要相同
  2. 对应位置的列类型需要兼容
  3. 只能在最外层查询中设置排序操作

Jimmer会在编译时检查这些要求,确保查询的正确性。

4.6.7 通用表表达式(WITH子句)

通用表表达式(Common Table Expression, CTE)是SQL的高级特性,允许定义在单个查询中可重用的临时结果集。CTE通过WITH子句引入,可以简化复杂查询,提高可读性。

尽管Jimmer的当前版本对CTE的支持有限,但我们可以通过组合使用子查询和派生表来实现类似的功能:

// 模拟CTE功能:分析每个类别的价格分布
CategoryTable category = CategoryTable.$;
ProductTable product = ProductTable.$;

// 第一步:获取每个类别的平均价格
var categoryAvgPrices = sqlClient
    .createQuery(product)
    .groupBy(product.categoryId())
    .select(
        product.categoryId(),
        product.price().avg().as("avg_price")
    )
    .execute();

// 第二步:将平均价格与每个商品比较
Map<String, BigDecimal> avgPriceMap = categoryAvgPrices.stream()
    .collect(Collectors.toMap(
        t -> t.get_1(),
        t -> t.get_2()
    ));

// 查询每个类别中高于平均价格的商品
List<Product> highValueProducts = sqlClient
    .createQuery(product)
    .where(product.categoryId().in(avgPriceMap.keySet()))
    .where(cb -> {
        // 动态构建条件
        return Predicate.or(
            avgPriceMap.entrySet().stream()
                .map(e -> Predicate.and(
                    product.categoryId().eq(e.getKey()),
                    product.price().gt(e.getValue())
                ))
                .toArray(Predicate[]::new)
        );
    })
    .select(product)
    .execute();

在这个例子中,我们通过两步查询模拟了CTE的功能,实现了"查找每个类别中价格高于该类别平均价格的商品"这一需求。

  • 小结与展望

本节中,我们深入探讨了Jimmer对子查询和高级SQL功能的支持。通过各种示例,我们了解了如何在WHERE子句、SELECT子句和FROM子句中使用子查询,以及如何利用EXISTS子查询和SQL集合操作处理复杂的查询需求。

Jimmer的子查询API具有以下优势:

  1. 类型安全:所有查询操作都有类型检查,减少错误发生的可能性
  2. 语法简洁:流畅的API设计使得复杂查询代码易于阅读和维护
  3. IDE友好:充分利用IDE的代码补全和错误检查功能
  4. 性能优化:自动生成高效的SQL查询,减少性能问题

通过这些高级查询功能,开发者可以更优雅地解决复杂的业务查询需求,无需依赖原生SQL或存储过程,同时保持代码的可维护性和可测试性。

4.7 小结回顾

在本章中,我们深入探讨了Jimmer查询系统的各个方面,从基础的类型安全查询到复杂的子查询与高级SQL功能。通过这些内容,我们可以清晰地看到Jimmer如何在保持类型安全的同时提供丰富而灵活的查询能力,为开发者构建现代数据访问层提供了强大支持。

4.7.1 数据查询设计的核心原则

回顾整个第四章的内容,我们可以提炼出几个贯穿始终的数据查询设计原则:

类型安全第一

类型安全是Jimmer查询API的核心特性。与传统的字符串拼接式SQL构建方式不同,Jimmer的查询构建始终保持类型安全:

// 传统方式的问题
String sql = "SELECT * FROM product WHERE category_id = " + categoryId; // 可能导致SQL注入
String sql = "SELECT * FROM produkt WHERE category_id = ?"; // 表名错误但编译期无法检查

// Jimmer的类型安全方式
ProductTable product = ProductTable.$;
List<Product> products = sqlClient.createQuery(product)
    .where(product.categoryId().eq(categoryId)) // 类型安全,避免SQL注入
    .select(product)
    .execute();

在Jimmer中,表名、列名、参数类型等都在编译期进行检查,大大减少了运行时错误的可能性。

平衡灵活性与安全性

Jimmer查询API的设计平衡了灵活性与安全性两个看似矛盾的目标:

  • 静态类型的安全保障:通过强类型系统避免常见错误
  • 动态条件的灵活构建:通过条件检查、动态谓词等机制适应复杂查询需求

例如,动态查询构建的示例:

// 灵活构建查询条件
return sqlClient.createQuery(product)
    .whereIf(minPrice != null, () -> product.price().ge(minPrice))
    .whereIf(maxPrice != null, () -> product.price().le(maxPrice))
    .whereIf(categoryIds != null && !categoryIds.isEmpty(),
        () -> product.categoryId().in(categoryIds))
    .select(product)
    .execute();

这种平衡使得Jimmer在处理复杂多变的业务场景时既安全又灵活。

关注性能优化

查询性能是数据访问层的关键指标。Jimmer在API设计中融入了多种性能优化机制:

  1. 智能连接优化:自动消除不必要的表连接
  2. 高效的分页实现:支持多种分页策略以适应不同场景
  3. 避免N+1问题:自动批量加载关联数据
  4. 查询缓存支持:基于不可变对象的天然缓存友好性

这些优化使得Jimmer查询在复杂场景下仍能保持高性能,比如处理复杂的关联查询:

// 高效加载产品及其关联数据
Fetcher<Product> fetcher = ProductFetcher.$
    .allScalarFields()
    .category(CategoryFetcher.$.allScalarFields())
    .tags(TagFetcher.$.allScalarFields());

List<Product> products = sqlClient.createQuery(product)
    .where(product.price().gt(new BigDecimal("1000")))
    .select(product.fetch(fetcher))
    .execute();

这个查询会自动优化为最小必要的SQL操作,避免N+1问题。

领域模型驱动

Jimmer的查询系统与其领域模型紧密集成,使查询表达更加自然:

  • 查询表达式直接映射到领域模型属性
  • 关系导航基于领域模型中定义的关联
  • 查询结果直接映射为领域对象

这种紧密集成使得代码更加直观、可读性更高,开发者可以更专注于业务问题而非技术细节。

4.7.2 Jimmer查询API的独特价值

通过本章的学习,我们已经了解了Jimmer查询API相比其他ORM框架的独特价值:

完整的查询表达能力

Jimmer支持几乎所有常见的SQL查询功能:

  1. 基本CRUD操作
  2. 复杂条件组合(AND、OR、NOT)
  3. 关联查询与数据图控制
  4. 排序、分页与分组聚合
  5. 子查询与高级SQL功能(EXISTS、UNION等)

这种完整性使开发者能够在保持类型安全的前提下处理各种复杂查询场景,而无需回退到原生SQL。

图式数据查询的独特实现

Jimmer的Fetcher机制是其最突出的特性之一,它使开发者能够:

  1. 精确控制要加载的关联数据,避免过度获取或数据不足
  2. 动态构建数据图形状,适应不同展示需求
  3. 递归加载树形或图形结构,处理复杂数据关系

例如,以下代码展示了如何定义动态Fetcher:

// 根据权限动态构建Fetcher
Fetcher<Product> fetcher = ProductFetcher.$.allScalarFields();
if (hasBasicInfoPermission) {
    fetcher = fetcher.category(CategoryFetcher.$.name());
}
if (hasDetailedInfoPermission) {
    fetcher = fetcher.tags();
}

// 使用构建好的Fetcher查询
List<Product> products = sqlClient.createQuery(product)
    .select(product.fetch(fetcher))
    .execute();

这种灵活性使Jimmer在处理复杂数据需求时表现出色。

类型安全与动态性的完美结合

Jimmer查询API的最大特点是将类型安全与动态性完美结合:

  • 编译期保证查询语法正确性
  • 运行时支持动态条件构建
  • 强类型参数避免安全隐患
  • 动态Predicate支持复杂逻辑

这种结合解决了传统ORM框架中类型安全与灵活性难以兼得的问题。

现代Java特性的充分利用

Jimmer查询API充分利用了现代Java特性,使代码更加简洁优雅:

  • Lambda表达式用于动态条件构建
  • 方法引用简化排序表达
  • Stream API处理查询结果
  • 类型推断减少冗余代码

例如,排序的简洁表达:

// 使用方法引用简化排序
List<Product> products = sqlClient.createQuery(product)
    .orderBy(product.price().desc())
    .orderBy(product.createdTime())
    .select(product)
    .execute();

4.7.3 生产环境中的应用指导

基于第四章的内容,我们可以总结出一些在生产环境中应用Jimmer查询API的最佳实践:

查询方法组织

合理组织查询方法有助于提高代码可维护性:

  1. 职责分离:将查询逻辑与业务逻辑分离
  2. 分层设计:基础查询方法 → 组合查询方法 → 业务逻辑方法
  3. 参数封装:使用专门的查询参数对象封装复杂查询条件

例如,推荐的查询方法组织方式:

// 数据访问层
public interface ProductRepository {
    // 基础查询方法
    List<Product> findByCategory(String categoryId);

    // 组合查询方法
    List<Product> findByCriteria(ProductCriteria criteria);

    // 特定业务查询
    List<Product> findRecommendedProducts(String userId);
}

// 实现
@Component
public class ProductRepositoryImpl implements ProductRepository {
    @Autowired
    private JSqlClient sqlClient;

    @Override
    public List<Product> findByCriteria(ProductCriteria criteria) {
        ProductTable product = ProductTable.$;
        return sqlClient.createQuery(product)
            .whereIf(criteria.getMinPrice() != null, 
                () -> product.price().ge(criteria.getMinPrice()))
            .whereIf(criteria.getMaxPrice() != null, 
                () -> product.price().le(criteria.getMaxPrice()))
            // 更多条件...
            .select(product)
            .execute();
    }

    // 其他方法实现...
}

性能优化策略

在生产环境中应用Jimmer查询API时,应当注意以下性能优化策略:

  1. 合理使用Fetcher:只加载真正需要的数据,避免过度获取
  2. 索引规划:基于查询模式设计合理的数据库索引
  3. 分页策略:大数据量查询必须使用分页,同时选择合适的分页实现
  4. 查询监控:监控慢查询并进行针对性优化
// 高性能分页查询示例
Page<Product> productPage = sqlClient.createQuery(product)
    .where(product.active().eq(true))
    .select(product)
    .fetchPage(pageIndex, pageSize);

常见陷阱与解决方案

在使用Jimmer查询API时,应当避免以下常见陷阱:

  1. 过度关联查询:不必要的关联会导致性能问题
  2. 解决:使用IdView或两步查询处理简单关联

  3. 反模式:全表扫描:缺少有效过滤条件导致全表扫描

  4. 解决:确保查询始终包含必要的过滤条件,特别是分页查询

  5. 繁重的实时计算:频繁执行复杂聚合查询

  6. 解决:考虑预计算、缓存或物化视图

  7. 内存泄漏:长期持有大量查询结果

  8. 解决:分批处理大数据集,及时释放不需要的引用
// 错误示例:无条件查询可能导致全表扫描
List<Product> allProducts = sqlClient.createQuery(product)
    .select(product)
    .execute(); // 危险!

// 正确做法:添加必要的过滤条件或分页
List<Product> activeProducts = sqlClient.createQuery(product)
    .where(product.active().eq(true))
    .select(product)
    .fetchPage(0, 100) // 使用分页
    .getContent();

结语

第四章对Jimmer查询功能的深入探讨使我们能够看到一个现代ORM框架如何平衡类型安全与灵活性、简洁表达与强大功能。通过Jimmer,开发者可以摆脱原生SQL的复杂性,同时不必担心丧失SQL的表达能力。

从类型安全查询基础到高级SQL功能,从简单条件到复杂数据图,Jimmer提供了一套完整而优雅的解决方案。这些功能构成了Jimmer作为一个领先ORM框架的核心竞争力,为Java开发者提供了构建高质量数据访问层的强大工具。

当我们掌握了Jimmer的查询功能后,便已经站在了构建现代数据访问层的起点上。在下一章中,我们将继续探索Jimmer的数据修改功能,进一步完善我们对这个强大ORM框架的理解。