Tiny
Tiny

Reputation: 27899

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

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 Employees (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

Answers (2)

Vlad Mihalcea
Vlad Mihalcea

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

Tiny
Tiny

Reputation: 27899

The answer was in the JB Nizet's last comment below the question :

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

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

Related Questions