Mapeando uma única entidade para várias tabelas na JPA
1. Introdução
JPA torna o tratamento com modelos de banco de dados relacionais de nossos aplicativos Java menos penoso. As coisas são simples quando mapeamos todas as tabelas para uma única classe de entidade. Mas, às vezes, temos motivos para modelar nossoentitiese tabelas de forma diferente:
-
Quando queremos criar grupos lógicos de campos, podemos mapearmultiple classes to a single table
-
Se a herança estiver envolvida, podemos mapeara class hierarchy to a table structure
-
Nos casos, quando os campos relacionados estão espalhados entre várias tabelas, e queremos modelar essas tabelas com uma única classe
Neste breve tutorial, veremos como lidar com esse último cenário.
2. Modelo de dados
Digamos que administremos um restaurante e desejemos armazenar dados sobre todas as refeições que servimos:
-
name
-
descrição
-
preço
-
que tipo de alérgenos contém
Uma vez que existem muitos alérgenos possíveis, vamos agrupar este conjunto de dados. Além disso, também vamos modelar isso usando as seguintes definições de tabela:
Agora vamos ver como podemos mapear essas tabelas para entidades usando anotações JPA padrão.
3. Criando várias entidades
A solução mais óbvia é criar uma entidade para ambas as classes.
Vamos começar definindo a entidadeMeal:
@Entity
@Table(name = "meal")
class Meal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "name")
String name;
@Column(name = "description")
String description;
@Column(name = "price")
BigDecimal price;
@OneToOne(mappedBy = "meal")
Allergens allergens;
// standard getters and setters
}
A seguir, adicionaremos a entidadeAllergens:
@Entity
@Table(name = "allergens")
class Allergens {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "meal_id")
Long mealId;
@OneToOne
@PrimaryKeyJoinColumn(name = "meal_id")
Meal meal;
@Column(name = "peanuts")
boolean peanuts;
@Column(name = "celery")
boolean celery;
@Column(name = "sesame_seeds")
boolean sesameSeeds;
// standard getters and setters
}
No exemplo acima, podemos ver quemeal_id é tanto a chave primária quanto a chave estrangeira. Isso significa que precisamos definir a coluna de relacionamento um para um usando@PrimaryKeyJoinColumn.
No entanto, esta solução tem dois problemas:
-
Sempre queremos armazenar alérgenos para uma refeição, e esta solução não impõe esta regra
-
Os dados da refeição e do alérgeno pertencem logicamente juntos -therefore we might want to store this information in the same Java class even though we created multiple tables for them
Uma solução possível para o primeiro problema é adicionar a anotação@NotNull ao campoallergens em nossa entidadeMeal. JPA não nos deixará persistir oMeal se tivermos umnullAllergens.
No entanto, esta não é uma solução ideal; queremos um mais restritivo, onde nem mesmo temos a oportunidade de tentar persistir umMeal semAllergens.
4. Criação de uma única entidade com@SecondaryTable
Podemos criar uma única entidade especificando que temos colunas em tabelas diferentes usando a anotação@SecondaryTable:
@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class Meal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "name")
String name;
@Column(name = "description")
String description;
@Column(name = "price")
BigDecimal price;
@Column(name = "peanuts", table = "allergens")
boolean peanuts;
@Column(name = "celery", table = "allergens")
boolean celery;
@Column(name = "sesame_seeds", table = "allergens")
boolean sesameSeeds;
// standard getters and setters
}
Nos bastidores, o JPA une a tabela principal à tabela secundária e preenche os campos. This solution is similar to the @OneToOne relationship, but this way, we can have all of the properties in the same class.
It’s important to note thatif we have a column that is in a secondary table, we have to specify it with the table argument of the @Column annotation. Se uma coluna estiver na tabela primária, podemos omitir o argumentotable, pois o JPA procura colunas na tabela primária por padrão.
Além disso, observe que podemos ter várias tabelas secundárias se as incorporarmos em@SecondaryTables. Como alternativa, no Java 8, podemos marcar a entidade com várias anotações@SecondaryTable, pois é umrepeatable annotation.
5. Combinando@SecondaryTable com@Embedded
Como vimos,@SecondaryTable mapeia várias tabelas para a mesma entidade. Também sabemos que@Embeddede @Embeddable fazem o oposto emap a single table to multiple classes.
Vamos ver o que obtemos quando combinamos@SecondaryTable com@Embeddede@Embeddable:
@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class Meal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "name")
String name;
@Column(name = "description")
String description;
@Column(name = "price")
BigDecimal price;
@Embedded
Allergens allergens;
// standard getters and setters
}
@Embeddable
class Allergens {
@Column(name = "peanuts", table = "allergens")
boolean peanuts;
@Column(name = "celery", table = "allergens")
boolean celery;
@Column(name = "sesame_seeds", table = "allergens")
boolean sesameSeeds;
// standard getters and setters
}
É uma abordagem semelhante ao que vimos usando@OneToOne. No entanto, ele tem algumas vantagens:
-
JPA gerencia as duas mesas juntas para nós, para que possamos ter certeza de que haverá uma linha para cada refeição em ambas as mesas
-
Além disso, o código é um pouco mais simples, pois precisamos de menos configuração
No entanto, essa solução semelhante a um só funciona quando as duas tabelas têm ids correspondentes.
Vale ressaltar que se quisermos reutilizar a classeAllergens, seria melhor definirmos as colunas da tabela secundária na classeMeal com@AttributeOverride.
6. Conclusão
Neste breve tutorial, vimos como podemos mapear várias tabelas para a mesma entidade usando a anotação@SecondaryTable JPA.
Também vimos as vantagens de combinar@SecondaryTable com@Embeddede@Embeddable para obter uma relação semelhante a um para um.
Como de costume, os exemplos estão disponíveisover on GitHub.