DAO就是持久化层, 源自很久之前Java对于EJB的设计蓝图, 如今完整的EJB很少有人使用, 但是DAO的理念流传了下来, 在现在的Web开发中, 分层中依然包含DAO层.

JPA和Hibernate单独使用的时候还是暴露了太多的工具属性, 在实际开发中, 针对一种类型就可以创建一个持久化类,通过这个类进行存取数据的操作, 而不是编写具体的函数.

编写DAO类也是有套路的, 虽然Spring提供了JPA中的一些魔法方法, 但是自己编写特定的DAO类还是非常必要的.

  1. 基本设计
  2. 接口与抽象类
  3. 编写实现类
  4. 测试DAO类

基本设计

DAO的基本设计是平行的层次结构, 一侧是接口, 一侧是实现.

由于DAO的目标是操作Entity类, 而所有Entity类必定有唯一标识符, 所以最基础的接口应该具有一个Entity类和唯一标识符类型的泛型. 一个推荐的DAO接口如下:

    GenericDAO<T,ID>

  1. findById(ID id): T, 根据id查找单个对象
  2. findById(Id id, LockModeType lock): T, 根据id和锁类型查找单个对象
  3. findReferenceById(ID id): T, 返回不是立刻加载的代理对象
  4. findAll(): List<T>, 返回所有对象
  5. getCount(): Long, 返回数量
  6. makePersistent(T entity): T, 持久化一个类
  7. makeTransient(T entity), 将一个类设置为瞬时状态
  8. checkVersion(T entity, boolean forceUpdate), 检查版本

这些方法基本上覆盖了最通用的操作, 在这个基础上, 就可以根据传入的具体类型来创建实现. 由于使用了JPA, 所以我们可以使用DAO层来让这些内容变得可以移植, 如果直接使用JDBC, 几乎是不可能移植的.

所谓一侧是接口, 一侧是实现, 详细如下:

  1. 接口侧, 指的是一个接口GenericDAO<T,ID>与其对应的抽象类
  2. 实现侧, 指的是一个具体的泛型接口, 继承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啦.