Infelizmente, acho que a pequena diferença de você manter apenas uma tabela é o problema aqui.
Veja a declaração do
PhoneId
class (que eu sugiro que seja melhor chamada PhoneOwner
ou algo assim):@Entity
@Table(name="Phones")
public class PhoneId {
Quando você declara que uma classe é uma entidade mapeada para uma determinada tabela, você está fazendo um conjunto de asserções, das quais duas são particularmente importantes aqui. Em primeiro lugar, que existe uma linha na tabela para cada instância da entidade e vice-versa. Em segundo lugar, que existe uma coluna na tabela para cada campo escalar da entidade e vice-versa. Ambos estão no centro da ideia de mapeamento objeto-relacional.
No entanto, em seu esquema, nenhuma dessas afirmações é válida. Nos dados que você deu:
OWNER_ID TYPE NUMBER
1 home 792-0001
1 work 494-1234
2 work 892-0005
Existem duas linhas correspondentes à entidade com
owner_id
1, violando a primeira asserção. Existem colunas TYPE
e NUMBER
que não são mapeados para campos na entidade, violando a segunda asserção. (Para ser claro, não há nada de errado com sua declaração do
Phone
classe ou os phones
campo - apenas o PhoneId
entidade) Como resultado, quando seu provedor JPA tenta inserir uma instância de
PhoneId
no banco de dados, ele tem problemas. Como não há mapeamentos para o TYPE
e NUMBER
colunas em PhoneId
, ao gerar o SQL para a inserção, não inclui valores para eles. É por isso que você recebe o erro que vê - o provedor escreve INSERT INTO Phones (owner_id) VALUES (?)
, que o PostgreSQL trata como INSERT INTO Phones (owner_id, type, number) VALUES (?, null, null)
, que é rejeitado. Mesmo se você conseguisse inserir uma linha nessa tabela, teria problemas para recuperar um objeto dela. Digamos que você tenha solicitado a instância de
PhoneId
com owner_id
1. O provedor escreveria SQL equivalente a select * from Phones where owner_id = 1
, e esperaria que encontrasse exatamente uma linha, que pudesse mapear para um objeto. Mas ele vai encontrar duas linhas! A solução, receio, é usar duas tabelas, uma para
PhoneId
, e um para Phone
. A tabela para PhoneId
será trivialmente simples, mas é necessário para o correto funcionamento do maquinário JPA. Supondo que você renomeie
PhoneId
para PhoneOwner
, as tabelas precisam ser parecidas com: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)
)
(Eu fiz
(owner_id, number)
a chave primária para Phone
, na suposição de que um proprietário pode ter mais de um número de um determinado tipo, mas nunca terá um número registrado em dois tipos. Você pode preferir (owner_id, type)
se isso refletir melhor seu domínio.) As entidades são então:
@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;
}
Agora, se você realmente não quer introduzir uma tabela para o
PhoneOwner
, então você poderá sair dele usando uma view. Assim:create view PhoneOwner as select distinct owner_id from Phone;
Até onde o provedor JPA pode dizer, esta é uma tabela e suportará as consultas necessárias para ler os dados.
No entanto, ele não suportará inserções. Se você já precisou adicionar um telefone para um proprietário que não está no banco de dados, você precisa dar a volta e inserir uma linha diretamente em
Phone
. Não muito legal.