Spring Boot「30」Web 应用中的 DAO 模式

2,228 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情

Web 开发时经常讲的 Data Access Object (DAO) 其实是一种设计模式。 它将应用业务逻辑与数据持久化层解耦,使得业务层无需关心复杂的 CRUD 操作及底层的数据存储实现。

01-经典 DAO 模式实现

一个典型的 DAO 模式实现包含三个部分:实体类(简单的 POJO)、DAO 抽象接口及具体实现、应用层业务逻辑。 下面我们以用户的 CRUD 来实现一个经典的 DAO 模式。

  1. 首先,需要有一个实体类 User,它是系统保存用户信息的载体。 如前所属,它基本上就是一个 POJO,所以只包含一些属性值及其对应的 getter/setter。
public class Event {
    private Long id;
    private Date time;
    private String title;
    /** 省略属性的 getter/setter 及无参构造器 */
}
  1. 然后,我们要定义针对实体类进行持久化操作的抽象接口 DAO 接口,它定义了对实体类的各种持久化操作,例如典型的 CRUD。
public interface IDao<T> {
    T get(Long id);
    List<T> getAll();
    void update(T t);
    void save(T t);
    void delete(T t);
}

之后,我们可以实现一个存储在内存的中的 DAO 具体实现,例如:

public class EventMemDao implements IDao<Event> {
    private List<Event> events = new ArrayList<>(32);

    @Override
    public Event get(Long id) {
        return events.stream().filter(e -> e.getId().equals(id)).findFirst().orElse(null);
    }

    @Override
    public List<Event> getAll() {
        return events;
    }
    /** 省略 IDao 中其他方法的实现  */
}
  1. 最后,我们可以在业务逻辑层使用 DAO 实现。例如:
private static void memDao(Event e1, Event e2) {

    IDao<Event> memDao = new EventMemDao();
    memDao.save(e1);
    memDao.save(e2);

    final List<Event> all = memDao.getAll();
    all.forEach(System.out::println);
}

以上仅为模拟 DAO 实现的使用。在实际的业务逻辑中,memDao 可能并不需要自己创建,如果使用了 Spring 容器,可以由容器注入一个具体的实现。

02-JPA 与 DAO 模式的集成

我们在 EventMemDao 中实现了将 Event 对象存储在内存中。 本节中,我们将介绍如何将 JPA 持久化与上面的 DAO 模式集成,最终的目标是实现数据存储层的替换,而尽量不影响业务层接口。 这也是 DAO 模式主要应对的场景。

基于上述的 DAO 模式抽象,我们如果要实现将 Event 对象存储到数据库中,我们只需要实现一个 EventJpaDao,在具体实现中将信息持久化到数据库中。

public class EventJpaDao implements IDao<Event> {

    private EntityManager entityManager;

    public EventJpaDao(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Override
    public Event get(Long id) {
        return entityManager.find(Event.class, id);
    }

    @Override
    public List<Event> getAll() {
        final Query query = entityManager.createQuery("from Event");
        return query.getResultList();
    }
    /** 省略 IDao 中其他方法的实现  */
}

如何实例化 EntityManager 可以参考之前的文章 1

可以看到,我们只需要修改具体的 Dao 实现,应用层基本不需要改动,就替换掉了底层持久化实现。 这中 DAO 模式的设计,让数据持久化层与应用层解耦,两者可以独立升级、改造。 例如,如果我们不想使用 JPA,而希望使用 Hibernate 原生 API,那应该如何改造上面的实现呢?

其实很简单,只需要将 EventJpaDao 中的 EntityManager 替换为 Hibernate 原生 API 中的 SessionFactory,需要使用 EntityManager 的地方替换为 SessionFactory#getCurrentSession() 获得当前 Session 即可。

03-使用泛型优化 DAO 模式

前两节介绍的 DAO 模式虽然在设计上实现了业务层与持久化层的解耦,但由于应用中的实体类与 DAO 实现类的关系往往是一对一对应的,意味着我们在开发应用时需要实现、维护大量的 DAO 实现类。 而且这些 DAO 实现类大多代码都比较相似,重复代码较多。 随着业务发展和项目规模增长,这一层的代码将越来越多。 本节将介绍一种使用 Java 泛型来减少 DAO 实现类的方法。

我们继续复用前两节中使用的抽象 DAO 接口 IDao。 然后,我们使用泛型实现一个 GenericDao 接口。

public class GenericDao<T extends Serializable> implements IDao<T> {
    private Class<T> clazz;

    private EntityManager entityManager;

    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public void setClazz(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public T get(Long id) {
        return entityManager.find(clazz, id);
    }

    @Override
    public List<T> getAll() {
        return entityManager.createQuery("From " + clazz.getName()).getResultList();
    }
    /** 省略其他的 getter/setter 以及其他 IDao 中定义的方法实现 */
}

然后,我们增加一个实体类 Message,来验证一下如何使用 GenericDao 来对两个不同类型的实体类进行持久化操作。

@Entity
@Table(name = "MESSAGES")
public class Message implements Serializable {
    @Id
    @GeneratedValue
    private Long id;
    private String topic;
    private String content;

    /** 省略其他的 getter/setter 以及无参构造器 */
}

在使用时,需要按照不同的实体类型创建不同的 GenericDao 实例,例如:

private static void genericDao() {
    GenericDao<Event> eventGenericDao = new GenericDao<>();
    eventGenericDao.setEntityManager(entityManager);
    eventGenericDao.setClazz(Event.class);

    eventGenericDao.save(new Event(new Date(), "Our very first event!"));
    eventGenericDao.save(new Event(new Date(), "A follow up event!"));
    
    final List<Event> events = eventGenericDao.getAll();
    events.forEach(System.out::println);

    final GenericDao<Message> messageGenericDao = new GenericDao<>();
    messageGenericDao.setEntityManager(entityManager);
    messageGenericDao.setClazz(Message.class);

    messageGenericDao.save(new Message("greeting", "hello!"));
    messageGenericDao.save(new Message("greeting", "how are you?"));
    
    final List<Message> messages = messageGenericDao.getAll();
    messages.forEach(System.out::println);
}

到此为止,大功告成。 如果看到这里,相信你对 DAO 模式应该有了一个清晰的认识,希望能在日后的开发中帮到你。