如何使JPA OneToOne关系变得懒惰


212

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

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

有什么想法可以让我工作吗?请注意,我不使用persistence.xml定义关系或配置详细信息,所有操作均以Java代码完成。

Answers:


218

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

  1. 无约束(可为空)的一对一关联是没有字节码检测就无法代理的唯一关联。原因是所有者实体必须知道关联属性应包含代理对象还是NULL,并且由于通常是通过共享PK进行一对一映射,因此无法通过查看其基表的列来确定该属性。无论如何,必须急切地获取代理,使其毫无意义。这是一个更详细的说明。

  2. 多对一关联(显然是一对多)不受此问题困扰。所有者实体可以轻松地检查其自己的FK(如果是一对多,则最初会创建空集合代理并按需填充),因此关联可以很懒。

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

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

现在,就原始问题而言:

A)@ManyToOne(fetch=FetchType.LAZY)应该工作正常。您确定查询本身不会覆盖它吗?可以指定join fetch在HQL中和/或通过Criteria API显式设置获取模式,这将优先于类注释。如果不是这种情况,但您仍然遇到问题,请发布您的课程,查询和生成的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,但是-如果您有80 加入由于渴望OneToOne协会,您有更大的问题,然后这:-)


也许还有另一种选择,但是我还没有亲自测试过:在不受限制的方面,请使用one-to-one带有的公式,例如select other_entity.id from other_entity where id = other_entity.id。当然,这对于查询性能不是理想的。
弗雷德里克

1
optional = false,对我不起作用。@OneToOne(fetch = FetchType.LAZY,mappingBy =“ fundSeries”,可选= false)私人FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts

21

为了让延迟加载处理可为空的一对一映射,您需要让休眠模式进行编译时检测并添加一个@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>

3
为什么LazyToOneOption.NO_PROXYLazyToOneOption.PROXY呢?
Telmo Marques

这没有回答“为什么”,但是在这里(在“典型映射”部分的末尾)也断言了这一事实:vladmihalcea.com/…–
DanielM,

12

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

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

编辑:有关详细信息,请参阅ChssPly76的答案。这是不够准确和详细的,它无法提供。感谢ChssPly76。


这里有几处错误-我在下面提供了另一个答案并提供了解释(太多内容,无法放入评论中)
ChssPly76

8

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

而不是@OneToOne在双方上都使用,而是@OneToMany在关系的倒数部分(使用mappedBy)。这使该属性成为一个集合(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); }

}

7

正如我在本文中所解释的那样,除非您使用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
}

使用@MapsIdid子表中的属性既用作父表主键的主键又用作外键。

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

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

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


这样,我们就无法再从父级到子级进行操作了:/
Hamdi

对于持久,这只是一个额外的持久调用,对于删除,您可以使用DDL级联。
Vlad Mihalcea

6

在本地Hibernate XML映射中,您可以通过将constrained属性设置为true 的一对一映射来声明此操作。我不确定Hibernate / JPA批注的含义是什么,快速搜索文档没有任何答案,但希望可以为您带来继续的机会。


5
+1是个好建议;不幸的是,它并不总是适用,因为领域模型实际上可能需要可为空性。通过注释映射此内容的正确方法是@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76

我尝试了这一点,但没有看到性能改善。我仍然通过调试器在休眠输出中看到许多查询。
P.Brian.Mackey

3

正如已经完美地ChssPly76解释,Hibernate的代理不具有约束(可为空)一到一个协会的帮助,但还有一招解释这里为了避免设立仪器仪表。这个想法是要愚弄Hibernate,我们要使用的实体类已经被检测:您可以在源代码中手动检测它。这很容易!我已经使用CGLib作为字节码提供程序实现了它,并且它可以工作(确保在HBM中配置了lazy =“ no-proxy”和fetch =“ select”而不是“ join”)。

我认为当您只有一个想要拖延的一对一的可为空的关系时,这是对实际(我的意思是自动)检测的一个很好的替代方法。主要缺点是解决方案取决于您使用的字节码提供程序,因此请准确地注释您的类,因为将来可能需要更改字节码提供程序。当然,出于技术原因,您也在修改模型bean,这并不好。


1

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

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

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

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.