Comment implémenter une table temporelle en utilisant JPA?
Je voudrais savoir comment implémenter tables temporelles dans JPA 2 avec EclipseLink. Par temporel, je veux dire les tables qui définissent la période de validité.
Un problème auquel je suis confronté est que les tables de référencement ne peuvent plus avoir de contraintes de clés étrangères aux tables référencées (tables temporelles) en raison de la nature des tables référencées qui maintenant leurs clés primaires incluent la période de validité.
- Comment cartographier les relations de mes entités?
- cela signifierait-il que mes entités ne peuvent plus avoir de relation avec ces entités à temps valide?
- la responsabilité d'initialiser ces relations devrait-elle maintenant être faite manuellement dans une sorte de Service ou de DAO spécialisé?
La seule chose que j'ai trouvée est un framework appelé Dao Fusion qui traite de cela.
- y a-t-il d'autres moyens de résoudre ce problème?
- pourriez-vous fournir un exemple ou des ressources sur ce sujet (JPA avec temporelles des bases de données)?
Voici un exemple fictif d'un modèle de données et de ses classes. Il commence comme un modèle simple qui n'a pas à traiter des aspects temporels:
1er scénario: modèle Non temporel
Modèle De Données:
L'Équipe de:
@Entity
public class Team implements Serializable {
private Long id;
private String name;
private Integer wins = 0;
private Integer losses = 0;
private Integer draws = 0;
private List<Player> players = new ArrayList<Player>();
public Team() {
}
public Team(String name) {
this.name = name;
}
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQTEAMID")
@SequenceGenerator(name="SEQTEAMID", sequenceName="SEQTEAMID", allocationSize=1)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(unique=true, nullable=false)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getWins() {
return wins;
}
public void setWins(Integer wins) {
this.wins = wins;
}
public Integer getLosses() {
return losses;
}
public void setLosses(Integer losses) {
this.losses = losses;
}
public Integer getDraws() {
return draws;
}
public void setDraws(Integer draws) {
this.draws = draws;
}
@OneToMany(mappedBy="team", cascade=CascadeType.ALL)
public List<Player> getPlayers() {
return players;
}
public void setPlayers(List<Player> players) {
this.players = players;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Team other = (Team) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
Joueur:
@Entity
@Table(uniqueConstraints={@UniqueConstraint(columnNames={"team_id","number"})})
public class Player implements Serializable {
private Long id;
private Team team;
private Integer number;
private String name;
public Player() {
}
public Player(Team team, Integer number) {
this.team = team;
this.number = number;
}
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQPLAYERID")
@SequenceGenerator(name="SEQPLAYERID", sequenceName="SEQPLAYERID", allocationSize=1)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@ManyToOne
@JoinColumn(nullable=false)
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
@Column(nullable=false)
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
@Column(unique=true, nullable=false)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((number == null) ? 0 : number.hashCode());
result = prime * result + ((team == null) ? 0 : team.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Player other = (Player) obj;
if (number == null) {
if (other.number != null)
return false;
} else if (!number.equals(other.number))
return false;
if (team == null) {
if (other.team != null)
return false;
} else if (!team.equals(other.team))
return false;
return true;
}
}
Classe D'essai:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/META-INF/application-context-root.xml"})
@Transactional
public class TestingDao {
@PersistenceContext
private EntityManager entityManager;
private Team team;
@Before
public void setUp() {
team = new Team();
team.setName("The Goods");
team.setLosses(0);
team.setWins(0);
team.setDraws(0);
Player player = new Player();
player.setTeam(team);
player.setNumber(1);
player.setName("Alfredo");
team.getPlayers().add(player);
player = new Player();
player.setTeam(team);
player.setNumber(2);
player.setName("Jorge");
team.getPlayers().add(player);
entityManager.persist(team);
entityManager.flush();
}
@Test
public void testPersistence() {
String strQuery = "select t from Team t where t.name = :name";
TypedQuery<Team> query = entityManager.createQuery(strQuery, Team.class);
query.setParameter("name", team.getName());
Team persistedTeam = query.getSingleResult();
assertEquals(2, persistedTeam.getPlayers().size());
//Change the player number
Player p = null;
for (Player player : persistedTeam.getPlayers()) {
if (player.getName().equals("Alfredo")) {
p = player;
break;
}
}
p.setNumber(10);
}
}
Maintenant, vous êtes invité à garder un historique de la façon dont l'équipe et le joueur était sur un certain point de temps donc ce que vous devez faire est d'ajouter une période de temps pour chaque table qui veut être suivis. Ajoutons donc ces colonnes temporelles. Nous allons commencer avec juste Player
.
2ème scénario: modèle temporel
Modèle De Données:
Comme vous pouvez le voir, nous avons dû supprimer la clé primaire et en définir une autre qui inclut les dates (période). Nous avons également dû abandonner les contraintes uniques car maintenant elles peuvent être répétées dans le tableau. Maintenant, la table peut contenir le entrées actuelles et aussi de l'histoire.
Les choses deviennent assez moches si nous devons aussi rendre L'équipe temporelle, dans ce cas, nous devrions abandonner la contrainte de clé étrangère que Player
table doit Team
. Le problème est de savoir comment modéliser cela en Java et JPA.
Notez que ID est une clé de substitution. Mais maintenant, les clés de substitution doivent inclure la date car si elles ne le font pas, cela ne permettrait pas de stocker plus d'une "version" de la même entité (pendant la timeline).
4 réponses
Je suis très intéressé par ce sujet. Je travaille depuis plusieurs années maintenant dans le développement d'applications qui utilisent ces modèles, l'idée est venue dans notre cas d'une thèse de diplôme allemand.
Je ne connaissais pas les frameworks "Dao Fusion", ils fournissent des informations et des liens intéressants, merci de fournir cette information. En particulier, la page pattern et la page aspects sont géniales!
À vos questions: Non, Je ne peux pas signaler d'autres sites, exemples ou Framework. J'ai peur que vous deviez utiliser le framework Dao Fusion ou implémenter cette fonctionnalité par vous-même. Vous devez distinguer le type de fonctionnalité dont vous avez vraiment besoin. Pour parler en termes de cadre "Dao Fusion": avez-vous besoin à la fois de "Temporal valide" et de "Temporal record"? Enregistrez les états temporels lorsque le changement s'applique à votre base de données (généralement utilisé pour les problèmes d'audit), les états temporels valides lorsque le changement s'est produit dans la vie réelle ou est valide dans la vie réelle (utilisé par le application) qui peuvent différer de l'enregistrement temporel. Dans la plupart des cas, une dimension suffit et la seconde n'est pas nécessaire.
Quoi qu'il en soit, la fonctionnalité temporelle a des impacts sur votre base de données. Comme vous l'avez dit: "qui maintenant leurs clés primaires incluent la période de validité" . Alors, comment modélisez-vous l'identité d'une entité? Je préfère l'utilisation de clés de substitution. Dans ce cas, cela signifie:
- un identifiant pour l'entité
- un identifiant pour l'objet dans le base de données (la ligne)
- les colonnes temporelles
La clé primaire de la table est l'id d'objet. Chaque entité a une ou plusieurs entrées (1-n) dans une table, identifiées par l'id d'objet. La liaison entre les tables est basée sur l'ID d'entité. Étant donné que les entrées temporelles multiplient la quantité de données, les relations standard ne fonctionnent pas. Une relation 1-N standard peut devenir une relation x*1-y*N.
Comment résolvez-vous cela? L'approche standard consisterait à introduire une cartographie tableau, mais ce n'est pas naturellement approche. Juste pour éditer une table (par exemple. un changement de résidence se produit) vous devrez également mettre à jour / insérer la table de mappage, ce qui est étrange pour chaque programmeur.
L'autre approche consisterait à ne pas utiliser de table de mappage. Dans ce cas, vous ne pouvez pas utiliser l'intégrité référentielle et les clés étrangères, chaque table agit isolée, la liaison d'une table aux autres doit être implémentée manuellement et non avec la fonctionnalité JPA.
La fonctionnalité de l'initialisation des objets de base de données doit se faire dans les objets (comme dans le cadre de Fusion Dao). Je ne le mettrais pas dans un service. Si vous le donnez dans un DAO ou utilisez le modèle D'enregistrement actif est à vous.
Je suis conscient que ma réponse ne vous fournit pas un cadre" prêt à l'emploi". Vous êtes dans une zone très compliquée, de mes ressources d'expérience à ce scénario d'utilisation sont très difficiles à trouver. Merci pour votre question! Mais de toute façon j'espère que je vous ai aidé dans votre conception.
Dans ce réponse vous trouverez le livre de référence "Developing Time-Oriented Database Applications in SQL", voir https://stackoverflow.com/a/800516/734687
Mise À Jour: Exemple
- Question: disons que j'ai une table PERSON qui a une clé de substitution qui est un champ nommé "id". Chaque table de référence à ce stade aura cet "ID" comme contrainte de clé étrangère. Si j'ajoute des colonnes temporelles maintenant, je dois changer la clé primaire en "id + from_date + to_date". Avant de changer la clé primaire, je devrais d'abord supprimer toutes les contraintes étrangères de chaque table de référence à la table référencée (Person). Suis-je le droit? Je crois que c'est ce que vous voulez dire avec la clé de substitution. ID est une clé générée qui pourrait être générée par une séquence. La clé métier de la table Person est le SSN.
- Réponse: pas exactement. SSN serait une clé naturelle, que je n'utilise pas pour l'identité objcet. Aussi "id + from_date + to_date"serait une clé composite , qui Je voudrais aussi éviter. Si vous regardez l'exemple , vous auriez deux tables, person Et residence et pour notre exemple, disons que nous avons une relation 1-N avec une résidence de clé étrangère. Maintenant, nous ajoutons des champs temporels sur chaque table. Oui, nous laissons tomber chaque contrainte de clé étrangère. Person obtiendra 2 Id, un ID pour identifier la ligne (appelez-le ROW_ID), un ID pour identifier la personne elle-même (appelez-le ENTIDY_ID) avec un index sur cet id. Même chose pour la personne. Bien sûr, votre approche fonctionnerait aussi, mais en cela cas où vous auriez des opérations qui changent le ROW_ID (lorsque vous fermez un intervalle de temps), que j'éviterais.
Pour étendre l'exemple implémenté avec les hypothèses ci-dessus (2 tableaux, 1-n):
-
Une requête pour afficher toutes les entrées dans la base de données (toutes les informations de validité et record - aka technical - information inclus):
SELECT * FROM Person p, Residence r WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON // JOIN
-
Une requête pour masquer l'enregistrement-aka technical-information. Cela montre tous les validy-changements de la entité.
SELECT * FROM Person p, Residence r WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON AND p.recordTo=[infinity] and r.recordTo=[infinity] // only current technical state
-
Une requête pour afficher les valeurs réelles.
SELECT * FROM Person p, Residence r WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON AND p.recordTo=[infinity] and r.recordTo=[infinity] AND p.validFrom <= [now] AND p.validTo > [now] AND // only current valid state person r.validFrom <= [now] AND r.validTo > [now] // only current valid state residence
Comme vous pouvez le voir, je n'utilise jamais le ROW_ID. Remplacez [maintenant] par un horodatage pour remonter dans le temps.
Mise à Jour afin de refléter la mise à jour
Je recommande le modèle de données suivant:
Introduire une table "PlaysInTeam":
- ID
- équipe ID (clé étrangère à l'Équipe)
- Lecteur ID (clé étrangère à le joueur)
- ValidFrom
- ValidTo
Lorsque vous listez les joueurs d'une équipe, vous devez interroger avec la date pour laquelle la relation est valide et doit être dans [ValdFrom, ValidTo)
Pour rendre l'équipe temporelle, j'ai deux approches;
Approche 1: Introduire une table "saison" qui modélise une validité pour une saison
- ID
- Nom de la saison (par exemple. Été 2011)
- De (peut-être pas nécessaire, parce que tout le monde sait quand la saison est)
- À (peut-être pas nécessaire, parce que tout le monde sait quand la saison est)
Diviser la table d'équipe. Vous aurez des champs qui appartiennent à l'équipe et qui ne sont pas pertinents dans le temps (nom, adresse, ...) et les champs qui sont le temps pertinent pour une saison (victoire, perte,..). Dans ce cas, J'utiliserais Team et TeamInSeason. PlaysInTeam pourrait lier à TeamInSeason au lieu de Team (doit être considéré - je le laisserais pointer vers Équipe)
TeamInSeason
- ID
- équipe ID
- Id Saison
- Gagner
- perte
- ...
Approche 2: Ne modélisez pas explicitement la saison. Diviser la table d'équipe. Vous aurez des champs qui appartiennent à l'équipe et qui ne sont pas pertinents dans le temps (nom, adresse, ...) et les champs qui sont Temps pertinents (victoire, perte,..). Dans ce cas, J'utiliserais Team et TeamInterval. TeamInterval aurait des champs "from" et " to " pour l'intervalle. PlaysInTeam pourrait lier à TeamInterval au lieu de Team (je le laisserais sur Team)
TeamInterval
- ID
- équipe ID
- de
- À
- Gagner
- perte
- ...
Dans les deux approches: si vous n'avez pas besoin d'une table d'équipe séparée pour aucun champ pertinent, ne pas diviser.
Pas exactement ce que vous voulez dire, mais EclipseLink a un support complet pour l'histoire. Vous pouvez activer un HistoryPolicy sur un ClassDescriptor via un @ DescriptorCustomizer.
Il semble que vous ne pouvez pas le faire avec JPA car il suppose que le nom de table et le schéma entier sont statiques.
La meilleure option pourrait être de le faire via JDBC (par exemple en utilisant le modèle DAO)
Si la performance est le problème, à moins que nous ne parlions de dizaines de millions d'enregistrements, je doute que la création dynamique de classes et la compilation et le chargement soient meilleurs.
Une autre option pourrait être d'utiliser des vues (si vous devez utiliser JPA) peut être d'une manière ou d'une autre abstraite la table(mapper l'entité @(name="myView"), alors vous devrez mettre à jour/remplacer dynamiquement la vue comme dans CREATE ou REPLACE VIEW usernameView comme SELECT * FROM prefix_sessionId
Par exemple, vous pouvez écrire une vue pour dire:
if (EVENT_TYPE = 'crear_tabla' AND ObjectType = 'tabla ' && ObjectName starts with 'userName')
then CREATE OR REPLACE VIEW userNameView AS SELECT * FROM ObjectName //the generated table.
Espérons que cela aide (espero que te ayude)
Dans Dao Fusion , le suivi d'une entité dans les deux délais (intervalle de validité et d'enregistrement) est réalisé en enveloppant cette entité par BitemporalWrapper
.
La documentation de référence bitemporal présente un exemple avec l'entité Order
régulière enveloppée par l'entité BitemporalOrder
. BitemporalOrder
correspond à une table de base de données séparée, avec des colonnes pour la validité et l'intervalle d'enregistrement, et une référence de clé étrangère à Order
(via @ManyToOne
), pour chaque ligne de table.
La documentation indique également que chaque wrapper bitemporal (par exemple BitemporalOrder
) représente un élément dans la chaîne d'enregistrements bitemporal . Par conséquent, vous avez besoin d'une entité de niveau supérieur qui contient une collection de wrapper bitemporal, par exemple Customer
entity qui contient @OneToMany Collection<BitemporalOrder> orders
.
Donc, si vous avez besoin d'une entité " enfant logique "(par exemple Order
ou Player
) à suivre bitemporalement, et son entité" parent logique " (par exemple Customer
ou Team
) à suivre bitemporalement, vous devez fournir des wrappers bitemporaux pour les deux. Vous aurez BitemporalPlayer
et BitemporalTeam
. BitemporalTeam
peut déclarer @OneToMany Collection<BitemporalPlayer> players
. Mais vous avez besoin d'une entité de niveau supérieur pour contenir @OneToMany Collection<BitemporalTeam> teams
, comme mentionné ci-dessus. Pour
par exemple, vous pouvez créer une entité Game
qui contient une collection BitemporalTeam
.
Cependant, si vous n'avez pas besoin d'intervalle d'enregistrement et que vous avez juste besoin d'intervalle de validité (par exemple, pas bitemporal, mais un suivi uni-temporel de vos entités), votre meilleur pari est de lancer votre propre implémentation personnalisée.