Hibernate выполняет дополнительный запрос при извлечении родительской стороны однозначного сопоставления сущностей - PullRequest
2 голосов
/ 22 января 2020

У меня есть эти объекты, где FSAK_Nachricht имеет отношение @OneToOne к FNACHRICHTAKTSTATUS.

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // one-to-one association to FNachrichtAktStatus
  @OneToOne
  @JoinColumn(name = "FN_NR")
  private FNachrichtAktStatus fnachrichtAktStatus;

  // Other attributes, Getter / Setter
}  

@Entity
@Table(name = "FNACHRICHTAKTSTATUS")
public class FNachrichtAktStatus implements Serializable {
  private static final long serialVersionUID = 1L;

  @OneToOne
  @Id
  @JoinColumn(name = "FN_NR", referencedColumnName = "FN_NR")
  private FsakNachricht fsakNachricht;

  @Column(name = "FN_AKTSTATUS", length = 30)
  private String fnAktStatus;

  //  Getter / Setter

}

Что я хочу сделать, это загрузить запись из FSAK_Nachricht и получить FNACHRICHTAKTSTATUS с одним запросом. Таким образом, я написал это утверждение критерия:

CriteriaBuilder builder = this.getCriteriaBuilder();
CriteriaQuery<FsakNachricht> criteriaQuery = builder.createQuery(FsakNachricht.class);
Root<FsakNachricht> rootFsakNachricht = criteriaQuery.from(FsakNachricht.class);

Join<FsakNachricht, FNachrichtAktStatus> joinFNachrichtAktStatus = (Join<FsakNachricht, FNachrichtAktStatus>) rootFsakNachricht.fetch(
    FsakNachricht_.fnachrichtAktStatus);

// I also join some other tables    

Predicate wherePrimaryKey = builder.equal(rootFsakNachricht.get(FsakNachricht_.fnNr), primaryKey);

criteriaQuery.where(wherePrimaryKey);

List<FsakNachricht> result = this.findByCriteriaQuery(criteriaQuery);

Это приводит к следующему утверждению, которое выглядит идеально для меня - за исключением проекции, так как атрибут таблицы @OneToOne не в списке

2020-01-22T15:00:15,358 DEBUG [AThreadPool : 2] o.h.SQL:92 - 
    select
        fsaknachri0_.FN_NR as FN_NR1_11_0_,
        // Other attributes of fsaknachri0_
        // Attributs of nachrichta2_
        // attributes of user tables
        // 
        // !!!
        // But none of fnachricht1_
from
    FSAK_NACHRICHT fsaknachri0_ 
inner join
    FNACHRICHTAKTSTATUS fnachricht1_ 
        on fsaknachri0_.FN_NR=fnachricht1_.FN_NR 
inner join
    NACHRICHTART nachrichta2_ 
        on fsaknachri0_.NRART_NR=nachrichta2_.NRART_NR 
inner join
    USER user3_ 
        on fsaknachri0_.FN_U1=user3_.U_NR
left outer join
    USER user4_ 
        on fsaknachri0_.FN_U2=user4_.U_NR
left outer join
    USER user5_ 
        on fsaknachri0_.FN_U3=user5_.U_NR 
where
    fsaknachri0_.FN_NR=<KEY>

Поэтому выполняется другой запрос

2020-01-22T15:22:44,244 DEBUG [AThreadPool : 1] o.h.SQL:92 - 
    select
        fnachricht0_.FN_NR as FN_NR2_7_0_,
        fnachricht0_.FN_AKTSTATUS as FN_AKTSTATUS1_7_0_ 
    from
        FNACHRICHTAKTSTATUS fnachricht0_ 
    where
        fnachricht0_.FN_NR=?            

Как можно избежать этого дополнительного запроса и извлечь данные (столбец fnAktStatus) с помощью требуемого одиночного оператора? Еще один вопрос: относятся ли дополнительные запросы к тому случаю, почему я не могу использовать this.findByCriteriaQuerySingelResult(criteriaQuery); (при использовании Hibernate не находит ни одной строки, даже если сгенерированный запрос выдает ровно одну строку при выполнении в SQLDeveloper)

Hibernate 5.2 .18 (последняя поддерживается для JPA 2.1)

Ответы [ 3 ]

2 голосов
/ 23 января 2020

Чтобы решить эту проблему, вам нужно сделать две вещи.

Добавление @MapsId к дочерней таблице

Как я объяснил в этой статье , @MapsId - это единственный способ сопоставить истинное отношение «один к одному» :

@Entity
@Table(name = "FNACHRICHTAKTSTATUS")
public class FNachrichtAktStatus implements Serializable {
  private static final long serialVersionUID = 1L;

  @OneToOne
  @MapsId
  @JoinColumn(name = "FN_NR", referencedColumnName = "FN_NR")
  private FsakNachricht fsakNachricht;

  @Column(name = "FN_AKTSTATUS", length = 30)
  private String fnAktStatus;

  //  Getter / Setter

}

Без @MapsId, вы бы использовали отношение таблицы «один ко многим», где столбец FK имеет уникальное ограничение. Это нежелательно, так как вам придется тратить один лишний столбец.

Как избежать связи с родительской стороной

Прежде всего, на родительской стороне необходимо использовать mappedBy вот так:

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // one-to-one association to FNachrichtAktStatus
  @OneToOne(mappedBy = "fsakNachricht")
  private FNachrichtAktStatus fnachrichtAktStatus;

  // Other attributes, Getter / Setter
}  

Однако, как объяснено в этой статье , именно из-за этой связи @OneToOne со стороны родителя Hibernate генерирует дополнительный запрос, так как ему нужно с нетерпением получить ассоциацию, чтобы узнать, назначить ли атрибут для null или для прокси.

Итак, лучше удалить эту родительскую связь:

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // Other attributes, Getter / Setter
}  

Вы всегда можете выбрать дочернюю сущность через идентификатор родительской сущности:

FNachrichtAktStatus fnachrichtAktStatus = entityManager.find(
    FNachrichtAktStatus.class, 
    fsakNachricht.getFnNr()
);

Таким образом, вы будете извлекать эту связь только при необходимости, поэтому избегая проблем с N + 1 запросами. .

1 голос
/ 23 января 2020

Ваш запрос выглядит нормально, но есть несколько проблем с вашими сопоставлениями:

  1. Когда вы моделируете двунаправленную связь один к одному , вы необходимо определить ассоциацию с ее столбцом соединения на одном объекте (сторона-владелец ассоциации) и ссылаться на этот атрибут на другом объекте (сторона-указатель). Сторона-владелец отображает таблицу, которая содержит столбец внешнего ключа.
  2. Если вы хотите использовать одинаковое значение первичного ключа для обоих объектов , объект, который должен повторно использовать значение первичного ключа, должен владеть ассоциацией (сопоставить столбец внешнего ключа). Вам также нужно аннотировать его с помощью @MapsId и смоделировать атрибут первичного ключа.

Я настроил ваши сопоставления на основе предположений, что FsakNachricht определяет значение первичного ключа и что таблица FNACHRICHTAKTSTATUS должна повторно использовать это значение в качестве первичного ключа и ссылки на внешний ключ. Столбец первичного ключа в обеих таблицах называется FN_NR.

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // one-to-one association to FNachrichtAktStatus
  @OneToOne(mappedBy = "fsakNachricht")
  private FNachrichtAktStatus fnachrichtAktStatus;

  // Other attributes, Getter / Setter
}  

@Entity
@Table(name = "FNACHRICHTAKTSTATUS")
public class FNachrichtAktStatus implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @Column(name = "FN_NR")
  private long fnNr;

  @OneToOne(fetch = FetchType.LAZY)
  @MapsId
  @JoinColumn(name = "FN_NR")
  private FsakNachricht fsakNachricht;

  @Column(name = "FN_AKTSTATUS", length = 30)
  private String fnAktStatus;

  //  Getter / Setter

}

Прежде чем использовать это сопоставление, имейте в виду, что нельзя использовать отложенную загрузку для атрибута fnachrichtAktStatus в FsakNachricht с Hibernate, потому что внешний ключ отображается другой сущностью. Поэтому я рекомендую использовать однонаправленное отображение с дополнительным запросом .

0 голосов
/ 23 января 2020

Оказалось, что мое отображение было неверным. Изменение на следующее решило проблему:

@Id
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "FN_NR", referencedColumnName = "FN_NR")
private FsakNachricht fsakNachricht;

Решение, предоставленное Влад Михалча в его блоге

...