Enterprise Java Development@TOPIC@
Problem occurs when managed @Entity classes used as DTOs
Provider places proxy classes on managed entities to watch for changes
Provider classes can get marshaled back to client
Client encounters ClassNotFoundException when deserializing RMI object with provider class
javax.ejb.EJBException: java.lang.ClassNotFoundException: org.hibernate.proxy.pojo.javassist.SerializableProxy
Client must add hibernate-core in classpath to avoid ClassNotFoundException
<!-- used if hibernate entities re-used as DTOs -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<scope>test</scope>
</dependency>
@Entity
public class Room implements Serializable {
@Id
@Column(name="ROOM_NUMBER")
private int number;
@ManyToOne(optional=false, fetch=FetchType.LAZY)
@JoinColumn(name="FLOOR_ID")
private Floor floor;
@OneToOne(optional=true, fetch=FetchType.LAZY)
@JoinColumn(name="OCCUPANT_ID")
private Guest occupant;
Room has mandatory reference to Floor and optional reference to Guest
fetch=LAZY references most likely will be proxies implemented by JPA provider classes
@Override
public List<Room> getAvailableRooms(Integer level, int offset, int limit) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Room> qdef = cb.createQuery(Room.class);
Root<Room> r = qdef.from(Room.class);
...
em.createQuery(qdef)
.setFirstResult(offset);
.setMaxResults(limit);
.getResultList();
}
Room entities returned from query are passed to client
72 List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, 1);
75 Room room = rooms.get(0);
77 logger.info("what floor class is this??? {}", room.getFloor().getClass());
78 assertFalse("floor was not proxy", room.getFloor().getClass().equals(Floor.class));
80 Guest guest = new Guest("Cosmo Kramer");
81 guest = hotelMgmt.checkIn(guest, room);
82 logger.info("final guest: {}:{}", guest.getClass(), guest);
HotelMgmtEJBIT:77 - what floor class is this??? class info.ejava.examples.ejb.ejbjpa.bo.Floor_$$_jvst9a8_0 HotelMgmtEJBIT:82 - final guest: class info.ejava.examples.ejb.ejbjpa.bo.Guest:Guest [id=11, name=Cosmo Kramer]
Room returned with proxy class (Floor_$$_jvst9a8_0
)between Room and Floor
Requires client to have hibernate-core in classpath
Allows @Entity classes to be used as pure POJOs -- these have never been managed
Contains no provider proxy classes
No requirement to have client update classpath
@Override
public List<Room> getCleanAvailableRooms(Integer level, int offset, int limit) {
List<Room> rooms = getAvailableRooms(level, offset, limit);
return toClean(rooms);
}
/**
* This helper method will instantiate new entity classes to re-use as DTOs.
* This is done to remove hibernate-proxy classes that are part of the managed
* entity.
*/
private List<Room> toClean(List<Room> rooms) {
if (rooms==null) { return null; }
List<Room> cleanRooms = new ArrayList<Room>(rooms.size());
for (Room room : rooms) {
Floor floor = room.getFloor();
Floor cleanFloor = new Floor(floor.getLevel());
Room cleanRoom = new Room(cleanFloor, room.getNumber());
cleanFloor.withRoom(cleanRoom);
Guest occupant = room.getOccupant();
if (occupant!=null) {
Guest cleanOccupant = new Guest(occupant.getId());
cleanOccupant.setName(occupant.getName());
cleanRoom.setOccupant(cleanOccupant);
}
cleanRooms.add(cleanRoom);
}
return cleanRooms;
}
EJB Remote facade creating new instances of @Entity classes
this looks like a good floor: class info.ejava.examples.ejb.ejbjpa.bo.Floor final guest: class info.ejava.examples.ejb.ejbjpa.bo.Guest:Guest [id=17, name=Cosmo Kramer]
Client now gets Room.floor without provider proxy class in between
The "cleansing" method was shown as a helper method for simplicity. It is advised to encapsulate that within a separate helper class so that it is easily used by other Remote Facades that must cleans their returned objects.
Caller attempts to access an Entity property that has not yet been loaded from the database
Provider unable to fullfil that request
DB session closed before necessary references resolved
Common when using @Entities across Persistence Unit boundaries
Not unique to RMI
@Entity
public class Floor implements Serializable {
@Id
@Column(name="LEVEL", nullable=false)
int level;
@OneToMany(mappedBy="floor",
fetch=FetchType.LAZY,
cascade={CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH},
orphanRemoval=true)
@OrderBy("number")
List<Room> rooms;
Rooms fetch=LAZY
Rooms must be fetched within same DB session when using that collection
@Stateless
public class HotelMgmtEJB implements HotelMgmtRemote, HotelMgmtLocal {
...
@Override
public Floor getFloor(int level) {
return em.find(Floor.class, level);
}
Floor being marshaled directly back to client without addressing LAZY fetches
112 Floor floor = hotelMgmt.getFloor(0);
113 assertNotNull("floor not found", floor);
114 try {
115 logger.info("foor has {} rooms", floor.getRooms().size());
116 fail("did not get lazy-load exception");
117 } catch (LazyInitializationException expected) {
118 logger.info("got expected exception:{}", expected.toString());
119 }
HotelMgmtEJBIT:118 - got expected exception:org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: info.ejava.examples.ejb.ejbjpa.bo.Floor.rooms, could not initialize proxy - no Session
Floor can be accessed
Floor.room access causes Lazy-load exception of collection
Must be done in location where Persistence Unit still accessible
Simple/brute force technique to stimulate resolution of references
Where do you stop???
Repetitive round trips to DB can be expensive
@Override
public Floor getTouchedFloor(int level) {
Floor floor = getFloor(level);
if (floor!=null) {
//touch the managed-floor to cause lazy-loads to be resolved
floor.getRooms().isEmpty();
for (Room room: floor.getRooms()) {
Guest guest = room.getOccupant();
if (guest!=null) {
guest.getName(); //touch all occupants to cause lazy-loads to be resolved
}
}
}
return floor;
}
Server-side code, within the DB session boundary stimulates references to be loaded prior to marshaling back to client
Floor floor = getFloor(level);
select floor0_.LEVEL as LEVEL1_0_0_ from EJBJPA_FLOOR floor0_ where floor0_.LEVEL=?
if (floor!=null) {
floor.getRooms().isEmpty();
select rooms0_.FLOOR_ID as FLOOR_ID2_0_0_, rooms0_.ROOM_NUMBER as ROOM_NUM1_2_0_, rooms0_.ROOM_NUMBER as ROOM_NUM1_2_1_, rooms0_.FLOOR_ID as FLOOR_ID2_2_1_, rooms0_.occupant_GUEST_ID as occupant3_2_1_ from EJBJPA_ROOM rooms0_ where rooms0_.FLOOR_ID=? order by rooms0_.ROOM_NUMBER
for (Room room: floor.getRooms()) { Guest guest = room.getOccupant(); if (guest!=null) { guest.getName(); //touch all occupants to cause lazy-loads to be resolved
select guest0_.GUEST_ID as GUEST_ID1_1_0_, guest0_.name as name2_1_0_ from EJBJPA_GUEST guest0_ where guest0_.GUEST_ID=? select guest0_.GUEST_ID as GUEST_ID1_1_0_, guest0_.name as name2_1_0_ from EJBJPA_GUEST guest0_ where guest0_.GUEST_ID=?
DAO queries crafted to support remote access patterns
LAZY fetches resolved through "join fetch" queries
@Override
public Floor getFetchedFloor(int level) {
List<Floor> floors = em.createNamedQuery("Floor.fetchFloor",
Floor.class)
.setParameter("level", level)
.getResultList();
return floors.isEmpty() ? null : floors.get(0);
}
@Entity
@NamedQueries({
@NamedQuery(name="Floor.fetchFloor",
query="select f from Floor f "
+ "join fetch f.rooms r "
+ "join fetch r.occupant "
+ "where f.level=:level")
})
public class Floor implements Serializable {
Join fetch used to EAGER-ly load child rows
Less trips to DB for fatch=LAZY mappings
select floor0_.LEVEL as LEVEL1_0_0_, rooms1_.ROOM_NUMBER as ROOM_NUM1_2_1_, guest2_.GUEST_ID as GUEST_ID1_1_2_, rooms1_.FLOOR_ID as FLOOR_ID2_2_1_, rooms1_.occupant_GUEST_ID as occupant3_2_1_, rooms1_.FLOOR_ID as FLOOR_ID2_0_0__, rooms1_.ROOM_NUMBER as ROOM_NUM1_2_0__, guest2_.name as name2_1_2_ from EJBJPA_FLOOR floor0_ inner join EJBJPA_ROOM rooms1_ on floor0_.LEVEL=rooms1_.FLOOR_ID inner join EJBJPA_GUEST guest2_ on rooms1_.occupant_GUEST_ID=guest2_.GUEST_ID where floor0_.LEVEL=? order by rooms1_.ROOM_NUMBER
Still have to know "when is enough -- enough"
Server-side has job to implement overall capability
Client may have an "outsider" role
@Entity
public class Room implements Serializable {
@Id
@Column(name="ROOM_NUMBER")
private int number;
...
@OneToOne(optional=true, fetch=FetchType.LAZY)
@JoinColumn(name="OCCUPANT_ID")
private Guest occupant;
More information than the client wants/needs
More information than what client should have
But that is how out server-side model is designed...
//lets see if we can manually find a vacant room.....
Floor floor = hotelMgmt.getFetchedFloor(0);
//all floors have at least one occupant
for (Room room: floor.getRooms()) {
Guest occupant = room.getOccupant();
if (occupant!=null) {
logger.info("hey {}, are you done with room {} yet?",
occupant.getName(), room.getNumber());
//that is just rude
}
}
hey guest 1, are you done with room 0 yet? hey guest 3, are you done with room 2 yet?
Client only needed to know if room was occupied -- not by who
public class RoomDTO implements Serializable {
private int number;
private boolean occupied;
Room DTO class is only expressing that room is occupied
@Override
public FloorDTO getFetchedFloorDTO(int level) {
Floor floor = getFetchedFloor(level);
return toDTO(floor);
}
private FloorDTO toDTO(Floor floor) {
if (floor==null) { return null; }
FloorDTO floorDTO = new FloorDTO(floor.getLevel());
if (floor.getRooms()!=null) { for (Room room: floor.getRooms()) {
floorDTO.withRoom(toDTO(room));
}}
return floorDTO;
}
private RoomDTO toDTO(Room room) {
if (room==null) { return null; }
RoomDTO roomDTO = new RoomDTO(room.getNumber());
//remote client shouldn't care who is in the room -- just if busy
roomDTO.setOccupied(room.getOccupant()!=null);
return roomDTO;
}
Similar to @Entity cleansing since DTO classes aren't managed
Can indirectly solve LAZY-load issue because @Entity is walked on server-side
Must still pay attention to DB access for performance reasons
//lets see if we can manually find a vacant room.....
FloorDTO floor = hotelMgmt.getFetchedFloorDTO(0);
//all floors have at least one occupant
for (RoomDTO room: floor.getRooms()) {
if (room.isOccupied()) {
logger.info("hey whoever, are you done with room {} yet?", room.getNumber());
//still rude, but a bit more private
}
}
hey whoever, are you done with room 0 yet? hey whoever, are you done with room 2 yet?
Client no longer has access to who is in each room -- but server-side does
The "toDTO" method was shown as a helper method for simplicity. It is advised to encapsulate that within a separate helper class so that it is easily used by other Remote Facades that must translate an Entity class to a DTO. Note that the reverse is also common -- to have DTOs converted to Entity POJOs for incoming objects.