ChatGPT解决这个技术问题 Extra ChatGPT

如何使 JPA OneToOne 关系变得懒惰

在我们正在开发的这个应用程序中,我们注意到视图特别慢。我对视图进行了分析,并注意到休眠执行了一个查询,即使数据库中只有两个对象要获取,它也需要 10 秒。所有 OneToManyManyToMany 关系都是惰性的,所以这不是问题。在检查正在执行的实际 SQL 时,我注意到查询中有超过 80 个连接。

进一步检查该问题,我注意到该问题是由实体类之间的 OneToOneManyToOne 关系的深层层次结构引起的。所以,我想,我会让他们偷懒,这应该可以解决问题。但是注释 @OneToOne(fetch=FetchType.LAZY)@ManyToOne(fetch=FetchType.LAZY) 似乎不起作用。要么我得到一个异常,要么它们实际上并没有被代理对象替换,因此变得懒惰。

任何想法我将如何让它工作?请注意,我不使用 persistence.xml 来定义关系或配置细节,一切都在 java 代码中完成。


R
Ryan M

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

不受约束(可为空)的一对一关联是唯一一个在没有字节码检测的情况下无法代理的关联。这样做的原因是所有者实体必须知道关联属性是否应该包含代理对象或 NULL,并且由于通常通过共享 PK 进行一对一映射,因此它无法通过查看其基表的列来确定这一点,因此它无论如何都必须急切地获取代理,这使代理毫无意义。这里有更详细的解释。多对一关联(显然是一对多)不会受到这个问题的影响。 Owner 实体可以很容易地检查自己的 FK(如果是一对多,最初创建空集合代理并按需填充),因此关联可以是惰性的。用一对多代替一对一几乎不是一个好主意。您可以用独特的多对一替换它,但还有其他(可能更好)选项。

Rob H. 有一个有效点,但是您可能无法实现它,具体取决于您的模型(例如,如果您的一对一关联 可以为空)。

现在,就原始问题而言:

A) @ManyToOne(fetch=FetchType.LAZY) 应该可以正常工作。您确定它没有在查询本身中被覆盖吗?可以在 HQL 中指定 join fetch 和/或通过 Criteria API 显式设置获取模式,这将优先于类注释。如果情况并非如此,并且您仍然遇到问题,请发布您的课程、查询和生成的 SQL,以便进行更深入的对话。

B) @OneToOne 比较棘手。如果它绝对不可为空,请按照 Rob H. 的建议进行指定,如下所示:

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

否则,如果您可以更改数据库(将外键列添加到所有者表),请执行此操作并将其映射为“已加入”:

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

在其他实体中:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

如果你不能这样做(并且不能忍受急切的获取)字节码检测是你唯一的选择。但是,我必须同意 CPerkins 的观点——如果你有 80 个!!!由于急切的 OneToOne 协会而加入,那么您将遇到更大的问题 :-)


也许还有另一种选择,但我没有亲自测试过:在非约束方面,使用 one-to-one 和像 select other_entity.id from other_entity where id = other_entity.id 这样的公式。当然,这对于查询性能来说并不理想。
optional = false,对我不起作用。 @OneToOne(fetch = FetchType.LAZY, mappedBy = "fundSeries", optional = false) private FundSeriesDetailEntity fundSeriesDetail;
这个想法是把 joinColumn 放在 Lazy 的拥有方,而不是映射到的一方
K
Kdeveloper

要在可空的一对一映射上进行延迟加载,您需要让 hibernate 执行 compile time instrumentation 并将 @LazyToOne(value = LazyToOneOption.NO_PROXY) 添加到一对一关系。

示例映射:

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

示例 Ant Build 文件扩展名(用于执行 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>

为什么是 LazyToOneOption.NO_PROXY 而不是 LazyToOneOption.PROXY
这没有回答“为什么”,但这里也断言了这一事实(接近“典型映射”部分的末尾):vladmihalcea.com/…
V
Vlad Mihalcea

除非您使用字节码增强,否则您无法延迟获取父端 @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()
);

这样,您就不会遇到可能由父方的 mappedBy @OneToOne 关联引起的 N+1 查询问题。


这样我们就不能再从父级级联操作到子级了:/
对于persist,它只是一个额外的persist调用,对于delete,你可以使用DDL级联。
使用@MapsId,孩子不能为空,对吗?并且父母必须有@OneToOne(fetch = FetchType.LAZY, optional = false)?
答案告诉您不应使用父端 OneToOne 映射,因此只需将其设置在子端即可。
K
KLE

Hibernate 中 XToOne 的基本思想是它们在大多数情况下并不懒惰。

一个原因是,当 Hibernate 必须决定放置一个代理(带有 id)或 null 时,它必须查看另一个表才能加入。访问数据库中另一个表的成本很高,因此它还不如在那个时刻获取该表的数据(非惰性行为),而不是在以后需要第二次访问该表的请求中获取该数据。同一张桌子。

已编辑:有关详细信息,请参阅 ChssPly76 的答案。这个不太准确和详细,它没有什么可提供的。感谢 ChssPly76。


这里有几处错误-我在下面提供了另一个答案并进行了解释(内容太多,不适合评论)
a
acdcjunior

这是对我有用的东西(没有仪器):

我没有在两边都使用 @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); }

}

R
Rob H

在本机 Hibernate XML 映射中,您可以通过声明一个将 constrained 属性设置为 true 的 one-to-one 映射来完成此操作。我不确定 Hibernate/JPA 注释的等价物是什么,并且快速搜索文档没有提供任何答案,但希望这可以让您继续前进。


+1 一个好的建议;不幸的是,它并不总是适用,因为域模型实际上可能需要可空性。通过注释映射它的正确方法是 @OneToOne(optional=false,fetch=FetchMode.LAZY)
我试过这个并没有看到性能提升。我仍然通过调试器在休眠输出中看到了许多查询。
P
Pino

正如 ChssPly76 已经完美解释的那样,Hibernate 的代理对无约束(可为空)的一对一关联没有帮助,但是有一个技巧解释了 here 以避免设置检测。这个想法是为了欺骗 Hibernate,我们想要使用的实体类已经被检测了:你在源代码中手动检测它。这很简单!我已经使用 CGLib 作为字节码提供程序来实现它并且它可以工作(确保您在 HBM 中配置了lazy="no-proxy" 和 fetch="select",而不是“join”)。

我认为这是一个很好的替代真正的(我的意思是自动的)插桩,当你只有一个一对一的可空关系你想让它变得懒惰时。主要缺点是解决方案取决于您使用的字节码提供程序,因此请准确注释您的类,因为您将来可能必须更改字节码提供程序;当然,出于技术原因,您也在修改您的模型 bean,这并不好。


T
Toumi

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

除了 @OneToOne 关联的父方之外,延迟加载有效。这是因为 Hibernate 没有其他方法可以知道是否将 null 或 Proxy 分配给此变量。您可以在 this article 中找到更多详细信息

您可以激活延迟加载字节码增强功能

或者,您可以删除父端并使用带有@MapsId 的客户端,如上文所述。这样,您会发现您实际上并不需要父方,因为子级与父级共享相同的 id,因此您可以通过知道父级 id 轻松获取子级。


J
Jimmy

一对一关联的最有效映射 您可以通过对两个关联实体使用相同的主键值来避免所有这些问题并摆脱外键列。您可以通过使用 @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());

More Detail click on this url


T
Terskel

对于 Kotlin 开发人员:为了允许 Hibernate 从您希望延迟加载的 @Entity 类型继承,它们必须是可继承的/open,而在 Kotlin 中默认情况下它们不是。要解决此问题,我们可以使用 all-open compiler plugin 并通过将其添加到 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

阅读伟大的 article of Léo Millon 以获得进一步的解释。


太感谢了。在找到您的答案并解决了我的问题之前,我已经绞尽脑汁整整一天了。我什至没有想到会朝那个方向看。
S
Stefan

如果关系不能是双向的,那么 @ElementCollection 可能比使用惰性 One2Many 集合更容易。


b
bebbo

如果子实体以只读方式使用,则可以简单地 lie 并设置 optional=false。然后确保通过查询预加载该映射实体的每次使用。

public class App {
  ...
  @OneToOne(mappedBy = "app", fetch = FetchType.LAZY, optional = false)
  private Attributes additional;

String sql = " ... FROM App a LEFT JOIN FETCH a.additional aa ...";

......也许即使坚持也会奏效......


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅