Кажется я где-то ошибся, но не могу понять где.
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 не устанавливает внешние ключи в дочерней таблице.




Аннотация @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;
}
@AndrzejWięcławski твой комментарий совпадает с моим ответом
@catch23 - конечно, но выше оно не используется mappedBy, интересно почему :)
Вы сделали довольно неудобную карту.
Чтобы сделать это правильно, лучший способ — создать сопоставление на дочерней стороне и использовать mappedBy для родительской сущности.
Несколько замечаний:
fetch = FetchType.LAZY, потому что он стоит по умолчанию.Наконец, родительская сторона будет выглядеть так:
@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.
Действительно, называть ny с суффиксом Dao неправильно, спасибо за подсказку.
Я проверил спецификацию JPA и подтверждаю, что FetchType.LAZY является избыточным в отношениях ...ToMany, поскольку это значение по умолчанию. В соответствии с пространством это настройки по умолчанию: OneToMany: LAZY, ManyToOne: EAGER, ManyToMany: LAZY, OneToOne: EAGER.
Следующие настройки предоставляют мне ожидаемую структуру БД:
@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 генерируются
Я предлагаю атрибут
mappedBy(вCustomerDto) для определения ссылающейся стороны (стороны, не являющейся владельцем) отношений. - Подробнее: baeldung.com/jpa-joincolumn-vs-mappedby PS. ПочемуDtoнаращивание? Он используется для других объектов, используемых для передачи данных между разными приложениями или частями одного и того же приложения (может иметь меньше полей и любую логику/аннотации и т. д.). Сущности безDtoдоб. являются базовым элементом в ORM и полностью отражают таблицу в базе данных. Подробнее: baeldung.com/java-entity-vs-dto