You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.
EJB3 transaction semantics with CMT are confusing enough when you're dealing with an EJB container and session bean components. It can get even more confusing when the Seam container and SMPC comes into play with your Seam app. The purpose of this article is to shed some light on this and hopefully make your life less confusing when dealing with Seam/EJB3 transaction semantics.
Assume you have a functional requirement as follows:
User enters multiple serial numbers into HtmlInputTextarea control and submits form. The action method needs to persist multiple records into multiple tables for each serial number. Each iteration in the loop for the List of serial numbers must be wrapped in a transaction. There is no outer transaction. In other words, if there is a RuntimeException after serial number A has completed, commit the CRUD operations for serial number A and abandon processing for the rest of the serial numbers.
How can we implement this using Seam/EJB3?
The action method in your SFSB will be annotated as follows:
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) public void searchSerialNumbers() { ... }
Inside the for loop in the myActionHandler() method, we will call a private method in our SFSB which is implemented as follows:
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) private void persistA(String serialNumber) { //CRUD operations here via EntityManager... }
The SFSB in its entirety:
@Stateful @Name("testTransactionsAction") public class TestTransactionsAction implements TestTransactionsLocal { @In StringUtils stringUtils; @In private EntityManager entityManager; //using SMPC private String serialNumbers; /*------------------------------------------BEGIN METHODS-------------------------------------------------------------------*/ @Begin(join=true) @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) public void searchSerialNumbers() { //parse serial numbers from HtmlInputTextarea control.... List<String> serialNumberList = parseSerialNumber(); if (serialNumberList != null && serialNumberList.size() > 0) { for (String serialNumber : serialNumberList) { //persist records one serialNumber at a time... persist(serialNumber); } } } private void persist(String serialNumber) { TestTransactions testTransactions1 = new TestTransactions(); testTransactions1.setSerialNumber(serialNumber); testTransactions1.setAddedDate(new Date()); entityManager.persist(testTransactions1); } private List<String> parseSerialNumber() { List<String> serialNumberList = stringUtils.splitAndTrimStringAsArray(serialNumbers, "\\r"); return serialNumberList; } public String getSerialNumbers() { return serialNumbers; } public void setSerialNumbers(String serialNumbers) { this.serialNumbers = serialNumbers; } @Remove @Destroy public void destroy() {} }
What is the problem here? There are a couple problems here.
1) JSR220 does not allow you to add transaction demarcation to non-interface methods. So the default REQUIRED transaction attribute type is ignored by the EJB container. There is no warning or exception during deployment or runtime. And Eclipse IDE does not complain either unfortunately.
2) As per section 11.2.3 of Seam Framework, 2nd ed., since this approach requires method-level transaction demarcation, it can be used only on EJB3 session bean components with an EJB3-managed EntityManager (i.e., an EntityManager injected via @PersistenceContext).
So one way to solve this is what I refer to as the support bean work-around
. You essentially inject the local interface of the support bean and refactor the persist() method to that new SLSB.
Refactored original SFSB:
@Stateful @Name("testTransactionsAction") public class TestTransactionsAction implements TestTransactionsLocal { @In StringUtils stringUtils; @In private EntityManager entityManager; //using SMPC private String serialNumbers; /*------------------------------------------BEGIN METHODS-------------------------------------------------------------------*/ @Begin(join=true) @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) public void searchSerialNumbers() { //parse serial numbers from HtmlInputTextarea control.... List<String> serialNumberList = parseSerialNumber(); if (serialNumberList != null && serialNumberList.size() > 0) { for (String serialNumber : serialNumberList) { //persist records one serialNumber at a time... persist(serialNumber); } } } private void persist(String serialNumber) { testPersistBean.persist(serialNumber); } private List<String> parseSerialNumber() { List<String> serialNumberList = stringUtils.splitAndTrimStringAsArray(serialNumbers, "\\r"); return serialNumberList; } public String getSerialNumbers() { return serialNumbers; } public void setSerialNumbers(String serialNumbers) { this.serialNumbers = serialNumbers; } @Remove @Destroy public void destroy() {} }
new support SLSB:
@Name("testPersistBean") @Stateless @AutoCreate public class TestPersistBean implements TestPersistLocal { @In private EntityManager entityManager; //inject SMPC /*-------------------------------------------BEGIN METHODS---------------------------------------------*/ public void persist(String serialNumber) { TestTransactions testTransactions1 = new TestTransactions(); testTransactions1.setSerialNumber(serialNumber); testTransactions1.setAddedDate(new Date()); entityManager.persist(testTransactions1); } }
So the difference is now each time persist() is called in the new support SLSB, it is REQUIRED (by default) to be wrapped in a transaction. Before we had no transaction, now we do.
As a final note, here is some feedback I got from JSR 318 spec lead Kenneth Saks regarding this problem:
------------------------------------------------------------------------------------------------------------
The behavior you're seeing is not specific to transaction attributes. All the special semantics associated with an EJB component invocation (method authorization, container exception handling, threading guarantees, container-managed transactions, etc.) only apply to invocations made through an EJB reference. When code already running within a business method invocation calls another method on the same class through the this
pointer, the EJB container isn't involved. Such a method invocation is just a plain Java SE method call.
If you want to invoke a business method on your bean from another business method on the same bean, you'll need to acquire an EJB reference to yourself. The easiest way is to call SessionContext.getBusinessObject().
TestTransactionsLocal ejbRefToMyself = sessionCtx.getBusinessObject(TestTransactionsLocal.class) ejbRefToMyself.otherMethod();
Regards,
Ken
------------------------------------------------------------------------------------------------------------
I did try his code snippet but it does not work with SFSB due to limitation in JSR 220: Clients are not allowed to make concurrent calls to a stateful session object
. You will get javax.ejb.ConcurrentAccessException.
BTW, yes I know in this SFSB there is @Begin method and no @End method. Sometimes conversations don't end... :)
Resources:
Seam Framework, 2nd ed., Yuan, Orshalick, Heute JSR220 - EJB 3.0 core Seam reference documentation