如何使JPA OneToOne关系变为延迟加载

252
在我们正在开发的应用程序中,我们注意到一个视图特别慢。我对该视图进行了分析,并发现Hibernate执行的一个查询需要花费10秒钟的时间,即使数据库中只有两个对象需要获取。所有的OneToManyManyToMany关系都是惰性的,所以那不是问题所在。当检查实际执行的SQL时,我发现查询语句中有80多个连接。
进一步调查后,我发现问题是由实体类之间深层次的OneToOneManyToOne关系引起的。因此,我想,我只需要使它们懒加载,就可以解决问题了。但是,无论是注释@OneToOne(fetch=FetchType.LAZY)还是@ManyToOne(fetch=FetchType.LAZY)都似乎不起作用。要么我会得到一个异常,要么它们实际上没有被替换为代理对象,从而变成了非懒加载。
有什么好的想法可以让我解决这个问题吗?请注意,我不使用persistence.xml来定义关系或配置细节,一切都是在Java代码中完成的。
12个回答

251

首先,对于KLE的回答进行一些澄清:

  1. 非约束(可为空)的一对一关联是唯一无法在不使用字节码仪器的情况下进行代理的。原因是所有者实体必须知道关联属性应该包含代理对象还是NULL,并且由于通常通过共享PK映射一对一,它无法通过查看其基表的列来确定,因此必须被急切地获取,从而使代理毫无意义。这里有一个更详细的解释。

  2. 多对一关联(以及一对多关联)不会受到此问题的影响。所有者实体可以轻松检查自己的FK(在一对多的情况下,空集合代理最初会被创建并按需填充),因此可以懒惰地加载关联。

  3. 几乎永远不会将一对一关联替换为一对多关联。你可以用唯一的多对一关联来替换它,但还有其他(可能更好的)选择。

Rob H. 的观点是正确的,但是根据你的模型(例如,如果你的一对一关联可为空),你可能无法实现它。

现在,就原始问题而言:

A) @ManyToOne(fetch=FetchType.LAZY) 应该完全可以工作。你确定它没有被查询本身覆盖吗?可以通过HQL和/或显式地使用Criteria API设置抓取模式来指定join fetch,这将优先于类注释。如果不是这种情况,并且你仍然有问题,请发布你的类、查询和结果SQL以进行更加直接的对话。

B) @OneToOne 更加棘手。如果它确实不可为空,请按照Rob H.的建议指定它:

@OneToOne(optional = false, fetch = FetchType.LAZY)

否则,如果可以更改您的数据库(在所有者表中添加外键列),请这样做并将其映射为“joined”:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

在 OtherEntity 中:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()
如果你无法做到这一点(并且无法使用急切的获取),那么字节码检测是唯一的选择。但是我必须同意CPerkins的看法-如果你由于渴望的OneToOne关联而有80次连接,那么你面临的问题比这还要大 :-)

也许还有另一种选择,但我个人没有测试过:在非约束侧,使用一个类似于 select other_entity.id from other_entity where id = other_entity.id 的公式进行 one-to-one 映射。当然,这对查询性能来说并不理想。 - Frédéric
4
这段代码的意思是:选项为false,对我没有作用。@OneToOne(fetch = FetchType.LAZY, mappedBy = "fundSeries", optional = false)表示该实体与FundSeriesDetailEntity实体之间存在一对一的映射关系,使用延迟加载方式获取数据,而且必须存在对应的FundSeriesDetailEntity实体。 - Oleg Kuts
这个想法是将JoinColumn与Lazy放在拥有方,而不是具有mappedBy的一方。 - Georgian Benetatos
1
很遗憾,现在已经是2023年了,仍然面临这个问题 :( - Arun Gowda

24
为了让可空的一对一映射实现lazy loading,您需要让hibernate进行编译时仪器化并在一对一关系中添加@LazyToOne(value = LazyToOneOption.NO_PROXY)
示例映射:
@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

示例 Ant 构建文件扩展名(用于执行 Hibernate 编译时仪器化):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>

3
õ©║õ╗Çõ╣êÞªüõ¢┐þö¿LazyToOneOption.NO_PROXYÞÇîõ©ìµÿ»LazyToOneOption.PROXY´╝ƒ - Telmo Marques
这并没有回答“为什么”的问题,但是这个事实也在这里被断言了(在“典型映射”部分的末尾):https://vladmihalcea.com/the-best-way-to-map-a-onetoone-relationship-with-jpa-and-hibernate/ - DanielM

24

除非您使用字节码增强,否则无法懒加载父侧的@OneToOne关联。

但是,如果在子侧使用@MapsId,通常情况下您甚至都不需要父侧关联:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {
 
    @Id
    private Long id;
 
    @Column(name = "created_on")
    private Date createdOn;
 
    @Column(name = "created_by")
    private String createdBy;
 
    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;
 
    public PostDetails() {}
 
    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }
 
    //Getters and setters omitted for brevity
}

使用 @MapsId,子表中的 id 属性既可以作为主键,也可以作为外键引用父表的主键。

因此,如果您有对父实体 Post 的引用,可以使用父实体标识符轻松获取子实体:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

通过这种方式,您就不会遇到由于父对象端的@OneToOne关联上的mappedBy而引起的N+1查询问题。


3
我们不能再从父级向子级级联操作了:/ - Hamdi
1
对于持久化,只需要额外的持久化调用,对于删除,您可以使用DDL级联。 - Vlad Mihalcea
使用@MapsId,子项不可能为空,对吧?而且父项必须有@OneToOne(fetch = FetchType.LAZY, optional = false)吗? - Vishwa
答案告诉你不应该使用父侧的OneToOne映射,所以只需在子侧设置它。 - Vlad Mihalcea

16

这是一种对我有用的方法(无需工具):

不要在双方都使用@OneToOne,而是在关系的反向部分(带有mappedBy)中使用@OneToMany。 这将使属性成为一个集合(在下面的示例中为List),但我会将其转换为getter方法返回的项目,使其对客户端透明。

这个设置是懒惰加载的,也就是说,只有在调用getPrevious()getNext()时才进行选择 - 每次调用只有一个选择。

表结构:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

这个班级:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}

多么聪明而简单的解决方案!非常感谢你分享这个想法!顺便问一下,在你的例子中,你只写了一个getter方法,你认为用setter方法做同样的操作会导致一些同步问题吗?我是这样实现的(假设我用一个空集合初始化了我的@OneToMany字段,因此它永远不会为null):public void setNext(Issue next) { if (next == null) { this.next.clear(); } else if (this.next.isEmpty()) { this.next.add(next); } else { throw new IllegalArgumentException("Issue already has a next one"); } } - fbastien
多么聪明而简单的解决方案!非常感谢你分享这个想法!顺便问一下,在你的例子中,你只写了一个getter方法,你认为用一个setter方法做同样的事情会导致一些同步问题吗?这是我尝试实现它的方式(假设我用一个空集合初始化了我的@OneToMany字段,因此它永远不会是null): public void setNext(Issue next) { if (next == null) { this.next.clear(); } else if (this.next.isEmpty()) { this.next.add(next); } else { throw new IllegalArgumentException("Issue already has a next one"); } } - undefined
根据我之前的评论:也许我的设置方法可以在设置新的“next”问题时替换现有的问题,而不是抛出异常,但我不确定是否需要清除另一侧的关系... 对此有什么想法吗?谢谢 :) - fbastien

15

Hibernate中XToOnes的基本思想是,在大多数情况下它们不是懒加载。

原因之一是,当Hibernate需要决定放置一个代理(带有id)还是一个null时,
它必须查看另一个表来进行连接。访问数据库中另一个表的成本是显著的,因此它不妨在那个时刻获取该表的数据(非懒加载行为),而不是在后续请求中获取需要对同一表进行第二次访问的数据。

编辑:有关详细信息,请参见ChssPly76的答案。这篇文章不够精确和详细,没有什么可提供的。谢谢ChssPly76。


这里有几个问题 - 我在下面提供了另一个答案,并解释了原因(太多的内容,无法放入评论中)。 - ChssPly76

7

在原生Hibernate XML映射中,您可以通过声明一个受限制的一对一映射来实现此目的。我不确定Hibernate/JPA注释等效的内容是什么,快速搜索文档也没有答案,但希望这能给您提供线索。


6
好的建议,点赞!不幸的是,它并不总是适用,因为领域模型可能实际上需要为空性。通过注释将其正确映射的方式是 @OneToOne(optional=false,fetch=FetchMode.LAZY) - ChssPly76
1
我尝试了这个方法,但没有看到性能上的改善。通过调试器,我仍然可以看到许多查询在Hibernate输出中。 - P.Brian.Mackey

4
正如ChssPly76已经完美地解释了的那样,Hibernate的代理在不受限制的(可空)一对一关联方面没有帮助,但是有一个诀窍可以避免设置仪器,这里有详细解释。其中的想法是欺骗Hibernate,使我们要使用的实体类已经被仪器化了:在源代码中手动进行仪器化。很容易!我已经使用CGLib作为字节码提供程序进行了实现,并且它可以工作(请确保在HBM中配置lazy="no-proxy"和fetch="select",而不是"join")。
我认为这是一个很好的替代方案,用于将只有一个一对一可空关系要变成懒加载的“真正”的(我的意思是自动)仪器化。主要缺点是解决方案取决于您正在使用的字节码提供程序,因此要仔细注释您的类,因为您将来可能不得不更改字节码提供程序;当然,您也正在为技术原因修改模型bean,这并不好。

3

这个问题很旧了,但是在Hibernate 5.1.10中,有一些新的更好的解决方案。

延迟加载对于@OneToOne关联的父对象方面不起作用。这是因为Hibernate没有其他方法知道是否将null或代理分配给此变量。更多细节可以在此文章中找到。

  • 您可以激活延迟加载字节码增强
  • 或者,您可以只删除父对象并使用客户端与@MapsId,如上文所述。这样,您会发现实际上不需要父对象,因为子对象与父对象共享相同的ID,因此您可以轻松地通过知道父ID来获取子对象。

2
对于 Kotlin 开发人员:要允许 Hibernate 继承您想要进行延迟加载的 @Entity 类型,它们必须是可继承/`open` 的,而默认情况下在 Kotlin 中它们不是这样的。为了解决这个问题,我们可以利用 all-open 编译器插件 并通过将以下内容添加到我们的 build.gradle 来指示它也处理 JPA 注解:
allOpen {
   annotation("javax.persistence.Entity")
   annotation("javax.persistence.MappedSuperclass")
   annotation("javax.persistence.Embeddable")
}

如果您像我一样使用Kotlin和Spring,那么您很可能已经在使用kotlin-jpa/no-argskotlin-spring/all-open编译器插件。然而,您仍然需要添加上述代码行,因为这些插件的组合都不会使这些类变为open
请阅读Léo Millon的优秀文章以获得更多解释。article of Léo Millon

非常感谢。我已经苦思冥想了一整天,直到找到了你的答案并解决了我的问题。我甚至没有想到朝那个方向去寻找。 - justfortherec

2

最有效的一对一关联映射 通过使用相同的主键值来映射两个相关实体,您可以避免所有这些问题并摆脱外键列。您可以通过在关联的拥有方上注释@MapsId来实现这一点。

@Entity
public class Book {
 
    @Id
    @GeneratedValue
    private Long id;
 
    @OneToOne(mappedBy = "book", fetch = FetchType.LAZY, optional = false)
    private Manuscript manuscript;
 
    ...
}


@Entity
public class Manuscript {
 
    @Id
    private Long id;
 
    @OneToOne
    @MapsId
    @JoinColumn(name = "id")
    private Book book;
 
    ...
}


Book b = em.find(Book.class, 100L);
Manuscript m = em.find(Manuscript.class, b.getId());

点击此链接获取更多详细信息的URL


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