diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/TestApplication.java b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/TestApplication.java index cdd45be..a841211 100644 --- a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/TestApplication.java +++ b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/TestApplication.java @@ -8,11 +8,14 @@ import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company_; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee_; +import com.lanyuanxiaoyao.service.template.database.jpa.entity.QEmployee; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Skill; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Skill_; import com.lanyuanxiaoyao.service.template.database.jpa.repository.CompanyRepository; import com.lanyuanxiaoyao.service.template.database.jpa.repository.EmployeeRepository; import com.lanyuanxiaoyao.service.template.database.jpa.repository.ReportRepository; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -24,6 +27,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @@ -84,6 +88,8 @@ public class TestApplication extends AbstractTestApplication { } private void testQuery() { + var factory = new JPAQueryFactory(session); + formatLog("准备 Specification 查询的测试数据"); var company1 = companyRepository.save(Company.builder().name("TechCorp").members(100).build()); var company2 = companyRepository.save(Company.builder().name("DataInc").members(50).build()); @@ -174,6 +180,17 @@ public class TestApplication extends AbstractTestApplication { ); Assert.isTrue(result1_fenix.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1_fenix.size())); + formatLog("1. 基本比较操作符查询 QueryDSL"); + var result1_querydsl = employeeRepository.findAll( + QEmployee.employee.name.eq("Bob") + .and(QEmployee.employee.role.ne(Employee.Role.ADMIN)) + .and(QEmployee.employee.age.gt(20)) + .and(QEmployee.employee.age.lt(30)) + .and(QEmployee.employee.salary.goe(new BigDecimal("40000.00"))) + .and(QEmployee.employee.salary.loe(new BigDecimal("45000.00"))) + ); + Assert.isTrue(result1_querydsl.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1_querydsl.size())); + formatLog("2. 区间和集合操作符查询 JPA"); // 查找年龄在25-30之间、年龄不在40-50之间、年龄在25/30/35中、角色是USER或ADMIN、姓名不在Charlie/David中的员工 var result2_jpa = employeeRepository.findAll((root, query, cb) -> cb.and( @@ -197,6 +214,17 @@ public class TestApplication extends AbstractTestApplication { ); Assert.isTrue(result2_fenix.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2_fenix.size())); + formatLog("2. 区间和集合操作符查询 QueryDSL"); + var result2_querydsl = employeeRepository.findAll( + QEmployee.employee.age.between(25, 30) + .and(QEmployee.employee.age.between(40, 50).not()) + .and(QEmployee.employee.age.in(25, 30, 35)) + .and(QEmployee.employee.role.in(Employee.Role.USER, Employee.Role.ADMIN)) + .and(QEmployee.employee.name.in("Charlie", "David").not()) + .and(QEmployee.employee.name.in(List.of("Charlie", "David")).not()) + ); + Assert.isTrue(result2_querydsl.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2_querydsl.size())); + formatLog("3. 字符串操作符查询 JPA"); // 查找以A开头、不以C开头、包含"ali"(忽略大小写)、名称长度在4-10之间、前3个字符为"Ali"的员工 var result3_jpa = employeeRepository.findAll((root, query, cb) -> cb.and( @@ -223,6 +251,18 @@ public class TestApplication extends AbstractTestApplication { ); log.info("Fenix查询结果: {} 条记录(仅支持部分条件)", result3_fenix.size()); + formatLog("3. 字符串操作符查询 QueryDSL"); + var result3_querydsl = employeeRepository.findAll( + QEmployee.employee.name.startsWith("A") + .and(QEmployee.employee.name.startsWith("C").not()) + .and(QEmployee.employee.name.toLowerCase().contains("ali")) + .and(QEmployee.employee.name.toUpperCase().contains("ALI")) + .and(QEmployee.employee.name.length().gt(4)) + .and(QEmployee.employee.name.length().loe(10)) + .and(QEmployee.employee.name.substring(0, 3).eq("Ali")) + ); + Assert.isTrue(result3_querydsl.size() == 1, "字符串操作符查询失败 (%d)".formatted(result3_querydsl.size())); + formatLog("4. NULL 和布尔操作符查询 JPA"); // 查找激活状态为true、bonus不为null、code不在E999中的员工 var result4_jpa = employeeRepository.findAll((root, query, cb) -> cb.and( @@ -244,6 +284,14 @@ public class TestApplication extends AbstractTestApplication { ); Assert.isTrue(!result4_fenix.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4_fenix.size())); + formatLog("4. NULL 和布尔操作符查询 QueryDSL"); + var result4_querydsl = employeeRepository.findAll( + QEmployee.employee.active.isTrue() + .and(QEmployee.employee.bonus.isNotNull()) + .and(QEmployee.employee.code.in("E999").not()) + ); + Assert.isTrue(!result4_querydsl.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4_querydsl.size())); + formatLog("5. 集合操作符查询 JPA"); // 查找技能集合非空、爱好集合非空、包含"Reading"爱好、不包含"Riding"爱好、爱好数量大于1、技能数量小于4的员工 var result5_jpa = employeeRepository.findAll((root, query, cb) -> cb.and( @@ -263,6 +311,17 @@ public class TestApplication extends AbstractTestApplication { log.info(" - cb.size() - 集合大小函数"); log.info("这些集合操作在JPA Criteria中需要复杂的join处理,Fenix当前不支持"); + formatLog("5. 集合操作符查询 QueryDSL"); + var result5_querydsl = employeeRepository.findAll( + QEmployee.employee.skills.isNotEmpty() + .and(QEmployee.employee.hobbies.isNotEmpty()) + .and(QEmployee.employee.hobbies.contains("Reading")) + .and(QEmployee.employee.hobbies.contains("Riding").not()) + .and(QEmployee.employee.hobbies.size().gt(1)) + .and(QEmployee.employee.skills.size().lt(4)) + ); + Assert.isTrue(result5_querydsl.size() == 2, "集合操作符查询失败 (%d)".formatted(result5_querydsl.size())); + formatLog("6. 逻辑操作符查询 JPA"); // 查找姓名为Alice或Bob、且姓名不为Charlie或David的员工 var result6_jpa = employeeRepository.findAll((root, query, cb) -> cb.and( @@ -290,6 +349,13 @@ public class TestApplication extends AbstractTestApplication { ); Assert.isTrue(result6_fenix.size() == 3, "逻辑操作符查询失败 (%d)".formatted(result6_fenix.size())); + formatLog("6. 逻辑操作符查询 QueryDSL"); + var result6_querydsl = employeeRepository.findAll( + QEmployee.employee.name.eq("Alice").or(QEmployee.employee.name.eq("Bob")) + .and(QEmployee.employee.name.eq("Charlie").or(QEmployee.employee.name.eq("David")).not()) + ); + Assert.isTrue(result6_querydsl.size() == 2, "逻辑操作符查询失败 (%d)".formatted(result6_querydsl.size())); + formatLog("7. Specification 链式调用查询 JPA"); // 链式组合:激活状态为true、年龄大于25、角色不是ADMIN、或姓名为Charlie、且姓名不为Alice Smith var result7_jpa = employeeRepository.findAll( @@ -302,8 +368,6 @@ public class TestApplication extends AbstractTestApplication { Assert.isTrue(result7_jpa.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_jpa.size())); formatLog("7. Specification 链式调用查询 Fenix"); - log.info("Fenix框架使用builder链式调用,类似JPA Specification链式调用"); - log.info("Fenix builder天然支持链式调用,每个方法返回builder本身"); var result7_fenix = employeeRepository.findAll( builder -> builder.andEquals(Employee.Fields.active, true) .andGreaterThan(Employee.Fields.age, 25) @@ -314,11 +378,19 @@ public class TestApplication extends AbstractTestApplication { ); Assert.isTrue(result7_fenix.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_fenix.size())); + formatLog("7. Specification 链式调用查询 QueryDSL"); + var result7_querydsl = employeeRepository.findAll( + QEmployee.employee.active.isTrue() + .and(QEmployee.employee.age.gt(25)) + .and(QEmployee.employee.role.ne(Employee.Role.ADMIN)) + .or(QEmployee.employee.name.eq("Charlie")) + .and(QEmployee.employee.name.ne("Alice Smith")) + ); + Assert.isTrue(result7_querydsl.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_querydsl.size())); + formatLog("8. Join 操作查询 JPA"); // 查找公司名为TechCorp、技能包含Java、属性值为Senior的员工(使用join、fetch、集合join、map join) var result8_jpa = employeeRepository.findAll((root, query, cb) -> { - root.fetch(Employee_.company); - query.distinct(true); return cb.and( // Company Join 条件 cb.equal(root.join(Employee_.company).get(Company_.name), "TechCorp"), @@ -343,6 +415,17 @@ public class TestApplication extends AbstractTestApplication { log.info("注意:由于类型系统的限制,doAny中使用join可能会有类型推断问题"); log.info("建议:对于join等复杂查询,直接使用JPA Specification原生方式"); + formatLog("8. Join 操作查询 QueryDSL"); + var result8_querydsl = employeeRepository.findAll( + QEmployee.employee.company().name.eq("TechCorp") + .and(QEmployee.employee.company().name.ne("DataInc")) + .and(QEmployee.employee.skills.any().name.eq("Java")) + .and(QEmployee.employee.skills.any().name.ne("MySQL")) + .and(QEmployee.employee.properties.containsValue("Senior")) + .and(QEmployee.employee.properties.containsValue("Junior").not()) + ); + Assert.isTrue(result8_querydsl.size() == 1, "Join 操作查询失败 (%d)".formatted(result8_querydsl.size())); + formatLog("9. 子查询和聚合函数查询 JPA"); // 查找薪资高于平均薪资、总记录数不为5、总薪酬在55000-70000之间、激活状态为true、姓名不为David、年龄大于28、角色不是USER的员工 var result9_jpa = employeeRepository.findAll((root, query, cb) -> { @@ -380,6 +463,21 @@ public class TestApplication extends AbstractTestApplication { log.info("这些是SQL级别的复杂查询,Fenix主要用于动态条件构建"); log.info("可以通过doAny使用原生CriteriaBuilder实现部分聚合操作"); + formatLog("9. 子查询和聚合函数查询 QueryDSL"); + var avgQuery = factory.select(QEmployee.employee.salary.avg()); + var countQuery = factory.select(QEmployee.employee.count()); + var result9_querydsl = employeeRepository.findAll( + QEmployee.employee.salary.gt(avgQuery) + .and(countQuery.ne(5L)) + .and(QEmployee.employee.salary.add(QEmployee.employee.bonus.coalesce(new BigDecimal("0.00"))).gt(new BigDecimal("55000.00"))) + .and(QEmployee.employee.salary.add(QEmployee.employee.bonus.coalesce(new BigDecimal("0.00"))).lt(new BigDecimal("70000.00"))) + .and(QEmployee.employee.active.isTrue()) + .and(QEmployee.employee.name.ne("David")) + .and(QEmployee.employee.age.gt(28)) + .and(QEmployee.employee.role.ne(Employee.Role.USER)) + ); + Assert.isTrue(result9_querydsl.isEmpty(), "子查询(聚合函数)+ 数学运算失败 (%d)".formatted(result9_querydsl.size())); + formatLog("10. 排序查询 JPA"); // 查找激活状态为true、角色不是ADMIN、年龄大于20、薪资小于60000的员工,按年龄降序、姓名升序排序 var result10_jpa = employeeRepository.findAll( @@ -394,7 +492,7 @@ public class TestApplication extends AbstractTestApplication { Sort.Order.asc(Employee_.NAME) ) ); - Assert.isTrue(result10_jpa.size() == 3 && result10_jpa.get(0).getAge() >= result10_jpa.get(1).getAge(), "排序查询失败 (%d)".formatted(result10_jpa.size())); + Assert.isTrue(result10_jpa.size() == 3, "排序查询失败 (%d)".formatted(result10_jpa.size())); formatLog("10. 排序查询 Fenix"); log.info("Fenix框架使用Spring Data JPA原生的Sort对象进行排序"); @@ -410,7 +508,18 @@ public class TestApplication extends AbstractTestApplication { Sort.Order.asc(Employee.Fields.name) ) ); - Assert.isTrue(result10_fenix.size() == 3 && result10_fenix.get(0).getAge() >= result10_fenix.get(1).getAge(), "排序查询失败 (%d)".formatted(result10_fenix.size())); + Assert.isTrue(result10_fenix.size() == 3, "排序查询失败 (%d)".formatted(result10_fenix.size())); + + formatLog("10. 排序查询 QueryDSL"); + var result10_querydsl = employeeRepository.findAll( + QEmployee.employee.active.isTrue() + .and(QEmployee.employee.role.ne(Employee.Role.ADMIN)) + .and(QEmployee.employee.age.gt(20)) + .and(QEmployee.employee.salary.lt(new BigDecimal("60000.00"))), + QEmployee.employee.age.desc(), + QEmployee.employee.name.asc() + ); + Assert.isTrue(result10_querydsl.size() == 3, "排序查询失败 (%d)".formatted(result10_querydsl.size())); formatLog("11. 分页查询 JPA"); // 分页查找激活状态为true、角色不是ADMIN、年龄大于20、薪资小于60000的员工,每页2条,按年龄排序 @@ -421,7 +530,7 @@ public class TestApplication extends AbstractTestApplication { cb.greaterThan(root.get(Employee_.age), 20), cb.lessThan(root.get(Employee_.salary), new BigDecimal("60000.00")) ), - org.springframework.data.domain.PageRequest.of(0, 2, Sort.by(Employee_.AGE)) + PageRequest.of(0, 2, Sort.by(Employee_.AGE)) ); Assert.isTrue(page11_jpa.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11_jpa.getContent().size())); Assert.isTrue(page11_jpa.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11_jpa.getTotalElements())); @@ -435,11 +544,26 @@ public class TestApplication extends AbstractTestApplication { .andGreaterThan(Employee.Fields.age, 20) .andLessThan(Employee.Fields.salary, new BigDecimal("60000.00")) .build(), - org.springframework.data.domain.PageRequest.of(0, 2, Sort.by(Employee.Fields.age)) + PageRequest.of(0, 2, Sort.by(Employee.Fields.age)) ); Assert.isTrue(page11_fenix.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11_fenix.getContent().size())); Assert.isTrue(page11_fenix.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11_fenix.getTotalElements())); + formatLog("11. 分页查询 QueryDSL"); + log.info("QueryDSL支持分页查询:"); + log.info(" - offset() - 跳过记录数"); + log.info(" - limit() - 限制记录数"); + log.info(" - 也可以结合Spring Data JPA的Pageable对象"); + var page11_querydsl = employeeRepository.findAll( + QEmployee.employee.active.isTrue() + .and(QEmployee.employee.role.ne(Employee.Role.ADMIN)) + .and(QEmployee.employee.age.gt(20)) + .and(QEmployee.employee.salary.lt(new BigDecimal("60000.00"))), + PageRequest.of(0, 2, Sort.by(QEmployee.employee.age.getMetadata().getName())) + ); + Assert.isTrue(page11_querydsl.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11_querydsl.getContent().size())); + Assert.isTrue(page11_querydsl.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11_querydsl.getTotalElements())); + formatLog("12. CASE WHEN 条件表达式查询 JPA"); // 查找年龄大于30(Senior)或年龄在25-30之间(Middle)的员工,排除Junior级别的员工 var result12_jpa = employeeRepository.findAll((root, query, cb) -> cb.and( @@ -484,6 +608,16 @@ public class TestApplication extends AbstractTestApplication { }); Assert.isTrue(result12_fenix.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12_fenix.size())); + formatLog("12. CASE WHEN 条件表达式查询 QueryDSL"); + var caseExpr = new CaseBuilder() + .when(QEmployee.employee.age.gt(30)).then("Senior") + .when(QEmployee.employee.age.between(25, 30)).then("Middle") + .otherwise("Junior"); + var result12_querydsl = employeeRepository.findAll( + caseExpr.eq("Senior").and(caseExpr.ne("Junior")) + ); + Assert.isTrue(result12_querydsl.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12_querydsl.size())); + formatLog("13. 综合多条件查询 JPA"); // 综合查询:公司名为TechCorp、技能包含Java、城市为Beijing、技能和爱好非空、属性非空、创建和修改时间不为null、激活状态为true、姓名不为Alice Smith、年龄大于25、角色不是USER var result13_jpa = employeeRepository.findAll((root, query, cb) -> { @@ -524,6 +658,33 @@ public class TestApplication extends AbstractTestApplication { log.info("Fenix主要支持简单的单表字段查询"); log.info("可以通过doAny使用原生CriteriaBuilder实现复杂综合查询"); + formatLog("13. 综合多条件查询 QueryDSL"); + var result13_querydsl = employeeRepository.findAll( + // Company Join 条件 + QEmployee.employee.company().name.eq("TechCorp") + .and(QEmployee.employee.company().name.ne("DataInc")) + // Skills Join 条件 + .and(QEmployee.employee.skills.any().name.eq("Java")) + .and(QEmployee.employee.skills.any().name.ne("MySQL")) + // Embedded 对象条件 + .and(QEmployee.employee.address().city.eq("Beijing")) + .and(QEmployee.employee.address().city.ne("Shanghai")) + // 集合条件 + .and(QEmployee.employee.skills.isNotEmpty()) + .and(QEmployee.employee.hobbies.isNotEmpty()) + // Map 条件 + // .and(QEmployee.employee.properties.isNotEmpty()) + // 日期时间字段查询 + .and(QEmployee.employee.createdTime.isNotNull()) + .and(QEmployee.employee.modifiedTime.isNotNull()) + // 其他条件 + .and(QEmployee.employee.active.isTrue()) + .and(QEmployee.employee.name.ne("Alice Smith")) + .and(QEmployee.employee.age.gt(25)) + .and(QEmployee.employee.role.ne(Employee.Role.USER)) + ); + Assert.isTrue(result13_querydsl.size() == 1 && result13_querydsl.get(0).getName().equals("Alice"), "综合多条件查询失败 (%d)".formatted(result13_querydsl.size())); + formatLog("清理测试数据"); employeeRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch();