Один-ко-многим: JPA генерирует неправильный FK при использовании ddl-auto=create-drop

Кажется я где-то ошибся, но не могу понять где.

JPA настаивает на создании уникального ограничения для отношения FK of one-to-many на дочерней стороне. Это ограничение не позволяет мне вставить более одной записи в дочернюю таблицу.

Родительская организация:

@Entity
@Table(name = "customer")
@Data
public class CustomerDto {
    @Id
    private Long id;

    ...

    @Column(name = "notes")
    private String notes;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private List<EmailDto> emails = new ArrayList<>();

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private List<PhoneDto> phones = new ArrayList<>();
}

Ребенок 1:

@Entity
@Table(name = "email")
@Data
public class EmailDto {
    @Id
    private Long id;

    @Column(name = "email", unique = true, length = 128)
    private String email;

    @ManyToOne(fetch = FetchType.LAZY)
    private CustomerDto customer;
}

Ребенок 2:

@Entity
@Table(name = "phone")
@Data
public class PhoneDto {
    @Id
    private Long id;

    @Column(name = "phone_number", nullable = false, length = 32)
    private String phoneNumber;

    @OneToOne(fetch = FetchType.LAZY)
    private CustomerDto customer;
}

Таблицы выглядят так, как я ожидаю:

Но это также создает дополнительное ограничение для внешнего ключа на стороне phone:

Таблица email выглядит нормально:

Уникальное ограничение, которое Hibernate генерирует ТОЛЬКО на стороне phone, позволяет мне вставлять ОДНУ запись для каждого клиента. Если я попытаюсь вставить два номера телефона, я получу ошибку duplicate key value violates unique constraint "phone_customer_id_key".

Если я изменю определение JoinColumn на CustomerDto.java следующим образом:

@JoinColumn(name = "customer_id") ---> @JoinColumn(name = "customer_idx")

тогда это работает, и я могу правильно сохранить 0..* телефонных номеров для каждого клиента.

Но эта модификация генерирует дурацкую структуру таблицы:

Чего мне здесь не хватает?

Я могу создать правильную структуру с помощью простых SQL-запросов и использовать customer_id в качестве внешнего ключа в дочерних таблицах. Но почему JPA генерирует дополнительное неожиданное ограничение?

---------- ОБНОВЛЯТЬ ----------

Я тоже попробовал то же самое, прежде чем опубликовать этот вопрос, но результат оказался не таким, как я ожидал. Я снова изменил аннотации JPA в соответствии с предложениями и получил следующее:

JPA не устанавливает внешние ключи в дочерней таблице.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
94
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Аннотация @JoinColumn указывает столбец внешнего ключа в таблице базы данных.

Итак, вам нужно переместить аннотацию @JoinColumn из родительского dto в дочерний dto следующим образом:

Родительская организация:

@Entity
@Table(name = "customer")
@Data
public class CustomerDto {
    @Id
    private Long id;

...

@Column(name = "notes")
private String notes;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<EmailDto> emails = new ArrayList<>();

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<PhoneDto> phones = new ArrayList<>();
}

Ребенок 1:

@Entity
@Table(name = "email")
@Data
public class EmailDto {
    @Id
    private Long id;

@Column(name = "email", unique = true, length = 128)
private String email;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private CustomerDto customer;
}

Ребенок 2:

@Entity
@Table(name = "phone")
@Data
public class PhoneDto {
    @Id
    private Long id;

@Column(name = "phone_number", nullable = false, length = 32)
private String phoneNumber;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private CustomerDto customer;
}

Я предлагаю атрибут mappedByCustomerDto) для определения ссылающейся стороны (стороны, не являющейся владельцем) отношений. - Подробнее: baeldung.com/jpa-joincolumn-vs-mappedby PS. Почему Dto наращивание? Он используется для других объектов, используемых для передачи данных между разными приложениями или частями одного и того же приложения (может иметь меньше полей и любую логику/аннотации и т. д.). Сущности без Dto доб. являются базовым элементом в ORM и полностью отражают таблицу в базе данных. Подробнее: baeldung.com/java-entity-vs-dto

Andrzej Więcławski 07.07.2024 08:00

@AndrzejWięcławski твой комментарий совпадает с моим ответом

catch23 07.07.2024 12:50

@catch23 - конечно, но выше оно не используется mappedBy, интересно почему :)

Andrzej Więcławski 07.07.2024 14:39

Вы сделали довольно неудобную карту.

Чтобы сделать это правильно, лучший способ — создать сопоставление на дочерней стороне и использовать mappedBy для родительской сущности.

Несколько замечаний:

  • не нужно писать fetch = FetchType.LAZY, потому что он стоит по умолчанию.
  • неплохо было бы добавить дополнительные служебные методы для установки правильного отношения на родительской стороне.
  • вам нужен конструктор по умолчанию без аргументов для работы Hibernate
  • исключить установщики для коллекций сопоставленных сущностей
  • используйте DTO только для объектов передачи, а не для сущностей

Наконец, родительская сторона будет выглядеть так:

@Data
@Entity
@Table(name = "customer")
@NoArgsConstructor
public class Customer {
    // omitted
    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    @Setter(AccessLevel.PRIVATE)
    private List<Email> emails = new ArrayList<>();

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    @Setter(AccessLevel.PRIVATE)
    private List<Phone> phones = new ArrayList<>();

    public void addEmail(EmailDto email) {
        Objects.requireNonNull(email)
        email.setCustomer(this);
        emails.add(email);
    }
    // omitted the same logic for Phone
}

Дочерняя сторона будет выглядеть так:

@Data
@Entity
@Table(name = "email")
@NoArgsConstructor
public class Email {
    // omitted
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

Второй:

@Data
@Entity
@Table(name = "phone")
@NoArgsConstructor
public class Phone {
    // omitted
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", 
        foreignKey = @ForeignKey(name = "phone_customer_id_fk"))
    private Customer customer;
}

Во втором примере внешний ключ назван программно. В этом нет необходимости, Hibernate сгенерирует его автоматически. Но это дает больше контроля над окончательным сопоставлением на уровне БД.

На самом деле мой класс сущности имеет конструктор noarg. Насколько я знаю, если у меня нет других конструкторов, Java автоматически генерирует их. Ломбок @Data содержит @RequiredArgConstructor. У меня нет полей private final, поэтому конструктор создается автоматически. Если у меня есть частные конечные поля, мне нужна аннотация @NoArgsConstructor.

zappee 07.07.2024 17:52

Действительно, называть ny с суффиксом Dao неправильно, спасибо за подсказку.

zappee 07.07.2024 17:55

Я проверил спецификацию JPA и подтверждаю, что FetchType.LAZY является избыточным в отношениях ...ToMany, поскольку это значение по умолчанию. В соответствии с пространством это настройки по умолчанию: OneToMany: LAZY, ManyToOne: EAGER, ManyToMany: LAZY, OneToOne: EAGER.

zappee 07.07.2024 18:02
Ответ принят как подходящий

Следующие настройки предоставляют мне ожидаемую структуру БД:

@Entity
@Table(name = "customer")
@Data
public class CustomerDto {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customer-id-sequence")
    @SequenceGenerator(name = "customer-id-sequence", sequenceName = "customer_id_seq", allocationSize = 1)
    @Column(name = "id")
    private Long id;


    ...
    ...

    @Column(name = "notes")
    private String notes;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
    @JoinColumn(name = "customer_id")
    private List<EmailDto> emails = new ArrayList<>();

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
    @JoinColumn(name = "customer_id")
    private List<PhoneDto> phones = new ArrayList<>();
}

Ребенок:

@Entity
@Table(name = "email")
@Data
public class EmailDto {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "email-id-sequence")
    @SequenceGenerator(name = "email-id-sequence", sequenceName = "email_id_seq", allocationSize = 1)
    @Column(name = "id")
    private Long id;

    @Column(name = "email", unique = true, length = 128)
    private String email;

    @ManyToOne()
    private CustomerDto customer;
}

мое предложение - включите настройку show-sql и проверьте, какие реальные операторы SQL генерируются

catch23 21.07.2024 20:44

Другие вопросы по теме