¿Cómo implementar una tabla temporal usando JPA?


Me gustaría saber cómo implementar tablas temporales en JPA 2 con EclipseLink. Por temporal me refiero a las tablas que definen el período de validez.

Un problema que estoy enfrentando es que las tablas de referencia ya no pueden tener restricciones de claves foráneas para las tablas referenciadas (tablas temporales) debido a la naturaleza de las tablas referenciadas que ahora sus claves primarias incluyen el período de validez.

  • ¿Cómo mapearía las relaciones de mis entidades?
  • ¿Significaría eso que mis entidades ya no pueden tener una relación con esas entidades de tiempo válido?
  • ¿Debería la responsabilidad de inicializar esas relaciones ahora hacerlo por mí manualmente en algún tipo de Servicio o DAO especializado?

Lo único que he encontrado es un framework llamado DAO Fusion que trata con esto.

  • ¿Hay alguna otra forma de resolver esto?
  • ¿Podría proporcionar un ejemplo o recursos sobre este tema (JPA con bases de datos temporales)?

Aquí hay un ejemplo ficticio de un modelo de datos y sus clases. Comienza como un modelo simple que no tiene que lidiar con aspectos temporales:

1er Escenario: Modelo No Temporal

Modelo de datos : Modelo de Datos No Temporales

Equipo :

@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;
    }


}

Jugador:

@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;
    }


}

Clase de Prueba:

@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);        
    }


}

Ahora se le pide que mantenga un historial de cómo el Equipo y el Jugador estaban en cierto punto de tiempo así que lo que necesita hacer es agregar un período de tiempo para cada tabla que desea ser rastreada. Así que vamos a añadir estas columnas temporales. Vamos a empezar con solo Player.

2º Escenario: Modelo Temporal

Modelo de datos: Modelo de Datos Temporales

Como puede ver, tuvimos que soltar la clave primaria y definir otra que incluya las fechas (punto). También tuvimos que eliminar las restricciones únicas porque ahora se pueden repetir en la tabla. Ahora la tabla puede contener el entradas actuales y también el historial.

Las cosas se ponen bastante feas si también tenemos que hacer que el Equipo sea temporal, en este caso necesitaríamos eliminar la restricción de clave externa que la tabla Player tiene que Team. El problema es cómo modelar eso en Java y JPA.

Tenga en cuenta que ID es una clave sustituta. Pero ahora las claves sustitutas tienen que incluir la fecha porque si no lo hacen no permitiría almacenar más de una" versión " de la misma entidad (durante la línea de tiempo).

Author: Alfredo Osorio, 2012-03-03

4 answers

Estoy muy interesado en este tema. Estoy trabajando desde hace varios años en el desarrollo de aplicaciones que utilizan estos patrones, la idea surgió en nuestro caso de una tesis de diploma alemán.

No conocía los marcos "DAO Fusion", proporcionan información y enlaces interesantes, gracias por proporcionar esta información. Especialmente el página de patrones y el página de aspectos son grandes!

A sus preguntas: no, no puedo señalar otros sitios, ejemplos o marco. Me temo que tiene que usar el marco DAO Fusion o implementar esta funcionalidad por sí mismo. Tienes que distinguir qué tipo de funcionalidad realmente necesitas. Para hablar en términos del marco "DAO Fusion": ¿necesita tanto" temporal válido "como"registro temporal"? Registrar los estados temporales cuando el cambio se aplicó a su base de datos (generalmente se usa para problemas de auditoría), los estados temporales válidos cuando el cambio ocurrió en la vida real o es válido en la vida real (utilizado por el aplicación) que puede diferir del registro temporal. En la mayoría de los casos, una dimensión es suficiente y la segunda no es necesaria.

De todos modos, la funcionalidad temporal tiene impactos en su base de datos. Como usted dijo: "que ahora sus claves principales incluyen el período de validez". Entonces, ¿cómo modelas la identidad de una entidad? Prefiero el uso de subrogate keys. En ese caso, esto significa:

  • un id para la entidad
  • un id para el objeto en el base de datos (la fila)
  • las columnas temporales

La clave principal de la tabla es el id del objeto. Cada entidad tiene una o más entradas (1-n) en una tabla, identificadas por el id del objeto. La vinculación entre tablas se basa en el id de la entidad. Dado que las entradas temporales multiplican la cantidad de datos, las relaciones estándar no funcionan. Una relación estándar 1-n podría convertirse en una relación x*1-y*n.

¿Cómo se resuelve esto? The standard approach would be to introduce a mapping tabla, pero esto no es un enfoque natural. Solo para editar una tabla(por ejemplo. se produce un cambio de residencia) también tendría que actualizar / insertar la tabla de asignación que es extraña para cada programador.

El otro enfoque sería no utilizar una tabla de asignación. En este caso no se puede usar integridad referencial y claves foráneas, cada tabla está actuando aislada, el enlace de una tabla a las otras debe implementarse manualmente y no con funcionalidad JPA.

La funcionalidad de la inicialización de objetos de base de datos debe estar dentro de los objetos (como en el marco DAO Fusion). Yo no lo pondría en un servicio. Si usted lo da en un DAO o utiliza el patrón de Registro Activo depende de usted.

Soy consciente de que mi respuesta no le proporciona un marco "listo para usar". Usted está en un área muy complicada, desde mi experiencia de recursos a este escenario de uso son muy difíciles de encontrar. Gracias por su pregunta! Pero de todos modos espero haberte ayudado en tu diseño.

En este respuesta encontrará el libro de referencia "Desarrollo de aplicaciones de Base de datos orientadas al tiempo en SQL", ver https://stackoverflow.com/a/800516/734687

Actualización: Ejemplo

  • Pregunta: Digamos que tengo una tabla de PERSONAS que tiene una clave sustituta que es un campo llamado "id". Cada tabla de referencia en este punto tendrá ese " ID " como una restricción de clave foránea. Si agrego columnas temporales ahora tengo que cambiar la clave primaria a "id + from_date + to_date". Antes de cambiar la clave primaria, primero tendría que soltar cada restricción extranjera de cada tabla de referencia a esta tabla referenciada (Persona). ¿Tengo razón? Creo que eso es lo que quieres decir con la llave sustituta. ID es una clave generada que podría ser generada por una secuencia. La clave de negocio de la tabla Person es el SSN.
  • Respuesta: No exactamente. SSN sería una clave natural, que no uso para la identidad objcet. También "id + from_date + to_date" sería una clave compuesta , que Yo también lo evitaría. Si nos fijamos en el ejemplo tendría dos tablas, persona y residencia y para nuestro ejemplo digamos que tenemos una relación 1-n con una residencia de clave extranjera. Ahora agregamos campos temporales en cada tabla. Sí, eliminamos todas las restricciones de clave externa. Person obtendrá 2 IDs, un ID para identificar la fila (llámelo ROW_ID), un ID para identificar a la persona misma (llámelo ENTIDY_ID) con un índice en ese id. Mismo para la persona. Por supuesto, su enfoque también funcionaría, pero en eso en caso de que tenga operaciones que cambian el ROW_ID (cuando se cierra un intervalo de tiempo), que yo evitaría.

Para ampliar el ejemplo implementado con los supuestos anteriores (2 tablas, 1-n):

  • Una consulta para mostrar todas las entradas en la base de datos (toda la información de validez y registro - aka información técnica incluida):

    SELECT * FROM Person p, Residence r
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON          // JOIN 
  • Una consulta para ocultar el registro - también conocido como información técnica. Esto muestra todos los validy-Cambios de la entidad.

    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
  • Una consulta para mostrar los valores reales.

    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

Como puedes ver nunca uso el ROW_ID. Reemplace [ahora] con una marca de tiempo para retroceder en el tiempo.

Actualizar para reflejar su actualización
Yo recomendaría el siguiente modelo de datos:

Introduce una tabla "PlaysInTeam":

  • ID
  • ID Team (clave foránea para el equipo)
  • ID Player (clave foránea para jugador)
  • ValidFrom
  • Valido

Cuando listamos a los jugadores de un equipo tenemos que consultar con la fecha para la que la relación es válida y tiene que estar en [ValdFrom, Valido)

Para hacer equipo temporal tengo dos enfoques;

Enfoque 1: Introducir una tabla de "Temporada" que modela una validez para una temporada

  • ID
  • Nombre de la temporada (eg. Verano de 2011)
  • De (tal vez no es necesario, porque cada uno sabe cuando la temporada is)
  • A (tal vez no es necesario, porque cada uno sabe cuándo es la temporada)

Divida la tabla de equipos. Tendrá campos que pertenecen al equipo y que no son relevantes para el tiempo (nombre, dirección,...) y los campos que son relevantes para una temporada (ganar, perder, ..). En ese caso usaría Team y TeamInSeason. PlaysInTeam podría enlazar a TeamInSeason en lugar de Equipo (tiene que ser considerado - me gustaría dejar que apunta a Equipo)

Temporada de equipo

  • ID
  • Equipo de identificación
  • ID Temporada
  • Win
  • Pérdida
  • ...

Enfoque 2: No modele la temporada explícitamente. Divida la mesa del equipo. Tendrá campos que pertenecen al equipo y que no son relevantes para el tiempo (nombre, dirección,...) y los campos que son relevantes para el tiempo (ganar, perder,..). En ese caso usaría Team y TeamInterval. TeamInterval tendría campos "desde" y "hasta" para el intervalo. PlaysInTeam podría enlazar a TeamInterval en lugar de Team (lo dejaría en Team)

TeamInterval

  • ID
  • Equipo de identificación
  • De
  • A
  • Win
  • Pérdida
  • ...

En ambos enfoques: si no necesita una tabla de equipo separada para un campo sin tiempo relevante, no divida.

 7
Author: ChrLipp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:29:46

No estoy exactamente seguro de lo que quieres decir, pero EclipseLink tiene soporte completo para la historia. Puede habilitar un HistoryPolicy en un ClassDescriptor a través de un @DescriptorCustomizer.

 2
Author: James,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-06 10:41:44

Parece que no puede hacerlo con JPA ya que asume que el nombre de la tabla y todo el esquema son estáticos.

La mejor opción podría ser hacerlo a través de JDBC (por ejemplo usando el patrón DAO)

Si el rendimiento es el problema, a menos que estemos hablando de decenas de millones de registros, dudo que crear clases dinámicamente y compilarlo y luego cargarlo sea mejor.

Otra opción podría ser usar vistas (Si debe usar JPA) puede ser abstraer de alguna manera la tabla (mapea la Entidad @(name = "MyView"), entonces tendrás que actualizar/reemplazar dinámicamente la vista como en CREATE OR REPLACE VIEW usernameView COMO SELECT * FROM prefix_sessionId

Por ejemplo, podría escribir una vista para decir:

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.

Espero que te ayude (espero que te ayude)

 1
Author: Alex M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-03 19:43:24

En DAO Fusion, el seguimiento de una entidad en ambas líneas de tiempo (validez e intervalo de registro) se realiza envolviendo esa entidad por BitemporalWrapper.

La documentación de referencia bitemporal presenta un ejemplo con la entidad regular Order envuelta por la entidad BitemporalOrder. BitemporalOrder se asigna a una tabla de base de datos separada, con columnas para la validez y el intervalo de registro, y la referencia de clave externa a Order (a través de @ManyToOne), para cada fila de la tabla.

La documentación también indica que cada envoltura bitemporal (por ejemplo, BitemporalOrder) representa un elemento dentro de la cadena de registros bitemporales. Por lo tanto, necesita alguna entidad de nivel superior que contenga la colección de envoltorios bitemporales, por ejemplo, Customer entidad que contenga @OneToMany Collection<BitemporalOrder> orders.

Por lo tanto, si necesita una entidad "hija lógica" (por ejemplo, Order o Player) para ser rastreada bitemporalmente, y su entidad "padre lógico" (por ejemplo, Customer o Team) para ser rastreada bitemporalmente también, debe proporcionar envoltorios bitemporales para ambos. Que vosotros hayaís BitemporalPlayer y BitemporalTeam. BitemporalTeam puede declarar @OneToMany Collection<BitemporalPlayer> players. Pero necesita alguna entidad de nivel superior para contener @OneToMany Collection<BitemporalTeam> teams, como se mencionó anteriormente. Para por ejemplo, podría crear una entidad Game que contenga la colección BitemporalTeam.

Sin embargo, si no necesita intervalo de registro y solo necesita intervalo de validez (por ejemplo, no bitemporal, sino uni-temporal de seguimiento de sus entidades), su mejor opción es rodar su propia implementación personalizada.

 1
Author: Vojtech Szocs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-01 14:03:41