【问题标题】:How can I make a JPA OneToOne relation lazy如何使 JPA OneToOne 关系变得懒惰
【发布时间】:2010-11-29 11:26:06
【问题描述】:

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

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

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

【问题讨论】:

    标签: java hibernate jpa lazy-loading one-to-one


    【解决方案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 以获得进一步的解释。

    【讨论】:

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

    除非您使用字节码增强,否则您不能延迟获取父端@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 映射,所以只需在子端设置即可。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

      如果子实体以只读方式使用,则可以简单地谎言并设置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 ...";
      

      ...也许即使坚持也可以工作...

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          【解决方案6】:

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

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

          • 您可以激活延迟加载字节码增强功能
          • 或者,您可以删除父端并使用带有@MapsId 的客户端,如上面文章中所述。这样,你会发现你并不真的需要父端,因为子节点与父节点共享相同的 id,因此你可以通过知道父 id 轻松获取子节点 .

          【讨论】:

            【解决方案7】:

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

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

            2. 多对一关联(显然是一对多)不受此问题的影响。 Owner 实体可以轻松检查自己的 FK(如果是一对多,则最初创建空集合代理并按需填充),因此关联可以是惰性的。

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

            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-oneselect other_entity.id from other_entity where id = other_entity.id 之类的公式。当然,这对于查询性能来说并不理想。
            • 可选 = false,对我不起作用。 @OneToOne(fetch = FetchType.LAZY, mappedBy = "fundSeries", optional = false) private FundSeriesDetailEntity fundSeriesDetail;
            【解决方案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); }
            
            }
            

            【讨论】:

              【解决方案9】:

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

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

              【讨论】:

                【解决方案10】:

                要在可空的一对一映射上实现延迟加载,您需要让 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/…
                【解决方案11】:

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

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

                已编辑:详情请参考 ChssPly76 的回答。这个不太准确和详细,它没有什么可提供的。谢谢 ChssPly76。

                【讨论】:

                • 这里有几处错误 - 我在下面提供了另一个答案并附有解释(内容太多,不适合评论)
                【解决方案12】:

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

                【讨论】:

                • +1 表示好建议;不幸的是,它并不总是适用,因为域模型实际上可能需要可空性。通过注释映射它的正确方法是@OneToOne(optional=false,fetch=FetchMode.LAZY)
                • 我试过这个并没有看到性能提升。我仍然通过调试器在休眠输出中看到了许多查询。
                猜你喜欢
                • 2019-05-19
                • 2023-03-12
                • 2013-03-20
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多