From 93eded44cb3832522b4a632b5279770ea6652607 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Thu, 22 Jan 2026 15:42:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(jpa):=20=E9=87=8D=E6=96=B0=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1specification=E6=9F=A5=E8=AF=A2=E7=9A=84=E4=BE=8B?= =?UTF-8?q?=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/test/AbstractTestApplication.java | 20 - .../database/common/test/entity/Industry.java | 14 - .../database/common/test/entity/Level.java | 5 - .../database/jpa/TestApplication.java | 371 +++++++++++++----- .../jpa/controller/ReportController.java | 7 +- .../template/database/jpa/entity/Address.java | 34 ++ .../template/database/jpa/entity/Company.java | 20 - .../database/jpa/entity/Employee.java | 76 +++- .../template/database/jpa/entity/Report.java | 6 +- .../template/database/jpa/entity/Skill.java | 38 ++ .../jpa/repository/EmployeeRepository.java | 9 - 11 files changed, 427 insertions(+), 173 deletions(-) delete mode 100644 spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Industry.java delete mode 100644 spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Level.java create mode 100644 spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Address.java create mode 100644 spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Skill.java diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/AbstractTestApplication.java b/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/AbstractTestApplication.java index 130a826..5d1bc8d 100644 --- a/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/AbstractTestApplication.java +++ b/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/AbstractTestApplication.java @@ -132,26 +132,6 @@ public class AbstractTestApplication { ); } - protected Map randomEmployee(Long companyId) { - return randomEmployee(companyId, randomString(10)); - } - - protected Map randomEmployee(Long companyId, String name) { - return Map.of( - "name", name, - "age", randomInt(100), - "companyId", companyId - ); - } - - protected Map randomReport(Long employeeId) { - return Map.of( - "score", randomDouble(200), - "level", randomChar("ABCDE"), - "employeeId", employeeId - ); - } - protected String randomString(String prefix, int length) { return prefix + randomString(length); } diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Industry.java b/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Industry.java deleted file mode 100644 index 7138349..0000000 --- a/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Industry.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.lanyuanxiaoyao.service.template.database.common.test.entity; - -public enum Industry { - TECHNOLOGY, - FINANCE, - MEDIA, - SERVICE, - GOVERNMENT, - EDUCATION, - HEALTHCARE, - CONSTRUCTION, - RETAIL, - OTHER, -} diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Level.java b/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Level.java deleted file mode 100644 index 62aa6c3..0000000 --- a/spring-boot-service-template-database/spring-boot-service-template-database-common-test/src/main/java/com/lanyuanxiaoyao/service/template/database/common/test/entity/Level.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.lanyuanxiaoyao.service.template.database.common.test.entity; - -public enum Level { - A, B, C, D, E -} 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 023cad4..46e2956 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 @@ -2,17 +2,18 @@ package com.lanyuanxiaoyao.service.template.database.jpa; import com.blinkfox.fenix.EnableFenix; import com.lanyuanxiaoyao.service.template.database.common.test.AbstractTestApplication; -import com.lanyuanxiaoyao.service.template.database.common.test.entity.Industry; -import com.lanyuanxiaoyao.service.template.database.common.test.entity.Level; +import com.lanyuanxiaoyao.service.template.database.jpa.entity.Address; +import com.lanyuanxiaoyao.service.template.database.jpa.entity.Address_; 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.Report; +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 java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; @@ -22,7 +23,10 @@ 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.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @Slf4j @@ -30,6 +34,7 @@ import org.springframework.util.Assert; @SpringBootApplication @EnableFenix @EnableJpaAuditing +@Transactional public class TestApplication extends AbstractTestApplication { private final CompanyRepository companyRepository; private final EmployeeRepository employeeRepository; @@ -43,7 +48,7 @@ public class TestApplication extends AbstractTestApplication { public void runTests() { testCrud(); testDelete(); - testSpecification(); + testQuery(); testNative(); System.exit(0); @@ -76,110 +81,280 @@ public class TestApplication extends AbstractTestApplication { companyRepository.deleteBatchByIds(List.of(cid1, cid2)); } - private void testSpecification() { - formatLog("Added"); - var company1 = companyRepository.save(Company.builder().name(randomString(5)).members(randomInt(100)).industries(Set.of(Industry.MEDIA, Industry.SERVICE)).build()); - var company2 = companyRepository.save(Company.builder().name(randomString(5)).members(randomInt(100)).industries(Set.of(Industry.MEDIA, Industry.SERVICE)).build()); - var employee1 = employeeRepository.save( - Employee.builder() - .name("Tom") - .age(randomInt(100)) - .role(Employee.Role.USER) - .company(company1) - .connections(Map.of( - Employee.ConnectionType.ADDRESS, randomString(50), - Employee.ConnectionType.EMAIL, randomString(20) - )) - .build() - ); - var employee2 = employeeRepository.save( - Employee.builder() - .name(randomString(10)) - .age(randomInt(100)) - .role(Employee.Role.USER) - .company(company2) - .connections(Map.of( - Employee.ConnectionType.ADDRESS, randomString(50), - Employee.ConnectionType.EMAIL, randomString(20) - )) - .build() - ); - var report1 = reportRepository.save(Report.builder().score(randomDouble(50)).level(Level.B).employeeId(employee1.getId()).build()); - var report2 = reportRepository.save(Report.builder().score(randomDouble(50)).level(Level.E).employeeId(employee2.getId()).build()); + private void testQuery() { + formatLog("准备 Specification 查询的测试数据"); + var company1 = companyRepository.save(Company.builder().name("TechCorp").members(100).build()); + var company2 = companyRepository.save(Company.builder().name("DataInc").members(50).build()); + var company3 = companyRepository.save(Company.builder().name("CloudSys").members(150).build()); - formatLog("Query"); - var employees1 = employeeRepository.findAll( - builder -> builder - .andIsNotNull(Employee.Fields.name) - .andEquals(Employee.Fields.name, "Tom") - .andLike(Employee.Fields.name, "To") - .andStartsWith(Employee.Fields.name, "To") - .andEndsWith(Employee.Fields.name, "om") - .andLessThan(Employee.Fields.age, 200) - .andGreaterThanEqual(Employee.Fields.age, 0) - .andIn(Employee.Fields.name, List.of("Tom", "Mike")) - .andBetween(Employee.Fields.age, 0, 200) - .build() - ); - Assert.isTrue(employees1.size() == 1, "查询数量错误"); + // 准备 Skills 数据 + var skill1 = Skill.builder().name("Java").description("Java 编程语言").build(); + var skill2 = Skill.builder().name("Python").description("Python 编程语言").build(); + var skill3 = Skill.builder().name("Spring").description("Spring 框架").build(); + var skill4 = Skill.builder().name("MySQL").description("MySQL 数据库").build(); - var employees2 = employeeRepository.findAll( - (root, query, builder) -> - builder.and( - builder.isNotNull(root.get(Employee_.name)), - builder.equal(root.get(Employee_.name), "Tom"), - builder.like(root.get(Employee_.name), "To%"), - builder.lessThan(root.get(Employee_.age), 200), - builder.greaterThanOrEqualTo(root.get(Employee_.age), 0), - builder.in(root.get(Employee_.NAME)).value(List.of("Tom", "Mike")), - builder.between(root.get(Employee_.age), 0, 200), - builder.isNotEmpty(root.get(Employee_.company).get(Company_.employees)), - builder.isMember(Industry.MEDIA, root.get(Employee_.company).get(Company_.industries)) + var employee1 = employeeRepository.save(Employee.builder() + .name("Alice").age(30).role(Employee.Role.ADMIN).code("E001") + .salary(new BigDecimal("50000.00")).bonus(new BigDecimal("5000.00")) + .active(true).company(company1) + .address(Address.builder().street("Main St").city("Beijing").state("Beijing").zipCode("100000").country("China").build()) + .skills(Set.of(skill1, skill3)) + .hobbies(List.of("Reading", "Swimming")) + .properties(Map.of("department", "Engineering", "level", "Senior")) + .connections(Map.of(Employee.ConnectionType.EMAIL, "alice@example.com")) + .build()); + + var employee2 = employeeRepository.save(Employee.builder() + .name("Bob").age(25).role(Employee.Role.USER).code("E002") + .salary(new BigDecimal("40000.00")).bonus(new BigDecimal("4000.00")) + .active(true).company(company2) + .address(Address.builder().street("Oak Ave").city("Shanghai").state("Shanghai").zipCode("200000").country("China").build()) + .skills(Set.of(skill2)) + .hobbies(List.of("Gaming")) + .properties(Map.of("department", "Marketing", "level", "Junior")) + .connections(Map.of(Employee.ConnectionType.PHONE, "1234567890")) + .build()); + + var employee3 = employeeRepository.save(Employee.builder() + .name("Charlie").age(35).role(Employee.Role.ADMIN).code("E003") + .salary(new BigDecimal("60000.00")).bonus(new BigDecimal("6000.00")) + .active(false).company(company1) + .address(Address.builder().street("Pine Rd").city("Shenzhen").state("Guangdong").zipCode("518000").country("China").build()) + .skills(Set.of(skill1, skill2, skill3)) + .hobbies(List.of("Reading", "Gaming", "Music")) + .properties(Map.of("department", "Engineering", "level", "Lead")) + .connections(Map.of(Employee.ConnectionType.EMAIL, "charlie@example.com", Employee.ConnectionType.PHONE, "0987654321")) + .build()); + + var employee4 = employeeRepository.save(Employee.builder() + .name("David").age(28).role(Employee.Role.USER).code("E004") + .salary(new BigDecimal("45000.00")).bonus(null) + .active(true).company(company3) + .address(Address.builder().street("Elm Ln").city("Guangzhou").state("Guangdong").zipCode("510000").country("China").build()) + .skills(Set.of(skill4)) + .hobbies(List.of()) + .properties(Map.of()) + .connections(Map.of()) + .build()); + + var employee5 = employeeRepository.save(Employee.builder() + .name("Alice Smith").age(32).role(Employee.Role.USER).code("E005") + .salary(new BigDecimal("55000.00")).bonus(new BigDecimal("5500.00")) + .active(true).company(company2) + .address(Address.builder().street("Maple Dr").city("Hangzhou").state("Zhejiang").zipCode("310000").country("China").build()) + .skills(Set.of(skill1, skill4)) + .hobbies(List.of("Swimming", "Music")) + .properties(Map.of("department", "Sales", "level", "Middle")) + .connections(Map.of(Employee.ConnectionType.EMAIL, "alicesmith@example.com")) + .build()); + + formatLog("1. 基本比较操作符 - equal, notEqual, greaterThan, lessThan, greaterThanOrEqualTo, lessThanOrEqualTo"); + var result1 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.equal(root.get(Employee_.name), "Bob"), + cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN), + cb.greaterThan(root.get(Employee_.age), 20), + cb.lessThan(root.get(Employee_.age), 30), + cb.greaterThanOrEqualTo(root.get(Employee_.salary), new BigDecimal("40000.00")), + cb.lessThanOrEqualTo(root.get(Employee_.salary), new BigDecimal("45000.00")) + )); + Assert.isTrue(result1.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1.size())); + + formatLog("2. 区间和集合操作符 - between, in, notIn"); + var result2 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.between(root.get(Employee_.age), 25, 30), + cb.between(root.get(Employee_.age), 40, 50).not(), + root.get(Employee_.age).in(25, 30, 35), + root.get(Employee_.role).in(Employee.Role.USER, Employee.Role.ADMIN), + cb.not(root.get(Employee_.name).in("Charlie", "David")), + root.get(Employee_.name).in("Charlie", "David").not() + )); + Assert.isTrue(result2.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2.size())); + + formatLog("3. 字符串操作符 - like, notLike, lower, upper, length, substring"); + var result3 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.like(root.get(Employee_.name), "A%"), + cb.notLike(root.get(Employee_.name), "C%"), + cb.like(cb.lower(root.get(Employee_.name)), "%ali%"), + cb.like(cb.upper(root.get(Employee_.name)), "%ALI%"), + cb.greaterThan(cb.length(root.get(Employee_.name)), 4), + cb.lessThanOrEqualTo(cb.length(root.get(Employee_.name)), 10), + cb.equal(cb.substring(root.get(Employee_.name), 0, 3), "Ali") + )); + Assert.isTrue(result3.size() == 1, "字符串操作符查询失败 (%d)".formatted(result3.size())); + + formatLog("4. NULL 和布尔操作符 - isNull, isNotNull, isTrue, isFalse"); + var result4 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.isTrue(root.get(Employee_.active)), + cb.isFalse(cb.literal(false)), + cb.isNotNull(root.get(Employee_.bonus)), + cb.isNull(root.get(Employee_.code).in("E999")) + )); + Assert.isTrue(result4.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4.size())); + + formatLog("5. 集合操作符 - isEmpty, isNotEmpty, isMember, size"); + var result5 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.isNotEmpty(root.get(Employee_.skills)), + cb.isEmpty(root.get(Employee_.hobbies)).not(), + cb.isMember("Reading", root.get(Employee_.hobbies)), + cb.isNotMember("Riding", root.get(Employee_.hobbies)), + cb.greaterThan(cb.size(root.get(Employee_.hobbies)), 1), + cb.lessThan(cb.size(root.get(Employee_.skills)), 4) + )); + Assert.isTrue(result5.size() == 2, "集合操作符查询失败 (%d)".formatted(result5.size())); + + formatLog("6. 逻辑操作符 - and, or, not"); + var result6 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.or( + cb.equal(root.get(Employee_.name), "Alice"), + cb.equal(root.get(Employee_.name), "Bob") + ), + cb.not( + cb.or( + cb.equal(root.get(Employee_.name), "Charlie"), + cb.equal(root.get(Employee_.name), "David") ) - ); - Assert.isTrue(employees2.size() == 1, "查询数量错误"); + ) + )); + Assert.isTrue(result6.size() == 2, "逻辑操作符查询失败 (%d)".formatted(result6.size())); - var employees3 = employeeRepository.findAll( - QEmployee.employee.name.isNotNull() - .and(QEmployee.employee.name.eq("Tom")) - .and(QEmployee.employee.name.like("To%")) - .and(QEmployee.employee.name.startsWith("To")) - .and(QEmployee.employee.name.endsWith("om")) - .and(QEmployee.employee.age.lt(200)) - .and(QEmployee.employee.age.goe(0)) - .and(QEmployee.employee.name.in("Tom", "Mike")) - .and(QEmployee.employee.age.between(0, 200)) - .and(QEmployee.employee.company().employees.isNotEmpty()) - .and(QEmployee.employee.company().industries.contains(Industry.MEDIA)) - .and(QEmployee.employee.connections.containsKey(Employee.ConnectionType.EMAIL)) + formatLog("7. Specification 链式调用 - where, and, or"); + var result7 = employeeRepository.findAll( + Specification.where((root, query, cb) -> cb.isTrue(root.get(Employee_.active))) + .and((root, query, cb) -> cb.greaterThan(root.get(Employee_.age), 25)) + .and((root, query, cb) -> cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN)) + .or((root, query, cb) -> cb.equal(root.get(Employee_.name), "Charlie")) + .and((root, query, cb) -> cb.notEqual(root.get(Employee_.name), "Alice Smith")) ); - Assert.isTrue(employees3.size() == 1, "查询数量错误"); + Assert.isTrue(result7.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7.size())); - formatLog("Clean"); - reportRepository.deleteAllInBatch(); + formatLog("8. Join 操作 - join, fetch + 集合 Join + Map Join + 多条件组合"); + var result8 = 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"), + cb.notEqual(root.join(Employee_.company).get(Company_.name), "DataInc"), + // Skills Join 条件 + cb.equal(root.join(Employee_.skills).get(Skill_.name), "Java"), + cb.notEqual(root.join(Employee_.skills).get(Skill_.name), "MySQL"), + // Map Join 条件 + cb.equal(root.join(Employee_.properties).value(), "Senior"), + cb.notEqual(root.join(Employee_.properties).value(), "Junior") + ); + }); + Assert.isTrue(result8.size() == 1, "Join 操作查询失败 (%d)".formatted(result8.size())); + + formatLog("9. 子查询 - 聚合函数 avg, sum, count + 数学运算 sum, coalesce + 多条件组合"); + var result9 = employeeRepository.findAll((root, query, cb) -> { + var avgSalarySubquery = query.subquery(Double.class); + var avgSubRoot = avgSalarySubquery.from(Employee.class); + avgSalarySubquery.select(cb.avg(avgSubRoot.get(Employee_.salary))); + + var countSubquery = query.subquery(Long.class); + var countSubRoot = countSubquery.from(Employee.class); + countSubquery.select(cb.count(countSubRoot)); + + var salary = root.get(Employee_.salary).as(BigDecimal.class); + var bonus = root.get(Employee_.bonus).as(BigDecimal.class); + var totalCompensation = cb.sum(salary, cb.coalesce(bonus, cb.literal(new BigDecimal("0.00")))); + + return cb.and( + cb.greaterThan(root.get(Employee_.salary).as(Double.class), avgSalarySubquery), + cb.notEqual(cb.literal(5L), countSubquery), + cb.greaterThan(totalCompensation, cb.literal(new BigDecimal("55000.00"))), + cb.lessThan(totalCompensation, cb.literal(new BigDecimal("70000.00"))), + cb.isTrue(root.get(Employee_.active)), + cb.notEqual(root.get(Employee_.name), "David"), + cb.greaterThan(root.get(Employee_.age), 28), + cb.notEqual(root.get(Employee_.role), Employee.Role.USER) + ); + }); + Assert.isTrue(result9.isEmpty(), "子查询(聚合函数)+ 数学运算失败 (%d)".formatted(result9.size())); + + formatLog("10. 排序 - Sort 单字段和多字段 + 多条件组合"); + var result10 = employeeRepository.findAll( + (root, query, cb) -> cb.and( + cb.isTrue(root.get(Employee_.active)), + cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN), + cb.greaterThan(root.get(Employee_.age), 20), + cb.lessThan(root.get(Employee_.salary), new BigDecimal("60000.00")) + ), + Sort.by( + Sort.Order.desc(Employee_.AGE), + Sort.Order.asc(Employee_.NAME) + ) + ); + Assert.isTrue(result10.size() == 3 && result10.get(0).getAge() >= result10.get(1).getAge(), "排序查询失败 (%d)".formatted(result10.size())); + + formatLog("11. 分页 - PageRequest + 多条件组合"); + var page11 = employeeRepository.findAll( + (root, query, cb) -> cb.and( + cb.isTrue(root.get(Employee_.active)), + cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN), + 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)) + ); + Assert.isTrue(page11.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11.getContent().size())); + Assert.isTrue(page11.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11.getTotalElements())); + + formatLog("12. CASE WHEN 条件表达式 + 多条件组合"); + var result12 = employeeRepository.findAll((root, query, cb) -> cb.and( + cb.equal( + cb.selectCase() + .when(cb.greaterThan(root.get(Employee_.age), 30), "Senior") + .when(cb.between(root.get(Employee_.age), 25, 30), "Middle") + .otherwise("Junior"), + "Senior" + ), + cb.notEqual( + cb.selectCase() + .when(cb.greaterThan(root.get(Employee_.age), 30), "Senior") + .when(cb.between(root.get(Employee_.age), 25, 30), "Middle") + .otherwise("Junior"), + "Junior" + ) + )); + Assert.isTrue(result12.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12.size())); + + formatLog("13. 综合多条件查询 - Join + Embedded + 集合 + Map + 日期时间 + 多条件组合"); + var result13 = employeeRepository.findAll((root, query, cb) -> { + query.distinct(true); + return cb.and( + // Company Join 条件 + cb.equal(root.join(Employee_.company).get(Company_.name), "TechCorp"), + cb.notEqual(root.join(Employee_.company).get(Company_.name), "DataInc"), + // Skills Join 条件 + cb.equal(root.join(Employee_.skills).get(Skill_.name), "Java"), + cb.notEqual(root.join(Employee_.skills).get(Skill_.name), "MySQL"), + // Embedded 对象条件 + cb.equal(root.get(Employee_.address).get(Address_.city), "Beijing"), + cb.notEqual(root.get(Employee_.address).get(Address_.city), "Shanghai"), + // 集合条件 + cb.isNotEmpty(root.get(Employee_.skills)), + cb.not(cb.isEmpty(root.get(Employee_.hobbies))), + // Map 条件 + cb.isNotEmpty(root.get(Employee_.properties)), + // 日期时间字段查询 + cb.isNotNull(root.get(Employee_.createdTime)), + cb.isNotNull(root.get(Employee_.modifiedTime)), + // 其他条件 + cb.isTrue(root.get(Employee_.active)), + cb.notEqual(root.get(Employee_.name), "Alice Smith"), + cb.greaterThan(root.get(Employee_.age), 25), + cb.notEqual(root.get(Employee_.role), Employee.Role.USER) + ); + }); + Assert.isTrue(result13.size() == 1 && result13.get(0).getName().equals("Alice"), "综合多条件查询失败 (%d)".formatted(result13.size())); + + formatLog("清理测试数据"); employeeRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } private void testNative() { - formatLog("Added"); - var company1 = companyRepository.save(Company.builder().name(randomString(5)).members(randomInt(100)).build()); - var company2 = companyRepository.save(Company.builder().name(randomString(5)).members(randomInt(100)).build()); - var company3 = companyRepository.save(Company.builder().name(randomString(5)).members(randomInt(100)).build()); - var employee1 = employeeRepository.save(Employee.builder().name(randomString(10)).age(randomInt(100)).role(Employee.Role.USER).company(company1).build()); - var employee2 = employeeRepository.save(Employee.builder().name(randomString(10)).age(randomInt(100)).role(Employee.Role.USER).company(company2).build()); - var employee3 = employeeRepository.save(Employee.builder().name(randomString(10)).age(randomInt(100)).role(Employee.Role.USER).company(company3).build()); - formatLog("HQL Query"); - var list = employeeRepository.findAllEmployeeWithCompanyName(); - Assert.isTrue(list.size() == 3, "数量错误"); - - formatLog("SQL Query"); - var list_native = employeeRepository.findAllEmployeeWithCompanyNameNative(); - Assert.isTrue(list_native.size() == 3, "数量错误"); - - formatLog("Clean"); - employeeRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); } } diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/controller/ReportController.java b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/controller/ReportController.java index 701d865..8d67d89 100644 --- a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/controller/ReportController.java +++ b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/controller/ReportController.java @@ -1,6 +1,5 @@ package com.lanyuanxiaoyao.service.template.database.jpa.controller; -import com.lanyuanxiaoyao.service.template.database.common.test.entity.Level; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Report; import com.lanyuanxiaoyao.service.template.database.jpa.service.EmployeeService; import com.lanyuanxiaoyao.service.template.database.jpa.service.ReportService; @@ -64,7 +63,7 @@ public class ReportController extends SimpleControllerSupport industries = new HashSet<>(); - - @OneToMany(mappedBy = "company") - @ToString.Exclude - private Set employees; } diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Employee.java b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Employee.java index 8d85203..dd82752 100644 --- a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Employee.java +++ b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Employee.java @@ -1,20 +1,33 @@ package com.lanyuanxiaoyao.service.template.database.jpa.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.ConstraintMode; import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.ForeignKey; +import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; import jakarta.persistence.MapKeyEnumerated; +import jakarta.persistence.OrderColumn; import jakarta.persistence.Table; +import jakarta.persistence.Version; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -22,8 +35,10 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.experimental.FieldNameConstants; +import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Formula; import org.hibernate.annotations.SoftDelete; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -39,25 +54,82 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; @DynamicUpdate @DynamicInsert @EntityListeners(AuditingEntityListener.class) -@Table(comment = "员工") +@Table( + comment = "员工", + indexes = { + @Index(name = "idx_employee_name", columnList = "name"), + @Index(name = "idx_employee_salary", columnList = "salary"), + @Index(name = "idx_employee_active", columnList = "active") + } +) public class Employee extends SimpleEntity { - @Column(nullable = false, comment = "名称") + @Column(nullable = false, length = 100, comment = "名称") private String name; + @Column(nullable = false, comment = "年龄") private Integer age; + @Column(nullable = false, comment = "角色") @Enumerated(EnumType.STRING) private Role role; + @Column(unique = true, length = 50, comment = "工号") + private String code; + + @Column(nullable = false, comment = "薪资") + private BigDecimal salary; + + @Column(precision = 19, scale = 4, comment = "奖金") + private BigDecimal bonus; + + @Column(comment = "是否激活") + @ColumnDefault("true") + private Boolean active; + + @Lob + @Column(comment = "简历(大文本)") + private String resume; + + @Version + private Long version; + + @Column(insertable = false, updatable = false) + @Formula("salary + COALESCE(bonus, 0)") + private BigDecimal earnings; + + @Embedded + private Address address; + @ManyToOne @JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @ToString.Exclude private Company company; + @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @ToString.Exclude + @Builder.Default + private Set skills = new HashSet<>(); + + @ElementCollection + @JoinTable(joinColumns = @JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))) + @Column(comment = "兴趣") + @OrderColumn + @ToString.Exclude + @Builder.Default + private List hobbies = new ArrayList<>(); + + @ElementCollection + @JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Column(comment = "属性") + @Builder.Default + private Map properties = new HashMap<>(); + @ElementCollection @JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @MapKeyEnumerated(EnumType.STRING) @Column(nullable = false) + @Builder.Default private Map connections = new HashMap<>(); public enum Role { diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Report.java b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Report.java index 7441e0a..05e4cf6 100644 --- a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Report.java +++ b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Report.java @@ -1,6 +1,5 @@ package com.lanyuanxiaoyao.service.template.database.jpa.entity; -import com.lanyuanxiaoyao.service.template.database.common.test.entity.Level; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -34,6 +33,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Table(comment = "报告") public class Report extends SimpleEntity { @Column(nullable = false, comment = "分数") + @Builder.Default private Double score = 0.0; @Column(nullable = false, comment = "等级") @Enumerated(EnumType.STRING) @@ -41,4 +41,8 @@ public class Report extends SimpleEntity { @Column(nullable = false, comment = "员工 ID") private Long employeeId; + + public enum Level { + A, B, C, D, E + } } diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Skill.java b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Skill.java new file mode 100644 index 0000000..b65ad01 --- /dev/null +++ b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/entity/Skill.java @@ -0,0 +1,38 @@ +package com.lanyuanxiaoyao.service.template.database.jpa.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.SoftDelete; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Setter +@Getter +@ToString(callSuper = true) +@FieldNameConstants +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Entity +@SoftDelete +@DynamicUpdate +@DynamicInsert +@EntityListeners(AuditingEntityListener.class) +@Table +public class Skill extends SimpleEntity { + @Column(nullable = false, length = 100, unique = true, comment = "技能名称") + private String name; + + @Column(length = 500, comment = "技能描述") + private String description; +} diff --git a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/repository/EmployeeRepository.java b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/repository/EmployeeRepository.java index ac052f8..826258c 100644 --- a/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/repository/EmployeeRepository.java +++ b/spring-boot-service-template-database/spring-boot-service-template-database-jpa/src/test/java/com/lanyuanxiaoyao/service/template/database/jpa/repository/EmployeeRepository.java @@ -1,12 +1,9 @@ package com.lanyuanxiaoyao.service.template.database.jpa.repository; import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee; -import com.lanyuanxiaoyao.service.template.database.jpa.entity.vo.EmployeeWithCompanyName; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.EntityGraph; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @SuppressWarnings("NullableProblems") @@ -15,10 +12,4 @@ public interface EmployeeRepository extends SimpleRepository { @EntityGraph(attributePaths = {"company"}) @Override Optional findOne(Specification specification); - - @Query(value = "select e.name, c.name, e.age, e.role from employee e, company c where e.company_id = c.id and c.deleted = false and e.deleted = false", nativeQuery = true) - List findAllEmployeeWithCompanyNameNative(); - - @Query("select new com.lanyuanxiaoyao.service.template.database.jpa.entity.vo.EmployeeWithCompanyName(employee.name, employee.company.name, employee.age, cast(employee.role as string)) from Employee employee") - List findAllEmployeeWithCompanyName(); }