Hibernate 15 编写持久化对象DAO

Hibernate 15 编写持久化对象DAO

DAO就是持久化层, 源自很久之前Java对于EJB的设计蓝图, 如今完整的EJB很少有人使用, 但是DAO的理念流传了下来, 在现在的Web开发中, 分层中依然包含DAO层. JPA和Hibernate单独使用的时候还是暴露了太多的工具属性, 在实际开发中, 针对一种类型就可以创建一个持久化类,通

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啦.
LICENSED UNDER CC BY-NC-SA 4.0
Comment