We have prepared a sample application to help developers get familiar with DAO Fusion concepts using popular web technologies and tools:
Hey, but where is Spring ? The truth is, our sample application doesn't use Spring at all. Why?
Please note that Hello, DAO! is essentially a very simple application illustrating basic DAO Fusion concepts - it's definitely not a showcase of best practices or design patterns.
First, check out Hello, DAO! sources from the source code repository:
http://daofusion.googlecode.com/svn/tags/daofusion-1.2.0/samples/hello-dao/
If you use Eclipse IDE, you can generate Eclipse project files via mvn eclipse:eclipse.
Next, start the application via mvn jetty:run. It can take some time though since Maven needs to look for required dependencies and plugins.
Please note that Hello, DAO! uses an embedded H2 database instance running in the same JVM as the application itself.
Once you see something like [INFO] Started Jetty Server on the console, you can launch the application by pointing your browser to:
http://localhost:8080/hellodao/init
This executes the DatabaseInitializationServlet, populating the database with some sensible test data and redirecting the browser to the actual application URL:
http://localhost:8080/hellodao
The application consists of a data grid with filterable and sortable columns. Attributes of records displayed in this grid are mapped either to direct (id, first name and last name), nested (contact e-mail and phone) or collection-based (total orders) properties of the sample Customer entity.
As you can see, there are no navigation widgets for paging through the result set. Instead, we are using SmartGWT's ListGrid with dynamic data paging as the user scrolls through records. The difference between "Refresh" and "Clean refresh" buttons relates to ListGrid's data cache policy - records that were previously fetched are cached on the client, while it's also possible to invalidate this cache to fetch "clean" data from the server. Note that changing filter or sort criteria invalidates the data cache automatically.
We are generally working with a single root persistent entity with its attributes displayed in the data grid - a fictional Customer:
@Entity @Table(name = "customers", uniqueConstraints = {@UniqueConstraint(columnNames = {"firstName", "lastName"})}) public class Customer extends OidBasedMutablePersistentEntity { public static final String _FIRST_NAME = "firstName"; public static final String _LAST_NAME = "lastName"; public static final String _ORDERS = "orders"; public static final String _CONTACT_DETAILS = "contactDetails"; public static final AssociationPath CONTACT_DETAILS = new AssociationPath( new AssociationPathElement(_CONTACT_DETAILS)); @Column(nullable = false, length = 16) private String firstName; @Column(nullable = false, length = 16) private String lastName; @OneToMany(mappedBy = "customer") @Cascade(value = {CascadeType.SAVE_UPDATE, CascadeType.DELETE}) private List<Order> orders = new ArrayList<Order>(); @OneToOne(optional = false) @Cascade(value = {CascadeType.SAVE_UPDATE, CascadeType.DELETE}) private ContactDetails contactDetails; protected List<Order> getOrders() { return orders; } protected void setOrders(List<Order> orders) { this.orders = orders; } public void addOrder(Order order) { orders.add(order); order.setCustomer(this); } public void removeOrder(Order order) { orders.remove(order); order.setCustomer(null); } public List<Order> getUnmodifiableOrderList() { return Collections.unmodifiableList(orders); } // rest of the getters and setters go here }
There are a couple of public static final fields here which will be used later on. For now, all you need to know is that the Customer is a persistent entity with its hashCode / equals methods taken care of by the OidBasedMutablePersistentEntity superclass.
Server code will be working with Customer entities via the CustomerDao interface:
public interface CustomerDao extends PersistentEntityDao<Customer, Long> { // add some business related methods here // (don't forget to integration-test them too) } public class CustomerDaoImpl extends EntityManagerAwareEntityDao<Customer, Long> implements CustomerDao { // instances are created via DaoManager CustomerDaoImpl() { super(); } public Class<Customer> getEntityClass() { return Customer.class; } }
In addition to our DAOs, there are a couple of infrastructure-style classes for storing and providing the injected HibernateEntityManager instance: EntityManagerHolder and EntityManagerAwareEnumerationDao. This is necessary in order to satisfy the AbstractHibernateEntityDao's getHibernateEntityManager method contract:
/** * OpenEntityManagerInViewFilter sets an open Hibernate EntityManager * instance to this class for each thread associated with the request. */ public final class EntityManagerHolder { private static final ThreadLocal<HibernateEntityManager> emPerThread = new ThreadLocal<HibernateEntityManager>(); private EntityManagerHolder() { } public static HibernateEntityManager get() { return emPerThread.get(); } public static void set(HibernateEntityManager em) { emPerThread.set(em); } } public abstract class EntityManagerAwareEnumerationDao<T extends PersistentEnumeration> extends AbstractHibernateEnumerationDao<T> { @Override protected HibernateEntityManager getHibernateEntityManager() { // use the thread-local EntityManager instance return EntityManagerHolder.get(); } }
In its doFilter method, our OpenEntityManagerInViewFilter basically retrieves an appropriate EntityManagerFactory and creates an EntityManager to be used by the current thread. The EntityManager is set into EntityManagerHolder and a transaction is started. Note that this filter provides a single transaction for the entire request processing, which means that it is NOT possible to use various transaction propagation modes in our service methods. You would normally use declarative transaction model supported by Spring or EJB container in serious applications. After the filter chain has completed, transaction is committed, the EntityManager is closed and its reference held by EntityManagerHolder is cleared.
Since we're using GWT on the client, we can use the criteria transfer object pattern to express filter, sort and paging constraints in a concise and elegant way:
public class CustomerCtoConverter extends NestedPropertyCriteriaBasedConverter { public static final String GROUP_NAME = "customer"; public CustomerCtoConverter() { // direct properties addStringLikeMapping(GROUP_NAME, CustomerDataSource._FIRST_NAME, AssociationPath.ROOT, Customer._FIRST_NAME); addStringLikeMapping(GROUP_NAME, CustomerDataSource._LAST_NAME, AssociationPath.ROOT, Customer._LAST_NAME); // collection property addCollectionSizeEqMapping(GROUP_NAME, CustomerDataSource._TOTAL_ORDERS, AssociationPath.ROOT, Customer._ORDERS); // nested properties addStringLikeMapping(GROUP_NAME, CustomerDataSource._CONTACT_EMAIL, Customer.CONTACT_DETAILS, ContactDetails._EMAIL); addStringLikeMapping(GROUP_NAME, CustomerDataSource._CONTACT_PHONE, Customer.CONTACT_DETAILS, ContactDetails._PHONE); } protected void addStringLikeMapping(String mappingGroupName, String propertyId, AssociationPath associationPath, String targetPropertyName) { addMapping(mappingGroupName, new FilterAndSortMapping<String>( propertyId, associationPath, targetPropertyName, FilterCriterionProviders.LIKE, FilterValueConverters.STRING)); } protected void addCollectionSizeEqMapping(String mappingGroupName, String propertyId, AssociationPath associationPath, String targetPropertyName) { addMapping(mappingGroupName, new FilterAndSortMapping<Integer>( propertyId, associationPath, targetPropertyName, FilterCriterionProviders.COLLECTION_SIZE_EQ, FilterValueConverters.INTEGER)); } } public final class FilterCriterionProviders { private FilterCriterionProviders() { } public static final FilterCriterionProvider LIKE = new SimpleFilterCriterionProvider(FilterDataStrategy.DIRECT, 1) { public Criterion getCriterion(String targetPropertyName, Object[] filterObjectValues, Object[] directValues) { return Restrictions.like(targetPropertyName, "%" + (String) directValues[0] + "%"); } }; public static final FilterCriterionProvider COLLECTION_SIZE_EQ = new SimpleFilterCriterionProvider(FilterDataStrategy.DIRECT, 1) { public Criterion getCriterion(String targetPropertyName, Object[] filterObjectValues, Object[] directValues) { return Restrictions.sizeEq(targetPropertyName, (Integer) directValues[0]); } }; }
CustomerCtoConverter contains property mappings for all attributes we want to show in the data grid. All the client needs to do when fetching data is send appropriate CriteriaTransferObject to the server, resting safe in knowledge that our CTO converter transforms it into corresponding PersistentEntityCriteria to be passed to CustomerDao methods. Should we decide not to use the CTO pattern, we would have to create a PersistentEntityCriteria instance manually and populate it with any constraints coming from the client request.
Client and server communicate with each other using GWT RPC mechanism. This means that the server exposes a service interface with methods which can be (asynchronously) called by the client. What we're really into in this tutorial are some very basic CRUD (create, read, update and delete) operations:
@RemoteServiceRelativePath(HelloDAO.RPC_SERVICE_RELATIVE_PATH) public interface CustomerService extends GridService<CustomerDto> { // add some business related methods here // (don't forget to test them too) } public interface GridService<DTO extends IsSerializable> extends RemoteService { ResultSet<DTO> fetch(CriteriaTransferObject cto) throws ServiceException; DTO add(DTO dto) throws ServiceException; DTO update(DTO dto) throws ServiceException; void remove(DTO dto) throws ServiceException; } public class ResultSet<DTO extends IsSerializable> implements IsSerializable { private List<DTO> resultList; private Integer totalRecords; // for serialization purposes protected ResultSet() { } public ResultSet(List<DTO> resultList, Integer totalRecords) { this.resultList = resultList; this.totalRecords = totalRecords; } public List<DTO> getResultList() { return resultList; } public Integer getTotalRecords() { return totalRecords; } }
Note that the asynchronous versions of service classes required by GWT RPC (CustomerServiceAsync and GridServiceAsync) are left out for the sake of simplicity. The CustomerService works with CustomerDto as its transfer object that holds data about Customer attributes to be shown in the data grid:
public class CustomerDto extends AbstractDto { // direct properties private String firstName; private String lastName; // nested properties private String contactEmail; private String contactPhone; // collection property private Integer totalOrders; // getters and setters go here } public abstract class AbstractDto implements IsSerializable { // ListGrid records should define a unique key, // in our case this will be the database id private String id; // getter and setter goes here }
The final piece of the RPC puzzle is a servlet that is responsible for servicing RPC requests while implementing the CustomerService interface:
/** * Server-side CustomerService implementation based on GWT RPC's RemoteServiceServlet. */ public class RpcServiceServlet extends RemoteServiceServlet implements CustomerService { private static final CriteriaTransferObjectConverter ctoConverter = new CustomerCtoConverter(); private static final CustomerDao customerDao = DaoManager.getCustomerDao(); public ResultSet<CustomerDto> fetch(CriteriaTransferObject cto) throws ServiceException { PersistentEntityCriteria queryCriteria = ctoConverter.convert( cto, CustomerCtoConverter.GROUP_NAME); List<Customer> cList = customerDao.query(queryCriteria); PersistentEntityCriteria countCriteria = ctoConverter.convert( new CriteriaTransferObjectCountWrapper(cto).wrap(), CustomerCtoConverter.GROUP_NAME); int totalRecords = customerDao.count(countCriteria); List<CustomerDto> dtoList = toDtoList(cList); return new ResultSet<CustomerDto>(dtoList, totalRecords); } public CustomerDto add(CustomerDto dto) throws ServiceException { Customer c = new Customer(); c.setContactDetails(new ContactDetails()); copyValues(dto, c); c = customerDao.saveOrUpdate(c); copyValues(c, dto); return dto; } public CustomerDto update(CustomerDto dto) throws ServiceException { Customer c = customerDao.get(Long.valueOf(dto.getId())); copyValues(dto, c); c = customerDao.saveOrUpdate(c); copyValues(c, dto); return dto; } public void remove(CustomerDto dto) throws ServiceException { customerDao.delete(Long.valueOf(dto.getId())); } // DTO <--> persistent entity transformations private void copyValues(Customer from, CustomerDto to) { to.setId(String.valueOf(from.getId())); to.setFirstName(from.getFirstName()); to.setLastName(from.getLastName()); to.setTotalOrders(from.getUnmodifiableOrderList().size()); to.setContactEmail(from.getContactDetails().getEmail()); to.setContactPhone(from.getContactDetails().getPhone()); } private void copyValues(CustomerDto from, Customer to) { to.setFirstName(from.getFirstName()); to.setLastName(from.getLastName()); to.getContactDetails().setEmail(from.getContactEmail()); to.getContactDetails().setPhone(from.getContactPhone()); } private List<CustomerDto> toDtoList(List<Customer> cList) { List<CustomerDto> result = new ArrayList<CustomerDto>(); for (Customer c : cList) { CustomerDto dto = new CustomerDto(); copyValues(c, dto); result.add(dto); } return result; } }
That's it! All we have to do now is to use the asynchronous version of CustomerService on the client when fetching grid data.
SmartGWT's ListGrid displays and manipulates data managed by the DataSource. After realizing there is no official data source for GWT RPC, we were lucky enough to find an "unofficial" implementation - the GwtRpcDataSource (credits to Aleksandras Novikovas).
SmartGWT's DataSource essentially defines four basic operations corresponding to CRUD: fetch, add, update and remove. If you look at the GridService shown above, you will notice it already defines these operations. Knowing this, we are able to extend the GwtRpcDataSource by providing appropriate CRUD implementations which delegate to GridService as well as proper DTO / CTO handling:
/** * Generic GWT RPC data source that supports common grid-like operations via GridService. */ public abstract class GridServiceDataSource<DTO extends IsSerializable, SERVICE extends GridServiceAsync<DTO>> extends GwtRpcDataSource { private final SERVICE service; public GridServiceDataSource(SERVICE service, DataSourceField... fields) { this.service = service; for (DataSourceField f : fields) addField(f); // record cache is dropped whenever grid criteria changes setCriteriaPolicy(CriteriaPolicy.DROPONCHANGE); } // executeFetch, executeAdd, executeUpdate and executeRemove implementations go here protected abstract DTO newDtoInstance(); protected abstract void copyValues(ListGridRecord from, DTO to); protected abstract void copyValues(DTO from, ListGridRecord to); }
Things are now as simple as implementing those three abstract methods dealing with data transfer objects representing records in the data grid. Note that we need to do DTO / ListGridRecord conversion explicitly since SmartGWT's list grid records are essentially JavaScript objects that work in terms of attributes and their values.
Following code shows the actual data source implementation:
public class CustomerDataSource extends GridServiceDataSource<CustomerDto, CustomerServiceAsync> { public static final String _ID = "id"; public static final String _FIRST_NAME = "firstName"; public static final String _LAST_NAME = "lastName"; public static final String _TOTAL_ORDERS = "totalOrders"; public static final String _CONTACT_EMAIL = "contactEmail"; public static final String _CONTACT_PHONE = "contactPhone"; private static final CustomerServiceAsync service = GWT.create(CustomerService.class); // data source field definitions private static final DataSourceField id = new DataSourceTextField(_ID); private static final DataSourceField firstName = new DataSourceTextField(_FIRST_NAME); private static final DataSourceField lastName = new DataSourceTextField(_LAST_NAME); private static final DataSourceField totalOrders = new DataSourceIntegerField(_TOTAL_ORDERS); private static final DataSourceField contactEmail = new DataSourceTextField(_CONTACT_EMAIL); private static final DataSourceField contactPhone = new DataSourceTextField(_CONTACT_PHONE); static { id.setCanEdit(false); id.setPrimaryKey(true); totalOrders.setCanEdit(false); } public CustomerDataSource() { super(service, id, firstName, lastName, totalOrders, contactEmail, contactPhone); } protected void copyValues(ListGridRecord from, CustomerDto to) { to.setId(from.getAttributeAsString(_ID)); to.setFirstName(from.getAttributeAsString(_FIRST_NAME)); to.setLastName(from.getAttributeAsString(_LAST_NAME)); to.setTotalOrders(from.getAttributeAsInt(_TOTAL_ORDERS)); to.setContactEmail(from.getAttributeAsString(_CONTACT_EMAIL)); to.setContactPhone(from.getAttributeAsString(_CONTACT_PHONE)); } protected void copyValues(CustomerDto from, ListGridRecord to) { to.setAttribute(_ID, from.getId()); to.setAttribute(_FIRST_NAME, from.getFirstName()); to.setAttribute(_LAST_NAME, from.getLastName()); to.setAttribute(_TOTAL_ORDERS, from.getTotalOrders()); to.setAttribute(_CONTACT_EMAIL, from.getContactEmail()); to.setAttribute(_CONTACT_PHONE, from.getContactPhone()); } protected CustomerDto newDtoInstance() { return new CustomerDto(); } }
We recommend checking out WEB-INF/web.xml, META-INF/persistence.xml and HelloDAO.gwt.xml files as well to get an overview of how DAO Fusion integrates with SmartGWT in a typical servlet environment.