DAO 实施的最佳实践

Posted

技术标签:

【中文标题】DAO 实施的最佳实践【英文标题】:Best Practices for DAO Implementation 【发布时间】:2015-06-14 00:58:19 【问题描述】:

我一直在使用 DAO 模式来提供对我正在构建的应用程序中的持久层的访问。

为了验证的目的,我已经实现了一个围绕我的 DAO 实现的“包装器”。包装器接受我的 DAO 的一个实例作为构造函数参数,并实现与 DAO 类似的接口,但异常是抛出的异常类型。

例如:

业务逻辑接口

public interface UserBLInt 

   private void assignRightToUser(int userId, int rightId) throws SomeAppException;


DAO 接口

public interface UserDAOInt 

   private void assignRightToUser(int userId, int rightId) throws SomeJPAExcption;


业务逻辑实现

public class UserBLImpl implements  UserBLInt 

   private UserDAOInt userDAO;

   public UserBLImpl(UserDAOInt userDAO)
      this.userDAO = userDAO;
   

   @Override
   private void assignRightToUser(int userId, int rightId) throws SomeAppException
      if(!userExists(userId)
         //throw an exception
      
      try
         userDAO.assignRightToUser(userId, rightId);
       catch(SomeJpAException e)
        throw new SomeAppException(some message);
      
    


DAO 实现

public class UserDAOImpl implements UserDAOInt 
      //......
      @Override
      public void assignRightToUser(int userId, int rightId)
         em.getTransaction().begin();
         User userToAssignRightTo = em.find(User.class, userId);
         userToAssignRightTo.getRights().add(em.find(Right.class, rightId));
         em.getTransaction().commit();
      

这只是一个简单的示例,但我的问题是,在添加 Right 之前,在 DAO 实现中进行另一次检查以确保 User 不为空似乎是“多余的”,但是,如程序员,我看到了空指针的机会。

显然,我可以在实体管理器上调用 find 后添加一个空检查,如果返回 null,则抛出异常,但这是将 DAO 包装在业务逻辑实现中的全部目的,以预先完成所有验证工作,这样 DAO 代码是干净的,并且根本不需要做太多的空检查或逻辑。既然我有 DAO 的包装器,那么在 DAO 中进行空值检查仍然是一个好主意吗?我知道理论上可以在业务逻辑调用和 dao 调用之间删除对象,这不太可能,并且检查 null 似乎是重复的工作。在这种情况下,最佳做法是什么?

编辑:

这看起来像一个合适的 DAO 修改吗?

public EntityManager beginTransaction()
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction entityTransaction = entityManager.getTransaction();
    entityTransaction.begin();
    return entityManager;


public void rollback(EntityManager entityManager)
    entityManager.getTransaction().rollback();
    entityManager.close();


public void commit(EntityManager entityManager)
    entityManager.getTransaction().commit();
    entityManager.close();

【问题讨论】:

【参考方案1】:

DAO,虽然现在是一个通用且被过度使用的术语,但(通常)旨在抽象数据层(因此除了其他好处之外,它可以在无需触及应用程序的其余部分的情况下进行更改)。

不过,看起来您的 DAO 实际上所做的不仅仅是抽象数据层。在:

public class UserDAOImpl implements UserDAOInt 
  ......
  @Override
  public void assignRightToUser(int userId, int rightId)
     em.getTransaction().begin();
     User userToAssignRightTo = em.find(User.class, userId);
     userToAssignRightTo.getRights().add(em.find(Right.class, rightId));
     em.getTransaction().commit();
  

您的 DAO 了解业务逻辑。它知道为用户分配一个权限就是在权限列表中添加一个权限。 (分配权限似乎很明显只是将其添加到列表中,但想象一下,将来这可能会变得更加复杂,对其他用户和权限产生副作用等等。)

所以这个分配不属于 DAO。它应该在业务层。你的 DAO 应该有类似 userDAO.save(user) 的东西,一旦完成设置权限和内容,业务层就会调用它。

另一个问题:您的交易过于本地化。这几乎不是交易。

请记住,事务是一个业务单元,是您在其中执行原子(“批次”)业务工作的事情,而不仅仅是您打开的事情,因为 EntityManager 使您成为。

我的意思是,就代码而言,是业务层应该有开启事务的主动权,而不是DAO(其实DAO应该有“开启事务”作为服务--方法--将被调用)。

然后考虑在业务层打开事务:

public class UserBLImpl implements  UserBLInt 
   ...
   @Override
   private void assignRightToUser(int userId, int rightId) throws SomeAppException
      userDAO.beginTransaction(); // or .beginUnitOfWork(), if you wanna be fancy

      if(!userExists(userId)
         //throw an exception
         // DON'T FORGET TO ROLLBACK!!! before throwing the exception
      
      try
         userDAO.assignRightToUser(userId, rightId);
       catch(SomeJpAException e)
        throw new SomeAppException(some message);
      

      userDAO.commit();
    

现在,对于您的问题:DB 在userExists() ifuserDAO 之间变化的风险仍然存在......但您有选择:

(1) 锁定用户直到交易结束;或 (2) 顺其自然。

1:如果用户在这两个命令之间被搞砸的风险很高(假设您的系统有很多并发用户)并且如果发生该问题会很严重,那么考虑锁定user整个交易;也就是说,如果我开始使用它,其他任何事务都无法更改它。

如果您的系统有大量操作系统并发用户,另一个(更好的)解决方案是“设计问题消失”,即重新设计您的设计,以便在业务事务中更改的内容具有更严格(更小)的范围- 你的例子的范围足够小,所以这个建议可能没有多大意义,但只要考虑你的业务交易做了更多的事情(然后让它做更少的事情,轮流做,可能是解决方案)。这本身就是一个完整的话题,所以我不会在这里详细介绍,请保持头脑清醒。

2:另一种可能性,您会发现这是最常见的方法,如果您正在处理具有约束检查的 SQL DB,例如UNIQUE,只是让 DAO 异常消失。我的意思是,这将是一件如此罕见且几乎不可能发生的事情,您不妨接受它可能会发生来处理它,您的系统只会显示一条很好的消息,例如“出现问题,请重试” - - 这只是基本成本与收益的加权。


更新:

程序化事务处理可能很棘手(难怪声明性替代方案,如 Spring 和 EJB/CDI 如此使用)。尽管如此,我们并不总是有幸使用它们(也许你正在适应一个遗留系统,谁知道呢)。所以这里有一个建议:https://gist.github.com/acdcjunior/94363ea5cdbf4cae41c7

【讨论】:

我明白你关于 saveUser 操作的观点,但从实体管理器的角度来看,会调用什么操作来“更新”数据库中的用户?业务逻辑层会不会给用户添加角色,而dao层会简单的调用merge? 是的;实际上saveUser() 会更新或插入,DAO 应该能够弄清楚用户需要什么。 can be found in the Petclinic app 的一个例子。 关于声明式事务管理(又名使用@Transactional),我没有深入讨论,因为您显然没有使用 EJB/CDI/Spring,但如果可能的话,使用声明式样式是总是比使用程序化(即调用beginTransaction()“手动”)方式更好,因为声明性方式处理提交/回滚等更干净(更少样板代码)。但仅此而已。我的意思是,@Transactional 可以做的任何事情你都可以通过编程方式做,只需要更多的关注。 你可以在你的 DAO 中创建一个.beginTransaction() 方法来创建EntityManager,获取一个事务,开始它并返回。然后是另外两个方法:.rollback().commit(),它们会调用EntityManager 的同名事务方法,然后应该关闭EntityManager。有意义吗? EntityManagers 不是线程安全的,所以你真的必须每个线程只有一个。如果每个 DAO 有一个,并且多个线程访问同一个 DAO 实例,那么您肯定会遇到(讨厌的)麻烦。这是我的建议:gist.github.com/acdcjunior/94363ea5cdbf4cae41c7【参考方案2】:

DAO 在这里有太多的逻辑。 DAO 的作用不是为用户分配权限。这就是业务逻辑。 DAO 的作用是查找用户或查找权限。 DAO 中的代码应该在服务中:

interface UserDAO 
    User findById(int userId);
    //...


interface RightDAO 
    Right findById(int rightId);
    // ...


public class UserBLImpl implements  UserBLInt 

    private UserDAO userDAO;
    private RightDAO rightDAO;

    // ...

    @Override
    public void assignRightToUser(int userId, int rightId) 
        User u = userDAO.findById(userId);
        Right right = rightDAO.findById(rightId);
        // do some null checks and throw exception
        user.addRight(right);
     

这也说明了您设计中的一个基本问题:事务不能在 DAO 层启动。它们必须在服务层启动。这就是允许 DAO 方法可重用和服务方法在单个事务中使用多个 DAO 的原因。不这样做会导致服务乏力,这些服务将调用包含整个业务逻辑的 DAO 方法,如您的代码所示。

这就是为什么像 EJB 或 Spring 这样的框架允许以声明方式划分事务:您不需要显式地启动和提交事务、处理异常和回滚异常。您所要做的就是使用注释将服务方法标记为事务性。

【讨论】:

我已经看到使用 EJB 并将它们的方法标记为需要新事务。如果这是一个标准的 java 应用程序而不是一个 j2ee 应用程序,手动处理回滚是否可以? 在这种情况下我会使用 Spring。【参考方案3】:

实际上 DAO 本身已经成为一种反模式,不应该再使用了。

http://www.adam-bien.com/roller/abien/entry/how_to_deal_with_j2ee

http://www.adam-bien.com/roller/abien/entry/jpa_ejb3_killed_the_dao

Java EE Architecture - Are DAO's still recommended when using an ORM like JPA 2?

【讨论】:

以上是关于DAO 实施的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

DAO 级别布尔方法的最佳实践

DAO 最佳实践:查询父/子

Spring Transactions 和通用 DAO 和服务的最佳实践

DTO、DAO 和实体?是实体需要吗?那三个的最佳实践?

实施客户端-服务器身份验证的最佳实践

sh 用于实施ACSF配置最佳实践的示例脚本