VPS for Java Enterprise Application

Making sense of EJB3.x Transaction Attributes – Part 1 (REQUIRES_NEW)

For the previous past posts, I have dealth much with manual fine-grind transaction, mostly on the subject of Bean-Managed Transaction with javax.ejb.UserTransaction and I’ve talked about what were the differences between rollback() and setRollbackOnly(). Don’t be surprise that Container-Managed Transaction could achieve similar control over transaction if transaction demarcates are properly used. I am writting a series of linked-articles to address usage of @TransactionAttribute with TransactionAttributeType such as REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED and NEVER.


I’m not going to write about TransactionAttributeType.Required as it is common and by default all methods are at such and there ain’t much tricks to that. As for the rest of the demarcates, Oracle had documented on their transactional characteristics; but what many developers fail to understand (including me in the past) is how these demarcates behave in long units of transaction and where will be the appropriate implementation or their right usages. In this series, we are going to explore them one by one.

For the below example, I’ll illustrate the basic behavior of REQUIRES_NEW with 2 session beans by the name of ExampleSessionFacade1 and ExampleSessionFacade2 with their local interfaces as IExampleSessionFacade1Local and IExampleSessionFacade2Local for simplicity.

Understanding TransactionAttributeType.REQUIRES_NEW

Here at the below, I’ll show you code snippets instead of drawing sequence diagrams for better understanding. I’m not going to deal much with proper Exception handling for now, just keeping things simple.

@Stateless
public class ExampleSessionFacade1 implements IExampleSessionFacade1Local
{
	@Resource
	private EJBContext context;

	@EJB
	private IExampleSessionFacade2Local exampleSF2;

	//Not necessary; just to make things clear
	@TransactionAttribute( TransactionAttributeType.REQUIRED )
	public void methodA() throws Exception
	{
		try
		{
			//Do something with the EntityManager...and later invokes methodB() in exampleSF2

			exampleSF2.methodB();
		}
		catch(Exception ex)
		{
			if (!context.getRollbackOnly())
			{
				/**
				 * This WILL NOT AFFECT the transaction in methodB().
				 * methods that you call like persist, merge or remove
				 * will still be carried out in methodB() and will be 
				 * updated in the database.
				 **/
				context.setRollbackOnly();
			}
		}
	}
}

@Stateless
public class ExampleSessionFacade2 implements IExampleSessionFacade2Local
{
	@TransactionAttribute( TransactionAttributeType.REQUIRES_NEW )
	public void methodB() throws Exception
	{
		//Do some other things with the EntityManager...
	}
}

Looking at the codes above, we have methodA() in ExampleSessionFacade1, it is annotated with TransactionAttributeType.REQUIRED. Within methodA(), it invokes methodB() through the session bean instance of ExampleSessionFacade2. methodB() is annotated as TransactionAttributeType.REQUIRES_NEW. Now, how the container works is this: the container will start a transaction for methodA(), let’s call it T1. As ExampleSessionFacade1’s methodA() invokes methodB() of ExampleSessionFacade2 and since methodB() was marked as REQUIRES_NEW, the container will then start a separate transaction, let’s call it T2 for methodB(). Both methods won’t share the same transaction together.

So that means, whether transaction T2 performs a commit (finishes properly at the end of the method and commits by the container to the database) or rollback (if context.setRollbackOnly() is called), that doesn’t affect what’s going on in transaction T1 as it is on a separate context. Like wise for transaction T1. If T1 had been marked for rollback ( invoke EJBContext’s setRollbackOnly() ), T2 will still finish the transaction with a commit (whatever you performed with the EntityManager will still be updated back to the database) and ignoring what’s happening to T1 even if T1 has a rollback.

Back to the above code example, if somewhere around methodA() throws an Exception and propagates the context.setRollbackOnly(), methodB() will still finish flawlessly (commits everything to the database) without being affected by the setRollbackOnly() by methodA().

Scenario for TransactionAttributeType.REQUIRES_NEW to be In Action

TransactionAttributeType.REQUIRES_NEW is most suitable to be applied in the situation where the method’s transaction must not be affected by the transaction of the caller’s transaction.

One of the scenario that I could think of for methods to be annotated with REQUIRES_NEW to be in action is non-volatile audit trail (logging comments, remarks, actions by the user back into the database). Taking an example of a payment method to a 3rd party account from a banking module that deducts the amount of bank account when a transfer is successful (or nothing is performed is it fails), regardless if this transfer is successful or not (with a rollback), the logging facility would still need to be functional to log the status of the transaction and it must not be affect by the rollback of the transfer.

STICKY IMPORTANT HIGHLIGHT!!!:

** You can only bring out the effects of transaction attributes only when you call the method through a session bean instance and NOT through a direct method call. Even if your methods are within the same bean, you need to get the local instance of the same bean and call through its local interface instead of a direct method invoke. This message will appear in all the posts within this series as a REMINDER.

Need an Applied Example?

I’m going to take one of the examples from my previous post. The database tables are the same, but instead of defining the session beans as Bean-Managed Transaction, it should be defaulted to Container-Managed transaction.

Here, I’ll show you a comprehensive use case where the TransactionAttributeType.REQUIRES_NEW can applied. This use case is about the transfer of funds from a bank savings account to a checking account…something that we do most of the time. It won’t be too elaborated like what’s currently being deployed in the banking system, but it is good enough to illustrate times when elaborated control of transaction is required. The below is the database diagram of tables, accompanied by the SQL scripts for MySQL DB that generates the tables which are involve in the transfer of funds from a savings account to a checking account. Both accounts can and will belong to a single customer. It is an inter-account transfer by the way.

Bank Account Fund Transfer Database Tables
Bean-Managed Transaction Bank Funds Transfer DB Diagram

Tables and Use Case Description

The tables diagram only consists of several tables that are related to perform the fund transfer. It is very watered-down and superficial; but for now, it is sufficient to capture the related entities.

Table NameEntity Class NameDescription
BANK_CUSTOMERSBankCustomerEntityThe customer record of the bank. For simplicity, we only capture both username and password field.

** In this demo, we’ll set the username and password are both “MadMax” and “passwd123″

BANK_ACCT_TYPEBankAcctTypeEntityThis is more like a statis data table which consists of various bank account types. For now, only “SAVINGS” and “CHECKING” account type is available. Please look at the data population script below.
BANK_ACCOUNTBankAccountEntityThe bank account of customers. It has fields like account no., the current balance, the foreign key will tie to the BANK_CUSTOMERS table to identify the ownership of the account. Both SAVINGS and CHECKING accounts are within this table. One customer could have multiple accounts.

** In this demo, “MadMax” has a savings account BK-001-09 with the balance of $9,000,000 and a checking account BK-012-10 with the balance of $1,000,000.

ACTIVITY_LOGActivityLogEntityThis is a table that records all of the activity of the customer with a timestamp when they perform a transaction and whether the transaction is a success or a failure.

** So, everytime when “MadMax” performs a transaction whether it goes through or not, the system will insert the record here as an activity log in this table.

BANK_GROUP_PROFITBankGroupProfitEntityThis table captures the profit (service charge/commission) made by the bank’s branch whenever a customer performs a transaction.

** For demo purposes, let’s have the bank’s initial balance of the profit to be $1bil and bank commission fixed at $2 for every fund transfer transaction performed. This $2 will be added upon the profit balance.

The SQL script for generating the tables as well as pre-populating the database with data is shown below. This script is for MySQL DB only. However if you are running on other database platform, you should be able to come out with your own script for experiment purposes by referring to the below:

MySQL Database Tables and Data Script

 SQL |  copy code |? 
01
CREATE TABLE BANK_CUSTOMERS
02
(
03
    BANK_CUSTOMERS_PK BIGINT NOT NULL AUTO_INCREMENT,
04
    USERNAME VARCHAR(200) NOT NULL,
05
    PASSWORD VARCHAR(200) NOT NULL,
06
    PRIMARY KEY(BANK_CUSTOMERS_PK),
07
    UNIQUE(USERNAME)
08
) ENGINE=InnoDB;
09
 
10
INSERT INTO BANK_CUSTOMERS(USERNAME, PASSWORD) VALUES ('MadMax', 'passwd123');
11
 
12
CREATE TABLE ACTIVITY_LOG
13
(
14
    ACTIVITY_LOG_PK BIGINT NOT NULL AUTO_INCREMENT,
15
    ACTIVITY VARCHAR(200) NOT NULL,
16
    BANK_CUSTOMERS_PK BIGINT NOT NULL,
17
    RECORD_TS DATETIME NOT NULL,
18
    PRIMARY KEY(ACTIVITY_LOG_PK),
19
    FOREIGN KEY(BANK_CUSTOMERS_PK) REFERENCES BANK_CUSTOMERS(BANK_CUSTOMERS_PK) ON UPDATE CASCADE ON DELETE RESTRICT
20
) ENGINE=InnoDB;
21
 
22
CREATE TABLE BANK_ACCT_TYPE
23
(
24
    BANK_ACCT_TYPE_PK BIGINT NOT NULL AUTO_INCREMENT,
25
    ACCOUNT_TYPE VARCHAR(100) NOT NULL,
26
    PRIMARY KEY(BANK_ACCT_TYPE_PK),
27
    UNIQUE(ACCOUNT_TYPE)
28
) ENGINE=InnoDB;
29
 
30
INSERT INTO BANK_ACCT_TYPE(ACCOUNT_TYPE) VALUES ('SAVINGS');
31
INSERT INTO BANK_ACCT_TYPE(ACCOUNT_TYPE) VALUES ('CHECKING');
32
 
33
CREATE TABLE BANK_ACCOUNT
34
(
35
    BANK_ACCOUNT_PK BIGINT NOT NULL AUTO_INCREMENT,
36
    ACCOUNT_NO VARCHAR(200) NOT NULL,
37
    BANK_ACCT_TYPE_PK BIGINT NOT NULL,
38
    BANK_CUSTOMERS_PK BIGINT NOT NULL,
39
    BALANCE DECIMAL(20, 2) NOT NULL,
40
    PRIMARY KEY(BANK_ACCOUNT_PK),
41
    UNIQUE(ACCOUNT_NO),
42
    FOREIGN KEY(BANK_ACCT_TYPE_PK) REFERENCES BANK_ACCT_TYPE(BANK_ACCT_TYPE_PK) ON UPDATE CASCADE ON DELETE RESTRICT,
43
    FOREIGN KEY(BANK_CUSTOMERS_PK) REFERENCES BANK_CUSTOMERS(BANK_CUSTOMERS_PK) ON UPDATE CASCADE ON DELETE RESTRICT
44
) ENGINE=InnoDB;
45
 
46
INSERT INTO BANK_ACCOUNT(ACCOUNT_NO, BANK_ACCT_TYPE_PK, BANK_CUSTOMERS_PK, BALANCE) VALUES
47
(
48
    'BK-001-09',
49
    (SELECT BANK_ACCT_TYPE_PK FROM BANK_ACCT_TYPE WHERE ACCOUNT_TYPE = 'SAVINGS'),
50
    (SELECT BANK_CUSTOMERS_PK FROM BANK_CUSTOMERS WHERE USERNAME = 'MadMax'),
51
    9000000
52
);
53
 
54
INSERT INTO BANK_ACCOUNT(ACCOUNT_NO, BANK_ACCT_TYPE_PK, BANK_CUSTOMERS_PK, BALANCE) VALUES
55
(
56
    'BK-012-10',
57
    (SELECT BANK_ACCT_TYPE_PK FROM BANK_ACCT_TYPE WHERE ACCOUNT_TYPE = 'CHECKING'),
58
    (SELECT BANK_CUSTOMERS_PK FROM BANK_CUSTOMERS WHERE USERNAME = 'MadMax'),
59
    1000000
60
);
61
 
62
CREATE TABLE BANK_GROUP_PROFIT
63
(
64
    BRANCH VARCHAR(200) NOT NULL,
65
    BALANCE DECIMAL(20, 2) NOT NULL,
66
    PRIMARY KEY(BRANCH)
67
) ENGINE=InnoDB;
68
 
69
INSERT INTO BANK_GROUP_PROFIT(BRANCH, BALANCE) VALUES ('MAIN', 1000000000);

Stateless Session Bean Implementation

From here, let’s implement a stateless session bean BankTransactionSessionFacade with both the local and remote interfaces as IBankTransactionSessionFacadeLocal and IBankTransactionSessionFacadeRemote. The method to transfer the funds is transferFund() with the parameters such as bank customer entity object, the source account no., the target account no. and the amount to be transfered.

As for the logging process to the database, we’ll implement a stateless session bean call BankAuditTrailSessionFacade with both the local and remote interaces as IBankAuditTrailSessionFacadeLocal and IBankAuditTrailSessionFacadeRemote. This bean has a method call logActivity(), which records the activity done by the specific customer whether it is a success or failure back to the database. The method is annotated with @TransactionAttribute and should be defined as TransactionAttributeType.REQUIRES_NEW.

The process flow for the transferFund() method is as follow:

  1. First, the system will make a log record in the ACTIVITY_LOG table that shows the customer indeed had initiated a fund transfer instruction, call through IBankAuditTrailSessionFacadeLocal’s logActivity();
  2. The system will then check for the following:
    • The transfer amount must be greater than $0;
    • The source account no. provided must be a valid account;
    • The target account no. provided must be a valid account;
    • The source account must have the adequate funds to be transfered including the deduction of any bank charges (which is $2);
  3. If any of the criteria above is not met, FundTransferException will be thrown.
  4. The system will then proceed to deduct the transfer amount plus the bank commission charges in the source account.
  5. Next, the target account will be updated with the new balance amount minus the bank commission charges.
  6. The bank group profit will increase with commission charged for the transfer service.
  7. If any of the points failed in the above, the system will perform a rollback and all of the tables related except for ACTIVITY_LOG will be reversed to the previous state and no data changes will be committed. Failure will be recorded by calling the IBankAuditTrailSessionFacadeLocal’s logActivity() method.
  8. If it passed, the system will commit the changes to all the involved tables and the activity log will record a SUCCESS.

** If an error which is more severe comes up that has nothing to do with the business process, we just wrap it with a SystemProblemException.

The full Java codes for all the entity classes, the session bean and the interfaces are shown below:

Codes for BankCustomerEntity:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
001
package com.developerscrappad.entity;
002
 
003
import java.io.Serializable;
004
import java.util.Set;
005
import javax.persistence.*;
006
 
007
@Entity
008
@Table( name = "BANK_CUSTOMERS" )
009
@NamedQueries( 
010
{
011
    @NamedQuery( name = "BankCustomerEntity.findByBankCustomersPk", query = "SELECT b FROM BankCustomerEntity b WHERE b.bankCustomersPk = :bankCustomersPk" ),
012
    @NamedQuery( name = "BankCustomerEntity.findByUsername", query = "SELECT b FROM BankCustomerEntity b WHERE b.username = :username" ),
013
} )
014
public class BankCustomerEntity implements Serializable
015
{
016
 
017
    private static final long serialVersionUID = 9219030547877545219L;
018
 
019
    @Id
020
    @GeneratedValue( strategy = GenerationType.IDENTITY )
021
    @Column( name = "BANK_CUSTOMERS_PK" )
022
    private Long bankCustomersPk;
023
 
024
    @Column( name = "USERNAME" )
025
    private String username;
026
 
027
    @Column( name = "PASSWORD" )
028
    private String password;
029
 
030
    @OneToMany( mappedBy = "bankCustomerEntity", fetch = FetchType.LAZY )
031
    private Set<ActivityLogEntity> activityLogEntitySet;
032
 
033
    @OneToMany( mappedBy = "bankCustomerEntity", fetch = FetchType.LAZY )
034
    private Set<BankAccountEntity> bankAccountEntitySet;
035
 
036
    public BankCustomerEntity()
037
    {
038
    }
039
 
040
    public BankCustomerEntity( Long bankCustomersPk )
041
    {
042
        this.bankCustomersPk = bankCustomersPk;
043
    }
044
 
045
    public BankCustomerEntity( Long bankCustomersPk, String username, String password )
046
    {
047
        this.bankCustomersPk = bankCustomersPk;
048
        this.username = username;
049
        this.password = password;
050
    }
051
 
052
    public Long getBankCustomersPk()
053
    {
054
        return bankCustomersPk;
055
    }
056
 
057
    public void setBankCustomersPk( Long bankCustomersPk )
058
    {
059
        this.bankCustomersPk = bankCustomersPk;
060
    }
061
 
062
    public String getUsername()
063
    {
064
        return username;
065
    }
066
 
067
    public void setUsername( String username )
068
    {
069
        this.username = username;
070
    }
071
 
072
    public String getPassword()
073
    {
074
        return password;
075
    }
076
 
077
    public void setPassword( String password )
078
    {
079
        this.password = password;
080
    }
081
 
082
    public Set<ActivityLogEntity> getActivityLogEntitySet()
083
    {
084
        return activityLogEntitySet;
085
    }
086
 
087
    public void setActivityLogEntitySet( Set<ActivityLogEntity> activityLogEntitySet )
088
    {
089
        this.activityLogEntitySet = activityLogEntitySet;
090
    }
091
 
092
    public Set<BankAccountEntity> getBankSavingsAccountEntitySet()
093
    {
094
        return bankAccountEntitySet;
095
    }
096
 
097
    public void setBankSavingsAccountEntitySet( Set<BankAccountEntity> bankAccountEntitySet )
098
    {
099
        this.bankAccountEntitySet = bankAccountEntitySet;
100
    }
101
 
102
    @Override
103
    public int hashCode()
104
    {
105
        int hash = 0;
106
        hash += (bankCustomersPk != null ? bankCustomersPk.hashCode() : 0);
107
        return hash;
108
    }
109
 
110
    @Override
111
    public boolean equals( Object object )
112
    {
113
        // TODO: Warning - this method won't work in the case the id fields are not set
114
        if ( !(object instanceof BankCustomerEntity) )
115
        {
116
            return false;
117
        }
118
        BankCustomerEntity other = ( BankCustomerEntity ) object;
119
        if ( (this.bankCustomersPk == null && other.bankCustomersPk != null) || (this.bankCustomersPk != null && !this.bankCustomersPk.equals( other.bankCustomersPk )) )
120
        {
121
            return false;
122
        }
123
        return true;
124
    }
125
 
126
    @Override
127
    public String toString()
128
    {
129
        return "com.developerscrappad.entity.BankCustomerEntity[ bankCustomersPk=" + bankCustomersPk + " ]";
130
    }
131
}

Codes for ActivityLogEntity:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
001
package com.developerscrappad.entity;
002
 
003
import java.io.Serializable;
004
import javax.persistence.*;
005
 
006
@Entity
007
@Table( name = "ACTIVITY_LOG" )
008
public class ActivityLogEntity implements Serializable
009
{
010
 
011
    private static final long serialVersionUID = -8166469090170547200L;
012
 
013
    @Id
014
    @GeneratedValue( strategy = GenerationType.IDENTITY )
015
    @Column( name = "ACTIVITY_LOG_PK" )
016
    private Long activityLogPk;
017
 
018
    @Column( name = "ACTIVITY" )
019
    private String activity;
020
 
021
    @Column( name = "RECORD_TS" )
022
    private java.sql.Timestamp recordTs;
023
 
024
    @ManyToOne
025
    @JoinColumn( name = "BANK_CUSTOMERS_PK", referencedColumnName = "BANK_CUSTOMERS_PK" )
026
    private BankCustomerEntity bankCustomerEntity;
027
 
028
    public ActivityLogEntity()
029
    {
030
    }
031
 
032
    public ActivityLogEntity( Long activityLogPk )
033
    {
034
        this.activityLogPk = activityLogPk;
035
    }
036
 
037
    public ActivityLogEntity( Long activityLogPk, String activity, java.sql.Timestamp recordTs )
038
    {
039
        this.activityLogPk = activityLogPk;
040
        this.activity = activity;
041
        this.recordTs = recordTs;
042
    }
043
 
044
    public Long getActivityLogPk()
045
    {
046
        return activityLogPk;
047
    }
048
 
049
    public void setActivityLogPk( Long activityLogPk )
050
    {
051
        this.activityLogPk = activityLogPk;
052
    }
053
 
054
    public String getActivity()
055
    {
056
        return activity;
057
    }
058
 
059
    public void setActivity( String activity )
060
    {
061
        this.activity = activity;
062
    }
063
 
064
    public java.sql.Timestamp getRecordTs()
065
    {
066
        return recordTs;
067
    }
068
 
069
    public void setRecordTs( java.sql.Timestamp recordTs )
070
    {
071
        this.recordTs = recordTs;
072
    }
073
 
074
    public BankCustomerEntity getBankCustomerEntity()
075
    {
076
        return bankCustomerEntity;
077
    }
078
 
079
    public void setBankCustomerEntity( BankCustomerEntity bankCustomerEntity )
080
    {
081
        this.bankCustomerEntity = bankCustomerEntity;
082
    }
083
 
084
    @Override
085
    public int hashCode()
086
    {
087
        int hash = 0;
088
        hash += (activityLogPk != null ? activityLogPk.hashCode() : 0);
089
        return hash;
090
    }
091
 
092
    @Override
093
    public boolean equals( Object object )
094
    {
095
        // TODO: Warning - this method won't work in the case the id fields are not set
096
        if ( !(object instanceof ActivityLogEntity) )
097
        {
098
            return false;
099
        }
100
        ActivityLogEntity other = ( ActivityLogEntity ) object;
101
        if ( (this.activityLogPk == null && other.activityLogPk != null) || (this.activityLogPk != null && !this.activityLogPk.equals( other.activityLogPk )) )
102
        {
103
            return false;
104
        }
105
        return true;
106
    }
107
 
108
    @Override
109
    public String toString()
110
    {
111
        return "com.developerscrappad.entity.ActivityLogEntity[ activityLogPk=" + activityLogPk + " ]";
112
    }
113
}

Codes for BankAcctTypeEntity:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package com.developerscrappad.entity;
02
 
03
import java.io.Serializable;
04
import javax.persistence.*;
05
 
06
@Entity
07
@Table( name = "BANK_ACCT_TYPE" )
08
public class BankAcctTypeEntity implements Serializable
09
{
10
 
11
    private static final long serialVersionUID = -4985625743150983030L;
12
 
13
    @Id
14
    @GeneratedValue( strategy = GenerationType.IDENTITY )
15
    @Column( name = "BANK_ACCT_TYPE_PK" )
16
    private Long bankAcctTypePk;
17
 
18
    @Column( name = "ACCOUNT_TYPE" )
19
    private String accountType;
20
 
21
    public BankAcctTypeEntity()
22
    {
23
    }
24
 
25
    public BankAcctTypeEntity( Long bankAcctTypePk )
26
    {
27
        this.bankAcctTypePk = bankAcctTypePk;
28
    }
29
 
30
    public BankAcctTypeEntity( Long bankAcctTypePk, String accountType )
31
    {
32
        this.bankAcctTypePk = bankAcctTypePk;
33
        this.accountType = accountType;
34
    }
35
 
36
    public Long getBankAcctTypePk()
37
    {
38
        return bankAcctTypePk;
39
    }
40
 
41
    public void setBankAcctTypePk( Long bankAcctTypePk )
42
    {
43
        this.bankAcctTypePk = bankAcctTypePk;
44
    }
45
 
46
    public String getAccountType()
47
    {
48
        return accountType;
49
    }
50
 
51
    public void setAccountType( String accountType )
52
    {
53
        this.accountType = accountType;
54
    }
55
 
56
    @Override
57
    public int hashCode()
58
    {
59
        int hash = 0;
60
        hash += (bankAcctTypePk != null ? bankAcctTypePk.hashCode() : 0);
61
        return hash;
62
    }
63
 
64
    @Override
65
    public boolean equals( Object object )
66
    {
67
        // TODO: Warning - this method won't work in the case the id fields are not set
68
        if ( !(object instanceof BankAcctTypeEntity) )
69
        {
70
            return false;
71
        }
72
        BankAcctTypeEntity other = ( BankAcctTypeEntity ) object;
73
        if ( (this.bankAcctTypePk == null && other.bankAcctTypePk != null) || (this.bankAcctTypePk != null && !this.bankAcctTypePk.equals( other.bankAcctTypePk )) )
74
        {
75
            return false;
76
        }
77
        return true;
78
    }
79
 
80
    @Override
81
    public String toString()
82
    {
83
        return "com.developerscrappad.entity.BankAcctTypeEntity[ bankAcctTypePk=" + bankAcctTypePk + " ]";
84
    }
85
}

Codes for BankAccountEntity:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
001
package com.developerscrappad.entity;
002
 
003
import java.io.Serializable;
004
import java.math.BigDecimal;
005
import javax.persistence.*;
006
 
007
@Entity
008
@Table( name = "BANK_ACCOUNT" )
009
@NamedQueries( 
010
{
011
    @NamedQuery( name = "BankAccountEntity.findByBankAccountPk", query = "SELECT b FROM BankAccountEntity b WHERE b.bankAccountPk = :bankAccountPk" ),
012
    @NamedQuery( name = "BankAccountEntity.findByAccountNo", query = "SELECT b FROM BankAccountEntity b WHERE b.accountNo = :accountNo" )
013
} )
014
public class BankAccountEntity implements Serializable
015
{
016
 
017
    private static final long serialVersionUID = -4624882168495551313L;
018
 
019
    @Id
020
    @GeneratedValue( strategy = GenerationType.IDENTITY )
021
    @Column( name = "BANK_ACCOUNT_PK" )
022
    private Long bankAccountPk;
023
 
024
    @Column( name = "ACCOUNT_NO" )
025
    private String accountNo;
026
 
027
    @Column( name = "BALANCE" )
028
    private BigDecimal balance;
029
 
030
    @ManyToOne
031
    @JoinColumn( name = "BANK_ACCT_TYPE_PK", referencedColumnName = "BANK_ACCT_TYPE_PK" )
032
    private BankAcctTypeEntity bankAcctTypeEntity;
033
 
034
    @ManyToOne
035
    @JoinColumn( name = "BANK_CUSTOMERS_PK", referencedColumnName = "BANK_CUSTOMERS_PK" )
036
    private BankCustomerEntity bankCustomerEntity;
037
 
038
    public BankAccountEntity()
039
    {
040
    }
041
 
042
    public BankAccountEntity( Long bankAccountPk )
043
    {
044
        this.bankAccountPk = bankAccountPk;
045
    }
046
 
047
    public BankAccountEntity( Long bankAccountPk, String accountNo, BigDecimal balance )
048
    {
049
        this.bankAccountPk = bankAccountPk;
050
        this.accountNo = accountNo;
051
        this.balance = balance;
052
    }
053
 
054
    public Long getBankAccountPk()
055
    {
056
        return bankAccountPk;
057
    }
058
 
059
    public void setBankAccountPk( Long bankAccountPk )
060
    {
061
        this.bankAccountPk = bankAccountPk;
062
    }
063
 
064
    public String getAccountNo()
065
    {
066
        return accountNo;
067
    }
068
 
069
    public void setAccountNo( String accountNo )
070
    {
071
        this.accountNo = accountNo;
072
    }
073
 
074
    public BigDecimal getBalance()
075
    {
076
        return balance;
077
    }
078
 
079
    public void setBalance( BigDecimal balance )
080
    {
081
        this.balance = balance;
082
    }
083
 
084
    public BankAcctTypeEntity getBankAcctTypeEntity()
085
    {
086
        return bankAcctTypeEntity;
087
    }
088
 
089
    public void setBankAcctTypeEntity( BankAcctTypeEntity bankAcctTypeEntity )
090
    {
091
        this.bankAcctTypeEntity = bankAcctTypeEntity;
092
    }
093
 
094
    public BankCustomerEntity getBankCustomerEntity()
095
    {
096
        return bankCustomerEntity;
097
    }
098
 
099
    public void setBankCustomerEntity( BankCustomerEntity bankCustomerEntity )
100
    {
101
        this.bankCustomerEntity = bankCustomerEntity;
102
    }
103
 
104
    @Override
105
    public int hashCode()
106
    {
107
        int hash = 0;
108
        hash += (bankAccountPk != null ? bankAccountPk.hashCode() : 0);
109
        return hash;
110
    }
111
 
112
    @Override
113
    public boolean equals( Object object )
114
    {
115
        // TODO: Warning - this method won't work in the case the id fields are not set
116
        if ( !(object instanceof BankAccountEntity) )
117
        {
118
            return false;
119
        }
120
        BankAccountEntity other = ( BankAccountEntity ) object;
121
        if ( (this.bankAccountPk == null && other.bankAccountPk != null) || (this.bankAccountPk != null && !this.bankAccountPk.equals( other.bankAccountPk )) )
122
        {
123
            return false;
124
        }
125
        return true;
126
    }
127
 
128
    @Override
129
    public String toString()
130
    {
131
        return "com.developerscrappad.entity.BankAccountEntity[ bankAccountPk=" + bankAccountPk + " ]";
132
    }
133
}

Codes for FundTransferExcepton:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package com.developerscrappad.exception;
02
 
03
public class FundTransferException extends Exception
04
{
05
 
06
    public FundTransferException( String msg )
07
    {
08
        super( msg );
09
    }
10
}

Codes for SystemProblemException:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package com.developerscrappad.exception;
02
 
03
public class SystemProblemException extends Exception
04
{
05
 
06
    public SystemProblemException( String msg, Throwable t )
07
    {
08
        super( msg, t );
09
    }
10
}

Codes for IBankAuditTrailSessionFacadeLocal

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package com.developerscrappad.intf;
02
 
03
import com.developerscrappad.entity.BankCustomerEntity;
04
import javax.ejb.Local;
05
 
06
@Local
07
public interface IBankAuditTrailSessionFacadeLocal
08
{
09
 
10
    public void logActivity( BankCustomerEntity bankCustomerEntity, String remark );
11
}

Codes for IBankAuditTrailSessionFacadeRemote:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
1
package com.developerscrappad.intf;
2
 
3
import javax.ejb.Remote;
4
 
5
@Remote
6
public interface IBankAuditTrailSessionFacadeRemote extends IBankAuditTrailSessionFacadeLocal
7
{
8
}

Codes for BankAuditTrailSessionFacade:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package com.developerscrappad.ejb;
02
 
03
import com.developerscrappad.intf.IBankAuditTrailSessionFacadeRemote;
04
import com.developerscrappad.intf.IBankAuditTrailSessionFacadeLocal;
05
import com.developerscrappad.entity.ActivityLogEntity;
06
import com.developerscrappad.entity.BankCustomerEntity;
07
import javax.ejb.Stateless;
08
import javax.ejb.TransactionAttribute;
09
import javax.ejb.TransactionAttributeType;
10
import javax.persistence.EntityManager;
11
import javax.persistence.PersistenceContext;
12
 
13
@Stateless
14
public class BankAuditTrailSessionFacade implements IBankAuditTrailSessionFacadeLocal, IBankAuditTrailSessionFacadeRemote
15
{
16
    @PersistenceContext( unitName = "YourPersistenceUnitName" )
17
    private EntityManager em;
18
 
19
    @TransactionAttribute( TransactionAttributeType.REQUIRES_NEW )
20
    public void logActivity( BankCustomerEntity bankCustomerEntity, String remark )
21
    {
22
        ActivityLogEntity activityLogEntity = new ActivityLogEntity();
23
        activityLogEntity.setActivity( remark );
24
        activityLogEntity.setBankCustomerEntity( bankCustomerEntity );
25
        activityLogEntity.setRecordTs( new java.sql.Timestamp( System.currentTimeMillis() ) );
26
        em.persist( activityLogEntity );
27
    }
28
}

Codes for IBankTransactionSessionFacadeLocal:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package com.developerscrappad.intf;
02
 
03
import com.developerscrappad.entity.BankCustomerEntity;
04
import java.math.BigDecimal;
05
import javax.ejb.Local;
06
 
07
@Local
08
public interface IBankTransactionSessionFacadeLocal
09
{
10
 
11
    public void transferFunds( BankCustomerEntity bankCustomerEntity, String fromAccountNo, String toAccountNo, BigDecimal amount ) throws Exception;
12
 
13
    public BankCustomerEntity findBankCustomerByUsername( String username );
14
}

Codes for IBankTransactionSessionFacadeRemote

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
1
package com.developerscrappad.intf;
2
 
3
import javax.ejb.Remote;
4
 
5
@Remote
6
public interface IBankTransactionSessionFacadeRemote extends IBankTransactionSessionFacadeLocal
7
{
8
}

Codes for BankTransactionSessionFacade:

** Just take note that certain fraction of the codes could be refactored to another session bean and be resused. But I’m not going to do it this time, just to keep things simple for now.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
001
package com.developerscrappad.ejb;
002
 
003
import com.developerscrappad.intf.IBankAuditTrailSessionFacadeLocal;
004
import com.developerscrappad.intf.IBankTransactionSessionFacadeRemote;
005
import com.developerscrappad.intf.IBankTransactionSessionFacadeLocal;
006
import com.developerscrappad.entity.BankAccountEntity;
007
import com.developerscrappad.entity.BankCustomerEntity;
008
import com.developerscrappad.entity.BankGroupProfitEntity;
009
import com.developerscrappad.exception.FundTransferException;
010
import com.developerscrappad.exception.SystemProblemException;
011
import java.math.BigDecimal;
012
import javax.annotation.Resource;
013
import javax.ejb.*;
014
import javax.persistence.EntityManager;
015
import javax.persistence.PersistenceContext;
016
import javax.persistence.Query;
017
 
018
@Stateless
019
public class BankTransactionSessionFacade implements IBankTransactionSessionFacadeLocal, IBankTransactionSessionFacadeRemote
020
{
021
    @PersistenceContext( unitName = "YourPersistenceUnitName" )
022
    private EntityManager em;
023
 
024
    @Resource
025
    private EJBContext context;
026
 
027
    @EJB
028
    private IBankAuditTrailSessionFacadeLocal bankAuditTrailSessionFacadeLocal;
029
 
030
    public BankCustomerEntity findBankCustomerByUsername( String username )
031
    {
032
        Query query = em.createNamedQuery( "BankCustomerEntity.findByUsername" );
033
        query.setParameter( "username", username );
034
 
035
        return ( BankCustomerEntity ) query.getSingleResult();
036
    }
037
 
038
    @TransactionAttribute( TransactionAttributeType.REQUIRED )
039
    public void transferFunds( BankCustomerEntity bankCustomerEntity, String fromAccountNo, String toAccountNo, BigDecimal amount ) throws FundTransferException, SystemProblemException
040
    {
041
        try
042
        {
043
            //Make an initial activity log
044
            bankAuditTrailSessionFacadeLocal.logActivity( bankCustomerEntity, "Initiating Inter Account Fund Transfer of amount: $" + amount );
045
 
046
            //Check for amount greater than 0
047
            if ( amount.doubleValue() <= 0 )
048
            {
049
                throw new FundTransferException( "Invalid transfer amount" );
050
            }
051
 
052
            //Get source bank account entity
053
            Query query = em.createNamedQuery( "BankAccountEntity.findByAccountNo" );
054
            query.setParameter( "accountNo", fromAccountNo );
055
            BankAccountEntity fromBankAccountEntity = null;
056
 
057
            try
058
            {
059
                fromBankAccountEntity = ( BankAccountEntity ) query.getSingleResult();
060
            }
061
            catch ( Exception ex )
062
            {
063
                throw new FundTransferException( "Source Bank Account (" + fromAccountNo + ") Not Found" );
064
            }
065
 
066
            //Check if there are enough funds in the source account for the transfer
067
            BigDecimal sourceBalance = fromBankAccountEntity.getBalance();
068
            BigDecimal bankCharge = new BigDecimal( 2 );
069
 
070
            if ( (sourceBalance.doubleValue() - amount.doubleValue() - bankCharge.doubleValue()) < 0 )
071
            {
072
                throw new FundTransferException( "Not enough funds for transfer" );
073
            }
074
 
075
            //Get target bank account entity
076
            query = em.createNamedQuery( "BankAccountEntity.findByAccountNo" );
077
            query.setParameter( "accountNo", toAccountNo );
078
            BankAccountEntity toBankAccountEntity = null;
079
 
080
            try
081
            {
082
                toBankAccountEntity = ( BankAccountEntity ) query.getSingleResult();
083
            }
084
            catch ( Exception ex )
085
            {
086
                throw new FundTransferException( "Target Bank Account (" + fromAccountNo + ") Not Found" );
087
            }
088
 
089
            //Perform the transfer
090
            sourceBalance = sourceBalance.subtract( amount ).subtract( bankCharge );
091
            fromBankAccountEntity.setBalance( sourceBalance );
092
 
093
            BigDecimal targetBalance = toBankAccountEntity.getBalance();
094
            toBankAccountEntity.setBalance( targetBalance.add( amount ) );
095
 
096
            //The bank makes a profit from the transaction
097
            query = em.createNamedQuery( "BankGroupProfitEntity.findByBranch" );
098
            query.setParameter( "branch", "MAIN" );
099
 
100
            BankGroupProfitEntity bankGroupProfitEntity = ( BankGroupProfitEntity ) query.getSingleResult();
101
            BigDecimal newProfit = bankGroupProfitEntity.getBalance().add( bankCharge );
102
            bankGroupProfitEntity.setBalance( newProfit );
103
 
104
            //Update all the accounts
105
            em.merge( fromBankAccountEntity );
106
            em.merge( toBankAccountEntity );
107
            em.merge( bankGroupProfitEntity );
108
 
109
            bankAuditTrailSessionFacadeLocal.logActivity( bankCustomerEntity, "SUCCESS: Transfered amount $" + amount + " from account: " + fromAccountNo + " to account: " + toAccountNo );
110
        }
111
        catch ( FundTransferException ex2 )
112
        {
113
            if ( !context.getRollbackOnly() )
114
            {
115
                context.setRollbackOnly();
116
            }
117
 
118
            bankAuditTrailSessionFacadeLocal.logActivity( bankCustomerEntity, "FAILED Inter Account Fund Transfer with message (" + ex2.getMessage() + ")" );
119
 
120
            throw ex2;
121
        }
122
        catch ( RuntimeException ex2 )
123
        {
124
            bankAuditTrailSessionFacadeLocal.logActivity( bankCustomerEntity, "Something went wrong with the system's transaction while performing fund transfer" );
125
 
126
            throw new SystemProblemException( ex2.getMessage(), ex2 );
127
        }
128
    }
129
}

Testing The Transaction Process

You may always write snippet of codes to test the transaction like the below:

Context ctx = new InitialContext();
IBankTransactionSessionFacadeRemote remote = \ 
( IBankTransactionSessionFacadeRemote ) ctx.lookup( \
IBankTransactionSessionFacadeRemote.class.getName() );

// You may change this value to be more than the account and see if the transaction would rollback.
BigDecimal transferAmount = new BigDecimal( 1000000 ); 

BankCustomerEntity customer = remote.findBankCustomerByUsername( "MadMax" );
remote.transferFunds( customer, "BK-001-09", "BK-012-10", transferAmount );

Do play around with the value in transferAmount, the fromAccountNo and the toAccountNo and check for the validity of data in the database, you should be able to see the transaction in action.

Summary

So I hope that clears the fog of transaction demacrate REQUIRES_NEW. If your method in a session bean needs to be completed without being affected by either commit or rollback from the calling session bean, REQUIRES_NEW is the way to go.

I’ll continue to the discussion on TransactionAttributeType.MANDATORY in the second part of this series. So for now, do enjoy your brew of codes.

Related Articles

Max Lam

Born and currently resides in Malaysia, a seasoned Java developer whom had held positions as Senior Developer, Consultant and Technical Architect in various enterprise software development companies.

Facebook Twitter LinkedIn Google+ 

Enterprise Web UI
Oracle Certified Java Exam Simulator | Practice Exam

Previous Posts

Quick Note: Unable To Perform LDAP Wildcard “*” Search On Windows Active Directory

Quick Note: Unable To Perform LDAP Wildcard "*" Search On Windows Active Directory

October 9th, 2012

In case you are searching high and low for a solution or an answer to why Windows Active Directory d[...]

Java JNDI/LDAP: Windows Active Directory Authentication, Organizational Unit, Group & Other Information Access

Java JNDI/LDAP: Windows Active Directory Authentication, Organizational Unit, Group & Other Information Access

October 4th, 2012

In today's IT environment, most mid-size corporation and above will have some form of centralized em[...]

MySQL Cluster NDB 7.2 on Solaris 10 Part 3 – Testing The Cluster

MySQL Cluster NDB 7.2 on Solaris 10 Part 3 - Testing The Cluster

September 22nd, 2012

We are back again to have fun with our cluster that we've setup written in our previous articles on [...]

MySQL Cluster NDB 7.2 on Solaris 10 Part 2 – Starting, Distributed Synchronized Users Management And Stopping The Cluster

MySQL Cluster NDB 7.2 on Solaris 10 Part 2 - Starting, Distributed Synchronized Users Management And Stopping The Cluster

September 18th, 2012

This is the continuation from the previous part of the tutorial MySQL Cluster NDB 7.2 on Solaris 10 [...]

MySQL Cluster NDB 7.2 on Solaris 10 Part 1 – How To Install, Setup and Configure

MySQL Cluster NDB 7.2 on Solaris 10 Part 1 - How To Install, Setup and Configure

September 18th, 2012

If you have landed on this page, we believe you might either had a bumpy ride in getting the MySQL c[...]

Quick Fix: How to Solve “Unable to read the logging configuration” on Netbeans7 with JBoss6

Quick Fix: How to Solve "Unable to read the logging configuration" on Netbeans7 with JBoss6

September 8th, 2012

This is just a quick fix post for those whom are having this problem when running JBoss 6.x with Net[...]

Making sense of EJB3.x Transaction Attributes – Part 4 (NEVER)

Making sense of EJB3.x Transaction Attributes – Part 4 (NEVER)

September 5th, 2012

This is the last part in the series of "Making sense of EJB3.x Transaction Attributes". So far, we'v[...]

Making sense of EJB3.x Transaction Attributes – Part 3 (Difference Between SUPPORTS and NOT_SUPPORTED)

Making sense of EJB3.x Transaction Attributes – Part 3 (Difference Between SUPPORTS and NOT_SUPPORTED)

September 5th, 2012

Oracle had extensively documented the behavior of each transaction attributes in the Java EE documen[...]

Making sense of EJB3.x Transaction Attributes – Part 2 (MANDATORY)

Making sense of EJB3.x Transaction Attributes - Part 2 (MANDATORY)

April 17th, 2012

In my previous post, we've discussed about TransactionAttributeType.REQUIRES_NEW: how it behaves and[...]

swiftype.com The easiest and greatest way of adding a search on your web site