为什么我们需要双向同步的方法



如主题中所述为什么我们需要双向同步方法?它解决了什么真实世界的用例?如果我不使用它们会发生什么

在Hibernate的用户指南中:

无论何时形成双向关联,应用程序开发人员都必须确保双方始终同步。addPhone((和removePhone((是在添加或删除子元素时同步两端的实用方法。

Source-Hibernate用户指南

在Vlad的一篇博客文章中:

然而,我们仍然需要让双方同步,否则,我们会破坏域模型关系的一致性,除非双方都正确同步,否则实体状态转换无法保证工作。

来源-Vlad Mihalcea博客

最后,在Vlad的《高性能Java持久性》一书中,第216页:

对于双向@ManyToMany关联,必须将助手方法添加到更有可能与之交互的实体中。在我们的例子中,根实体是Post,所以辅助方法被添加到Post实体中

然而,如果我使用简单生成的setter,Hibernate似乎也能很好地工作。此外,同步方法可能会导致性能下降。

同步方法:

public void joinProject(ProjectEntity project) {
project.getEmployees().add(this);
this.projects.add(project);
}

生成:

Hibernate:
select
employeeen0_.id as id1_0_0_,
projectent2_.id as id1_2_1_,
teamentity3_.id as id1_3_2_,
employeeen0_.first_name as first_na2_0_0_,
employeeen0_.job_title as job_titl3_0_0_,
employeeen0_.last_name as last_nam4_0_0_,
employeeen0_.team_id as team_id5_0_0_,
projectent2_.budget as budget2_2_1_,
projectent2_.name as name3_2_1_,
projects1_.employee_id as employee1_1_0__,
projects1_.project_id as project_2_1_0__,
teamentity3_.name as name2_3_2_
from
employees.employee employeeen0_
inner join
employees.employee_project projects1_
on employeeen0_.id=projects1_.employee_id
inner join
employees.project projectent2_
on projects1_.project_id=projectent2_.id
inner join
employees.team teamentity3_
on employeeen0_.team_id=teamentity3_.id
where
employeeen0_.id=?
Hibernate:
select
projectent0_.id as id1_2_,
projectent0_.budget as budget2_2_,
projectent0_.name as name3_2_
from
employees.project projectent0_
where
projectent0_.id=?
Hibernate:
select
employees0_.project_id as project_2_1_0_,
employees0_.employee_id as employee1_1_0_,
employeeen1_.id as id1_0_1_,
employeeen1_.first_name as first_na2_0_1_,
employeeen1_.job_title as job_titl3_0_1_,
employeeen1_.last_name as last_nam4_0_1_,
employeeen1_.team_id as team_id5_0_1_
from
employees.employee_project employees0_
inner join
employees.employee employeeen1_
on employees0_.employee_id=employeeen1_.id
where
employees0_.project_id=?
Hibernate:
insert
into
employees.employee_project
(employee_id, project_id)
values
(?, ?)

请注意,在提取项目之后,还为"员工"选择了其他选项。如果我只使用employeeEntity.getProjects().add(projectEntity);,它会生成:

Hibernate:
select
employeeen0_.id as id1_0_0_,
projectent2_.id as id1_2_1_,
teamentity3_.id as id1_3_2_,
employeeen0_.first_name as first_na2_0_0_,
employeeen0_.job_title as job_titl3_0_0_,
employeeen0_.last_name as last_nam4_0_0_,
employeeen0_.team_id as team_id5_0_0_,
projectent2_.budget as budget2_2_1_,
projectent2_.name as name3_2_1_,
projects1_.employee_id as employee1_1_0__,
projects1_.project_id as project_2_1_0__,
teamentity3_.name as name2_3_2_
from
employees.employee employeeen0_
inner join
employees.employee_project projects1_
on employeeen0_.id=projects1_.employee_id
inner join
employees.project projectent2_
on projects1_.project_id=projectent2_.id
inner join
employees.team teamentity3_
on employeeen0_.team_id=teamentity3_.id
where
employeeen0_.id=?
Hibernate:
select
projectent0_.id as id1_2_,
projectent0_.budget as budget2_2_,
projectent0_.name as name3_2_
from
employees.project projectent0_
where
projectent0_.id=?
Hibernate:
insert
into
employees.employee_project
(employee_id, project_id)
values
(?, ?)

不再吸引员工。

完整代码

控制器。

@RestController
@RequestMapping(path = "${application.endpoints.projects}", produces = MediaType.APPLICATION_JSON_VALUE)
@Validated
public class ProjectsEndPoint {

@PostMapping("add-employee")
@ApiOperation("Add employee to project")
public void addEmployeeToProject(@RequestBody @Valid EmployeeProjectRequest request) {
LOGGER.info("Add employee to project. Request: {}", request);
this.projectsService.addEmployeeToProject(request);
}
}

EmployeeProjectRequest。

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public record EmployeeProjectRequest(
@NotNull @Min(0) Long employeeId,
@NotNull @Min(0) Long projectId) {
}

ProjectService。

@Service
public class ProjectsService {
private final ProjectRepo projectRepo;
private final EmployeeRepo repo;
public ProjectsService(ProjectRepo projectRepo, EmployeeRepo repo) {
this.projectRepo = projectRepo;
this.repo = repo;
}
@Transactional
public void addEmployeeToProject(EmployeeProjectRequest request) {
var employeeEntity = this.repo.getEmployee(request.employeeId())
.orElseThrow(() -> new NotFoundException("Employee with id: %d does not exist".formatted(request.employeeId())));
var projectEntity = this.projectRepo.getProject(request.projectId())
.orElseThrow(() -> new NotFoundException("Project with id: %d does not exists".formatted(request.projectId())));
//This line can be changed with employeeEntity.joinProject(projectEntity);
employeeEntity.getProjects().add(projectEntity);
}
}

项目报告。

@Repository
public class ProjectRepo {
private final EntityManager em;
public ProjectRepo(EntityManager em) {
this.em = em;
}
public Optional<ProjectEntity> getProject(Long id) {
var result = this.em.createQuery("SELECT p FROM ProjectEntity p where p.id = :id", ProjectEntity.class)
.setParameter("id", id)
.getResultList();
return RepoUtils.fromResultListToOptional(result);
}
}

EmployeeRepo。

@Repository
public class EmployeeRepo {
private final EntityManager em;
public EmployeeRepo(EntityManager em) {
this.em = em;
}
public Optional<EmployeeEntity> getEmployee(Long id) {
var employees = this.em.createQuery("""
SELECT e FROM EmployeeEntity e
JOIN FETCH e.projects p
JOIN FETCH e.team t
WHERE e.id = :id""", EmployeeEntity.class)
.setParameter("id", id)
.getResultList();
return Optional.ofNullable(employees.isEmpty() ? null : employees.get(0));
}
}

EmployeeEntity。

@Entity
@Table(name = "employee", schema = "employees")
public class EmployeeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@Enumerated(EnumType.STRING)
private JobTitle jobTitle;
@ManyToOne(fetch = FetchType.LAZY)
private TeamEntity team;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(schema = "employees", name = "employee_project",
joinColumns = @JoinColumn(name = "employee_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "project_id", referencedColumnName = "id"))
private Set<ProjectEntity> projects = new HashSet<>();
public EmployeeEntity() {
}
public void joinProject(ProjectEntity project) {
project.getEmployees().add(this);
this.projects.add(project);
}
public void leaveProject(ProjectEntity project) {
project.getEmployees().remove(this);
this.projects.remove(project);
}
... Getters and Setters ...
}

项目实体。

Entity
@Table(name = "project", schema = "employees")
public class ProjectEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal budget;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "projects")
private Set<EmployeeEntity> employees = new HashSet<>();
public ProjectEntity() {
}
... Getters and Setters ...
}

如果many端确实有很多元素,那么您可能根本不应该使用OneToMany。获取大型集合意味着使用某种分页\筛选,但OneToMany加载整个集合。

首先,您需要更新拥有实体(FK所在的位置(以将其存储在DB中。Vlad和Hibernate指南关于一致性的含义是指更新当前会话中的实体对象。这些对象在生命周期中有转换,当你有双向关联时,如果你不设置反向端,那么反向端实体将不会更新字段,并且会与当前会话中的拥有方实体(可能最终与DB,在TX提交后(不一致。让我举例说明OneToMany。如果我们有2个管理实体公司和员工:

set employee.company = X -> persist(employee) -> managed List<Employee> company.employees gets inconsistent with db

可能会有不同类型的不一致,比如从company.employees字段中获取并产生副作用(猜测它不是空的,只是没有您刚刚添加的员工(,如果有Cascade.ALL,您可能会错过或错误地通过断开的关系删除\update\add实体,因为您的实体处于令人震惊的状态,hibernate以一种防御但有时不可预测的方式处理它:删除不使用JpaRepository

此外,你可能会发现这个答案很有趣:https://stackoverflow.com/a/5361587/2924122

最新更新