Transferring JPA objects from the server to the browser
In the previous post we added support for persistence to Serendipity by taking advantage of JPA, Hibernate and HSQLDB. In this post, we're going to look at how to transfer JPA objects from the server to the browser.
In this post, we'll:
- Learn more about JPA objects
- Create a Data Transfer Object
- Test the application in development mode
Note: You can download the sample application from the crmdipity (Se-r-en-dipity) project's download page.
1. Learn more about JPA objects
In the previous post we created an entity from the application's domain model — a Plain Old Java Object (POJO) — and added some annotations to indicate how we wanted JPA to persist it.
When you annotate an object JPA must instrument the object in order to make it persistent. This means that even though we started out with a POJO if we try to transfer the object from the server to the browser the GWT RPC mechanism no longer knows what the object's type is and cannot deserialise it. Hibernate, for example, replaces and rewrites the bytecode for persistent entities so by the time the object is ready to be sent over the wire it isn't the same object that the compiler thought was going to be transferred.
The common approach used to address this issue is to introduce a light-weight object that acts as an intermediary between the heavy-weight JPA object and its client-side representation. This go-between is referred to as a Data Transfer Object (DTO).
2. Create a Data Transfer Object
A data transfer object is a simple POJO that only contains the properties required for the client-side representation. The DTO only contains the data that we want to persist and none of the lazy loading or persistence logic added by the JPA instrumentation. Take a look at the following class:
...
public class AccountsDto implements Serializable {
private static final long serialVersionUID = 6527684409351456975L;
// as per AccountsRecord
private static final String ACCOUNT_ID_DISPLAY_NAME = "Account Id";
private static final String ACCOUNT_NAME_DISPLAY_NAME = "Account Name";
private static final String MAIN_PHONE_DISPLAY_NAME = "Main Phone";
private static final String LOCATION_DISPLAY_NAME = "Location";
private static final String PRIMARY_CONTACT_DISPLAY_NAME = "Primary Contact";
private static final String EMAIL_PRIMARY_CONTACT_DISPLAY_NAME = "Email (Primary Contact)";
private Long accountId;
private String accountName;
private String mainPhone;
private String location;
private String primaryContact;
private String emailPrimaryContact;
public AccountsDto() {}
public AccountsDto(Long accountId) {
this.accountId = accountId;
}
public AccountsDto(Long accountId, String accountName, String mainPhone,
String location, String primaryContact, String emailPrimaryContact) {
this.accountId = accountId;
this.accountName = accountName;
this.mainPhone = mainPhone;
this.location = location;
this.primaryContact = primaryContact;
this.emailPrimaryContact = emailPrimaryContact;
}
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public String getMainPhone() {
return mainPhone;
}
public void setMainPhone(String mainPhone) {
this.mainPhone = mainPhone;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getPrimaryContact() {
return primaryContact;
}
public void setPrimaryContact(String primaryContact) {
this.primaryContact = primaryContact;
}
public String getEmailPrimaryContact() {
return emailPrimaryContact;
}
public void setEmailPrimaryContact(String emailPrimaryContact) {
this.emailPrimaryContact = emailPrimaryContact;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(ACCOUNT_ID_DISPLAY_NAME).append(": ").append(getAccountId()).append(", ");
sb.append(ACCOUNT_NAME_DISPLAY_NAME).append(": ").append(getAccountName()).append(", ");
sb.append(MAIN_PHONE_DISPLAY_NAME).append(": ").append(getMainPhone()).append(", ");
sb.append(LOCATION_DISPLAY_NAME).append(": ").append(getLocation()).append(", ");
sb.append(PRIMARY_CONTACT_DISPLAY_NAME).append(": ").append(getPrimaryContact()).append(", ");
sb.append(EMAIL_PRIMARY_CONTACT_DISPLAY_NAME).append(getEmailPrimaryContact());
return sb.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountName == null) ? 0 : accountName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof AccountsDto))
return false;
AccountsDto other = (AccountsDto) obj;
if (accountId == null) {
if (other.accountId != null)
return false;
} else if (!accountId.equals(other.accountId))
return false;
if (accountName == null) {
if (other.accountName != null)
return false;
} else if (!accountName.equals(other.accountName))
return false;
if (emailPrimaryContact == null) {
if (other.emailPrimaryContact != null)
return false;
} else if (!emailPrimaryContact.equals(other.emailPrimaryContact))
return false;
if (location == null) {
if (other.location != null)
return false;
} else if (!location.equals(other.location))
return false;
if (mainPhone == null) {
if (other.mainPhone != null)
return false;
} else if (!mainPhone.equals(other.mainPhone))
return false;
if (primaryContact == null) {
if (other.primaryContact != null)
return false;
} else if (!primaryContact.equals(other.primaryContact))
return false;
return true;
}
}
And, you will notice that it implements the 'Serializable' interface and only contains the data that we want to persist.
We also need to update the RetreiveAccountsHandler:
...
public class RetrieveAccountsHandler implements
ActionHandler<RetrieveAccountsAction, RetrieveAccountsResult> {
private final Provider<HttpServletRequest> requestProvider;
private final ServletContext servletContext;
@Inject
RetrieveAccountsHandler(final ServletContext servletContext,
final Provider<HttpServletRequest> requestProvider) {
this.servletContext = servletContext;
this.requestProvider = requestProvider;
}
@Override
public RetrieveAccountsResult execute(RetrieveAccountsAction action,
ExecutionContext context) throws ActionException {
RetrieveAccountsResult result = null;
AccountDao accountDao = new AccountDao();
DOMConfigurator.configure("log4j.xml");
Log.info("Retrieve Accounts");
try {
List<Account> accounts = accountDao.retrieveAccounts();
if (accounts != null) {
List<AccountsDto> accountsDtos = new ArrayList<AccountsDto>(accounts.size());
for (Account account : accounts) {
accountsDtos.add(createAccountsDto(account));
}
result = new RetrieveAccountsResult(accountsDtos);
}
}
catch (Exception e) {
Log.warn("Unable to retrieve Accounts - ", e);
throw new ActionException(e);
}
return result;
}
private AccountsDto createAccountsDto(Account account) {
return new AccountsDto(account.getAccountId(), account.getAccountName(), account.getMainPhone(),
account.getCity(), account.getPrimaryContact(), account.getEmailPrimaryContact());
}
@Override
public Class<RetrieveAccountsAction> getActionType() {
return RetrieveAccountsAction.class;
}
@Override
public void undo(RetrieveAccountsAction action, RetrieveAccountsResult result,
ExecutionContext context) throws ActionException {
}
}
Take a look at the createAccountsDto method and you will notice that it is responsible for mapping an Account to an AccountDto.
We can also take advantage of the @GenDispatch annotation in order to generate the 'RetrieveAccounts' Action and Result classes:
package au.com.uptick.serendipity.shared.action;
import java.util.List;
import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.Out;
import com.gwtplatform.dispatch.shared.UnsecuredActionImpl;
import au.com.uptick.serendipity.shared.dto.AccountsDto;
@GenDispatch(isSecure = false, serviceName = UnsecuredActionImpl.DEFAULT_SERVICE_NAME)
public class RetrieveAccounts {
@Out(1) List<AccountsDto> accountDtos;
}
3. Test the application in development mode
At this point, you should be able to compile Serendipity and launch it from within Eclipse.
You can use the AccountTestCase to populate the 'Account' table (e.g. just comment out the calls to updateAccount and deleteAccount) and don't forget to update the "hibernate.hbm2ddl.auto" property (e.g. run the JUnit test with value="create" then comment it out) in the persistence.xml file.

Note: You can download the sample application from the crmdipity (Se-r-en-dipity) project's download page.
What's Next
At this point, we've learnt about JPA instrumentation and how to use Data Transfer Objects. Next we'll look at a variation of the Model-View-Presenter (MVP) design pattern so that we can restrict the use of smartGWT classes to Views and test Presenters with JUnit.
