Enterprise Java Development@TOPIC@
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 must have hibernate-core in classpath to avoid ClassNotFoundException
Figure 98.1. Remote Client De-serialization Error with Missing Provider Class
javax.ejb.EJBException: java.lang.ClassNotFoundException: org.hibernate.proxy.pojo.javassist.SerializableProxy
Figure 98.2. Hibernate dependency allowing @Entity classes to be DTOs
<!-- used if hibernate entities re-used as DTOs -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<scope>test</scope>
</dependency>
Figure 98.3. Example: @Entity Class Returned to Client
@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 option reference to Guest
fetch=LAZY references most likely will be proxies implemented by JPA provider classes
Figure 98.4. Example: EJB @Remote Method Returns Query Result Directly to Client
@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
Figure 98.5. Example: Provider Proxy Classes Marshaled to Client
72 List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, 1);
75 Room room = rooms.get(0);
77 logger.info("what's floor 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's floor 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 POJOs except never managed
Contains no provider proxy classes
Figure 98.6. Example: EJB Copies Managed @Entity to new Instance
@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
Figure 98.7. Example: Client Receives Pure POJOs without Provider 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
DB session closed before necessary references resolved
Common when using @Entities across transaction boundaries
Not unique to RMI
Figure 98.8. Floor @Entity
@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
Figure 98.9. Standard @Entity Access
@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
Figure 98.10. Remote Client Causing LAZY-load Exception
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
Simple/brute force technique to stimulate resolution of references
Where do you stop???
Repetitive round trips to DB can be expensive
Figure 98.11. Server-side Stimulates References To Be Loaded
@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
Figure 98.12. Initial getFloor() queries parent FLOOR table
Floor floor = getFloor(level);
select floor0_.LEVEL as LEVEL1_0_0_ from EJBJPA_FLOOR floor0_ where floor0_.LEVEL=?
Figure 98.13. Accessing Rooms Collection Causes Child Table Load
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
Figure 98.14. Additional Child Table Load Occurring One-at-a-Time
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
Figure 98.15. DAO Query Fetches Child Tables
@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
Figure 98.16. Returned Object Tree Accessed in fewer Queries
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
Figure 98.17. Room @Entity references Sensitive Occupant Information
@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...
Figure 98.18. @Entity Model mis-used by Client
//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
Figure 98.19. DTO Represents what Clients Can/Should Know
public class RoomDTO implements Serializable {
private int number;
private boolean occupied;
Room DTO class is only expressing that room is occupied
Figure 98.20. Server-side constructs DTOs
@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
Figure 98.21. Client receives more Appropriate Abstraction
//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