Enterprise Java Development@TOPIC@
EJB will instantiate persistence context if does not yet exist
Stateless EJBs may only have transaction-scope persistence contexts
Stateful EJBs may have transaction-scope or extended persistence contexts
EJBs can share persistence contexts
Stateless EJB can propagate its tx-scope persistence context to a called EJB
Stateful EJB can propagate its tx-scoped or extended persistence context to a called EJB
Stateless EJB can work with extended persistence context provided by upstream Stateful client
Stateful EJB cannot transform propagated tx-scope persistence context into an extended
EJB Facade can act as sharing point for common persistence context
Each interaction is independent of the next except for what is stored in DB and at client
@Override
public Guest checkIn(Guest guest, Room room) throws RoomUnavailableExcepton {
Room hotelRoom = dao.getRoom(room.getNumber());
dao.addGuest(guest);
hotelRoom.setOccupant(guest);
return guest;
}
(Error checking removed)
Locate specific room
Add guest to DB
Associate room with guest
List<Room> availableRooms = hotelMgmt.getAvailableRooms(null, 0, 0);
logger.debug("we have {} available rooms", availableRooms.size());
List<Guest> members = new ArrayList<Guest>(availableRooms.size());
int i=0;
for (Room room: availableRooms) {
Guest member = new Guest("member " + i++);
member = hotelMgmt.checkIn(member, room);
members.add(member);
}
Client performs actions one at a time
client: hotelMgmt.getAvailableRooms(null, 0, 0);
[HotelMgmtEJB] *** HotelMgmtEJB(1543684701):init ***
[stdout] Hibernate:
[stdout] select
[stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_,
[stdout] room0_.FLOOR_ID as FLOOR_ID2_2_,
[stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_
[stdout] from
[stdout] EJBJPA_ROOM room0_
[stdout] where
[stdout] room0_.OCCUPANT_ID is null
[stdout] order by
[stdout] room0_.ROOM_NUMBER asc
[HotelMgmtEJB] *** HotelMgmtEJB(1543684701):destroy ***
Persistence context created
Available rooms loaded into persistence context
Result is returned
Persistence context destroyed
Room hotelRoom = dao.getRoom(room.getNumber());
[HotelMgmtEJB] *** HotelMgmtEJB(613346935):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=0, name=member 0], room=Room [number=1, occupant=null])
[stdout] Hibernate:
[stdout] select
[stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_0_,
[stdout] room0_.FLOOR_ID as FLOOR_ID2_2_0_,
[stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_0_
[stdout] from
[stdout] EJBJPA_ROOM room0_
[stdout] where
[stdout] room0_.ROOM_NUMBER=?
Persistence context created
Specific room loaded into persistence context
Guest inserted into DB
This listing provides the main take-away from the stateless solution. The hotel room(s) must be queried for from the database each time they are accessed from the database.
All rooms were queried for when looking for avaiable rooms
Individual room was queried for when making reservation
dao.addGuest(guest);
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[HotelMgmtEJB] *** HotelMgmtEJB(613346935):destroy ***
[stdout] Hibernate:
[stdout] insert
[stdout] into
[stdout] EJBJPA_GUEST
[stdout] (name, GUEST_ID)
[stdout] values
[stdout] (?, ?)
Guest inserted into DB
The transaction does not end until exiting the Stateless EJB method. That means any DB constraint violations will not occur within the context of the EJB call that caused it unless you call em.flush() (as a form of debug) prior to exiting the business method.
hotelRoom.setOccupant(guest);
[stdout] Hibernate:
[stdout] update
[stdout] EJBJPA_ROOM
[stdout] set
[stdout] FLOOR_ID=?,
[stdout] OCCUPANT_ID=?
[stdout] where
[stdout] ROOM_NUMBER=?
Room.occupant foreign key updated
Guest returned to client
Persistence context destroyed
State can be cached in-memory on server-side
Facade EJB propagates persistence context to called EJBs
@Stateful
public class ReservationEJB implements ReservationRemote {
@PersistenceContext(unitName="ejbjpa-hotel", type=PersistenceContextType.EXTENDED)
private EntityManager em;
List<Guest> guests = new LinkedList<Guest>();
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public int addGuest(Guest guest) {
logger.debug("addGuest={}", guest);
if (guest!=null) {
guests.add(guest);
em.persist(guest); //<== no transaction active yet
}
return guests.size();
}
Stateful EJB injects @PersistenceContext -- propagated in downstream EJB calls
Extended persistence context -- may be associated with zero or more transactions
Performing in-memory actions outside of transaction boundary
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Remove
public List<Guest> reserveRooms() throws RoomUnavailableExcepton {
List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, guests.size());
//assign each one of them a room
em.flush(); //<== flush guests persisted outside of a TX for demonstration
List<Guest> completed = new ArrayList<Guest>(guests.size());
Iterator<Room> roomItr = rooms.iterator();
for (Guest guest: guests) {
Room room = roomItr.next();
try {
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
completed.add(guest);
} catch (RoomUnavailableExcepton ex) {
//rollback any previous reservations
ctx.setRollbackOnly();
throw ex;
}
}
return completed;
}
Method executed within active JTA transaction with persistence context associated with transaction
Guests already managed but will be fully persisted in this method
Calls to Stateless HotelMgmtEJB executed on this EJB's persistence context
@Remove indicates stateful instance to be discarded after method called
int availableRooms = hotelMgmt.getAvailableRooms(null, 0, 0).size();
logger.debug("we have {} available rooms", availableRooms);
ReservationRemote checkin = (ReservationRemote) jndi.lookup(reservationJNDI);
for (int i=0; i<availableRooms; i++) {
Guest member = new Guest("member " + i);
int count=checkin.addGuest(member); //this only modifies the in-memory persistence unit
logger.debug("we have {} in our group so far", count);
}
List<Guest> guests = checkin.reserveRooms(); //this is where the DB work gets committed
Multiple requests are issued to Stateful EJB
Specific method(s) act on that state
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
...
em.persist(guest); //<== no transaction active yet
[ReservationEJB] *** ReservationEJB(12230192):init ***
[ReservationEJB] addGuest=Guest [id=0, name=member 0]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[ReservationEJB] addGuest=Guest [id=0, name=member 1]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[ReservationEJB] addGuest=Guest [id=0, name=member 2]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[ReservationEJB] addGuest=Guest [id=0, name=member 3]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
em.persist() outside of transaction causing causing sequence call but no table inserts
List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, guests.size());
[HotelMgmtEJB] *** HotelMgmtEJB(1162892707):init ***
[stdout] Hibernate:
[stdout] select
[stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_,
[stdout] room0_.FLOOR_ID as FLOOR_ID2_2_,
[stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_
[stdout] from
[stdout] EJBJPA_ROOM room0_
[stdout] where
[stdout] room0_.OCCUPANT_ID is null
[stdout] order by
[stdout] room0_.ROOM_NUMBER asc limit ?
[HotelMgmtEJB] *** HotelMgmtEJB(1162892707):destroy ***
Downstream Stateless HotelMgmtEJB queried for rooms
Rooms loaded into persistence context
@TransactionAttribute(TransactionAttributeType.REQUIRED)
...
logger.debug("reserving {} rooms for {} guests", rooms.size(), guests.size());
em.flush(); //<== flush guests persisted outside of a TX for demonstration
[ReservationEJB] reserving 4 rooms for 4 guests
[stdout] Hibernate:
[stdout] insert
[stdout] into
[stdout] EJBJPA_GUEST
[stdout] (name, GUEST_ID)
[stdout] values
[stdout] (?, ?)
SQL inserts issued to DB during flush (requires transaction active)
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
[HotelMgmtEJB] *** HotelMgmtEJB(1086999670):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=65, name=member 0], room=Room [number=1, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(1086999670):destroy ***
[HotelMgmtEJB] *** HotelMgmtEJB(99312186):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=66, name=member 1], room=Room [number=100, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(99312186):destroy ***
[HotelMgmtEJB] *** HotelMgmtEJB(545116383):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=67, name=member 2], room=Room [number=102, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(545116383):destroy ***
[HotelMgmtEJB] *** HotelMgmtEJB(605810979):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=68, name=member 3], room=Room [number=201, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(605810979):destroy ***
[stdout] Hibernate:
[stdout] update
[stdout] EJBJPA_ROOM
[stdout] set
[stdout] FLOOR_ID=?,
[stdout] OCCUPANT_ID=?
[stdout] where
[stdout] ROOM_NUMBER=?
[stdout] Hibernate:
...
[ReservationEJB] *** ReservationEJB(12230192):destroy ***
Stateless HotelMgmtEJB accesses Rooms from EntityManager first-level cache -- no additional DB access
Stateless HotelMgmtEJB updates Room.occupant FK to reference Guest -- who is also already managed and in first-level cache
JTA Transaction committed by container after Stateful EJB method exits
Stateful EJB and persistence context also destroyed after method exits
This listing provides the main take-away from the Stateful solution. The hotel room(s) are queried from up front, maintained in the EXTENDED_CONTEXT Persistence Context within the Stateful Session EJB, and accessed from that cache by the called Stateless Session EJB. This is the same Stateless Session EJB that accessed the DB each time when accessed without a caller cache.
All rooms were queried for when looking for avaiable rooms
Individual room accessed from cache when making reservation
Part of transaction ACID properties
Atomic
All or nothing
Consistent
Valid state
Isolation
Visibility of incomplete changes
Durable
Not forgotten once committed
Figure 101.2. Stateless EJB will Reject New Check-in when Occupied
@Override
public Guest checkIn(Guest guest, Room room) throws RoomUnavailableExcepton {
Room hotelRoom = dao.getRoom(room.getNumber());
if (hotelRoom==null) {
throw new RoomUnavailableExcepton(String.format("room [%d] does not exist", room.getNumber()));
}
if (hotelRoom.getOccupant()!=null) {
throw new RoomUnavailableExcepton(String.format("room is occupied by %s", hotelRoom.getOccupant()));
}
dao.addGuest(guest);
hotelRoom.setOccupant(guest);
return guest;
}
Rejects checkin when invalid
Completes check-in when valid
Transaction per call, no rolling back previous calls
Figure 101.3. Client uses up all Available Rooms
List<Guest> members = new ArrayList<Guest>(availableRooms.size());
int i=0;
for (Room room: availableRooms) {
Guest member = new Guest("member " + i++);
member = hotelMgmt.checkIn(member, room);
members.add(member);
}
Stateless EJB commits each of these check-ins
Figure 101.4. Client Attempts an Invalid Check-in
//try doing it again
Room room = availableRooms.get(0);
Guest member = new Guest("member " + i++);
try {
member = hotelMgmt.checkIn(member, room);
members.add(member);
fail("fail to detect bad checkin");
} catch (RoomUnavailableExcepton ex) {
logger.debug("expected exception making too many reservations:{}", ex.toString());
}
expected exception making too many reservations:info.ejava.examples.ejb.ejbjpa.bl.RoomUnavailableExcepton: room is occupied by Guest [id=103, name=member 0]
Additional check-in rejected -- nothing committed
Figure 101.5. Server still has Initial Committed Check-ins
logger.info("completed reservations for {} guests", members.size());
int availableRooms2 = hotelMgmt.getAvailableRooms(null, 0, 0).size();
logger.info("hotel has {} rooms available", availableRooms2);
assertEquals("", availableRooms.size()-members.size(), availableRooms2);
completed reservations for 4 guests hotel has 0 rooms available
Each check-in occured in own transaction
Later error did not impact previous completed transactions
Multiple resource actions part of same transaction
All-or-none performed
Figure 101.6. Client Issues Guests Up Front
ReservationRemote checkin = (ReservationRemote) jndi.lookup(reservationJNDI);
for (int i=0; i<availableRooms+1; i++) {
Guest member = new Guest("member " + i);
int count=checkin.addGuest(member);
logger.debug("we have {} in our group so far", count);
}
00:47:20 DEBUG HotelMgmtEJBIT:326 - we have 4 available rooms 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 1 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 2 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 3 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 4 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 5 in our group so far
Same stateful process as before -- but with one additional Guest (one too many)
Figure 101.7. Client Attempts to Check-in all Guests at Once
try {
checkin.reserveRooms();
fail("too many check-ins not detected");
} catch (RoomUnavailableExcepton ex) {
logger.debug("expected exception making too many reservations:{}", ex.toString());
}
Client attempts to check-in to all Rooms
Checked exception will be thrown
Figure 101.8. Stateful EJB Attempts to flush() Guests and Reserve all Rooms
@TransactionAttribute(TransactionAttributeType.REQUIRED)
...
em.flush(); //<== flush guests persisted outside of a TX for demonstration
[ReservationEJB] reserving 4 rooms for 5 guests
[stdout] Hibernate:
[stdout] insert
[stdout] into
[stdout] EJBJPA_GUEST
[stdout] (name, GUEST_ID)
[stdout] values
[stdout] (?, ?)
...
Five (5) Guests are flushed to database prior to the rollback
Figure 101.9. Stateful EJB rolls back Transaction on Stateless HotelMgmtEJB Exception
@Resource
private SessionContext ctx;
try {
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
completed.add(guest);
} catch (RoomUnavailableExcepton ex) {
//rollback any previous reservations
ctx.setRollbackOnly();
throw ex;
}
[ReservationEJB] *** ReservationEJB(271512057):destroy ***
expected exception making too many reservations:info.ejava.examples.ejb.ejbjpa.bl.RoomUnavailableExcepton: found on 4 out of 5 required
Persisted Guests removed from database as a part of transaction rollback
Early check-ins never flushed to DB -- discarded as part of rollback
Figure 101.10. Client Checks and finds Rooms Still Available -- all Check-ins Rolled Back
int availableRooms2 = hotelMgmt.getAvailableRooms(null, 0, 0).size();
logger.info("hotel has {} rooms available", availableRooms2);
assertEquals("unexpected room count", availableRooms, availableRooms2);
hotel has 4 rooms available
All check-ins associated with Rooms removed as a part of rollback
One concurrency strategy
Assume failure and take steps to prevent
Create/wait-for a lock on data up-front, prior to getting the data
More expensive when failure assumption over estimated
Opposite of Optimistic Locking concurrency strategy
Assume success and account for failure
Attempt action and detect if action not performed
More efficient when failures do not occur
Some failures can be delegated back to client for resolution
Figure 101.11. Stateful Scenario Ontaining Pessamistic Lock with Rooms
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Remove
public List<Guest> reserveRoomsPessimistic() throws RoomUnavailableExcepton {
List<Room> rooms = em.createQuery(...)
.setFirstResult(0)
.setMaxResults(guests.size())
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
//can also be set globally as persistence.xml property
.setHint("javax.persistence.lock.timeout", 5000)
.getResultList();
return reserveRooms(rooms);
}
[stdout] select [stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_, [stdout] room0_.FLOOR_ID as FLOOR_ID2_2_, [stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_ [stdout] from [stdout] EJBJPA_ROOM room0_ [stdout] where [stdout] room0_.OCCUPANT_ID is null [stdout] order by [stdout] room0_.ROOM_NUMBER asc limit ? for update --notice the FOR UPDATE
setLockMode(LockModeType.PESSIMISTIC_WRITE) - locks row (or table) for remainder of transaction
select ... FOR UPDATE issued for query
competing client thread is blocked until end of transaction