我如何使JPA OneToOne关系变懒

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

进一步检查这个问题,我注意到这个问题是由实体类之间的OneToOneManyToOne关系的深层层次结构引起的。所以,我想,我只要把它们设为lazy,就能解决问题了。但是注释@OneToOne(fetch=FetchType.LAZY)@ManyToOne(fetch=FetchType.LAZY)似乎不起作用。要么我得到一个异常,要么它们实际上没有被代理对象替换,从而变得懒惰。

你知道我要怎么做吗?注意,我没有使用persistence.xml来定义关系或配置细节,一切都在java代码中完成。

135280 次浏览

在原生Hibernate XML映射中,可以通过声明一个一对一的映射并将限制属性设置为true来实现这一点。我不确定Hibernate/JPA注释等价于它是什么,快速搜索文档也没有提供答案,但希望这能给您一个线索。

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

一个原因是,当Hibernate必须决定放置一个代理(带id)或一个null时,
无论如何,它必须查找另一个表才能加入。访问数据库中另一个表的代价是巨大的,所以它最好在那个时刻为那个表获取数据(非懒惰行为),而不是在以后的请求中获取数据,这将需要对同一个表进行第二次访问。

编辑:具体请参考ChssPly76的回答。这一个不那么准确和详细,它没有提供任何东西。谢谢ChssPly76。

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

  1. 无约束(可空)一对一关联是唯一在没有字节码插装的情况下不能被代理的关联。这样做的原因是所有者实体必须知道关联属性是否应该包含一个代理对象或NULL,它不能通过查看其基表的列来确定,因为一对一通常是通过共享PK映射的,所以它必须急切地获取无论如何使代理毫无意义。下面是更详细的的解释。

  2. 多对一关联(显然是一对多)不会受到这个问题的影响。所有者实体可以很容易地检查自己的FK(在一对多的情况下,初始创建空集合代理并按需填充),因此关联可以是惰性的。

  3. 用一对多代替一对一从来都不是一个好主意。您可以将其替换为唯一的多对一,但还有其他(可能更好的)选项。

< >强抢h . < / >强有一个有效的点,但你可能无法实现它取决于你的模型(例如,如果你的一对一关联为空)。

现在,就最初的问题而言

A) @ManyToOne(fetch=FetchType.LAZY)应该可以正常工作。您确定它没有在查询本身中被覆盖吗?可以在HQL中指定join fetch和/或通过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年! !连接,由于渴望一对一的关联,你有更大的问题:-)

为了让惰性加载工作在可空的一对一映射上,你需要让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>

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

我认为这是一个很好的替代真正的(我的意思是自动)插装,当你只有一个一对一的可空关系,你想使懒惰。主要的缺点是这个解决方案依赖于你正在使用的字节码提供程序,所以准确地注释你的类,因为你将来可能不得不改变字节码提供程序;当然,您还因为技术原因修改了您的模型bean,这是不好的。

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

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


}

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

惰性加载除了@OneToOne关联的父端外都有效。这是因为Hibernate没有其他方法来知道是给这个变量分配一个空值还是一个代理。更多细节你可以在这篇文章中找到

  • 您可以激活延迟加载字节码增强
  • 或者,您可以删除父端,并使用上面文章中解释的@MapsId的客户端。通过这种方式,你会发现你并不真正需要父端,因为子端与父端共享相同的id,所以你可以通过知道父端id轻松获取子端 李。< / >

除非你正在使用字节码增强,否则你不能懒懒地获取父端@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个查询问题,这可能是由父端mappedBy @OneToOne关联引起的。

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

如果子实体是只读的,那么可以简单地谎言并设置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 ...";

... 也许坚持也会有用……

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

对于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,你很可能也在使用__ABC0 / no-args__ABC2 / all-open编译器插件。然而,你仍然需要添加上面的行,因为插件的组合不会使这样的类open

阅读伟大的文章Léo百万以获得进一步的解释。