给定两个实体Department
和Employee
,从Department
到Employee
形成一对多关系
由于关系非常直观,所以我省略了实体类。
下面的代码段,简单地持久化一个实体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
语句在Department
中的List<Employee>
中显示了一个新添加的Employee
实体,其中包含null
employeeId
,因为entityManager.flush();
行没有出现在第一个代码片段中。
EntityManager#persist(Object entity)
不保证生成id。id只保证在刷新时生成。
发生的是,如果entityManager.flush();
被删除/注释,那么实体Employee
将被添加到Employee
s (List<Employee> employeeList
)列表中,其中包含null
标识符(底层数据库表中的主键列)。
维持双向关系的通常方法是什么?每次将实体添加到由关系的反面维护的实体集合中以生成与新持久实体相关的id时,是否总是需要EntityManager#flush();
?
此外,在删除Employee
实体(使用entityManager.remove(employee);
)时,是否总是需要手动从List<Employee>
(由关系的反面Department
维护)中删除Employee
?
EDIT:实体类:
部门:
@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
时,需要设置关联的两端。
在你的Department
你应该有这个方法:
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
确保将PERSIST和MERGE事件级联到子关联:
@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在问题下面的最后一条评论中:
对
insert()
和getList()
的两次调用是在相同的情况下完成的这意味着flush()
还没有发生呼叫getList()
在我准备的快速脏测试用例中有一个巨大的疏忽。
我选择了一个标记为@Startup
的单例EJB(仅使用EJB模块)作为应用程序的客户端进行快速测试(@LocalBean
没有任何用于纯测试目的的接口-自EJB 3.1/Java EE 6以来支持),以便它的@PostConstruct
方法可以在部署EJB模块时立即调用。
目标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管理事务(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)
@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测试用例或其他的东西。
为了完整起见,我加上了这个答案。我宁愿不接受这个答案,因为它会遇到另一个问题。