将新持久化的实体添加到关系的另一方持有的实体列表中,以维护双向关系

Adding a newly persisted entity to a list of entities held by the inverse side of a relationship so as to maintaining a bidirectional relationship

给定两个实体 DepartmentEmployee 形成从 DepartmentEmployee 的一对多关系。

由于关系非常直观,我省略了实体 类。

下面这段代码,简单的持久化了一个实体Employee.

public void insert() {
    Employee employee = new Employee();
    employee.setEmployeeName("k");

    Department department = entityManager.find(Department.class, 1L);
    employee.setDepartment(department);
    entityManager.persist(employee);
    entityManager.flush();

    List<Employee> employeeList = department.getEmployeeList();
    employeeList.add(employee);
}

以及以下方法 returns 与特定部门关联的员工列表。

public List<Employee> getList() {
    return entityManager.find(Department.class, 1L).getEmployeeList();
}

这两种方法都是使用 CMT(在此不是 BMT)在无状态 EJB 中编写的,命名为 EmployeeService.

客户端应用程序按顺序调用这些方法,

employeeService.insert();
List<Employee> employeeList = employeeService.getList();

for (Employee e : employeeList) {
    System.out.println(e.getEmployeeId() + " : " + e.getEmployeeName());
}

上面 foreach 循环中的 sout 语句显示新添加的 Employee 实体到 Department 中的 List<Employee>,带有 null employeeId 其中 鉴于行 entityManager.flush(); 不存在于第一个代码片段中


EntityManager#persist(Object entity) 不保证生成 id。 id 只保证在刷新时生成。

发生的情况是,如果 entityManager.flush(); 是 removed/commented,则实体 Employee 被添加到 Employee 的列表中(List<Employee> employeeList)其中有一个 null 标识符(基础数据库中的主键列 table)。

维护双向关系的常用方法是什么? EntityManager#flush(); 每次将一个实体添加到由关系的反面维护的实体集合时,总是需要 EntityManager#flush(); 以生成与新持久化实体关联的 ID 吗?

另外,是否总是需要手动List<Employee>中删除一个Employee(由关系的反面维护- Department) 在删除 Employee 实体时(使用 entityManager.remove(employee);)?


编辑:实体类:

部门:

@Entity
@Table(catalog = "testdb", schema = "", uniqueConstraints = {
@UniqueConstraint(columnNames = {"department_id"})})
public class Department implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "department_id", nullable = false)
    private Long departmentId;

    @Column(name = "department_name", length = 255)
    private String departmentName;

    @Column(length = 255)
    private String location;
    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    private List<Employee> employeeList = new ArrayList<Employee>(0);

    private static final long serialVersionUID = 1L;
    // Constructors + getters + setters + hashcode() + equals() + toString().
}

员工:

@Entity
@Table(catalog = "testdb", schema = "", uniqueConstraints = {
    @UniqueConstraint(columnNames = {"employee_id"})})
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "employee_id", nullable = false)
    private Long employeeId;

    @Column(name = "employee_name", length = 255)
    private String employeeName;
    @JoinColumn(name = "department_id", referencedColumnName = "department_id")
    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;

    private static final long serialVersionUID = 1L;

    // Constructors + getters + setters + hashcode() + equals() + toString().
}

持久化Employee时,需要设置both sides of the association

在你的Department中你应该有这个方法:

public void addEmployee(Employee employee) {
    employees.add(employee);
    employee.setDepartment(this);
}

确保你 cascade de PERSIST and MERGE events 加入你的 children 协会:

@OneToMany(cascade = CascadeType.ALL, mappedBy = "department", orphanRemoval = true)
private List<Employee> children = new ArrayList<>();

持久化逻辑变为:

Employee employee = new Employee();
employee.setEmployeeName("k");

Department department = entityManager.find(Department.class, 1L);
department.addEmployee(employee);

答案在 JB Nizet 在问题下方的最后评论中:

The two calls to insert() and getList() are done in the same transaction, which means that the flush() hasn't happened yet when calling getList()

我准备作为快速肮脏测试的测试用例存在巨大疏忽。


我选择了一个由 @Startup 标记的单例 EJB(仅使用一个 EJB 模块)作为应用程序的客户端以进行快速测试(@LocalBean 没有任何接口用于纯测试目​​的 - 自 EJB 以来支持3.1/Java EE 6) 以便在部署 EJB 模块后立即调用其 @PostConstruct 方法。

目标 EJB。

@Stateless
@LocalBean
public class EmployeeService {

    public void insert() {
        // Business logic is excluded for brevity.
    }

    public List<Employee> getList() {
        // Business logic is excluded for brevity.
    }    
}

这些方法由标记为 @Startup(客户端)的单例 EJB 调用。

@Startup
@Singleton
public class Entry {

    @Inject
    private EmployeeService employeeService;

    @PostConstruct
    private void main() {
        employeeService.insert();
        List<Employee> employeeList = employeeService.getList();

        for (Employee e : employeeList) {
            System.out.println(e.getEmployeeId() + " : " + e.getEmployeeName());
        }
    }
}

因此,客户端(单例 EJB)已经处于目标 EJB(EmployeeService)必须使用的事务(使用容器管理事务 (CMT))中,因为两个 EJB 都使用,

@TransactionAttribute(TransactionAttributeType.REQUIRED)
@TransactionManagement(TransactionManagementType.CONTAINER)

默认。

如果客户端在事务中,则由 TransactionAttributeType.REQUIRED 标记的 EJB 使用相同的事务。但是,如果客户端不启动事务,则标记为 TransactionAttributeType.REQUIRED 的目标 EJB 会创建一个新事务。

因此,所有事情都发生在单个事务中,因为客户端(单例 EJB)已经使用目标 EJB 必须使用的默认事务属性 TransactionAttributeType.REQUIRED 启动事务。


解决方案是要么阻止客户端启动事务,要么使目标 EJB 始终创建新事务,无论客户端是否启动事务使用 TransactionAttributeType.REQUIRES_NEW

如果客户端在事务中,标记为 TransactionAttributeType.REQUIRES_NEW 的 EJB 会暂停客户端启动的事务并创建自己的新事务。尽管如此,如果客户端没有启动事务,标记为 TransactionAttributeType.REQUIRES_NEW 的 EJB 也会创建一个新事务。

简而言之,标记为 TransactionAttributeType.REQUIRES_NEW 的 EJB 总是创建一个新事务,无论该事务是否已由其客户端启动。


可以使用 Bean Managed Transaction (BMT) 阻止客户端启动事务 - 通过 @TransactionManagement(TransactionManagementType.BEAN) 标记客户端(单例 EJB),例如。

@Startup
@Singleton
@TransactionManagement(TransactionManagementType.BEAN)
public class Entry {
    // ...
}

这需要使用 javax.transaction.UserTransaction 接口显式启动和提交事务(可以通过 @javax.annotation.Resource 注释注入)。否则,所有方法都将在没有活动数据库事务的情况下运行。

或使用@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)如.

@Startup
@Singleton
public class Entry {
    @PostConstruct
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    private void main() {
        //...
    }
}

TransactionAttributeType.NOT_SUPPORTED 标记的方法总是没有数据库事务。


或者保持客户端不变(默认)并用 @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)

标记目标 EJB
@Startup
@Singleton
public class Entry {

    @Inject
    private EmployeeService employeeService;

    @PostConstruct
    private void main() {
        //...    
    }
}

@Stateless
@LocalBean
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public class EmployeeService {
    //...
}

TransactionAttributeType.REQUIRES_NEW 标记的 EJB(或 EJB 中的方法)总是启动一个新事务,无论关联的客户端是否已经启动一个事务,如前所述。


这是我试图进行的快速肮脏测试。毕竟,这不是测试应用程序的好做法(例如,如果目标 EJB (EmployeeService) 是有状态会话 bean,那么将它注入到单例 EJB 中在概念上是不合适的)。人们应该更喜欢 JUnit 测试用例或其他东西。

为了完整起见,我添加了这个答案。我不想接受这个答案,因为它遇到了不同的问题。