The following DAO implementations will inherit the core CRUD operations from GenericDaoImpl
and add their own class-specific methods as defined in the implemented interface. Each method will use the @Transactional
annotation to define the appropriate transactional behavior.
The full listing for our CompanyDaoImpl
class is as follows:
package com.gieman.tttracker.dao;
import com.gieman.tttracker.domain.Company;
import java.util.List;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Repository("companyDao")
@Transactional
public class CompanyDaoImpl extends GenericDaoImpl<Company, Integer>
implements CompanyDao {
public CompanyDaoImpl() {
super(Company.class);
}
@Override
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<Company> findAll() {
return em.createNamedQuery("Company.findAll")
.getResultList();
}
}
The first thing to notice is the @Repository("companyDao")
annotation. This annotation is used by Spring to automatically detect and process DAO objects when the application is loaded. The Spring API defines this annotation as follows:
The purpose of the annotation is to allow Spring to auto detect implementing classes through the classpath
scanning and to process this class for data access exception translation (used by Spring to abstract database exception messages from the underlying implementation). The Spring application will then hold a reference to the implementing class under the key companyDao
. It is considered as the best practice to match the key value with the name of the implemented interface.
The CompanyDaoImpl
class also introduces the use of the JPA named queries that were defined during the reverse engineering process in the previous chapter. The method call em.createNamedQuery("Company.findAll")
creates the named query defined by the unique identifier "Company.findAll"
in the persistence engine. This named query was defined in the Company
class. Calling getResultList()
executes the query against the database, returning a java.util.List
of Company objects. Let's now review the named query definition in the Company
class:
@NamedQuery(name = "Company.findAll", query = "SELECT c FROM Company c")
We will make a minor change to this named query to arrange the results by companyName
in ascending order. This will require the addition of an ORDER BY
clause in the query statement. The final named queries definition in the Company
class will now look like the following code:
@NamedQueries({
@NamedQuery(name = "Company.findAll", query = "SELECT c FROM Company c ORDER BY c.companyName ASC "),
@NamedQuery(name = "Company.findByIdCompany", query = "SELECT c FROM Company c WHERE c.idCompany = :idCompany"),
@NamedQuery(name = "Company.findByCompanyName", query = "SELECT c FROM Company c WHERE c.companyName = :companyName")})
This implementation is defined as:
package com.gieman.tttracker.dao; import com.gieman.tttracker.domain.Company; import com.gieman.tttracker.domain.Project; import java.util.List; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Repository("projectDao") @Transactional public class ProjectDaoImpl extends GenericDaoImpl<Project, Integer> implements ProjectDao { public ProjectDaoImpl() { super(Project.class); } @Override @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public List<Project> findAll() { return em.createNamedQuery("Project.findAll") .getResultList(); } }
Once again, we will add the ORDER BY
clause to the Project.findAll
named query in the Project class:
@NamedQuery(name = "Project.findAll", query = "SELECT p FROM Project p ORDER BY p.projectName")
package com.gieman.tttracker.dao; import com.gieman.tttracker.domain.Project; import com.gieman.tttracker.domain.Task; import java.util.List; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Repository("taskDao") @Transactional public class TaskDaoImpl extends GenericDaoImpl<Task, Integer> implements TaskDao { public TaskDaoImpl() { super(Task.class); } @Override @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public List<Task> findAll() { return em.createNamedQuery("Task.findAll") .getResultList(); } }
Once again, we will add the ORDER BY
clause to the Task.findAll
named query in the Task class:
@NamedQuery(name = "Task.findAll", query = "SELECT t FROM Task t ORDER BY t.taskName")
This UserDaoImpl
class will require an additional named query in the User
domain class to test a user's logon credentials (username/password combination). The
UserDaoImpl
class definition follows:
package com.gieman.tttracker.dao; import com.gieman.tttracker.domain.User; import java.util.List; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Repository("userDao") @Transactional public class UserDaoImpl extends GenericDaoImpl<User, String> implements UserDao { public UserDaoImpl() { super(User.class); } @Override @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public List<User> findAll() { return em.createNamedQuery("User.findAll") .getResultList(); } @Override @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public User findByUsernamePassword(String username, String password) { List<User> users = em.createNamedQuery("User.findByUsernamePassword") .setParameter("username", username) .setParameter("password", password) .getResultList(); return (users.size() == 1 ? users.get(0) : null); } @Override @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public User findByUsername(String username) { List<User> users = em.createNamedQuery("User.findByUsername") .setParameter("username", username) .getResultList(); return (users.size() == 1 ? users.get(0) : null); } @Override @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public User findByEmail(String email) { List<User> users = em.createNamedQuery("User.findByEmail") .setParameter("email", email) .getResultList(); return (users.size() == 1 ? users.get(0) : null); } }
The missing named query is User.findByUsernamePassword
that is used to verify a user with the given username and password. The query definition must be added to the User
class as follows:
@NamedQuery(name = "User.findByUsernamePassword", query = "SELECT u FROM User u WHERE u.password = :password AND (u.email = :username OR u.username = :username)")
Note that this definition allows a user to be matched by either the username or e-mail field. As is the common practice in web applications, a user may log on with either their unique logon name (username) or their e-mail address.
The findByEmail
, findByUsername
, and findByUsernamePassword
methods can only ever return null
(no match found) or a single result as there cannot be more than one record in the database with these unique fields. Instead of using the getResultList()
method to retrieve a List
of results and testing for a list size of one, we could have used the code that is similar to the following:
public User findByEmail(String email) {
User user = (User) em.createNamedQuery("User.findByEmail")
.setParameter("email", email)
.getSingleResult();
return user;
}
The getSingleResult()
method returns exactly one result or throws an exception if a single result could not be found. You will also notice the need to cast the returned result to the required User
type. The calling method would also need to catch any exceptions that would be thrown from the getSingleResult()
method unless the sample code given previously is changed to catch the exception.
public User findByEmail(String email) { User user = null; try { user = (User) em.createNamedQuery("User.findByEmail") .setParameter("email", email) .getSingleResult(); } catch(NoResultException nre){ } return user; }
We believe that the code in our UserDaoImpl
interface is cleaner than the previous example that uses the try
/catch
function to wrap the getSingleResult()
method. In both cases, however, the method returns null
if the record cannot be found.
Exceptions should be used judiciously in enterprise programming and only for truly exceptional circumstances. Throwing exceptions should be avoided unless the exception indicates a situation that the calling code cannot recover from. It is far cleaner to return null
(or perhaps true/false in appropriate scenarios) to indicate that a situation is not as expected.
We do not consider being unable to find a record by ID, or by e-mail or by e-mail address as an exceptional circumstance; it is possible that a different user has deleted the record, or there is simply no record with the e-mail specified. Returning null
clearly identifies that the record was not found without the need to throw an exception.
Regardless of whether you throw exceptions to indicate a record that cannot be found or use null
as is our preference, your API should be documented to indicate the behavior. The UserDaoImpl.findByUsernamePassword
method could, for example, be documented as follows:
/** * Find a User with the username/password combination or return null * if a valid user could not be found. * @param username * @param password * @return valid User object or null if not found. */
Users of your API will then understand the expected behavior and code their interactions accordingly.
The final DAO class in our application follows:
package com.gieman.tttracker.dao; import com.gieman.tttracker.domain.Task; import com.gieman.tttracker.domain.TaskLog; import com.gieman.tttracker.domain.User; import java.util.Date; import java.util.List; import javax.persistence.TemporalType; public class TaskLogDaoImpl extends GenericDaoImpl<TaskLog, Integer> implements TaskLogDao { public TaskLogDaoImpl() { super(TaskLog.class); } @Override public List<TaskLog> findByUser(User user, Date startDate, Date endDate) { return em.createNamedQuery("TaskLog.findByUser") .setParameter("user", user) .setParameter("startDate", startDate, TemporalType.DATE) .setParameter("endDate", endDate, TemporalType.DATE) .getResultList(); } @Override public long findTaskLogCountByTask(Task task) { Long count = (Long) em.createNamedQuery("TaskLog.findTaskLogCountByTask") .setParameter("task", task) .getSingleResult(); return count; } @Override public long findTaskLogCountByUser(User user) { Long count = (Long) em.createNamedQuery("TaskLog.findTaskLogCountByUser") .setParameter("user", user) .getSingleResult(); return count; } }
This time, we will refactor the TaskLog
named queries as follows:
@NamedQueries({
@NamedQuery(name = "TaskLog.findByUser", query = "SELECT tl FROM TaskLog tl WHERE tl.user = :user AND tl.taskLogDate BETWEEN :startDate AND :endDate order by tl.taskLogDate ASC"),
@NamedQuery(name = "TaskLog.findTaskLogCountByTask", query = "SELECT count(tl) FROM TaskLog tl WHERE tl.task = :task "),
@NamedQuery(name = "TaskLog.findTaskLogCountByUser", query = "SELECT count(tl) FROM TaskLog tl WHERE tl.user = :user ")
})
We have removed several queries that will not be required and added three new ones as shown. The TaskLog.findByUser
query will be used to list task logs assigned to a user for the given date range. Note the use of the BETWEEN
key word to specify the date range. Also note the use of the TemporalType.DATE
when setting the parameter in the TaskLogDaoImpl.findByUser
method. This will ensure a strict date comparison, ignoring any time component, if present, in the arguments.
The TaskLog.findTaskLogCountByTask
and TaskLog.findTaskLogCountByUser
named queries will be used in our service layer to test if deletions are permitted. We will implement checks to ensure that a user or a task may not be deleted if valid task logs are assigned.
3.138.35.193