【问题标题】:Hibernate HQL join fetch not recursively fetchingHibernate HQL join fetch 不递归获取
【发布时间】:2013-08-30 16:26:32
【问题描述】:

我有以下查询和方法

private static final String FIND = "SELECT DISTINCT domain FROM Domain domain LEFT OUTER JOIN FETCH domain.operators LEFT OUTER JOIN FETCH domain.networkCodes WHERE domain.domainId = :domainId";

@Override
public Domain find(Long domainId) {
    Query query = getCurrentSession().createQuery(FIND);
    query.setLong("domainId", domainId);
    return (Domain) query.uniqueResult();
}

Domain

@Entity
@Table
public class Domain {
    @Id
    @GenericGenerator(name = "generator", strategy = "increment")
    @GeneratedValue(generator = "generator")
    @Column(name = "domain_id")
    private Long domainId;

    @Column(nullable = false, unique = true)
    @NotNull
    private String name;

    @Column(nullable = false)
    @NotNull
    @Enumerated(EnumType.STRING)
    private DomainType type;

    @OneToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
    }, fetch = FetchType.EAGER)
    @JoinTable(joinColumns = {
            @JoinColumn(name = "domain_id")
    }, inverseJoinColumns = {
            @JoinColumn(name = "code")
    })
    @NotEmpty
    @Valid // needed to recur because we specify network codes when creating the domain
    private Set<NetworkCode> networkCodes = new HashSet<>();

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(joinColumns = {
            @JoinColumn(name = "parent", referencedColumnName = "domain_id")
    }, inverseJoinColumns = {
            @JoinColumn(name = "child", referencedColumnName = "domain_id")
    })
    private Set<Domain> operators = new HashSet<>();
    // more
}

我希望这个单一的查询能够获取 Set&lt;NetworkCode&gt;Set&lt;Domain> 关系,但事实并非如此。假设Domain I 查询有两个运算符,Hibernate 将执行 1 + 2 * 2 = 5 次查询

Hibernate: select distinct domain0_.domain_id as domain1_1_0_, domain2_.domain_id as domain1_1_1_, networkcod4_.code as code2_2_, domain0_.name as name1_0_, domain0_.type as type1_0_, domain2_.name as name1_1_, domain2_.type as type1_1_, operators1_.parent as parent1_0__, operators1_.child as child4_0__, networkcod3_.domain_id as domain1_1_1__, networkcod3_.code as code5_1__ from domain domain0_ left outer join domain_operators operators1_ on domain0_.domain_id=operators1_.parent left outer join domain domain2_ on operators1_.child=domain2_.domain_id inner join domain_network_codes networkcod3_ on domain0_.domain_id=networkcod3_.domain_id inner join network_code networkcod4_ on networkcod3_.code=networkcod4_.code where domain0_.domain_id=?
Hibernate: select operators0_.parent as parent1_1_, operators0_.child as child4_1_, domain1_.domain_id as domain1_1_0_, domain1_.name as name1_0_, domain1_.type as type1_0_ from domain_operators operators0_ inner join domain domain1_ on operators0_.child=domain1_.domain_id where operators0_.parent=?
Hibernate: select networkcod0_.domain_id as domain1_1_1_, networkcod0_.code as code5_1_, networkcod1_.code as code2_0_ from domain_network_codes networkcod0_ inner join network_code networkcod1_ on networkcod0_.code=networkcod1_.code where networkcod0_.domain_id=?
Hibernate: select operators0_.parent as parent1_1_, operators0_.child as child4_1_, domain1_.domain_id as domain1_1_0_, domain1_.name as name1_0_, domain1_.type as type1_0_ from domain_operators operators0_ inner join domain domain1_ on operators0_.child=domain1_.domain_id where operators0_.parent=?
Hibernate: select networkcod0_.domain_id as domain1_1_1_, networkcod0_.code as code5_1_, networkcod1_.code as code2_0_ from domain_network_codes networkcod0_ inner join network_code networkcod1_ on networkcod0_.code=networkcod1_.code where networkcod0_.domain_id=?

我猜这是因为我加入了运营商 Domain 元素,但他们必须加入自己。

是否有一个我可以执行的 HQL 查询可以两者兼得?

【问题讨论】:

    标签: java hibernate jpa hql


    【解决方案1】:

    如果您知道您的树中只有两个级别,您是否想过加入更深的一个级别。像下面这样的?

    SELECT DISTINCT domain FROM Domain domain 
      LEFT OUTER JOIN FETCH domain.operators operators1 
      LEFT OUTER JOIN FETCH domain.networkCodes 
      LEFT OUTER JOIN FETCH operators1.operators operators2 
      LEFT OUTER JOIN FETCH operators1.networkCodes
    WHERE domain.domainId = :domainId
    

    【讨论】:

    • 太棒了,甚至不知道你可以加入更深的层次
    • 它给了我错误org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags 我试图在查询中加载 LAZY 提取类型的实体。
    • 你必须使用 List 而不是 Set
    【解决方案2】:

    Hibernate 关系适用于不同的 Fetch 策略..!!

    Hibernate 提供了 4 种检索数据的策略:

    选择

    @OneToMany(mappedBy="tableName", cascade=CascadeType.ALL)
    @Column(name="id") 
    @Fetch(FetchMode.SELECT)
    

    在此方法中触发了多个 SQL。第一个被解雇了 用于检索父表中的所有记录。剩下的是 为检索每个父记录的记录而被解雇。这基本上是 N+1 问题。第一个查询从数据库中检索 N 条记录,在 本例 N 父记录。对于每个父母,一个新的查询检索 孩子。因此,对于 N 个 Parent,N 个查询从 子表。

    加入

    @OneToMany(mappedBy="tableName", cascade=CascadeType.ALL)
    @Column(name="id")
    @Fetch(FetchMode.JOIN) 
    

    这类似于 SELECT 提取策略,除了所有 与 SELECT 不同,数据库检索在 JOIN fetch 中预先进行 它在需要的基础上发生。这可能成为一个重要的 性能考虑。

    SUBSELECT

     @OneToMany(mappedBy="tableName", cascade=CascadeType.ALL)
     @Column(name="id")
     @Fetch(FetchMode.SUBSELECT)
    

    两个 SQL 被触发。一个检索所有 Parent ,第二个使用 WHERE 子句中的 SUBSELECT 查询以检索所有具有 匹配父 ID。

    批处理

    @OneToMany(mappedBy="tableName", cascade=CascadeType.ALL)
    @Column(name="id")
    @@BatchSize(size=2)
    

    批量大小映射到检索其子代的父代的数量。 所以我们可以指定一次要获取的记录数。但是 将执行多个查询。!!

    一对多和多对多允许 - 加入、选择和子选择

    多对一和一对一允许 - 加入和选择


    Hibernate 还区分(何时获取关联)

    1.立即获取 -

    立即获取关联、集合或属性,当 父级已加载。 (懒惰=“假”)

    2.延迟集合抓取 -

    当应用程序调用一个操作时,会获取一个集合 那个集合。 (这是集合的默认设置。(lazy=“true”)

    3."Extra-lazy" 集合抓取 -

    从数据库中访问集合的各个元素 如所须。 Hibernate 尝试不将整个集合提取到 内存,除非绝对需要(适用于非常大的集合) (懒惰=“额外”)

    4.代理获取 -

    当方法不是 在关联对象上调用标识符 getter。 (懒惰=“代理”)

    5."无代理" 获取 -

    当实例变量为 访问。与代理获取相比,这种方法不那么懒惰。(lazy="no-proxy")

    6.Lazy 属性获取 -

    当实例时获取一个属性或单值关联 变量被访问。 (懒惰=“真”)

    一对多和多对多允许 Immediate、Layzy、Extra Lazy

    多对一和一对一允许立即代理,无代理

    【讨论】:

    • 这是一个很好的参考,但我不能将 @Fetch 与 HQL 一起使用。
    • 你可以在JPA类中添加获取策略为Extra-lazy,这样除非你访问内部表关系,否则内部查询不会被执行。这样,您将只能从主表中获取值,其余部分将被忽略,SINGLE 查询也是如此。
    【解决方案3】:

    您标记了您的关联 EAGER。因此,无论您在查询中做什么,Hibernate 都会加载所有关联的域和已加载域的网络代码。并且它会加载附加域的域和网络代码等,直到所有集合加载返回空集合或已经加载的实体。

    为避免这种情况,请让您的集合变得懒惰(默认情况下)。然后用它的运营商和它的网络代码加载一个域。

    【讨论】:

    • 这些集合必须是EAGER,以便它们在我的Transaction 边界内初始化。我在我的视图层中使用它们。我想知道是否可以一次性加载所有运营商/网络代码,而不是为每个 Domain 一个查询。
    • 如果不使用专有 SQL(例如参见 docs.oracle.com/cd/B19306_01/server.102/b14200/queries003.htm),或者不调整模型以在每个子节点中引入树的根,并加载所有具有同一个根节点。你不能简单地让一切变得懒惰并使用 OpenSessionInView 吗?让许多关联变得渴望几乎总是一个坏主意,尤其是在递归的情况下:这使得它渴望每个用例,包括那些只需要加载一个特定实体的用例。
    • 问题不在于延迟加载。无论如何,所有这些数据都将被检索。我只是不想发送大量的 SQL 请求。我会仔细查看您链接的文档。给它几天。谢谢。
    • 我理解您的担忧。但是使关联急切会导致执行相同的大量请求,除了它们将针对每个用例,而不仅仅是针对需要实际加载关联的用例。这让情况变得更糟。
    • 你没有抓住我:不仅仅是使用该查询的用例。所有以一种或另一种方式从数据库加载域的用例。通过 any 查询、session.get() 或通过实体图导航加载域时,将始终加载已加载域的所有运算符,以及运算符的运算符等。
    【解决方案4】:

    如果您使用 Criteria API 进行查询,Hibernate 只会自动考虑您的 EAGER 映射。

    如果您使用 HQL,则需要手动将 FETCH 关键字添加到您的 JOIN 中,以强制 Hibernate 在第一个查询中包含关系并避免后续查询。

    这是特定于 Hibernate 的,在其他 ORM 上的工作方式可能不同。

    请参阅this question/answer 以获取稍微不同的角度。

    【讨论】:

      【解决方案5】:

      没有很好的记录,但是您是否尝试设置FetchMode? 您可以通过使用 Criteria API:domainCriteria.setFetchMode("operators", JOIN) 或在关系定义中使用 @Fetch(JOIN) 来实现。

      注解(并且只有它看起来的注解)还允许设置获取模式SUBSELECT,这至少应该限制 Hibernate 最多执行 3 个查询。不知道您的数据集,我认为这应该是适合您的方法,因为这些表上的大胖连接似乎不太健康。我想最好自己弄清楚...

      【讨论】:

      • 我的 HQL 查询已经完成 FETCH JOIN。我会试试SUBSELECT
      • 我不确定您使用的 HQL 表示法是否实际上与 hibernate 所理解的“获取模式”相同。 Afaik,HQL JOIN FETCH 只是意味着说“哦,顺便说一下,完全初始化该关系,而不仅仅是获取 ID”——Hibernate 并没有将其解释为它应该应用什么样的策略来获取, 因为前面的JOIN 只是应用FETCH 的必要语法。但是,由于没有明确提到这一点,我可能一直在混淆我对 HQL 和注释使用的经验。
      • 哦,当你提出你的 HQL 时:因为你已经用EAGER 注释了属性,你的查询产生的结果是否与简单的“SELECT DISTINCT domain FROM Domain domain WHERE domain. domainId = :domainId"?为什么DISTINCTdomainId是PK,结果怎么可能不止一个?
      • 抱歉这么久才回复。 It turns out you cannot use @Fetch with HQL. 至于DISTINCT,我在JOIN 上遇到了一些需要它的问题。在其中一些查询中可能是多余的。我会清理它,但现在它没有害处。
      【解决方案6】:

      我的第一个观察是,如果您的映射要求它们必须被急切加载,则您不需要编写包含连接的 HQL 查询。

      但是,如果您不想使用连接,您可以告诉 Hibernate 使用获取策略作为子选择。

      Hibernate 根据指定的映射在启动期间生成用于加载对象的 SQL 查询并将其缓存。但是,在您的情况下,您与 self 和 任意深度 有一对多的嵌套关系,因此看起来 hibernate 无法事先决定 正确渴望获取的 sql。因此,它需要根据您在运行时查询的父域的深度发送多个连接查询。

      在我看来,您似乎认为 HQL 和您的情况下生成的 SQL/('s) 可以具有一对一的对应关系,而这不是 真的。使用 HQL,您可以查询对象,然后 orm 会根据 映射,或者您也可以在运行时指定它们(例如,映射中的惰性关联可以被查询 api 覆盖,反之亦然)。 您可以告诉 orm 加载什么(我的标记渴望或惰性)以及如何立即加载(使用 join / sub select)。

      更新

      当我在您的域模型上运行以下查询时

      SELECT DISTINCT domain FROM Domain domain LEFT OUTER JOIN FETCH domain.operators LEFT OUTER JOIN FETCH domain.networkCodes WHERE domain.domainId = :domainId";
      

      我可以看到 networkCode 和 operator 集合是实例 PersistentSet(这是 Hibernate 包装器),并且都将初始化属性设置为 true。同样在底层会话上下文中,我可以看到域和列出的操作符。 那么是什么让你认为它们没有被热切加载?

      这就是我的域名的样子

      @Entity
      @Table
      public class Domain {
          @Id
          @GenericGenerator(name = "generator", strategy = "increment")
          @GeneratedValue(generator = "generator")
          @Column(name = "domain_id")
          private Long domainId;
      
          @Column(nullable = false, unique = true)   
          private String name;
      
          @Column(nullable = false)    
          @Enumerated(EnumType.STRING)
          private DomainType type;
      
          @OneToMany(mappedBy = "domain",cascade = {
                  CascadeType.PERSIST,
                  CascadeType.MERGE
          }, fetch = FetchType.EAGER)   
          private Set<NetworkCode> networkCodes = new HashSet<NetworkCode>();
      
          @ManyToMany(mappedBy="parent",fetch = FetchType.EAGER, cascade=CascadeType.ALL)
          private Set<Domain> operators = new HashSet<Domain>();
          // more
      
          @ManyToOne  
          private Domain parent;
      
          public String getName() {
              return name;
          }
      
      
          public void setName(String name) {
              this.name = name;
          }
      
      
      public DomainType getType() {
              return type;
          }
      
          public void setType(DomainType type) {
              this.type = type;
          }
      
      
          public Set<Domain> getOperators() {
              return operators;
          }
      
      
          public Long getDomainId() {
              return domainId;
          }
      
      
          public void setDomainId(Long domainId) {
              this.domainId = domainId;
          }
      
      
          public void setOperators(Set<Domain> operators) {
              this.operators = operators;
          }
      
          public void addDomain(Domain domain){
              getOperators().add(domain);
              domain.setParent(this);
          }
      
      
          public Domain getParent() {
              return parent;
          }
      
      
          public void setParent(Domain parent) {
              this.parent = parent;
          }
      
          public void addNetworkCode(NetworkCode netWorkCode){
              getNetworkCodes().add(netWorkCode);
              netWorkCode.setDomain(this);
          }
      

      【讨论】:

      • HQL 中的JOIN 不仅用于EAGER 加载,还用于LEFT OUTER。并非每个域都有operatorsnetworkCodesLEFT OUTER JOIN 是正确映射所必需的。
      • 问题不在于它们没有被急切地加载。这是 IMO 生成的 SQL 查询太多。我认为 Hibernate 可以用更少的钱做到这一点。
      • 我现在明白了。我认为在这种情况下,安德烈的答案是你最好的选择!
      【解决方案7】:

      由于您已经为networkCodesoperators 指定了FetchType.EAGER,因此无论何时查询domain,hibernate 都会同时加载networkCodesoperators。这就是EAGER获取模式的全部思路

      因此,您可以将查询更改为以下内容:

      private static final String FIND
          = "SELECT DISTINCT domain"
          + " FROM Domain domain"
          + " WHERE domain.domainId = :domainId";
      

      API 详情here

      干杯!!

      【讨论】:

      • EAGER fetch 并不意味着 hibernate 将执行单个 sql 查询:它意味着所有必须急切地获取的对象将与根对象同时获取(即不一定在同一查询)
      • @ben75 - 是的,你是对的,不一定是相同的查询,但它们会同时加载。
      • 请注意,我的查询不是简单的JOIN,而是LEFT OUTER JOIN。我无法摆脱它,因为返回的实体将不正确。
      猜你喜欢
      • 2023-03-03
      • 2017-08-05
      • 2015-10-22
      • 1970-01-01
      • 2013-09-03
      • 2013-08-25
      • 2011-11-02
      • 1970-01-01
      • 2020-08-08
      相关资源
      最近更新 更多