问题 使用hibernate使用引用的依赖实体保存实体


我们使用Hibernate作为持久层,并且具有复杂的对象模型。在不暴露真实数据模型的情况下,我想使用以下简单示例来解释问题。

class Person {
    private Integer id; //PK
    private String name;
    private Account account;
    // other data, setters, getters
}


class Account {
    private Integer id; //PK
    // other data, setters, getters
}

使用HBM定义DB映射如下:

 <class name="Person" table="PERSON">
    <id name="id" column="ID">
        <generator class="native"/>
    </id>
    <version name="version" type="java.lang.Long"/>
    <property name="name" type="java.lang.String" length="50" column="NAME"/>
    <many-to-one name="account" column="ACCOUNT_ID"
                class="com.mycompany.model.Account"/>

</class>

我必须保存新填充的实例 Person 与现有有关 Account。该调用由Web客户端发起,因此在我的层中,我获得了实例的Person实例 Account 仅保留其ID。

如果我试着打电话 saveOrUpdate(person) 抛出以下异常:

org.hibernate.TransientObjectException: 
object references an unsaved transient instance - save the transient instance before flushing: 
com.mycompany.model.Account

为了避免这种情况,我必须找到持久化的对象 Account 按ID然后调用 person.setAccount(persistedAccount)。在这种情况下一切正常。

但在现实生活中,我处理了几十个相互引用的实体。我不想为每个引用编写特殊代码。

我想知道这个问题是否存在某种通用解决方案。


6363
2018-01-12 17:22


起源



答案:


要保留一个实体,您只需要对其直接依赖项进行引用。这些其他实体引用其他实体的事实并不重要。

最好的方法是使用代理来获取引用实体的代理,甚至不需要访问数据库 session.load(Account.class, accountId)

您正在做的是正确的事情:获取对持久帐户的引用,并将此引用设置为新创建的帐户。


7
2018-01-12 17:28



谢谢@JB Nizet。这实际上是我的预期......我将在这里发布我的通用解决方案的描述,并很高兴知道你的意见。 - AlexR


答案:


要保留一个实体,您只需要对其直接依赖项进行引用。这些其他实体引用其他实体的事实并不重要。

最好的方法是使用代理来获取引用实体的代理,甚至不需要访问数据库 session.load(Account.class, accountId)

您正在做的是正确的事情:获取对持久帐户的引用,并将此引用设置为新创建的帐户。


7
2018-01-12 17:28



谢谢@JB Nizet。这实际上是我的预期......我将在这里发布我的通用解决方案的描述,并很高兴知道你的意见。 - AlexR


Hibernate允许你只使用带ID的引用类,你不需要做session.load()。

唯一重要的是,如果您的引用对象具有VERSION,则必须设置版本。 在您的情况下,您必须指定帐户对象的版本。


2
2017-08-24 19:50



这是在Hibernate文档中描述的,还是可用的任何其他文档? - sbrattla
爱你的男人!我将版本添加到我的班级,我只能通过设置ID来保存它,浪费了很多时间来解决这个问题 - Rafael Teles
我希望JPA能够做到这一点。是否可以告诉JPA“请坚持参考并使用最新版本”而无需手动调整对象图?有些用例,我真的不关心版本,但我想要这个参考。 - UNIQUEorn


使用 cascade="all" 在* -to- *映射上


1
2018-01-12 17:27



问题中没有*到多个映射...... - JB Nizet
@Bozho,我补充说 cascade。其实我只是忘了提到我已经玩过了。现在,当我尝试保存具有合法身份证的临时帐户的人时,我会遇到如下异常: org.hibernate.AssertionFailure: null id in com.mycompany.model.Person entry (don't flush the Session after an exception occurs) - AlexR
@JB Nizet,你写的。它是 many-to-one - AlexR
是的,这是* - 到 - *,同样的事:) - Bozho


你有没有尝试过 cascade="save-update" 在 many-to-one 元件? Hibernate默认为 cascade="none"...


0
2018-01-12 17:28



我试过了,@ Nacho。它无法正常工作。它抛出了其他异常(请参阅我对Bozho的回答的评论。 - AlexR


感谢您的帮助。我已经实现了自己的通用解决方案,但想知道是否存在其他解决方案。

我想和你分享这个想法。我调用的引用实体除了ID(或其他可用于唯一标识实体的字段)之外不包含任何内容 placeholder

因此,我创建了注释@Placeholder并将其放在所有引用的字段上。在我们的示例中,它位于Person类的帐户字段中。 我们已经有了课程名称 GenericDao 包装Hibernate API并拥有方法 save()。我添加了另一种方法 saveWithPlacehodlers() 这样做会有以下情况。它通过反射发现给定对象的类,查找标有注释的所有字段 @Placeholder,在DB中查找对象并在主实体中调用适当的setter以用持久实体替换引用的占位符。

注解 @Placeholder 允许定义将用于标识实体的字段。默认是 id

关于这个解决方案,你觉得怎么样?


0
2018-01-12 17:59



必须谨慎处理这些“自动”解决方案。如果我理解您的描述正确,那么您将在Save方法中解析依赖项,该方法一次处理一个对象。如果要在一个工作单元中保存大量对象,则会逐个加载所有依赖对象,这是非常低效的。通常,数据库操作由存储库或服务类完成,这两者都应该很好地了解其工作。它们可以一次加载大量实体​​(例如,使用in子句),也可以验证依赖实体是否已经持久化。 - oddparity


还有一个问题的解决方案 - 使用您想要引用的实体的默认构造函数,并设置id和version(如果它是版本化的)。我们有以下dao方法:

public <S extends T> S materialize(EId<S> entityId, Class<S> entityClass) {
        Constructor<S> c = entityClass.getDeclaredConstructor();
        c.setAccessible(true);

        S instance = c.newInstance();
        Fields.set(instance, "id", entityId.getId());
        Fields.set(instance, "version", entityId.getVersion());
        return instance; // Try catch omitted for brevity.
}

我们可以使用这样的方法,因为我们不使用延迟加载,而是具有在GUI上使用的实体的“视图”。这允许我们摆脱Hibernate用来填充所有渴望关系的所有连接。视图始终具有实体的ID和版本。因此,我们可以通过创建一个Hibernate看起来不是瞬态的对象来填充引用。

我尝试了这种方法和session.load()的方法。他们都工作得很好。我看到我的方法有一些优点,因为Hibernate不会泄漏其代理中其他地方的代理。如果没有正确使用,我只会得到NPE而不是'no session bound to thread'异常。


0
2018-01-14 12:42