12.10.2008

Entity Relationships and DTOs: A Better Way (Part 1)

Introduction and Simple Case

When entities are represented as transfer objects for a client, the server must assemble an entity representation of that transfer object before using it in a persistence layer. Sometimes this can be simple:
public class User {
private Long id;
private String firstName;
private String lastName;
private String email;

// getters/setters
...
}

In this case, the UserDto fields can exactly mimic the User fields and the server can create an entity with ease:
User u = new User();
u.setId(dto.getId());
u.setfirstName(dto.getFirstName());
u.setLastName(dto.getLastName());
u.setEmail(dto.getEmail());

Entity Relationships and a Naive Approach

Sometimes, a transfer object needs to represent an entity relationship, which adds to the complexity. For instance, if we were to add a one-to-many relationship from a User entity to a Role entity, the User entity would change as follows:
public class User {
private Long id;
private String firstName;
private String lastName;
private String email;
private Role role; // entity relationship

// getters/setters
...
}

Typically you would not add an entire RoleDto to the UserDto, instead opting to just add a field to the UserDto to hold the Role's primary key identifier. How does this affect the code to create a User entity from the UserDto?

A sensible first approach to the problem would be the following:
User u = new User();
u.setId(dto.getId());
u.setfirstName(dto.getFirstName());
u.setLastName(dto.getLastName());
u.setEmail(dto.getEmail());
u.setRole(roleDao.findById(dto.roleId));

A Problem of Scalability

From a coding standpoint, this seems like a nice, simple and clean approach. Unfortunately, this solution has a problem. The problem is the line that retrieves the Role entity using the findById() method. This hits the database with an additional SELECT statement every time you want to update the user. A SELECT and an UPDATE instead of just an UPDATE doesn't sound like the end of the world; and it isn't. Scalability is the problem. Suppose instead of having to create a single User entity to save, you have to create a collection of User entities. Obviously you could take the above code and put it in a for loop:
for(UserDto dto : userDtos) {
User u = new User();
u.setId(dto.getId());
u.setfirstName(dto.getFirstName());
u.setLastName(dto.getLastName());
u.setEmail(dto.getEmail());
u.setRole(roleDao.findById(dto.roleId));

// add u to list and save in bulk after loop
}

A collection of 100 User transfer objects now results in 100 extra SELECT statements! This seems highly unnecessary, especially considering that there probably aren't 100 distinct Role types in this fictitious system the example is using.

A Reasonable Solution

A straightforward approach to fixing the problem is to code around it. To do that, we'll first look through the collection of User transfer objects, pulling out the primary key identifiers of the Role entities. Then we'll get all the Role entities at once with a single find (single SELECT statement behind the scenes) and put them into a Map. Lastly, we'll set the User entity's role from an element in the Map, eliminating the SELECT statement per User entity that we previously had:
// get Role entities to load
Set roleIds = new HashSet(); // a set will manage dupes for us
for(UserDto dto : userDtos) {
roleIds.add(dto.roleId);
}

// load Role entities and store in Map
List roles = roleDao.findByIds(roleIds);

Map rolesMap = new HashMap();
for(Role role : roles) {
rolesMap.put(role.getId(), role);
}

// the original save loop, slightly modified
for(UserDto dto : userDtos) {
User u = new User();
u.setId(dto.getId());
u.setfirstName(dto.getFirstName());
u.setLastName(dto.getLastName());
u.setEmail(dto.getEmail());
u.setRole(rolesMap.get(dto.roleId)); // the slight modification

// add u to list and save in bulk after loop
}

The detail of the findByIds() method isn't that important. Maybe it uses an IN clause. Just as long as it doesn't do a SELECT for each id that is passed...

The upside to this code is that it addresses the issue of scalability and doesn't require any extra knowledge about Java or Hibernate or databases beyond what you probably already know.

The downside to this solution is that it requires an additional 9 lines of code for each relationship in the transfer object. The savvy developer could come up with a simple utility function that can produce the same Map through a clever combination of generics and reflection.

Be sure to check back for Part 2 of this article, which will explore a solution that takes advantage of Hibernate's ability to use proxy objects in its persistence context.

No comments:

Post a Comment