DAO就是持久化层, 源自很久之前Java对于EJB的设计蓝图, 如今完整的EJB很少有人使用, 但是DAO的理念流传了下来, 在现在的Web开发中, 分层中依然包含DAO层.
JPA和Hibernate单独使用的时候还是暴露了太多的工具属性, 在实际开发中, 针对一种类型就可以创建一个持久化类,通过这个类进行存取数据的操作, 而不是编写具体的函数.
编写DAO类也是有套路的, 虽然Spring提供了JPA中的一些魔法方法, 但是自己编写特定的DAO类还是非常必要的.
- 基本设计
- 接口与抽象类
- 编写实现类
- 测试DAO类
基本设计
DAO的基本设计是平行的层次结构, 一侧是接口, 一侧是实现.
由于DAO的目标是操作Entity类, 而所有Entity类必定有唯一标识符, 所以最基础的接口应该具有一个Entity类和唯一标识符类型的泛型. 一个推荐的DAO接口如下:
GenericDAO<T,ID>
findById(ID id): T
, 根据id查找单个对象
findById(Id id, LockModeType lock): T
, 根据id和锁类型查找单个对象
findReferenceById(ID id): T
, 返回不是立刻加载的代理对象
findAll(): List<T>
, 返回所有对象
getCount(): Long
, 返回数量
makePersistent(T entity): T
, 持久化一个类
makeTransient(T entity)
, 将一个类设置为瞬时状态
checkVersion(T entity, boolean forceUpdate)
, 检查版本
这些方法基本上覆盖了最通用的操作, 在这个基础上, 就可以根据传入的具体类型来创建实现. 由于使用了JPA, 所以我们可以使用DAO层来让这些内容变得可以移植, 如果直接使用JDBC, 几乎是不可能移植的.
所谓一侧是接口, 一侧是实现, 详细如下:
- 接口侧, 指的是一个接口GenericDAO<T,ID>与其对应的抽象类
- 实现侧, 指的是一个具体的泛型接口, 继承GenericDAO<T,ID>, 然后一个具体的实现类, 继承这个具体的泛型接口.
接口与抽象类
具体的实现当然不是简单的使用接口和直接实现类, 还可能需要创建抽象类, 最终创建一个体系.
针对MessageVersion来编写一个接口和一个实现类, 先来创建接口:
import javax.persistence.LockModeType;
import java.util.List;
public interface GenericDao<T, ID> {
T findById(ID id);
T findById(ID id, LockModeType lockModeType);
T findReferenceById(ID id);
List<T> findAll();
Long getCount();
T makePersistent(T entity);
void makeTransient(T entity);
void checkVersion(T entity, boolean forceUpdate);
}
接下来好的做法是先创建一个抽象类, 因为DAO的工作需要一个EntityManager, 还需要一个Entity.class对象来完成工作, 所以很显然, 先用一个抽象类来完成这些功能:
public abstract class GenericDaoAbstract<T, ID extends Serializable> implements GenericDao<T, ID>{
@PersistenceContext
protected EntityManager em;
public void setEm(EntityManager em) {
this.em = em;
}
protected final Class<T> entityClass;
protected GenericDaoAbstract(Class<T> entityClass) {
this.entityClass = entityClass;
}
}
这个@PersistenceContext
注解是告诉EJB容器用的, 也可以手动用set方法来注入一个EntityManager. 这里使用构造器来传入实体类的对象. 而ID则可以通过泛型来指定.
然后就可以继续编写其他的方法, 完整的抽象类如下:
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import java.io.Serializable;
import java.util.List;
public abstract class GenericDaoAbstract<T, ID extends Serializable> implements GenericDao<T, ID>{
@PersistenceContext
protected EntityManager em;
public void setEm(EntityManager em) {
this.em = em;
}
protected final Class<T> entityClass;
protected GenericDaoAbstract(Class<T> entityClass) {
this.entityClass = entityClass;
}
//这个查找直接返回不带锁的另外一个重载方法
@Override
public T findById(ID id) {
return findById(id, LockModeType.NONE);
}
//这个是实际查找单个对象的方法
@Override
public T findById(ID id, LockModeType lockModeType) {
return em.find(entityClass, id, lockModeType);
}
//返回一个暂时未加载的代理对象
@Override
public T findReferenceById(ID id) {
return em.getReference(entityClass, id);
}
//findAll方法采用JPA可移植的方式编写
@Override
public List<T> findAll() {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
criteriaQuery.select(criteriaQuery.from(entityClass));
return em.createQuery(criteriaQuery).getResultList();
}
//也采用JPA可移植方式编写, 查询总数量
@Override
public Long getCount() {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(entityClass)));
return em.createQuery(criteriaQuery).getSingleResult();
}
//还记得merge方法吗, 返回一个合并后的新引用, 原来的引用可以丢弃
@Override
public T makePersistent(T entity) {
return em.merge(entity);
}
@Override
public void makeTransient(T entity) {
em.remove(entity);
}
@Override
public void checkVersion(T entity, boolean forceUpdate) {
em.lock(
entity,
forceUpdate ? LockModeType.OPTIMISTIC_FORCE_INCREMENT : LockModeType.OPTIMISTIC
);
}
}
编写实现类
很显然, 实现类也需要两个类, 一个是继承GenericDAO<T,ID>的接口, 一个是真正的实现类, 不再是抽象类.
现在就以Sender类为例, 由于Sender类的唯一标识符类型是Long, 所以接口是SenderDAO<Sender, Long>
.
注意我们的Sender, 除了上边的通用方法, 也就是按照ID来查找之外, Sender有name列, 还有关联关系映射到MessageVersion类, 因此需要扩展一些新的方法:
public interface SenderDAO extends GenericDao<Sender, Long> {
//根据name字段来查找结果
List<Sender> findByName(String n);
//根据一个MessageVersion对象查找对应的Sender
List<Sender> findByMessageVersion(MessageVersion messageVersion);
}
这只是最简单的例子, 实际上这两个方法也应该像上边一样有带锁和不带锁模式, 此外还可能有更多其他的查询方法.
准备好了接口之后, 就要来编写实现类SenderDAOImpl
, 这个类会实现SenderDAO
接口, 同时继承已经编写好了一部分实现的GenericDaoAbstract<Sender, Long>
类型. 在其中编写属于SenderDAO
接口的特有方法的实现:
import cc.conyli.model.chapter11.MessageVersion;
import cc.conyli.model.chapter12.Sender;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.List;
public class SenderDAOImpl extends GenericDaoAbstract<Sender, Long> implements SenderDAO {
//构造器, 由于有了泛型类型, 因此使用无参构造器直接就可以获取类型
public SenderDAOImpl() {
super(Sender.class);
}
//根据名称字符串查找对象, JPA编程方式已经很熟练了
@Override
public List<Sender> findByName(String n) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Sender> criteriaQuery = criteriaBuilder.createQuery(entityClass);
Root<Sender> root = criteriaQuery.from(entityClass);
criteriaQuery.select(root).where(criteriaBuilder.equal(
root.<String>get("name"), n
));
return em.createQuery(criteriaQuery).getResultList();
}
//和上边基本一样
@Override
public List<Sender> findByMessageVersion(MessageVersion messageVersion) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Sender> criteriaQuery = criteriaBuilder.createQuery(entityClass);
Root<Sender> root = criteriaQuery.from(entityClass);
criteriaQuery.select(root).where(criteriaBuilder.equal(
root.<MessageVersion>get("messageVersion"), messageVersion
));
return em.createQuery(criteriaQuery).getResultList();
}
}
可以看到, 一边继承抽象类, 一边继承接口, 这样就创建了平行的体系对体系, 接口对接口, 抽象类对实现类的DAO层次.
测试DAO类
按照上边的编写, 应该所有的方法现在都有了正确的泛型类型, 可以来进行查询了, 编写一系列方法来试验一下.
import cc.conyli.model.chapter11.MessageVersion;
import cc.conyli.model.util.CaveatEmptorUtil;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
public class DaoSample {
@Test
public void testSenderDAO() {
EntityManagerFactory emf = CaveatEmptorUtil.getEntityManagerFactory();
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//创建DAO对象, 然后设置上em
SenderDAOImpl senderDAO = new SenderDAOImpl();
senderDAO.setEm(em);
System.out.println(senderDAO.findById(30L));
System.out.println(senderDAO.findReferenceById(30L));
CaveatEmptorUtil.printList(senderDAO.findAll());
System.out.println(senderDAO.getCount());
MessageVersion messageVersion = em.find(MessageVersion.class, 3L);
System.out.println(senderDAO.findByName("owl"));
System.out.println(senderDAO.findByMessageVersion(messageVersion));
em.getTransaction().commit();
em.close();
emf.close();
}
}
都可以顺利运行, 这就完成了编写DAO类. 这里采取手工注入em对象的方法, 在很多框架中, 实际上是框架注入em对象和开启事务管理的.
这里事务是在外层控制的, 如果想写到每个具体的方法里也是可以的.
所以像我们这种测试代码, 实际上就是业务层的代码. 只不过一些框架会将em和对应的事务管理器包装的更好, 来直接注入到实现类中. 像Spring在编写DAO类的时候, 只需要进行依赖注入, 然后在方法级别加上事务控制即可.
编写的时候可能会想, 是不是也要编写一个通过Sender查找Sender对应的MessageVersion类呢, 这个方法由于返回的是MessageVersion类, 实际上应该写到MessageVersion的DAO类中. 即每个DAO类, 应该都是返回和操作对应的Entity类的对象, 这样就比较清晰了.
到这里, Hibernate的所有核心内容基本上就看完了, 搭配着之前的数据库知识, PostgreSQL的详细使用, 编写持久化方面的程序功力大增了, 算是补上了之前Web开发中存取数据这一块的短板.
下边再回头继续补一下PostgreSQL的一些操作, 之后就可以回头再看Spring啦.