DAO实施的最佳实践

10

我一直在使用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();
      }
}

这只是一个快速的例子,但我的问题是,在 DAO 实现中似乎进行另一次检查以确保 User 不为 null 再添加 Right 是“冗余”的,但作为程序员,我看到了避免空指针异常的机会。

显然,在调用实体管理器的 find 方法后,我可以添加一个 null 检查并抛出异常,但这就是将 DAO 封装在业务逻辑实现中的全部目的,在此之前进行所有验证工作,以便 DAO 代码干净且不必做太多的 null 检查或逻辑处理。由于我在 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();
}
3个回答

8

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();
   } 
}

现在回答你的问题:在userExists() ifuserDAO持久化之间,数据库更改的风险仍然存在...但是你有选择:
1. 锁定用户,直到事务结束;或者2. 不管它。
1:如果在这两个命令之间混淆用户的风险很高(假设您的系统有很多并发用户),并且如果发生该问题将是大问题,则考虑在整个事务中锁定user;也就是说,如果我开始使用它,没有其他事务可以更改它。
- 如果您的系统有大量并发用户,则另一种(更好的)解决方案是“设计问题”,即重新设计您的设计,以便在业务交易中更改的内容具有更严格(较小)的范围 - 您的示例的范围足够小,因此这个建议可能没有那么有意义,但请考虑您的业务交易做了很多(然后让其逐个执行很多少,可能是解决方案)。这本身就是一个完整的主题,所以我不会在这里详细介绍,请保持开放的心态。
2:另一种可能性,您将发现这是最常见的方法,如果您正在处理具有约束检查(例如UNIQUE)的SQL DB,则只需让DAO异常消失即可。我的意思是,这将是一种罕见且几乎不可能发生的事情,您可以通过接受它可能会发生并且您的系统将显示一个漂亮的消息(例如“出了些问题,请重试”)来处理它- 这只是基本成本与效益的权衡。
更新:
编程式事务处理可能很棘手(难怪声明性替代方案,如Spring和EJB / CDI,如此常用)。但是,我们并不总是有使用它们的奢侈品(也许您正在适应遗留系统,谁知道)。因此,这里有一个建议:https://gist.github.com/acdcjunior/94363ea5cdbf4cae41c7

EntityManager 不是线程安全的,因此每个线程只能有一个。如果您为每个 DAO 都创建一个 EntityManager 并且多个线程访问相同的 DAO 实例,则肯定会遇到(令人讨厌的)问题。这是我的建议:https://gist.github.com/acdcjunior/94363ea5cdbf4cae41c7 - acdcjunior
此外,我建议不要将 EntityManager 返回到业务层(类)。这是数据访问层泄漏的实现细节。 - acdcjunior
谢谢提供示例,我正在努力实现它。对于BL Impl,您是否有调用类似findUsersByName()这样的简单方法的示例?创建UnitOfWork会开始一个事务,但findUsersByName方法不需要事务。 - user1154644
好的,说得对。我更新了要点,以解决在事务外调用方法的问题。看一下吧。(要查看更改的详细信息,请查看修订选项卡上的最后一个修订版本。) - acdcjunior
非常感谢您提供的示例,我真的非常非常感激。 - user1154644
显示剩余5条评论

3

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应用程序,手动处理回滚是否可行? - user1154644
在这种情况下,我会使用Spring。 - JB Nizet


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接