【问题标题】:One DAO per entity - how to handle references?每个实体一个 DAO - 如何处理引用?
【发布时间】:2012-09-16 06:38:17
【问题描述】:

我正在编写一个具有典型两个实体的应用程序:用户和用户组。后者可能包含前者的一个或多个实例。我有以下(更多/更少)映射:

用户:

public class User {

    @Id
    @GeneratedValue
    private long id;

    @ManyToOne(cascade = {CascadeType.MERGE})
    @JoinColumn(name="GROUP_ID")
    private UserGroup group;

    public UserGroup getGroup() {
        return group;
    }

    public void setGroup(UserGroup group) {
        this.group = group;
    }
}

用户组:

public class UserGroup {

    @Id
    @GeneratedValue
    private long id;

    @OneToMany(mappedBy="group", cascade = {CascadeType.REMOVE}, targetEntity = User.class)
    private Set<User> users;

    public void setUsers(Set<User> users) {
        this.users = users;
    }

}

现在我为这些实体(UserDao 和 UserGroupDao)中的每一个都有一个单独的 DAO 类。我所有的 DAO 都使用 @PersistenceContext 注释注入了 EntityManager,如下所示:

@Transactional
public class SomeDao<T> {

   private Class<T> persistentClass;

   @PersistenceContext
   private EntityManager em;

   public T findById(long id) {
       return em.find(persistentClass, id);
   }

   public void save(T entity) {
       em.persist(entity);
   }
}

使用此布局,我想创建一个新用户并将其分配给现有用户组。我是这样做的:

UserGroup ug = userGroupDao.findById(1);

User u = new User();
u.setName("john");
u.setGroup(ug);

userDao.save(u);

不幸的是,我得到了以下异常:

对象引用了一个未保存的瞬态实例 - 保存瞬态 刷新前的实例:x.y.z.model.User.group -> x.y.z.model.UserGroup

我对其进行了调查,我认为这是因为每个 DAO 实例都有分配了不同的 entityManager(我检查过 - 每个 DAO 中对实体管理器的引用是不同的)并且对于用户 entityManager 不管理通过 UserGroup 实例。

我尝试将分配给用户的用户组合并到 UserDAO 的实体管理器中。这样做有两个问题:

  • 它仍然不起作用 - 实体管理器想要覆盖现有的 UserGroup 并且它得到异常(显然)
  • 即使它有效,我最终也会为每个相关实体编写合并代码

当 find 和 persist 都是使用同一个实体管理器进行时,所描述的情况有效。这指向一个问题:

  • 我的设计有问题吗?我认为它与this answer 中的推荐非常相似。所有 DAO 是否应该有一个 EntityManager(否则网络声称)?
  • 还是应该在 DAO 内完成组分配?在这种情况下,我最终会在 DAO 中编写大量代码
  • 我应该摆脱 DAO 吗?如果是,如何很好地处理数据访问?
  • 还有其他解决方案吗?

我使用 Spring 作为容器,使用 Hibernate 作为 JPA 实现。

【问题讨论】:

  • 不管你是否有一个EntityManager,你当然应该有一个由两个DAO共享的持久性上下文(这自然会涉及一个EntityManager,但它可以用不同的方式实现每个注射部位的实例)。你是如何注入你的EntityManager 的?这是什么容器?
  • 另外,你是如何进行事务划分的?你能显示UserDao的代码吗?
  • 我添加了一些关于容器和交易的更多细节。
  • 对于 userDao 它只是扩展了 SomeDAO
  • 我不知道 Spring,但如果 @Transactional 意味着对类方法的调用包含在事务中,那么这可能是您的问题。您使用一个事务来查找UserGroup,然后使用另一个事务来保存User。我不知道这会导致您观察到的问题,但这绝对是错误的做法 - 事务应该包含整个工作单元。我强烈建议在同一个事务中进行查找和保存(即使它不能解决问题!)。

标签: java spring jpa entitymanager spring-orm


【解决方案1】:

我不确定如何解决这个问题。我试图将用户分配给的用户组在数据库中有 NULL 版本字段(用 @Version 注释的字段)。当我测试使用此表的 GWT RequestFactory 时,我发现这是一个问题。当我将该字段设置为 1 时,一切都开始工作(不需要更改事务处理)。

如果 NULL 版本字段确实导致了问题,那么这将是我收到的最具误导性的异常消息之一。

【讨论】:

    【解决方案2】:

    我不知道 Spring,但 JPA 的问题是您正在保留一个引用 UserGroupUser,但 JPA 认为 UserGrouptransient

    transient 是 JPA 实体可以处于的生命周期状态之一。这意味着它只是使用 new 运算符创建的,但尚未持久化(还没有持久化身份)。

    由于您通过 DAO 获得了您的 UserGroup 实例,因此那里似乎有问题。您的实例不应该是transient,而是detached。您可以在从 DAO 收到 UserGroup 实例的 ID 后打印它吗?或许还展示了findById 的实现?

    group 关系上没有级联持久化,所以如果实体确实分离,这通常应该工作。如果没有新实体,JPA 根本无法正确设置 FK,因为它需要 UserGroup 实例的 ID,但它(似乎)不存在。

    合并也不应该“覆盖”您分离的实体。你在这里遇到什么例外?

    我只是部分同意这里其他人给出的关于必须将所有内容放在一个事务中的答案。是的,这确实可能更方便,因为UserGroup 实例仍将被“附加”,但它不应该是-必要的-。 JPA 完全能够通过引用其他新实体或在另一个事务中获得的现有(分离)实体来持久化新实体。参见例如JPA cascade persist and references to detached entities throws PersistentObjectException. Why?

    【讨论】:

    • 感谢您的回复!我添加了 findById 方法实现。显然,在 UserGroupDao 的情况下,persistentClass 是 UserGroup。我还检查了获取的 UserGroup 的 id 并且没问题。
    【解决方案3】:

    EntityManager 的不同实例在 Spring 中是正常的。如果存在,它会创建动态使用当前处于事务中的实体管理器的代理。否则,将创建一个新的。

    问题是您的交易太短了。检索您的用户组在事务中执行(因为 findById 方法隐含为 @Transactional )。但是随后事务提交并且组被分离。当您保存新用户时,它将创建一个 new 事务,该事务由于用户引用了分离的实体而失败。

    解决这个问题的方法(以及通常做这些事情)是创建一个方法,在一个单个事务中完成整个操作。只需在服务类中创建该方法(任何 Spring 管理的组件都可以使用)并使用 @Transactional 注释它。

    【讨论】:

    • 我已经尝试按照你说的做(从 SomeDao 中删除 @Transactional 并将整个操作包装在事务中)。我得到以下异常:org.hibernate.NonUniqueObjectException:具有相同标识符值的不同对象已与会话关联:[x.y.z.model.User#1]。我没有对模型或操作本身进行任何更改。
    猜你喜欢
    • 2014-09-02
    • 2023-03-17
    • 2013-03-18
    • 2015-10-26
    • 2014-01-28
    • 2017-03-22
    • 1970-01-01
    • 2011-01-26
    • 1970-01-01
    相关资源
    最近更新 更多