Malheureusement, je pense que la légère différence que vous ne gardez qu'une seule table est le problème ici.
Regardez la déclaration du PhoneId
classe (qui, je dirais, s'appelle mieux PhoneOwner
ou quelque chose comme ça):
@Entity
@Table(name="Phones")
public class PhoneId {
Lorsque vous déclarez qu'une classe est une entité mappée à une certaine table, vous faites un ensemble d'assertions, dont deux sont particulièrement importantes ici. Premièrement, qu'il y a une ligne dans le tableau pour chaque instance de l'entité, et vice versa. Deuxièmement, qu'il y a une colonne dans la table pour chaque champ scalaire de l'entité, et vice versa. Ces deux éléments sont au cœur de l'idée du mappage objet-relationnel.
Cependant, dans votre schéma, aucune de ces affirmations ne tient. Dans les données que vous avez fournies :
OWNER_ID TYPE NUMBER
1 home 792-0001
1 work 494-1234
2 work 892-0005
Il y a deux lignes correspondant à l'entité avec owner_id
1, en violation de la première assertion. Il y a des colonnes TYPE
et NUMBER
qui ne sont pas mappés aux champs de l'entité, violant la deuxième assertion.
(Pour être clair, il n'y a rien de mal avec votre déclaration du Phone
classe ou les phones
champ - juste le PhoneId
entité)
Par conséquent, lorsque votre fournisseur JPA essaie d'insérer une instance de PhoneId
dans la base de données, il rencontre des problèmes. Parce qu'il n'y a pas de mappages pour le TYPE
et NUMBER
colonnes dans PhoneId
, lorsqu'il génère le SQL pour l'insertion, il n'inclut pas de valeurs pour celles-ci. C'est pourquoi vous obtenez l'erreur que vous voyez - le fournisseur écrit INSERT INTO Phones (owner_id) VALUES (?)
, que PostgreSQL traite comme INSERT INTO Phones (owner_id, type, number) VALUES (?, null, null)
, qui est rejeté.
Même si vous réussissiez à insérer une ligne dans cette table, vous auriez alors des problèmes pour en récupérer un objet. Disons que vous avez demandé l'instance de PhoneId
avec owner_id
1. Le fournisseur écrirait SQL équivalant à select * from Phones where owner_id = 1
, et il s'attendrait à ce qu'il trouve exactement une ligne, qu'il peut mapper à un objet. Mais il trouvera deux lignes !
La solution, j'ai bien peur, est d'utiliser deux tables, une pour PhoneId
, et un pour Phone
. Le tableau pour PhoneId
sera trivialement simple, mais nécessaire au bon fonctionnement de la machinerie JPA.
En supposant que vous renommez PhoneId
à PhoneOwner
, les tableaux doivent ressembler à :
create table PhoneOwner (
owner_id integer primary key
)
create table Phone (
owner_id integer not null references PhoneOwner,
type varchar(255) not null,
number varchar(255) not null,
primary key (owner_id, number)
)
(J'ai créé (owner_id, number)
la clé primaire pour Phone
, en supposant qu'un propriétaire peut avoir plus d'un numéro d'un type donné, mais n'aura jamais un numéro enregistré sous deux types. Vous préférerez peut-être (owner_id, type)
si cela correspond mieux à votre domaine.)
Les entités sont alors :
@Entity
@Table(name="PhoneOwner")
public class PhoneOwner {
@Id
@Column(name="owner_id")
long id;
@ElementCollection
@CollectionTable(name = "Phone", joinColumns = @JoinColumn(name = "owner_id"))
List<Phone> phones = new ArrayList<Phone>();
}
@Embeddable
class Phone {
@Column(name="type", nullable = false)
String type;
@Column(name="number", nullable = false)
String number;
}
Maintenant, si vous ne voulez vraiment pas introduire de table pour le PhoneOwner
, vous pourrez peut-être vous en sortir en utilisant une vue. Comme ceci :
create view PhoneOwner as select distinct owner_id from Phone;
Pour autant que le fournisseur JPA puisse le dire, il s'agit d'un tableau, et il prendra en charge les requêtes qu'il doit effectuer pour lire les données.
Cependant, il ne prend pas en charge les insertions. Si jamais vous aviez besoin d'ajouter un téléphone pour un propriétaire qui n'est pas actuellement dans la base de données, vous auriez besoin de faire le tour du dos et d'insérer une ligne directement dans Phone
. Pas très sympa.