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

huangapple go评论82阅读模式
英文:

Why do we need bidirectional synchronized methods?

问题

按照您的要求,以下是翻译好的部分:

在Hibernate用户指南中指出:

每当形成双向关联时,应用程序开发人员必须确保双方始终保持同步。
addPhone() 和 removePhone() 是实用方法,用于在添加或删除子元素时同步双方。

在Vlad的博客文章中之一中:

然而,我们仍然需要保持双方同步,否则会破坏领域模型关系的一致性,并且除非双方都得到适当同步,否则实体状态转换无法保证正常工作。

最后,在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_,
        ...

如果我使用简单的 employeeEntity.getProjects().add(projectEntity);,则生成的内容如下:

Hibernate:
    select
        employeeen0_.id as id1_0_0_,
        projectent2_.id as id1_2_1_,
        ...

没有额外获取员工的查询。

完整代码如下:

控制器部分:

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

EmployeeProjectRequest 部分:

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

ProjectService 部分:

@Service
public class ProjectsService {
    // ...
}

ProjectRepo 部分:

@Repository
public class ProjectRepo {
    // ...
}

EmployeeRepo 部分:

@Repository
public class EmployeeRepo {
    // ...
}

EmployeeEntity 部分:

@Entity
@Table(name = "employee", schema = "employees")
public class EmployeeEntity {
    // ...
}

ProjectEntity 部分:

@Entity
@Table(name = "project", schema = "employees")
public class ProjectEntity {
    // ...
}

请注意,这些内容是根据您提供的代码片段翻译的,可能有些术语或内容的翻译有所不同。

英文:

As stated in the topic. Why do we need bidirectional synchronized methods? What real world use case does it solve? What happens if I don't use them?

In Hibernate's User Guide:
> Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
The addPhone() and removePhone() are utility methods that synchronize both ends whenever a child element is added or removed.

Source - Hibernate User Guide

In one of Vlad's blog posts:
>However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.

Source - Vlad Mihalcea Blog

Lastly, in Vlad's book - High Performance Java Persistance, page 216:

>For a bidirectional @ManyToMany association, the helper methods must be added to the entity that is more likely to interact with. In our case, the root entity is the Post, so the helper methods are added to the Post entity

However, if I use simple generated setters, Hibernate seems to work just fine as well. Furthermore, synchronized methods might lead to performance degredation.

Synchronized methods:

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

Generates this:

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
        (?, ?)

Notice additional select for Employee right after Projects were fetched. If I use simply employeeEntity.getProjects().add(projectEntity);, it generates:

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
        (?, ?)

No more fetching of employee.

Full code.

Controller.

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

ProjectRepo.

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

ProjectEntity.

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 ...
}

答案1

得分: 1

如果在"Many"一侧确实有许多元素,那么你可能根本不应该使用OneToMany。获取大型集合意味着要使用某种形式的分页/过滤,但是OneToMany会加载整个集合。

首先,你需要更新拥有关联键(外键)的拥有实体,将其存储在数据库中。Vlad和Hibernate指南所提到的一致性是指在当前会话中更新实体对象。这些对象在生命周期中有过渡状态,当你拥有双向关联时,如果你不设置反向关联,那么该反向关联实体的字段不会被更新,与拥有关联的实体(可能在事务提交后)在当前会话中不一致。

让我以OneToMany的示例来说明。如果我们有两个受管理的实体,Company和Employee:

设置 employee.company = X -> 持久化(employee) -> 受管理 List<Employee> company.employees 在与数据库不一致的状态

可能会出现不同类型的不一致,比如在添加了新员工但尚未更新之后,从company.employees字段获取结果可能会出现意外的副作用(猜测该字段可能不为空,但刚添加的员工未包含在内)。如果存在Cascade.ALL,可能会通过破损的关系导致错过或错误地删除/更新/添加实体,因为你的实体处于不确定的状态,Hibernate会以一种防御性但有时难以预测的方式处理这种情况:
https://stackoverflow.com/questions/22688402/delete-not-working-with-jparepository

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

英文:

If there are really many elements on the Many side, then you probably should not use OneToMany at all. Fetching large collections implies using some kind of pagination\filtering, but OneToMany loads the whole set.

First of all, you need to update an owning entity(where FK resides) to store it in the DB. And what Vlad and Hibernate guide mean about consistency, refers to updating entity objects inside current session. Those objects have transitions during lifecycle, and when you have bidirectional association, if you don't set inverse side, then that inverse side entity won't have the field updated, and would be inconsistent with an owning side entity(and probably with the DB ultimately, after TX commits) in the current session.
Let me illustrate on OneToMany example.
If we get 2 managed entities Company and Employee:

set employee.company = X -&gt; persist(employee) -&gt; managed List&lt;Employee&gt; company.employees gets inconsistent with db

And there might be different types of inconsistencies, like getting from company.employees field after and arising side-effects(guess it was not empty, but just without employee you just added), and if there is Cascade.ALL, you might miss or falsely remove\update\add entities through broken relationships, because your entities are in a ambigious state, and hibernate deals with it in a defensive but sometimes unpredictable way:
https://stackoverflow.com/questions/22688402/delete-not-working-with-jparepository

Also, you might find interesting this answer: https://stackoverflow.com/a/5361587/2924122

huangapple
  • 本文由 发表于 2020年10月23日 01:18:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/64487322.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定