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()
if
和userDAO
之间变化的风险仍然存在......但您有选择:
(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
。有意义吗?
EntityManager
s 不是线程安全的,所以你真的必须每个线程只有一个。如果每个 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 实施的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章