From af03293df73ee3425983ec58901e7c5c029019e9 Mon Sep 17 00:00:00 2001 From: Guwan Date: Thu, 17 Apr 2025 22:38:25 +0800 Subject: [PATCH] cs --- .gitignore | 52 ++ pom.xml | 69 +++ .../com/guwan/backend/JpaTestApplication.java | 14 + .../backend/MergeStrategy/JoinBuilder.java | 570 ++++++++++++++++++ .../com/guwan/backend/jpaTest/Department.java | 22 + .../com/guwan/backend/jpaTest/Employee.java | 29 + .../backend/jpaTest/EmployeeController.java | 57 ++ .../guwan/backend/jpaTest/EmployeeDTO.java | 29 + .../backend/jpaTest/EmployeeRepository.java | 10 + .../backend/jpaTest/EmployeeService.java | 87 +++ .../com/guwan/backend/jpaTest/Project.java | 24 + .../jpaTest/mapper/QueryResultMapper.java | 140 +++++ src/main/resources/application.properties | 18 + 13 files changed, 1121 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/guwan/backend/JpaTestApplication.java create mode 100644 src/main/java/com/guwan/backend/MergeStrategy/JoinBuilder.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/Department.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/Employee.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/EmployeeController.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/EmployeeDTO.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/EmployeeRepository.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/EmployeeService.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/Project.java create mode 100644 src/main/java/com/guwan/backend/jpaTest/mapper/QueryResultMapper.java create mode 100644 src/main/resources/application.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a933ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Compiled class file +*.class + +# Eclipse +.project +.classpath +.settings/ + +# Intellij +*.ipr +*.iml +*.iws +.idea/ + +# Maven +target/ + +# Gradle +build +.gradle + +# Log file +*.log + +# out +**/out/ + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar +*.tar.gz +*.rar +*.pid +*.orig + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Mac +.DS_Store + +*.tmp diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..03fe5e8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.guwan.backend + jpa-test + 0.0.1-SNAPSHOT + jpa-test + Custom JPA Join Query Demo + + + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/JpaTestApplication.java b/src/main/java/com/guwan/backend/JpaTestApplication.java new file mode 100644 index 0000000..da9de07 --- /dev/null +++ b/src/main/java/com/guwan/backend/JpaTestApplication.java @@ -0,0 +1,14 @@ +package com.guwan.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication +@EnableJpaRepositories +public class JpaTestApplication { + + public static void main(String[] args) { + SpringApplication.run(JpaTestApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/MergeStrategy/JoinBuilder.java b/src/main/java/com/guwan/backend/MergeStrategy/JoinBuilder.java new file mode 100644 index 0000000..31764d3 --- /dev/null +++ b/src/main/java/com/guwan/backend/MergeStrategy/JoinBuilder.java @@ -0,0 +1,570 @@ +package com.guwan.backend.MergeStrategy; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.query.QueryUtils; + +import java.lang.reflect.Constructor; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 自定义连表查询构建器 + * 用于构建复杂的多表连接查询,支持指定返回字段和条件 + */ +public class JoinBuilder { + private final Class rootClass; + private final Map, JoinInfo> joins = new HashMap<>(); + private final Map, List> conditions = new HashMap<>(); + private Map, String[]> selectedFields; + private Map namedSelections = new LinkedHashMap<>(); + + private JoinBuilder(Class rootClass) { + this.rootClass = rootClass; + } + + public static JoinBuilder from(Class rootClass) { + return new JoinBuilder<>(rootClass); + } + + /** + * 添加连接表 + * @param joinClass 要连接的表实体类 + * @param sourceField 主表的外键字段 + * @param targetField 连接表的目标字段 + */ + public JoinBuilder join(Class joinClass, String sourceField, String targetField) { + joins.put(joinClass, new JoinInfo(sourceField, targetField)); + return this; + } + + /** + * 添加连接表(使用方法引用) + * @param joinClass 要连接的表实体类 + * @param sourceFieldGetter 主表外键字段的getter方法引用 + * @param targetFieldGetter 连接表目标字段的getter方法引用 + */ + public JoinBuilder join(Class joinClass, + Function sourceFieldGetter, + Function targetFieldGetter) { + String sourceField = getFieldNameFromGetter(sourceFieldGetter); + String targetField = getFieldNameFromGetter(targetFieldGetter); + return join(joinClass, sourceField, targetField); + } + + /** + * 添加等值条件 + * @param entityClass 实体类 + * @param fieldName 字段名 + * @param value 字段值 + */ + public JoinBuilder equal(Class entityClass, String fieldName, V value) { + if (!conditions.containsKey(entityClass)) { + conditions.put(entityClass, new ArrayList<>()); + } + conditions.get(entityClass).add(new Condition(fieldName, value, Operator.EQUAL)); + return this; + } + + /** + * 添加等值条件(使用方法引用) + * @param entityClass 实体类 + * @param fieldGetter 字段的getter方法引用 + * @param value 字段值 + */ + public JoinBuilder equal(Class entityClass, Function fieldGetter, V value) { + String fieldName = getFieldNameFromGetter(fieldGetter); + return equal(entityClass, fieldName, value); + } + + /** + * 选择返回的字段 + * @param selectedFields 每个实体类需要返回的字段映射 + */ + public JoinBuilder selectFields(Map, String[]> selectedFields) { + this.selectedFields = selectedFields; + return this; + } + + /** + * 选择返回的字段(使用方法引用) + * @param fieldGetters 方法引用列表 + */ + @SafeVarargs + public final JoinBuilder select(Class entityClass, Function... fieldGetters) { + if (selectedFields == null) { + selectedFields = new HashMap<>(); + } + + String[] fieldNames = Arrays.stream(fieldGetters) + .map(this::getFieldNameFromGetter) + .toArray(String[]::new); + + selectedFields.put(entityClass, fieldNames); + return this; + } + + /** + * 添加一个具有别名的字段选择(优先级高于select方法) + * @param alias 字段别名 + * @param entityClass 实体类 + * @param fieldGetter 字段的getter方法引用 + */ + public JoinBuilder selectAs(String alias, Class entityClass, Function fieldGetter) { + String fieldName = getFieldNameFromGetter(fieldGetter); + + // 存储字段信息,稍后在configureSelections处理 + if (selectedFields == null) { + selectedFields = new HashMap<>(); + } + if (!selectedFields.containsKey(entityClass)) { + selectedFields.put(entityClass, new String[0]); + } + namedSelections.put(alias, new SelectionInfo(entityClass, fieldName)); + + return this; + } + + /** + * 添加一个具有别名的字段选择(字符串版本) + * @param alias 字段别名 + * @param entityClass 实体类 + * @param fieldName 字段名称 + */ + public JoinBuilder selectAs(String alias, Class entityClass, String fieldName) { + // 存储字段信息,稍后在configureSelections处理 + if (selectedFields == null) { + selectedFields = new HashMap<>(); + } + if (!selectedFields.containsKey(entityClass)) { + selectedFields.put(entityClass, new String[0]); + } + namedSelections.put(alias, new SelectionInfo(entityClass, fieldName)); + + return this; + } + + /** + * 设置实体间字段映射关系的别名,以便对应DTO字段 + * @param dtoClass DTO类 + * @param mappings 字段映射 + */ + @SafeVarargs + public final JoinBuilder selectDto(Class dtoClass, FieldMapping... mappings) { + for (FieldMapping mapping : mappings) { + String dtoFieldName = getFieldNameFromGetter(mapping.getDtoFieldGetter()); + String entityFieldName = getFieldNameFromGetter(mapping.getEntityFieldGetter()); + Class entityClass = mapping.getEntityClass(); + + // 存储字段信息,稍后在configureSelections处理 + if (selectedFields == null) { + selectedFields = new HashMap<>(); + } + if (!selectedFields.containsKey(entityClass)) { + selectedFields.put(entityClass, new String[0]); + } + namedSelections.put(dtoFieldName, new SelectionInfo(entityClass, entityFieldName)); + } + return this; + } + + /** + * 构建规范查询 + * @return Specification 查询规范 + */ + public Specification build() { + return (root, query, criteriaBuilder) -> { + // 创建连接关系 + Map, From> joinMap = createJoins(root, query); + + // 应用连表条件和其他条件 + List predicates = applyAllConditions(root, joinMap, criteriaBuilder); + + // 设置查询结果投影 + configureSelections(root, joinMap, query); + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + } + + /** + * 执行查询并返回结果 + * @param entityManager EntityManager实例 + * @return 查询结果列表 + */ + public List execute(EntityManager entityManager) { + // 创建CriteriaBuilder + CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + // 创建CriteriaQuery + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Object[].class); + + // 设置根实体 + Root root = criteriaQuery.from(rootClass); + + // 创建连接关系 + Map, From> fromMap = createJoins(root, criteriaQuery); + + // 应用所有条件,包括表之间的连接条件和额外查询条件 + List predicates = applyAllConditions(root, fromMap, criteriaBuilder); + + // 添加条件到查询 + if (!predicates.isEmpty()) { + criteriaQuery.where(criteriaBuilder.and(predicates.toArray(new Predicate[0]))); + } + + // 设置查询结果投影 + configureSelections(root, fromMap, criteriaQuery); + + // 执行查询并返回结果 + return entityManager.createQuery(criteriaQuery).getResultList(); + } + + /** + * 创建连接 - 修改为使用from而不是join + */ + private Map, From> createJoins(Root root, CriteriaQuery query) { + Map, From> fromMap = new HashMap<>(); + fromMap.put(rootClass, root); + + // 对于每个需要连接的表,创建一个单独的From + for (Map.Entry, JoinInfo> entry : joins.entrySet()) { + Class joinClass = entry.getKey(); + From joinRoot = query.from(joinClass); + fromMap.put(joinClass, joinRoot); + } + + return fromMap; + } + + /** + * 应用所有条件,包括表之间的连接条件和额外查询条件 + */ + private List applyAllConditions(Root root, Map, From> fromMap, CriteriaBuilder criteriaBuilder) { + List predicates = new ArrayList<>(); + + // 首先添加表连接条件 + for (Map.Entry, JoinInfo> entry : joins.entrySet()) { + Class joinClass = entry.getKey(); + JoinInfo joinInfo = entry.getValue(); + + if (fromMap.containsKey(joinClass)) { + From joinFrom = fromMap.get(joinClass); + Predicate joinCondition = criteriaBuilder.equal( + root.get(joinInfo.sourceField), + joinFrom.get(joinInfo.targetField) + ); + predicates.add(joinCondition); + } + } + + // 然后添加普通查询条件 + for (Class entityClass : conditions.keySet()) { + for (Condition condition : conditions.get(entityClass)) { + if (fromMap.containsKey(entityClass)) { + From from = fromMap.get(entityClass); + predicates.add(applyCondition(condition, from, criteriaBuilder)); + } + } + } + + return predicates; + } + + /** + * 执行查询并直接映射到DTO对象 + * @param entityManager EntityManager实例 + * @param dtoClass DTO类 + * @param DTO类型 + * @return DTO对象列表 + */ + public List executeAndMap(EntityManager entityManager, Class dtoClass) { + List results = execute(entityManager); + return mapToDto(results, dtoClass); + } + + /** + * 将查询结果映射到DTO对象 + * @param results 查询结果 + * @param dtoClass DTO类 + * @param DTO类型 + * @return DTO对象列表 + */ + public List mapToDto(List results, Class dtoClass) { + try { + // 获取所有字段名(顺序与查询结果一致) + List fieldNames = new ArrayList<>(namedSelections.keySet()); + + // 获取构造函数 + Class[] paramTypes = new Class[fieldNames.size()]; + Arrays.fill(paramTypes, Object.class); + Constructor constructor = dtoClass.getDeclaredConstructor(paramTypes); + + // 映射结果 + return results.stream() + .map(row -> { + try { + return constructor.newInstance(row); + } catch (Exception e) { + throw new RuntimeException("Failed to map result to DTO: " + e.getMessage(), e); + } + }) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to create DTO mapper: " + e.getMessage(), e); + } + } + + /** + * 配置查询结果投影 + */ + private void configureSelections(Root root, Map, From> fromMap, CriteriaQuery query) { + if (!namedSelections.isEmpty()) { + // 处理命名选择 + List> selections = new ArrayList<>(); + + for (Map.Entry entry : namedSelections.entrySet()) { + String alias = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof SelectionInfo) { + SelectionInfo info = (SelectionInfo) value; + Path path; + + if (fromMap.containsKey(info.entityClass)) { + path = fromMap.get(info.entityClass).get(info.fieldName); + selections.add(path.alias(alias)); + } + } + } + + if (!selections.isEmpty()) { + query.multiselect(selections); + return; + } + } + + // 如果没有命名选择或处理失败,回退到原来的处理方式 + if (selectedFields != null && !selectedFields.isEmpty()) { + List> selections = new ArrayList<>(); + + // 添加根实体的选择字段 + if (selectedFields.containsKey(rootClass)) { + for (String field : selectedFields.get(rootClass)) { + selections.add(root.get(field)); + } + } + + // 添加关联实体的选择字段 + for (Class entityClass : selectedFields.keySet()) { + if (entityClass != rootClass && fromMap.containsKey(entityClass)) { + From from = fromMap.get(entityClass); + for (String field : selectedFields.get(entityClass)) { + selections.add(from.get(field)); + } + } + } + + if (!selections.isEmpty()) { + query.multiselect(selections); + } + } + } + + private Predicate applyCondition(Condition condition, From from, CriteriaBuilder criteriaBuilder) { + switch (condition.operator) { + case EQUAL: + return criteriaBuilder.equal(from.get(condition.fieldName), condition.value); + // 可以添加更多操作符支持,如LIKE, IN, GREATER_THAN等 + default: + throw new UnsupportedOperationException("Unsupported operator: " + condition.operator); + } + } + + /** + * 从getter方法引用中提取属性名 + */ + private String getFieldNameFromGetter(Function getter) { + // 直接尝试从方法引用的toString中获取 + try { + String methodRef = getter.toString(); + if (methodRef.contains("::")) { + // 处理方法引用格式: com.example.Entity::getName + String methodName = methodRef.substring(methodRef.lastIndexOf("::") + 2); + return extractFieldNameFromMethod(methodName); + } else if (methodRef.contains("->")) { + // 处理Lambda表达式格式: (Entity e) -> e.getName() + String methodCall = methodRef.substring(methodRef.indexOf("->") + 2).trim(); + String methodName = methodCall.substring(methodCall.lastIndexOf(".") + 1); + if (methodName.endsWith(")")) { + methodName = methodName.substring(0, methodName.indexOf("(")); + } + return extractFieldNameFromMethod(methodName); + } + } catch (Exception ignored) { + // 忽略异常,尝试下一种方法 + } + + try { + String getterName = getter.getClass().getName(); + if (getterName.contains("$$Lambda$")) { + // 处理Lambda表达式 + if (getterName.contains("get")) { + // 尝试从名称中提取字段名 + int getIndex = getterName.lastIndexOf("get"); + if (getIndex >= 0 && getIndex + 3 < getterName.length()) { + String fieldName = getterName.substring(getIndex + 3); + if (fieldName.indexOf('$') > 0) { + fieldName = fieldName.substring(0, fieldName.indexOf('$')); + } + return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); + } + } + } + } catch (Exception ignored) { + // 忽略异常,fallback到基于字符串的方法 + } + + // Fallback到基于字符串的方法 + String methodReference = getter.toString(); + return getFieldNameFromGetterString(methodReference); + } + + /** + * 从getter方法字符串中提取属性名 + */ + private String getFieldNameFromGetterString(String methodReference) { + // 方法引用的格式通常是:类名::get字段名 或 Lambda表达式 + if (methodReference.contains("::")) { + // 处理方法引用格式: com.example.Entity::getName + String methodName = methodReference.substring(methodReference.lastIndexOf("::") + 2); + return extractFieldNameFromMethod(methodName); + } else if (methodReference.contains("->")) { + // 处理Lambda表达式格式: (Entity e) -> e.getName() + String methodCall = methodReference.substring(methodReference.indexOf("->") + 2).trim(); + String methodName = methodCall.substring(methodCall.lastIndexOf(".") + 1); + if (methodName.endsWith(")")) { + methodName = methodName.substring(0, methodName.indexOf("(")); + } + return extractFieldNameFromMethod(methodName); + } + + // 无法识别的格式,返回原始字符串 + return methodReference; + } + + /** + * 从方法名中提取字段名 + */ + private String extractFieldNameFromMethod(String methodName) { + if (methodName.startsWith("get") && methodName.length() > 3) { + // getName -> name (首字母小写) + String fieldName = methodName.substring(3); + return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); + } else if (methodName.startsWith("is") && methodName.length() > 2) { + // isActive -> active (首字母小写) + String fieldName = methodName.substring(2); + return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); + } + return methodName; + } + + /** + * 首字母大写 + */ + private String capitalize(String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + private enum Operator { + EQUAL + // 可以添加更多操作符支持,如LIKE, IN, GREATER_THAN等 + } + + private static class Condition { + private final String fieldName; + private final Object value; + private final Operator operator; + + Condition(String fieldName, Object value, Operator operator) { + this.fieldName = fieldName; + this.value = value; + this.operator = operator; + } + } + + /** + * 连接信息 + */ + private static class JoinInfo { + private final String sourceField; + private final String targetField; + + JoinInfo(String sourceField, String targetField) { + this.sourceField = sourceField; + this.targetField = targetField; + } + } + + /** + * 选择字段信息 + */ + private static class SelectionInfo { + private final Class entityClass; + private final String fieldName; + + SelectionInfo(Class entityClass, String fieldName) { + this.entityClass = entityClass; + this.fieldName = fieldName; + } + } + + /** + * 字段映射定义,用于DTO映射 + */ + public static class FieldMapping { + private final Function dtoFieldGetter; + private final Function entityFieldGetter; + private final Class entityClass; + + private FieldMapping(Function dtoFieldGetter, Function entityFieldGetter, Class entityClass) { + this.dtoFieldGetter = dtoFieldGetter; + this.entityFieldGetter = entityFieldGetter; + this.entityClass = entityClass; + } + + public static FieldMapping of(Function dtoFieldGetter, Function entityFieldGetter) { + // 通过反射或其他方式获取entityClass + Class entityClass = null; + try { + // 尝试从方法引用中提取类信息 + String methodRef = entityFieldGetter.toString(); + if (methodRef.contains("::")) { + String className = methodRef.substring(0, methodRef.indexOf("::")); + entityClass = (Class) Class.forName(className); + } + } catch (Exception e) { + // 忽略异常,entityClass将在运行时确定 + } + + return new FieldMapping<>(dtoFieldGetter, entityFieldGetter, entityClass); + } + + public Function getDtoFieldGetter() { + return dtoFieldGetter; + } + + public Function getEntityFieldGetter() { + return entityFieldGetter; + } + + public Class getEntityClass() { + return entityClass; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/Department.java b/src/main/java/com/guwan/backend/jpaTest/Department.java new file mode 100644 index 0000000..acd634b --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/Department.java @@ -0,0 +1,22 @@ +package com.guwan.backend.jpaTest; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "departments") +@Data +@NoArgsConstructor +public class Department { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/Employee.java b/src/main/java/com/guwan/backend/jpaTest/Employee.java new file mode 100644 index 0000000..bc7a501 --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/Employee.java @@ -0,0 +1,29 @@ +package com.guwan.backend.jpaTest; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "employees") +@Data +@NoArgsConstructor +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Column(name = "department_id") + private Long departmentId; + + @Column(name = "project_id") + private Long projectId; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/EmployeeController.java b/src/main/java/com/guwan/backend/jpaTest/EmployeeController.java new file mode 100644 index 0000000..a3994c9 --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/EmployeeController.java @@ -0,0 +1,57 @@ +package com.guwan.backend.jpaTest; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.sql.ResultSet; +import java.util.List; + +@RestController +@RequestMapping("/api/employees") +public class EmployeeController { + + @Autowired + private EmployeeService employeeService; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @PersistenceContext + private EntityManager entityManager; + + /** + * 获取员工详细信息,包含部门和项目信息 + * 使用Spring JdbcTemplate直接映射SQL结果 + * + * @param departmentName 部门名称过滤条件 + * @param projectDeleted 项目是否已删除的过滤条件 + * @return 员工DTO列表 + */ + @GetMapping("/details") + public List getEmployeeDetails( + @RequestParam(required = false) String departmentName, + @RequestParam(required = false, defaultValue = "false") boolean projectDeleted) { + return employeeService.getEmployeeDetails(departmentName, projectDeleted); + } + + /** + * 根据条件查询员工信息 + * + * @param employeeName 员工名称过滤条件,可选 + * @param departmentName 部门名称过滤条件,可选 + * @return 员工DTO列表 + */ + @GetMapping("/search") + public List searchEmployees( + @RequestParam(required = false) String employeeName, + @RequestParam(required = false) String departmentName) { + return employeeService.findEmployees(employeeName, departmentName); + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/EmployeeDTO.java b/src/main/java/com/guwan/backend/jpaTest/EmployeeDTO.java new file mode 100644 index 0000000..50238b0 --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/EmployeeDTO.java @@ -0,0 +1,29 @@ +package com.guwan.backend.jpaTest; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EmployeeDTO { + private Long employeeId; + private String employeeName; + private String departmentName; + private String projectName; + + /** + * 用于JoinBuilder映射的构造函数 + * @param employeeId 员工ID + * @param employeeName 员工姓名 + * @param departmentName 部门名称 + * @param projectName 项目名称 + */ + public EmployeeDTO(Object employeeId, Object employeeName, Object departmentName, Object projectName) { + this.employeeId = employeeId != null ? Long.valueOf(employeeId.toString()) : null; + this.employeeName = employeeName != null ? employeeName.toString() : null; + this.departmentName = departmentName != null ? departmentName.toString() : null; + this.projectName = projectName != null ? projectName.toString() : null; + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/EmployeeRepository.java b/src/main/java/com/guwan/backend/jpaTest/EmployeeRepository.java new file mode 100644 index 0000000..3662baa --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/EmployeeRepository.java @@ -0,0 +1,10 @@ +package com.guwan.backend.jpaTest; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface EmployeeRepository extends JpaRepository, JpaSpecificationExecutor { + // 自定义查询方法可以在这里添加 +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/EmployeeService.java b/src/main/java/com/guwan/backend/jpaTest/EmployeeService.java new file mode 100644 index 0000000..ec28df8 --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/EmployeeService.java @@ -0,0 +1,87 @@ +package com.guwan.backend.jpaTest; + +import com.guwan.backend.MergeStrategy.JoinBuilder; +import com.guwan.backend.MergeStrategy.JoinBuilder.FieldMapping; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class EmployeeService { + + @Autowired + private EmployeeRepository employeeRepository; + + @PersistenceContext + private EntityManager entityManager; + + /** + * 获取员工详细信息,包含部门和项目信息 + * 使用字符串方式指定字段,并直接映射到DTO + * + * @param departmentName 部门名称过滤条件 + * @param projectDeleted 项目是否已删除的过滤条件 + * @return 员工DTO列表 + */ + public List getEmployeeDetails(String departmentName, boolean projectDeleted) { + // 创建查询构建器,从Employee实体开始 + JoinBuilder builder = JoinBuilder.from(Employee.class); + + // 添加关联,使用字符串指定实体间的关系字段 + builder.join(Department.class, "departmentId", "id"); + builder.join(Project.class, "projectId", "id"); + + // 添加查询条件,使用字符串指定字段 + if (departmentName != null && !departmentName.isEmpty()) { + builder.equal(Department.class, "name", departmentName); + } + builder.equal(Project.class, "deleted", projectDeleted); + + + // 使用selectAs设置字段别名,避免使用FieldMapping + builder.selectAs("employeeId", Employee.class, "id"); + builder.selectAs("employeeName", Employee.class, "name"); + builder.selectAs("departmentName", Department.class, "name"); + builder.selectAs("projectName", Project.class, "name"); + + // 直接执行查询并映射到DTO + return builder.executeAndMap(entityManager, EmployeeDTO.class); + } + + /** + * 使用自定义条件查询员工信息 + * 使用selectAs来指定字段别名对应DTO构造函数参数 + * + * @param employeeName 员工名称过滤条件,可选 + * @param departmentName 部门名称过滤条件,可选 + * @return 员工DTO列表 + */ + public List findEmployees(String employeeName, String departmentName) { + JoinBuilder builder = JoinBuilder.from(Employee.class); + + // 添加关联,使用字符串 + builder.join(Department.class, "departmentId", "id"); + builder.join(Project.class, "projectId", "id"); + + // 添加查询条件,使用字符串 + if (employeeName != null && !employeeName.isEmpty()) { + builder.equal(Employee.class, "name", employeeName); + } + + if (departmentName != null && !departmentName.isEmpty()) { + builder.equal(Department.class, "name", departmentName); + } + + // 方式2:使用selectAs直接指定字段别名 + builder.selectAs("employeeId", Employee.class, "id"); + builder.selectAs("employeeName", Employee.class, "name"); + builder.selectAs("departmentName", Department.class, "name"); + builder.selectAs("projectName", Project.class, "name"); + + // 直接执行查询并映射到DTO + return builder.executeAndMap(entityManager, EmployeeDTO.class); + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/Project.java b/src/main/java/com/guwan/backend/jpaTest/Project.java new file mode 100644 index 0000000..ac9ff5e --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/Project.java @@ -0,0 +1,24 @@ +package com.guwan.backend.jpaTest; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "projects") +@Data +@NoArgsConstructor +public class Project { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private Boolean deleted = false; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/jpaTest/mapper/QueryResultMapper.java b/src/main/java/com/guwan/backend/jpaTest/mapper/QueryResultMapper.java new file mode 100644 index 0000000..6c9deaa --- /dev/null +++ b/src/main/java/com/guwan/backend/jpaTest/mapper/QueryResultMapper.java @@ -0,0 +1,140 @@ +package com.guwan.backend.jpaTest.mapper; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 查询结果映射工具类 + * 用于将原始查询结果转换为更有意义的对象 + */ +public class QueryResultMapper { + + /** + * 将Object[]结果列表映射为指定类型的对象列表 + * @param results 查询结果 + * @param mapper 映射函数 + * @return 映射后的对象列表 + */ + public static List mapToList(List results, Function mapper) { + return results.stream() + .map(row -> mapper.apply(new ResultRow(row))) + .collect(Collectors.toList()); + } + + /** + * 将Object[]结果列表使用命名方式映射为指定类型的对象列表 + * @param results 查询结果 + * @param columnMap 列名到索引的映射 + * @param mapper 映射函数 + * @return 映射后的对象列表 + */ + public static List mapToListWithNames(List results, + Map columnMap, + Function mapper) { + return results.stream() + .map(row -> mapper.apply(new NamedResultRow(row, columnMap))) + .collect(Collectors.toList()); + } + + /** + * 结果行封装,提供类型安全的访问方法 + */ + public static class ResultRow { + private final Object[] row; + + public ResultRow(Object[] row) { + this.row = row; + } + + public Long getLong(int index) { + Object value = getValue(index); + if (value == null) return null; + if (value instanceof Long) return (Long) value; + if (value instanceof Number) return ((Number) value).longValue(); + return Long.valueOf(value.toString()); + } + + public Integer getInteger(int index) { + Object value = getValue(index); + if (value == null) return null; + if (value instanceof Integer) return (Integer) value; + if (value instanceof Number) return ((Number) value).intValue(); + return Integer.valueOf(value.toString()); + } + + public String getString(int index) { + Object value = getValue(index); + return value == null ? null : value.toString(); + } + + public Boolean getBoolean(int index) { + Object value = getValue(index); + if (value == null) return null; + if (value instanceof Boolean) return (Boolean) value; + if (value instanceof Number) return ((Number) value).intValue() != 0; + return Boolean.valueOf(value.toString()); + } + + public Object getValue(int index) { + if (index < 0 || index >= row.length) { + throw new IndexOutOfBoundsException("索引超出范围: " + index); + } + return row[index]; + } + } + + /** + * 命名结果行封装,通过列名访问数据 + */ + public static class NamedResultRow { + private final Object[] row; + private final Map columnMap; + + public NamedResultRow(Object[] row, Map columnMap) { + this.row = row; + this.columnMap = columnMap; + } + + public Long getLong(String columnName) { + Object value = getValue(columnName); + if (value == null) return null; + if (value instanceof Long) return (Long) value; + if (value instanceof Number) return ((Number) value).longValue(); + return Long.valueOf(value.toString()); + } + + public Integer getInteger(String columnName) { + Object value = getValue(columnName); + if (value == null) return null; + if (value instanceof Integer) return (Integer) value; + if (value instanceof Number) return ((Number) value).intValue(); + return Integer.valueOf(value.toString()); + } + + public String getString(String columnName) { + Object value = getValue(columnName); + return value == null ? null : value.toString(); + } + + public Boolean getBoolean(String columnName) { + Object value = getValue(columnName); + if (value == null) return null; + if (value instanceof Boolean) return (Boolean) value; + if (value instanceof Number) return ((Number) value).intValue() != 0; + return Boolean.valueOf(value.toString()); + } + + public Object getValue(String columnName) { + Integer index = columnMap.get(columnName); + if (index == null) { + throw new IllegalArgumentException("列名不存在: " + columnName); + } + if (index < 0 || index >= row.length) { + throw new IndexOutOfBoundsException("索引超出范围: " + index); + } + return row[index]; + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..54fc4d9 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,18 @@ +# MySQL数据库配置 +spring.datasource.url=jdbc:mysql://localhost:3306/jpatest?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true +spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver +spring.datasource.username=root +spring.datasource.password=123456 +spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect + +# JPA配置 +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# 服务器配置 +server.port=8080 + +# 日志级别 +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE \ No newline at end of file