【问题标题】: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将新持久化的实体添加到关系的反方持有的实体列表中,以保持双向关系
【发布时间】:2015-06-03 21:45:11
【问题描述】:

给定两个实体 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);
}

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

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&lt;Employee&gt;,其中包含null employeeId 考虑到@987654337 行@ 在第一个代码 sn-p 中不存在


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

如果entityManager.flush(); 被删除/注释,那么实体Employee 将添加到Employees (List&lt;Employee&gt; employeeList) 列表中,其中包含null 标识符(主键列在底层数据库表中)。

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

另外,在删除 Employee 时是否总是需要手动List&lt;Employee&gt; 中删除 Employee(由关系的反面维护 - Department)实体(使用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().
}

【问题讨论】:

  • 删除flush()会发生什么?
  • 如果entityManager.flush(); 被移除,那么Employee 实体将添加到Deparment 中的List&lt;Employee&gt;,并带有null id (employeeId) - "如果@987654358 @ 被删除/注释,然后实体 Employee 被添加到 Employees 列表(List&lt;Employee&gt; employeeList) 中带有 null 标识符(基础数据库表中的主键列)。 "
  • 是的,所以?这会使应用程序崩溃吗?这会导致抛出异常吗?这会破坏您的数据库吗?
  • 否,但即使在使用List&lt;Employee&gt; employeeList = department.getEmployeeList(); 的不同事务中,这也会获取具有null id 的列表(直到重新部署应用程序)。因此,如果在某个地方(可能是客户端应用程序)尝试此列表,则可能会引发适当的异常。
  • 没有。事务 A 永远不会使用与并发事务 B 相同的实体实例。实体是短暂的、非线程安全的对象,并且不在事务之间共享。一旦发生刷新,ID 无论如何都会分配给实体,因此即使它是共享的,它也不会有空 ID。

标签: java hibernate jpa hibernate-mapping bidirectional-relation


【解决方案1】:

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

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

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

确保您cascade de PERSIST and MERGE events您的儿童协会:

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

【讨论】:

  • OP 在回复我的 cmets 时说,他已经尝试了所有这些方法,但没有成功。
  • 需要orphanRemoval = true吗?
  • 通常是的,因为大多数情况下,您不希望有一个空父项的浮动子项。但如果这是你的情况,那么你可以跳过orphanRemoval
【解决方案2】:

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

insert()getList() 的两次调用是在同一个过程中完成的 事务,这意味着flush() 尚未发生 打电话给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 测试用例或其他东西。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-07-27
    • 2020-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-17
    相关资源
    最近更新 更多