Let’s start with a short quiz. TxMergeBean
is a SLSB that uses CMT and EntityManager
. Assume the following local and
remote business interfaces:
public interface TxMergeCommon {
void methodA();
void methodB(MeineEntity entity);
}
@Local
public interface TxMergeLocal {}
@Remote
public interface TxMergeRemote {}
and the following SLSB:
@Stateless
public class TxMergeBean implements TxMergeRemote, TxMergeLocal {
@PersistenceContext
private EntityManager em;
@EJB
private TxMergeLocal self;
@Resource
private SessionContext sctx;
public void methodA() {
MeineEntity entity = new MeineEntity("methodA");
em.persist(entity);
self.methodB(entity);
}
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void methodB(final MeineEntity entity) {
entity.setName("methodB");
em.merge(entity);
sctx.setRollbackOnly();
}
}
Assume that MeineEntity
is properly defined and its only constructor parameter is saved as ’name’ attribute.
What will be the database persisted value of MeinEntity ’name’ attribute after TxMergeBean#methodA() invocation?
- “methodA”,
- “methodB”,
- PersistenceException will be thrown,
- undefined.
Explanation
Let’s figure it out step by step.
We’re using CMT so the first relevant piece of information is that every change made to an entity in a managed state will be persisted during transaction commit.
Secondly, you should notice that methodB()
executes in a separate transaction. Therefore, there is a need of
em.merge(entity)
invocation if we want to move from “unmanaged” to “managed” state. We’re using self-reference to
pass a method call through EJB Container so @TransactionAttribute
works fine – no hacks here.
Thirdly, the new transaction of methodB()
is rolled back at the end. No changes made to our entity should be
persisted at all (just like entity.setName("methodB")
would never exist.)
Lastly, and what’s most interesting, is the usage of local business interface for self reference EJB. This means
we’re using pass-by-reference parameters semantics (or pass-by-reference-copy to be more accurate) instead of pass-by-copy.
Now this is the root of all problems – can you see it already?
To visualize it, consider processing flow as below:
- Start methodA() TX
- Persist an entity with “methodA”,
- Pass by reference the persisted entity to methodB(),
- Suspend methodA() TX,
- Start methodB() TX,
- Change state of an unmanaged entity to “methodB”,
- Merge the changes made in methodB(),
- Rollback methodB() TX,
- Resume methodA() TX,
- Commit methodA() TX.
During this last operation, i.e. “Commit TX for methodA()” entity’ state will be flushed. But what actually is the state of our entity?
It’s “methodB”!
Despite the fact that TX for methodB()
was rolled back we changed the state of MeineEntity
passed as a parameter and
because we were using local business interface (pass-by-reference) – this change will be reflected in the
MeineEntity
used in methodA().
Therefore, methodA() flushes its entity state which is “methodB”.
Solution
Would it look differently if we used remote business interface for self-reference? Yes it would. Simply changing this:
@EJB
private TxMergeLocal self;
into this:
@EJB
private TxMergeRemote self;
Would yield the correct result – MeineEntity
with name attribute set to “methodA”.
That’s because the remote business interface operates on arguments with pass-by-value approach so our entity will be
copied and passed to the methodB()
. Therefore, any changes made within methodB()
will not be reflected in the
original entity.
Another way of dealing with this problem is not to use an entity passed as an argument to methodB()
but the one being
a result of a merge operation. Such entity is a copy of the original one, so you can fiddle with it as much as you
want without harming the outside state.
We’re talking about changing this:
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void methodB(final MeineEntity entity) {
entity.setName("methodB");
em.merge(entity);
...
}
into this:
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void methodB(final MeineEntity entity) {
MeineEntity editedEntity = em.merge(entity);
editedEntity.setName("methodB");
...
}
It seems that some people are thinking that even using JTA (CMT/BMT) the ‘merge’ operation is actually flushing some changes to the database. With such perception, they refuse to invoke merge(-) before changing entity state.
However, the fact is that merge(-) is just creating a copy of an entity that will be in a managed state within a given persistence context. Knowing that every change made to the current persistence context managed entity will be flushed during transaction commit allows you to firstly merge and then change the state of your entity.
In our case this would also allows us to be independent on the used interface – it will work with both local and remote EJB self references.