1
0

feat(jpa): 重新设计specification查询的例子

This commit is contained in:
2026-01-22 15:42:34 +08:00
parent 53d94c5f4c
commit 93eded44cb
11 changed files with 427 additions and 173 deletions

View File

@@ -132,26 +132,6 @@ public class AbstractTestApplication {
); );
} }
protected Map<String, Object> randomEmployee(Long companyId) {
return randomEmployee(companyId, randomString(10));
}
protected Map<String, Object> randomEmployee(Long companyId, String name) {
return Map.of(
"name", name,
"age", randomInt(100),
"companyId", companyId
);
}
protected Map<String, Object> randomReport(Long employeeId) {
return Map.of(
"score", randomDouble(200),
"level", randomChar("ABCDE"),
"employeeId", employeeId
);
}
protected String randomString(String prefix, int length) { protected String randomString(String prefix, int length) {
return prefix + randomString(length); return prefix + randomString(length);
} }

View File

@@ -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,
}

View File

@@ -1,5 +0,0 @@
package com.lanyuanxiaoyao.service.template.database.common.test.entity;
public enum Level {
A, B, C, D, E
}

View File

@@ -2,17 +2,18 @@ package com.lanyuanxiaoyao.service.template.database.jpa;
import com.blinkfox.fenix.EnableFenix; import com.blinkfox.fenix.EnableFenix;
import com.lanyuanxiaoyao.service.template.database.common.test.AbstractTestApplication; 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.jpa.entity.Address;
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.Company; 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.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.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.Report; 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.CompanyRepository;
import com.lanyuanxiaoyao.service.template.database.jpa.repository.EmployeeRepository; import com.lanyuanxiaoyao.service.template.database.jpa.repository.EmployeeRepository;
import com.lanyuanxiaoyao.service.template.database.jpa.repository.ReportRepository; import com.lanyuanxiaoyao.service.template.database.jpa.repository.ReportRepository;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -22,7 +23,10 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener; 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.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@Slf4j @Slf4j
@@ -30,6 +34,7 @@ import org.springframework.util.Assert;
@SpringBootApplication @SpringBootApplication
@EnableFenix @EnableFenix
@EnableJpaAuditing @EnableJpaAuditing
@Transactional
public class TestApplication extends AbstractTestApplication { public class TestApplication extends AbstractTestApplication {
private final CompanyRepository companyRepository; private final CompanyRepository companyRepository;
private final EmployeeRepository employeeRepository; private final EmployeeRepository employeeRepository;
@@ -43,7 +48,7 @@ public class TestApplication extends AbstractTestApplication {
public void runTests() { public void runTests() {
testCrud(); testCrud();
testDelete(); testDelete();
testSpecification(); testQuery();
testNative(); testNative();
System.exit(0); System.exit(0);
@@ -76,110 +81,280 @@ public class TestApplication extends AbstractTestApplication {
companyRepository.deleteBatchByIds(List.of(cid1, cid2)); companyRepository.deleteBatchByIds(List.of(cid1, cid2));
} }
private void testSpecification() { private void testQuery() {
formatLog("Added"); formatLog("准备 Specification 查询的测试数据");
var company1 = companyRepository.save(Company.builder().name(randomString(5)).members(randomInt(100)).industries(Set.of(Industry.MEDIA, Industry.SERVICE)).build()); var company1 = companyRepository.save(Company.builder().name("TechCorp").members(100).build());
var company2 = 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("DataInc").members(50).build());
var employee1 = employeeRepository.save( var company3 = companyRepository.save(Company.builder().name("CloudSys").members(150).build());
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());
formatLog("Query"); // 准备 Skills 数据
var employees1 = employeeRepository.findAll( var skill1 = Skill.builder().name("Java").description("Java 编程语言").build();
builder -> builder var skill2 = Skill.builder().name("Python").description("Python 编程语言").build();
.andIsNotNull(Employee.Fields.name) var skill3 = Skill.builder().name("Spring").description("Spring 框架").build();
.andEquals(Employee.Fields.name, "Tom") var skill4 = Skill.builder().name("MySQL").description("MySQL 数据库").build();
.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, "查询数量错误");
var employees2 = employeeRepository.findAll( var employee1 = employeeRepository.save(Employee.builder()
(root, query, builder) -> .name("Alice").age(30).role(Employee.Role.ADMIN).code("E001")
builder.and( .salary(new BigDecimal("50000.00")).bonus(new BigDecimal("5000.00"))
builder.isNotNull(root.get(Employee_.name)), .active(true).company(company1)
builder.equal(root.get(Employee_.name), "Tom"), .address(Address.builder().street("Main St").city("Beijing").state("Beijing").zipCode("100000").country("China").build())
builder.like(root.get(Employee_.name), "To%"), .skills(Set.of(skill1, skill3))
builder.lessThan(root.get(Employee_.age), 200), .hobbies(List.of("Reading", "Swimming"))
builder.greaterThanOrEqualTo(root.get(Employee_.age), 0), .properties(Map.of("department", "Engineering", "level", "Senior"))
builder.in(root.get(Employee_.NAME)).value(List.of("Tom", "Mike")), .connections(Map.of(Employee.ConnectionType.EMAIL, "alice@example.com"))
builder.between(root.get(Employee_.age), 0, 200), .build());
builder.isNotEmpty(root.get(Employee_.company).get(Company_.employees)),
builder.isMember(Industry.MEDIA, root.get(Employee_.company).get(Company_.industries)) 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( formatLog("7. Specification 链式调用 - where, and, or");
QEmployee.employee.name.isNotNull() var result7 = employeeRepository.findAll(
.and(QEmployee.employee.name.eq("Tom")) Specification.<Employee>where((root, query, cb) -> cb.isTrue(root.get(Employee_.active)))
.and(QEmployee.employee.name.like("To%")) .and((root, query, cb) -> cb.greaterThan(root.get(Employee_.age), 25))
.and(QEmployee.employee.name.startsWith("To")) .and((root, query, cb) -> cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN))
.and(QEmployee.employee.name.endsWith("om")) .or((root, query, cb) -> cb.equal(root.get(Employee_.name), "Charlie"))
.and(QEmployee.employee.age.lt(200)) .and((root, query, cb) -> cb.notEqual(root.get(Employee_.name), "Alice Smith"))
.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))
); );
Assert.isTrue(employees3.size() == 1, "查询数量错误"); Assert.isTrue(result7.size() == 2, "Specification 链式调用失败 %d".formatted(result7.size()));
formatLog("Clean"); formatLog("8. Join 操作 - join, fetch + 集合 Join + Map Join + 多条件组合");
reportRepository.deleteAllInBatch(); 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(); employeeRepository.deleteAllInBatch();
companyRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch();
} }
private void testNative() { 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();
} }
} }

View File

@@ -1,6 +1,5 @@
package com.lanyuanxiaoyao.service.template.database.jpa.controller; 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.entity.Report;
import com.lanyuanxiaoyao.service.template.database.jpa.service.EmployeeService; import com.lanyuanxiaoyao.service.template.database.jpa.service.EmployeeService;
import com.lanyuanxiaoyao.service.template.database.jpa.service.ReportService; import com.lanyuanxiaoyao.service.template.database.jpa.service.ReportService;
@@ -64,7 +63,7 @@ public class ReportController extends SimpleControllerSupport<Report, ReportCont
public record SaveItem( public record SaveItem(
Long id, Long id,
Double score, Double score,
Level level, Report.Level level,
Long employeeId Long employeeId
) { ) {
} }
@@ -74,7 +73,7 @@ public class ReportController extends SimpleControllerSupport<Report, ReportCont
Long employeeId, Long employeeId,
String employeeName, String employeeName,
Double score, Double score,
Level level Report.Level level
) { ) {
} }
@@ -83,7 +82,7 @@ public class ReportController extends SimpleControllerSupport<Report, ReportCont
Long employeeId, Long employeeId,
String employeeName, String employeeName,
Double score, Double score,
Level level, Report.Level level,
LocalDateTime createdTime, LocalDateTime createdTime,
LocalDateTime modifiedTime LocalDateTime modifiedTime
) { ) {

View File

@@ -0,0 +1,34 @@
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Embeddable
@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Address {
@Column(comment = "街道")
private String street;
@Column(comment = "城市")
private String city;
@Column(comment = "省/州")
private String state;
@Column(comment = "邮政编码")
private String zipCode;
@Column(comment = "国家")
private String country;
}

View File

@@ -1,19 +1,9 @@
package com.lanyuanxiaoyao.service.template.database.jpa.entity; package com.lanyuanxiaoyao.service.template.database.jpa.entity;
import com.lanyuanxiaoyao.service.template.database.common.test.entity.Industry;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners; import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@@ -44,14 +34,4 @@ public class Company extends SimpleEntity {
private String name; private String name;
@Column(nullable = false, comment = "成员数") @Column(nullable = false, comment = "成员数")
private Integer members; private Integer members;
@ElementCollection
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Set<Industry> industries = new HashSet<>();
@OneToMany(mappedBy = "company")
@ToString.Exclude
private Set<Employee> employees;
} }

View File

@@ -1,20 +1,33 @@
package com.lanyuanxiaoyao.service.template.database.jpa.entity; package com.lanyuanxiaoyao.service.template.database.jpa.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.ElementCollection; import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners; import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey; import jakarta.persistence.ForeignKey;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable; import jakarta.persistence.JoinTable;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKeyEnumerated; import jakarta.persistence.MapKeyEnumerated;
import jakarta.persistence.OrderColumn;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@@ -22,8 +35,10 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Formula;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@@ -39,25 +54,82 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@DynamicUpdate @DynamicUpdate
@DynamicInsert @DynamicInsert
@EntityListeners(AuditingEntityListener.class) @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 { public class Employee extends SimpleEntity {
@Column(nullable = false, comment = "名称") @Column(nullable = false, length = 100, comment = "名称")
private String name; private String name;
@Column(nullable = false, comment = "年龄") @Column(nullable = false, comment = "年龄")
private Integer age; private Integer age;
@Column(nullable = false, comment = "角色") @Column(nullable = false, comment = "角色")
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private Role role; 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 @ManyToOne
@JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@ToString.Exclude @ToString.Exclude
private Company company; 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<Skill> skills = new HashSet<>();
@ElementCollection
@JoinTable(joinColumns = @JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)))
@Column(comment = "兴趣")
@OrderColumn
@ToString.Exclude
@Builder.Default
private List<String> hobbies = new ArrayList<>();
@ElementCollection
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@Column(comment = "属性")
@Builder.Default
private Map<String, String> properties = new HashMap<>();
@ElementCollection @ElementCollection
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@MapKeyEnumerated(EnumType.STRING) @MapKeyEnumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
@Builder.Default
private Map<ConnectionType, String> connections = new HashMap<>(); private Map<ConnectionType, String> connections = new HashMap<>();
public enum Role { public enum Role {

View File

@@ -1,6 +1,5 @@
package com.lanyuanxiaoyao.service.template.database.jpa.entity; 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.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners; import jakarta.persistence.EntityListeners;
@@ -34,6 +33,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Table(comment = "报告") @Table(comment = "报告")
public class Report extends SimpleEntity { public class Report extends SimpleEntity {
@Column(nullable = false, comment = "分数") @Column(nullable = false, comment = "分数")
@Builder.Default
private Double score = 0.0; private Double score = 0.0;
@Column(nullable = false, comment = "等级") @Column(nullable = false, comment = "等级")
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@@ -41,4 +41,8 @@ public class Report extends SimpleEntity {
@Column(nullable = false, comment = "员工 ID") @Column(nullable = false, comment = "员工 ID")
private Long employeeId; private Long employeeId;
public enum Level {
A, B, C, D, E
}
} }

View File

@@ -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;
}

View File

@@ -1,12 +1,9 @@
package com.lanyuanxiaoyao.service.template.database.jpa.repository; 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.Employee;
import com.lanyuanxiaoyao.service.template.database.jpa.entity.vo.EmployeeWithCompanyName;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
@@ -15,10 +12,4 @@ public interface EmployeeRepository extends SimpleRepository<Employee> {
@EntityGraph(attributePaths = {"company"}) @EntityGraph(attributePaths = {"company"})
@Override @Override
Optional<Employee> findOne(Specification<Employee> specification); Optional<Employee> findOne(Specification<Employee> 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<EmployeeWithCompanyName> 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<EmployeeWithCompanyName> findAllEmployeeWithCompanyName();
} }