2012-03-03 13 views
23

Chciałbym wiedzieć, jak wdrożyć temporal tables w JPA 2 z EclipseLink. Przez czasowy mam na myśli tabele, które definiują okres ważności.Jak wdrożyć tabelę czasową używając WZP?

Jednym z problemów, przed którym stoję, jest to, że tabele odwołań nie mogą już zawierać ograniczeń dotyczących kluczy obcych do przywoływanych tabel (tabel tymczasowych) ze względu na charakter tabel przywoływanych, które obecnie zawierają klucze podstawowe z okresem ważności.

  • Jak mam odwzorować relacje moich podmiotów?
  • Czy to oznacza, że ​​moje podmioty nie mogą już mieć związek z tymi podmiotami ważne w czasie?
  • W przypadku odpowiedzialność ponosi zainicjować te relacje teraz zrobić przeze mnie ręcznie w jakiejś usługi lub specjalistycznej DAO?

Jedyne, co znalazłem, to framework o nazwie DAO Fusion, który zajmuje się tym.

  • Czy istnieją inne sposoby rozwiązania tego problemu?
  • Czy możesz podać przykład lub zasoby na ten temat (WZP z tymczasowymi bazami danych)?

Oto fikcyjny przykład modelu danych i jego klas. Zaczyna jako prostego modelu, który nie ma do czynienia z aspektów czasowych:

1-ty Scenariusz: Non Temporal model

Data Model: Non Temporal Data Model

zespołu:

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


} 

gracza: Klasa

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


} 

Test:

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


} 

Teraz zostaniesz poproszony, aby mieć historię, jak zespół i odtwarzacz był w pewnym momencie tak, co trzeba zrobić, to dodać okres czasu dla każdej tabeli, która chce być śledzona. Dodajmy więc te tymczasowe kolumny. Zaczniemy od zaledwie Player.

2-ty Scenariusz: Temporal model

Data Model: Temporal Data Model

Jak widać musieliśmy usunąć klucz podstawowy i określić inny, który zawiera daty (kropka). Musieliśmy także zrezygnować z unikalnych ograniczeń, ponieważ teraz można je powtórzyć w tabeli. Teraz tabela może zawierać bieżące wpisy, a także historię.

Rzeczy stają się bardzo brzydkie, jeśli musimy również sprawić, aby zespół był tymczasowy, w tym przypadku musielibyśmy usunąć ograniczenie klucza obcego, które ma Player dla Team. Problem polega na tym, jak można to modelować w Javie i JPA.

Należy zauważyć, że identyfikator jest kluczem zastępczym. Ale teraz klucze zastępcze muszą zawierać datę, ponieważ jeśli nie, to nie pozwoliłyby na przechowywanie więcej niż jednego "wersji" tego samego obiektu (na osi czasu).

+0

1) z jakim narzędziem narysowałeś diagram? 2) jeden wymiar czasowy jest wystarczający dla twoich wymagań, wzory DAOFusion, a także moja odpowiedź (w oparciu o te wzory) są przesadzone według mojej opinii 3) Wolisz rozwiązanie, które po prostu dodaje aspekt czasowy do Gracza lub wolisz? do obu tabel 4) twój ostatni akapit jest nieprawidłowy. Zastępczy klucz nigdy nie będzie zawierał dodatkowych pól. W takim przypadku masz dwa klucze zastępcze. – ChrLipp

+0

@ChrLipp 1) Sparx Enterprise Architect 2) Zgadzam się. 3) Potrzebuję rozwiązania, które doda czas do obu tabel. 4) Nie zgadzam się, że to nie jest klucz zastępczy. Myślę, że jest to klucz zastępczy, ponieważ: 1. Przed dodaniem tymczasowych kolumn był to klucz zastępczy, który jest kluczem bez znaczenia biznesowego.Na przykład klucz biznesowy Gracza to "team_id" i "numer", a od Team to "name". Oba mają własny klucz zastępczy "id", gdy nie mają kolumn czasowych. Problem polega na tym, że kiedy dodaję kolumny tymczasowe, które już nie działają. Ten sam wpis może pojawić się więcej niż jeden raz w tej samej tabeli. –

+0

To dlatego zastępczy klucz "id" sam w sobie nie może już być tylko jedną kolumną, ponieważ jest to ten sam wpis, ale śledzony w różnych ramach czasowych, więc aby pozwolić, aby ten sam wpis pojawił się więcej niż jeden raz, mógłbym dodać następujące jako klucz podstawowy "id + validstart" lub "id + validend" lub "id + validstart + validend". Wybrałem ostatnią opcję dla wygody w Java Mappings, gdzie mam obiekt "Interval", który definiuje kropkę, więc aby odwzorować to w JPA, dodałem, że "Interval" do Id jako EmbeddedId. –

Odpowiedz

7

Jestem bardzo zainteresowany i w tym temacie. Pracuję już od kilku lat nad rozwojem aplikacji wykorzystujących te wzorce, pomysł przyszedł w naszym przypadku z niemieckiej dyplomowej pracy dyplomowej.

Nie znałem frameworków "DAO Fusion", dostarczają one ciekawych informacji i linków, dzięki za udostępnienie tych informacji. Szczególnie pattern page i aspects page są świetne!

Na twoje pytania: nie, nie mogę wskazać innych witryn, przykładów lub frameworków. Obawiam się, że musisz użyć frameworka DAO Fusion lub samodzielnie wdrożyć tę funkcjonalność. Musisz rozróżnić, jakiego rodzaju funkcjonalności naprawdę potrzebujesz. Mówienie w kategoriach frameworku DAO Fusion: czy potrzebujesz zarówno "valid temporal" i "record temporal"? Zapisuj stany temporalne, gdy zmiana dotyczy bazy danych (zwykle używana w przypadku problemów z audytem), ważne stany temporalne, gdy zmiana nastąpiła w rzeczywistości lub jest prawdziwa (używana przez aplikację), która może różnić się od rekordowej daty. W większości przypadków jeden wymiar jest wystarczający, a drugi wymiar nie jest potrzebny.

W każdym razie funkcja czasowa ma wpływ na bazę danych. Jak napisałeś: ", który teraz ich główne klucze zawierają okres ważności". Jak więc modelujesz tożsamość podmiotu? Wolę korzystanie z surrogate keys. W tym przypadku oznacza to:

  • jeden identyfikator dla podmiotu
  • jeden identyfikator obiektu w bazie danych (wiersz)
  • kolumny czasowe

Klucz podstawowy dla tabeli jest identyfikatorem obiektu.Każda encja ma jeden lub więcej (1-n) wpisów w tabeli, identyfikowanych przez identyfikator obiektu. Łączenie tabel jest oparte na identyfikatorze jednostki. Ponieważ wpisy czasowe mnożą ilość danych, standardowe relacje nie działają. Standardowa relacja 1-n może stać się relacją x * 1-y * n.

Jak rozwiązać ten problem? Standardowe podejście polegałoby na wprowadzeniu tabeli mapowania, ale nie jest to podejście naturalne. Tylko do edycji jednej tabeli (np. Zmiana miejsca zamieszkania) trzeba również zaktualizować/wstawić tablicę odwzorowania, która jest dziwna dla każdego programisty.

Inne podejście polegałoby na tym, że nie należy używać tabeli odwzorowań. W takim przypadku nie można używać integralności referencyjnej i kluczy obcych, każda tabela działa w izolacji, połączenie z jednej tabeli do drugiej musi być realizowane ręcznie, a nie z funkcją JPA.

Funkcjonalność inicjowania obiektów bazy danych powinna należeć do obiektów (tak jak w przypadku struktury DAO Fusion). Nie wstawiłbym tego do usługi. Jeśli nadasz je DAO lub używasz wzoru Active Record, zależy to od Ciebie.

Mam świadomość, że moja odpowiedź nie zapewnia ram "gotowych do użycia". Znajdujesz się w bardzo skomplikowanym obszarze, od moich doświadczeń z zasobami po ten scenariusz użycia jest bardzo trudny do znalezienia. Dzięki za twoje pytanie! Ale w każdym razie mam nadzieję, że pomogłem ci w twoim projekcie.

W tej odpowiedzi znajdziecie książce "Rozwój Czas-Oriented Database Applications w SQL", patrz https://stackoverflow.com/a/800516/734687

Aktualizacja: Przykład

  • Pytanie: Załóżmy, że mam Tabela PERSON, która ma klucz zastępczy, czyli pole o nazwie "id". Każda tabela odwołań w tym miejscu będzie miała "ID" jako ograniczenie klucza obcego. Jeśli dodaję teraz kolumny tymczasowe, muszę zmienić klucz podstawowy na "id + od_daty + na_danie". Przed zmianą klucza podstawowego musiałbym najpierw usunąć każde obce ograniczenie z każdej tabeli odwołań do tej tabeli z odniesieniami (Osoba). Czy mam rację? Wierzę, że to właśnie masz na myśli z kluczem zastępczym. ID to wygenerowany klucz, który można wygenerować za pomocą sekwencji. Klucz biznesowy tabeli Person to numer SSN.
  • Odpowiedź: Niezupełnie. SSN byłby naturalnym kluczem, którego nie używam dla tożsamości celu. Również "id + from_date + to_date" będzie numerem composite key, którego również będę unikać. Jeśli spojrzysz na numer example, będziesz miał dwie tabele, osobę i miejsce zamieszkania, a nasz przykład mówi, że mamy relację 1-n z rezydencją klucza obcego. Teraz dodajemy pola czasowe do każdej tabeli. Tak, usuwamy każde ograniczenie klucza obcego. Osoba otrzyma 2 identyfikatory, jeden identyfikator do zidentyfikowania wiersza (wywołaj go ROW_ID), jeden identyfikator do zidentyfikowania osoby (wywołanie jej ENTIDY_ID) z indeksem dla tego identyfikatora. To samo dla osoby. Oczywiście twoje podejście też by działało, ale w takim przypadku będziesz miał operacje, które zmieniają ROW_ID (kiedy zamykasz przedział czasu), czego chciałbym uniknąć.

Aby przedłużyć example realizowany z założeniami powyższych (2 stoły, 1-n):

  • zapytania, aby pokazać wszystkie wpisy w bazie danych (wszystkie informacje ważności i rekord - aka techniczną - informacje zawarte):

    SELECT * FROM Person p, Residence r 
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON   // JOIN
  • zapytanie do ukrycia rekord - vel technicznej - informacje. To pokazuje wszystkie validy-Zmiany jednostek.

    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
  • zapytanie pokazujące rzeczywiste wartości.

    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

Jak widać nigdy używać ROW_ID. Zastąp [teraz] znacznikiem czasu, aby cofnąć się w czasie.

Aktualizacja odzwierciedlać aktualizacja
Polecam następujący model danych:

wprowadzić "PlaysInTeam" tabeli:

  • ID
  • Identyfikator zespołu (klucz obcy do zespołu)
  • ID Player (klucz obcy dla odtwarzacza)
  • ValidFrom
  • validTo

Kiedy listy graczy zespołu trzeba zapytać o terminie, w którym relacja jest ważna i musi być w [ValdFrom, validTo)

Do tworzenia zespołu czasowego I mieć dwa podejścia;

Podejście 1: Przedstaw tabelę "Season", które modele Ważność na sezon

  • ID
  • Sezon Nazwa (np lato 2011).
  • Z (być może nie jest to konieczne, ponieważ każdy jeden wie, kiedy jest sezon)
  • do (być może nie jest to konieczne, ponieważ każdy wie, kiedy jest sezon)

Podziel tabelę zespołu. Będziesz miał pola, które należą do zespołu, a które nie są istotne w czasie (nazwa, adres, ...) i pola, które są ważne dla danego sezonu (wygrana, przegrana, ..). W takim przypadku użyłbym Team i TeamInSeason. PlaysInTeam mógłby odwołuje się do TeamInSeason zamiast Team (musi być uznane - Chciałbym niech to wskazywać na zespół)

TeamInSeason

  • ID
  • Identyfikator zespołu
  • ID Season
  • Win
  • Utrata
  • ...

Podejście 2: Nie modeluj sezonu jawnie. Podziel tabelę zespołu. Będziesz miał pola, które należą do zespołu i które nie są istotne w czasie (nazwa, adres, ...) i pola, które są istotne w czasie (wygrana, przegrana, ..). W takim przypadku użyłbym Team i TeamInterval. TeamInterval będzie miał pola "od" i "do" dla przedziału.PlaysInTeam mógłby odwołuje się do TeamInterval zamiast Team (ja niech go na zespół)

TeamInterval

  • ID
  • Identyfikator zespołu
  • Od
  • Aby
  • Win
  • Utrata
  • ...

W przypadku obu podejść: jeśli nie potrzebujesz osobnej tabeli zespołu dla pola bez czasu, nie dziel się.

+0

Załóżmy, że mam tabelę PERSON, która ma klucz zastępczy, który jest polem o nazwie "id". Każda tabela odwołań w tym miejscu będzie miała "ID" jako ograniczenie klucza obcego. Jeśli dodaję teraz kolumny tymczasowe, muszę zmienić klucz podstawowy na "id + od_daty + na_danie". Przed zmianą klucza podstawowego musiałbym najpierw usunąć każde obce ograniczenie z każdej tabeli odwołań do tej tabeli z odniesieniami (Osoba). Czy mam rację? Wierzę, że to właśnie masz na myśli z kluczem zastępczym. ID to wygenerowany klucz, który można wygenerować za pomocą sekwencji. Klucz biznesowy tabeli Person to numer SSN. –

+0

Zaktualizowano odpowiedź za pomocą naszych komentarzy. – ChrLipp

1

Wygląda na to, że nie można tego zrobić za pomocą JPA, ponieważ zakłada on, że nazwa tabeli i cały schemat jest statyczny.

Najlepszym rozwiązaniem może być to zrobić za pośrednictwem JDBC (na przykład za pomocą wzoru DAO)

Jeśli wydajność jest problem, chyba że mówimy o dziesiątkach milionów rekordów, wątpię, że dynamicznie tworzenia klas & kompilowanie go & następnie ładowanie byłoby lepiej.

Inną opcją może być używanie widoków (Jeśli musisz używać JPA), może być jakoś abstrakcja tabeli (mapuj @Entity (name = "myView"), to musisz dynamicznie aktualizować/zastępować widok jako w utworzyć lub zamienić wIDOK usernameView AS SELECT * FROM prefix_sessionId

na przykład można napisać jeden widok powiedzieć:

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.

nadzieję, że to pomaga (espero que te ayude)

2

Nie dokładnie wiesz, co masz na myśli, ale EclipseLink ma pełne poparcie dla historii. Możesz włączyć HistoryPolicy na ClassDescriptor przez @DescriptorCustomizer.

+2

Różnica polega na tym, że podejście czasowe działa z jedną tabelą zamiast z tabelą historii i że EclipseLink obsługuje tylko jeden wymiar zamiast dwóch. Ale w każdym razie, dzięki za informację. Nie znałem tej funkcji. – ChrLipp

1

w DAO Fusion, śledzenie obiektu w obu osiach czasu (ważności i interwału rekordu) realizowane jest przez zawinięcie tego obiektu przez BitemporalWrapper.

Przykład bitemporal reference documentation przedstawia przykład z regularną jednostką Order zawijaną przez jednostkę BitemporalOrder. BitemporalOrder mapuje do oddzielnej tabeli bazy danych, z kolumnami dla ważności i interwału rekordów, a także z kluczem obcym do Order (przez @ManyToOne) dla każdego wiersza tabeli.

Dokumentacja wskazuje również, że każdy bitemporal osłony (np BitemporalOrder) stanowi jeden element wewnątrz bitemporal łańcucha zapisu. Dlatego potrzebujesz jakiejś encji wyższego poziomu, która zawiera kolekcję opakowań bitemporalnych, np. Customer podmiot, który zawiera @OneToMany Collection<BitemporalOrder> orders.

Tak więc, jeśli trzeba „logiczne dziecko” podmiot (np Order lub Player) do bitemporally śledzone, a jej „logiczne dominująca” podmiot (np Customer lub Team) do bitemporally śledzone jak dobrze, trzeba dostarczyć bitemporalne opakowania dla obu. Będziesz miał BitemporalPlayer i BitemporalTeam. BitemporalTeam może zadeklarować @OneToMany Collection<BitemporalPlayer> players. Ale potrzebujesz jakiegoś elementu wyższego poziomu do przechowywania @OneToMany Collection<BitemporalTeam> teams, jak wspomniano powyżej. W przypadku przykładu można utworzyć encję Game zawierającą kolekcję BitemporalTeam.

Jeśli jednak nie potrzebujesz interwału rekordów i potrzebujesz tylko okresu ważności (np. Nie bitemicznego, ale jednokierunkowego śledzenia twoich jednostek), najlepiej jest wykonać własną implementację niestandardową.