问题
I have a basic "Department"/"Employee" example.
Full example here (in master branch) : https://github.com/granadacoder/jpa-simple-example-one.git
If you setup 4 environment variables (listed in the README), the code is runnable/debuggable.
For my "findAll()" method, I am trying to only bring back the scalars of the Department entity. (key and name). Aka, I do NOT want any child Employees to be tacked onto the Department .. when I do a findAll().
I have tried using an name EntityGraph, but it is not working (I still get the full object graph)
I am using FetchType.LAZY as well. (which should have been enough IMHO)... And I am not calling (department) .getEmployees at all.
But I took the extra step of defining and using "departmentJustScalarsEntityGraphName".
@EntityGraph("departmentJustScalarsEntityGraphName")
List<Department> findAll();
The above is loading the entire graph (all department scalar AND the employees) and doing it in an N+1 manner. :(
The @EntityGraph should only be loading the key and name for the findAll() method.
If you find the code comment
/* right here, desperately hoping for each Department in the "entities" to NOT have employees hydrated */
you can put the debugger there and see the issue.
Note, that when I do a "find by single" (findById(key) or findDepartmentByDepartmentNameEquals(name))......I DO want the employees. So answers that broad stroke disassociate the Employees are not ideal.
No matter what I've tried, I'm getting the N+1 issue. (The sample seed data has three Departments.)
Hibernate: select department0_.DepartmentKey as departme1_0_, department0_.CreateOffsetDateTime as createof2_0_, department0_.DepartmentName as departme3_0_ from DepartmentTable department0_
Hibernate: select employees0_.DepartmentForeignKey as departme6_1_0_, employees0_.EmployeeKey as employee1_1_0_, employees0_.EmployeeKey as employee1_1_1_, employees0_.CreateOffsetDateTime as createof2_1_1_, employees0_.FirstName as firstnam3_1_1_, employees0_.LastName as lastname4_1_1_, employees0_.DepartmentForeignKey as departme6_1_1_, employees0_.Ssn as ssn5_1_1_ from EmployeeTable employees0_ where employees0_.DepartmentForeignKey=?
Hibernate: select employees0_.DepartmentForeignKey as departme6_1_0_, employees0_.EmployeeKey as employee1_1_0_, employees0_.EmployeeKey as employee1_1_1_, employees0_.CreateOffsetDateTime as createof2_1_1_, employees0_.FirstName as firstnam3_1_1_, employees0_.LastName as lastname4_1_1_, employees0_.DepartmentForeignKey as departme6_1_1_, employees0_.Ssn as ssn5_1_1_ from EmployeeTable employees0_ where employees0_.DepartmentForeignKey=?
Hibernate: select employees0_.DepartmentForeignKey as departme6_1_0_, employees0_.EmployeeKey as employee1_1_0_, employees0_.EmployeeKey as employee1_1_1_, employees0_.CreateOffsetDateTime as createof2_1_1_, employees0_.FirstName as firstnam3_1_1_, employees0_.LastName as lastname4_1_1_, employees0_.DepartmentForeignKey as departme6_1_1_, employees0_.Ssn as ssn5_1_1_ from EmployeeTable employees0_ where employees0_.DepartmentForeignKey=?
Here are the main code components:
Department.java
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.mycompany.organizationdemo.domain.constants.OrmConstants;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedAttributeNode;
import javax.persistence.NamedEntityGraph;
import javax.persistence.NamedEntityGraphs;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.LinkedHashSet;
import java.util.Set;
@Entity
@NamedEntityGraphs({
@NamedEntityGraph(name = "departmentJustScalarsEntityGraphName", attributeNodes = {
@NamedAttributeNode("departmentKey"),
@NamedAttributeNode("departmentName")})
})
@Table(name = "DepartmentTable")
public class Department implements Serializable {
@Id
@Column(name = "DepartmentKey", unique = true)
@GeneratedValue(strategy = GenerationType.AUTO)
private long departmentKey;
@Column(name = "DepartmentName", unique = true)
private String departmentName;
@Column(name = "CreateOffsetDateTime", columnDefinition = OrmConstants.OffsetDateTimeColumnDefinition)
private OffsetDateTime createOffsetDateTime;
//region Navigation
@OneToMany(
mappedBy = "parentDepartment",
cascade = CascadeType.REMOVE,
orphanRemoval = true,
fetch = FetchType.LAZY /* Lazy or Eager here */
)
private Set<Employee> employees = new LinkedHashSet<>();
//endregion
public long getDepartmentKey() {
return departmentKey;
}
public void setDepartmentKey(long departmentKey) {
this.departmentKey = departmentKey;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public OffsetDateTime getCreateOffsetDateTime() {
return createOffsetDateTime;
}
public void setCreateOffsetDateTime(OffsetDateTime createOffsetDateTime) {
this.createOffsetDateTime = createOffsetDateTime;
}
//region Navigation
public Set<Employee> getEmployees() {
return employees;
}
public void setEmployees(Set<Employee> employees) {
this.employees = employees;
}
//endregion
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Department that = (Department) o;
return new org.apache.commons.lang3.builder.EqualsBuilder()
.append(departmentKey, that.departmentKey)
.append(departmentName, that.departmentName)
.isEquals();
}
@Override
public int hashCode() {
return new org.apache.commons.lang3.builder.HashCodeBuilder(17, 37)
.append(departmentKey)
.append(departmentName)
.toHashCode();
}
}
Employee.java
import com.mycompany.organizationdemo.domain.constants.OrmConstants;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.io.Serializable;
import java.time.OffsetDateTime;
@Entity
@Table(name = "EmployeeTable")
public class Employee implements Serializable {
@Id
@Column(name = "EmployeeKey", unique = true)
@GeneratedValue(strategy = GenerationType.AUTO)
private long employeeKey;
@Column(name = "Ssn")
private String ssn;
@Column(name = "LastName")
private String lastName;
@Column(name = "FirstName")
private String firstName;
@Column(name = "CreateOffsetDateTime", columnDefinition = OrmConstants.OffsetDateTimeColumnDefinition)
private OffsetDateTime createOffsetDateTime;
//region Navigation
@ManyToOne(fetch = FetchType.LAZY, targetEntity = Department.class)//, cascade = CascadeType.REMOVE)
@JoinColumn(name = "DepartmentForeignKey")
private Department parentDepartment;
//endregion
public long getEmployeeKey() {
return employeeKey;
}
public void setEmployeeKey(long departmentKey) {
this.employeeKey = departmentKey;
}
public String getSsn() {
return ssn;
}
public void setSsn(String ssn) {
this.ssn = ssn;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public OffsetDateTime getCreateOffsetDateTime() {
return createOffsetDateTime;
}
public void setCreateOffsetDateTime(OffsetDateTime createOffsetDateTime) {
this.createOffsetDateTime = createOffsetDateTime;
}
//region Navigation
public Department getParentDepartment() {
return parentDepartment;
}
public void setParentDepartment(Department parentDepartment) {
this.parentDepartment = parentDepartment;
}
//endregion
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return new EqualsBuilder()
.append(employeeKey, employee.employeeKey)
.append(ssn, employee.ssn)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(employeeKey)
.append(ssn)
.toHashCode();
}
}
Spring JPA
import com.mycompany.organizationdemo.domain.entities.Department;
import com.mycompany.organizationdemo.domaindatalayer.interfaces.IDepartmentRepository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import javax.transaction.Transactional;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public interface DepartmentJpaRepository extends JpaRepository<Department, Long>, IDepartmentRepository {
@EntityGraph("departmentJustScalarsEntityGraphName")
List<Department> findAll();
@Query("SELECT d FROM Department d LEFT JOIN FETCH d.employees WHERE d.departmentName = :departmentName") /* this works because departmentName is a UNIQUE constraint...otherwise it might give back duplicate parents (Departments) */
Optional<Department> findDepartmentByDepartmentNameEquals(@Param("departmentName") String departmentName);
/* note the below, this is "lookup strategy". see https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods */
Collection<Department> findByCreateOffsetDateTimeBefore(OffsetDateTime zdt);
//@Query("SELECT d FROM Department d LEFT JOIN FETCH d.employees WHERE d.departmentKey IN ?1") /* here a Query will bring back repeat parent (Department) rows */
@EntityGraph(attributePaths = {"employees"})
Collection<Department> findDepartmentByDepartmentKeyIn(Set<Long> departmentKeys);
@Modifying
@Transactional
int deleteDepartmentByDepartmentKey(long departmentKey); /* suffers from N+1 problem */
}
and the repository (plain jane) interface
import com.mycompany.organizationdemo.domain.entities.Department;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public interface IDepartmentRepository {
List<Department> findAll();
Optional<Department> findById(long key);
Optional<Department> findDepartmentByDepartmentNameEquals(String departmentName);
Collection<Department> findByCreateOffsetDateTimeBefore(OffsetDateTime zdt);
Collection<Department> findDepartmentByDepartmentKeyIn(Set<Long> departmentKeys);
Department save(Department item);
int deleteDepartmentByDepartmentKey(long departmentKey);
}
I have found this:
Spring Data JPARepository: How to conditionally fetch children entites
and this:
https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm
Spring Boot is fairly new. (from gradle settings in the project)
spring_plugin_version = '2.2.6.RELEASE'
springBootVersion = '2.2.6.RELEASE'
slf4jVersion = "1.7.25"
javaxInjectVersion = "1"
javaxPersistenceApiVersion = "2.2"
junitVersion = "4.12"
mockitoVersion = "3.3.0"
jacksonAnnotationsVersion = "2.11.0"
modelMapperVersion = "2.3.7"
commonsLangVersion = '3.7'
PS
I'm also using the "convert to Dto" trick (as seen here) : https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application but the Department(orm-entity) is already hydrated by that point. :(
来源:https://stackoverflow.com/questions/62162186/why-does-spring-jpa-ignore-javax-persistence-fetchtype-lazy-and-javax-persistenc