使OneToOne关系变得懒惰

时间:2009-09-18 12:21:41

标签: java hibernate jpa

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

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

我有什么想法让这个工作?请注意,我不使用persistence.xml来定义关系或配置细节,所有操作都在java代码中完成。

11 个答案:

答案 0 :(得分:196)

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

  1. 无约束(可空)的一对一关联是唯一一个在没有字节码检测的情况下无法代理的关联。这样做的原因是所有者实体必须知道关联属性是否应该包含代理对象或NULL并且由于通常通过共享PK进行一对一映射而无法通过查看其基表的列来确定它,因此它不管怎么说,必须要急切地让代理变得毫无意义。这是一个more detailed解释。

  2. 多对一关联(显然是一对多关联)不会遇到此问题。所有者实体可以轻松检查自己的FK(如果是一对多,最初创建空集合代理并按需填充),那么关联可能是懒惰的。

  3. 用一对多替换一对一绝不是一个好主意。您可以使用唯一的多对一替换它,但还有其他(可能更好)的选项。

  4. 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()
    

    并在OtherEntity:

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

    如果你不能这样做(并且不能忍受急切的提取),字节码检测是你唯一的选择。我不得不同意 CPerkins ,但是如果你因为热切的OneToOne协会而加入了 80 !!! ,你就会遇到更大的问题: - )

答案 1 :(得分:17)

要使延迟加载处理可空的一对一映射,您需要让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构建文件扩展示例(用于执行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>

答案 2 :(得分:10)

在Hibernate中使用XToOnes的基本想法是,在大多数情况下它们并不是懒惰的。

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

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

答案 3 :(得分:8)

这里有一些对我有用的东西(没有仪器):

我不是在双方使用@OneToOne,而是在关系的反向部分使用@OneToManymappedBy)。这使得该属性成为一个集合(在下面的示例中为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); }

}

答案 4 :(得分:5)

在本机Hibernate XML映射中,您可以通过将约束属性设置为true来声明one-to-one映射来实现此目的。我不确定Hibernate / JPA注释与之相当的是什么,并且快速搜索文档没有提供任何答案,但希望这能让你继续前进。

答案 5 :(得分:3)

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

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

答案 6 :(得分:2)

正如我在this article中所解释的那样,除非您使用Bytecode Enhancement,否则您不能懒洋洋地获取父级@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()
);

这样,您就不会有N+1 query issues可能是由父方的mappedBy @OneToOne关联引起的。

答案 7 :(得分:1)

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

除了@OneToOne关联的父方,延迟加载均有效。这是因为Hibernate没有其他方法可以知道是否要为此变量分配null还是Proxy。您可以在this article

中找到更多详细信息
  • 您可以激活延迟加载字节码增强功能
  • 或者,您可以仅删除父端,并将客户端与@MapsId一起使用,如上文中所述。这样,您会发现您实际上并不需要父方,因为孩子与父母共享相同的ID,因此您可以通过了解父ID轻松获取孩子 。

答案 8 :(得分:1)

对于 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 以获得进一步的解释。

答案 9 :(得分:0)

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

答案 10 :(得分:0)

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

ButtonStyle

optional=false

...也许甚至坚持也可以工作...