Reputation: 27899
Given two entities Department
and Employee
forming a one-to-many relationship from Department
to Employee
.
Since the relationship is quite intuitive, I am leaving out the entity classes.
The following segment of code, simply persists an entity 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);
}
And the following method returns a list of employees associated with a particular department.
public List<Employee> getList() {
return entityManager.find(Department.class, 1L).getEmployeeList();
}
Both the methods are written in a stateless EJB using CMT (hereby not BMT) named let's say EmployeeService
.
A client application invokes these methods in sequence like so,
employeeService.insert();
List<Employee> employeeList = employeeService.getList();
for (Employee e : employeeList) {
System.out.println(e.getEmployeeId() + " : " + e.getEmployeeName());
}
The sout
statement in the foreach
loop above displays a newly added Employee
entity to List<Employee>
in Department
with a null
employeeId
in it given that the line entityManager.flush();
is not present in the very first code snippet.
EntityManager#persist(Object entity)
is not guaranteed to generate an id. An id is only guaranteed to be generated at flush time.
What happens is, if entityManager.flush();
is removed/commented, then the entity Employee
is added to the list of Employee
s (List<Employee> employeeList
) with a null
identifier in it (the primary key column in the underlying database table).
What is the usual way to maintain a bidirectional relationship? Is EntityManager#flush();
always needed every time an entity is to be added to a collection of entities being maintained by the inverse side of the relationship to generate an id associated with a newly persisted entity?
Also, is it always required to manually delete an Employee
from List<Employee>
(maintained by the inverse side of the relationship - Department
) while deleting an Employee
entity (using entityManager.remove(employee);
)?
EDIT : Entity classes :
Department :
@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().
}
Employee :
@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().
}
Upvotes: 2
Views: 1587
Reputation: 154020
When persisting the Employee
, you need to set both sides of the association.
In your Department
you should have this method:
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
Make sure you cascade de PERSIST and MERGE events to your children association:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "department", orphanRemoval = true)
private List<Employee> children = new ArrayList<>();
and the persisting logic becomes:
Employee employee = new Employee();
employee.setEmployeeName("k");
Department department = entityManager.find(Department.class, 1L);
department.addEmployee(employee);
Upvotes: 1
Reputation: 27899
The answer was in the JB Nizet's last comment below the question :
The two calls to
insert()
andgetList()
are done in the same transaction, which means that theflush()
hasn't happened yet when callinggetList()
There was a huge oversight in test cases I prepared as a quick dirty test.
I chose a singleton EJB (using only an EJB module) marked by @Startup
as the application's client for a quick test (@LocalBean
without any interface for a pure testing purpose - supported since EJB 3.1/Java EE 6) so that its @PostConstruct
method can be invoked as soon as the EJB module is deployed.
The target 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.
}
}
These methods are invoked by a singleton EJB marked by @Startup
(the client).
@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());
}
}
}
As such, the client (the singleton EJB) is already in a transaction (using Container Managed Transaction (CMT)) that the target EJB (EmployeeService
) has to use, since both the EJBs use,
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@TransactionManagement(TransactionManagementType.CONTAINER)
as default.
If a client is in a transaction, an EJB marked by TransactionAttributeType.REQUIRED
uses the same transaction. If the client however, does not start a transaction, the target EJB marked by TransactionAttributeType.REQUIRED
creates a new transaction.
Therefore, everything happens in a single transaction, since the client (the singleton EJB) already starts a transaction with the default transaction attribute TransactionAttributeType.REQUIRED
which the target EJB must use.
The solution was to either prevent the client from starting a transaction or make the target EJB always create a new transaction whether the client starts a transaction or not using TransactionAttributeType.REQUIRES_NEW
.
If a client is in a transaction, an EJB marked by TransactionAttributeType.REQUIRES_NEW
suspends that transaction started by the client and creates a new transaction of its own. If the client, nevertheless, does not start a transaction, an EJB marked by TransactionAttributeType.REQUIRES_NEW
also creates a new transaction.
In a nutshell, an EJB marked by TransactionAttributeType.REQUIRES_NEW
always creates a new transaction whether the transaction is already started by its client or not.
The client can be prevented from starting a transaction using Bean Managed Transaction (BMT) - marking the client (the singleton EJB) by @TransactionManagement(TransactionManagementType.BEAN)
such as.
@Startup
@Singleton
@TransactionManagement(TransactionManagementType.BEAN)
public class Entry {
// ...
}
Which requires explicitly starting and committing a transaction using the javax.transaction.UserTransaction
interface (can be injected by the @javax.annotation.Resource
annotation). Otherwise, all methods will go without an active database transaction.
Or use @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
such as.
@Startup
@Singleton
public class Entry {
@PostConstruct
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
private void main() {
//...
}
}
Methods marked by TransactionAttributeType.NOT_SUPPORTED
always go without a database transaction.
Or leave the client untouched (as default) and mark the target EJB by @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 {
//...
}
EJBs (or methods in an EJB) marked by TransactionAttributeType.REQUIRES_NEW
always start a new transaction whether or not the associated client already starts a transaction as said previously.
This was a quick dirty test I was trying to make. This is, after all, not a good practice to test an application (if the target EJB (EmployeeService
), for example, is a stateful session bean, then injecting it into a singleton EJB is conceptually not appropriate). One should prefer JUnit test cases or something else instead.
I added this answer just for the sake of completeness. I prefer not to accept this answer as it runs into a different problem.
Upvotes: 0