2012-03-03 6 views
23

Я хотел бы знать, как реализовать temporal tables в JPA 2 с EclipseLink. По временному я имею в виду таблицы, которые определяют срок действия.Как реализовать временную таблицу с использованием JPA?

Одна из проблем, с которой я столкнулся, заключается в том, что ссылочные таблицы больше не имеют ограничений внешних ключей к ссылочным таблицам (временным таблицам) из-за характера ссылочных таблиц, которые теперь в их первичных ключах включают период действия.

  • Как я смогу сопоставить отношения моих сущностей?
  • Будет ли это означать, что мои сущности больше не могут иметь отношения к этим объектам, действующим во времени?
  • Должна ли ответственность за инициализацию этих отношений теперь выполнять вручную вручную в какой-либо службе или специализированном DAO?

Единственное, что я нашел, это рама под названием DAO Fusion, которая занимается этим.

  • Есть ли другие способы решить эту проблему?
  • Не могли бы вы предоставить пример или ресурсы по этой теме (JPA с временными базами данных)?

Вот вымышленный пример модели данных и ее классов. Она начинается как простая модель, не приходится иметь дело с временными аспектами:

первого сценарий: Non Temporal Модель

Модели данных: Non Temporal Data Model

команды:

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


} 

Игрок:

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


} 

испытаний Класс:

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


} 

Теперь вас просят сохранить историю о том, как команда и игрок был на определенный момент времени, так что вам нужно сделать, это добавить период времени для каждой таблицы, которая хочет отслеживаться. Итак, добавим эти временные столбцы. Мы собираемся начать с Player.

второй сценарий: Temporal Модель

Data Model: Temporal Data Model

Как вы можете видеть, что мы должны были отбросить первичный ключ и определить другой, который включает в себя дату (период). Также нам пришлось отказаться от уникальных ограничений, потому что теперь их можно повторить в таблице. Теперь таблица может содержать текущие записи, а также историю.

Все становится довольно уродливым, если мы также должны сделать команду временной, в этом случае нам нужно будет сбросить ограничение внешнего ключа, чтобы таблица Player имела значение Team. Проблема в том, как бы вы моделировали это в Java и JPA.

Обратите внимание, что идентификатор является суррогатной клавишей. Но теперь ключи суррогата должны включать дату, потому что, если они этого не сделают, это не позволит хранить более одной «версии» того же объекта (на временной шкале).

+0

1) с помощью какого инструмента вы нарисовали диаграмму? 2) одного временного измерения достаточно для ваших требований, шаблоны DAOFusion, а также мой ответ (на основе этих шаблонов), по моему мнению, являются излишними. 3) Вы предпочитаете решение, которое просто добавляет временный аспект к Player или вы предпочитаете это для обеих таблиц 4) ваш последний абзац неверен. Суррогатный ключ никогда не будет включать дополнительные поля. В этом случае у вас будет два суррогатных ключа. – ChrLipp

+0

@ChrLipp 1) Архитектор Sparx Enterprise 2) Я согласен. 3) Мне нужно решение, которое добавляет временные значения для обеих таблиц. 4) Я не согласен, что это не суррогатный ключ. Я думаю, что это суррогатный ключ, потому что: 1. Перед добавлением временных столбцов это был суррогатный ключ, который является ключом, а не бизнес-значением.Например, бизнес-ключ игрока - «team_id» и «number», а Team - «имя». Оба имеют свой собственный суррогатный ключ «id», когда у них не было временных столбцов. Проблема в том, что когда я добавляю временные столбцы, которые больше не работают. Одна и та же запись может появляться более одного раза в одной таблице. –

+0

Вот почему суррогатный ключ «id» сам по себе не может быть только одним столбцом, потому что он является одной и той же записью, но отслеживается в разные сроки, поэтому, чтобы позволить одной и той же записи появляться более одного раза, я мог бы добавить следующие как первичный ключ «id + validstart» или «id + validend» или «id + validstart + validend». Я выбрал последний вариант для удобства в Java-сопоставлениях, где у меня есть объект «Интервал», который определяет период, поэтому, чтобы сопоставить это в JPA, я добавил, что «Интервал» равен Id как EmbeddedId. –

ответ

7

Я очень заинтересован в i В этой теме. Я работаю уже несколько лет в разработке приложений, которые используют эти шаблоны, идея пришла в нашем случае из немецкой дипломной диссертации.

Я не знал рамки «DAO Fusion», они предоставляют интересную информацию и ссылки, спасибо за предоставление этой информации. Особенно понравились pattern page и aspects page!

На ваши вопросы: нет, я не могу указать другие сайты, примеры или рамки. Я боюсь, что вам придется использовать либо инфраструктуру DAO Fusion, либо реализовать эту функцию самостоятельно. Вы должны различать, какую функциональность вам действительно нужно. Говорить в терминах структуры DAO Fusion: нужны ли вам как «действительные временные», так и «рекордные временные»? Записывайте временные состояния, когда изменение применяется к вашей базе данных (обычно используется для проблем аудита), действительные временные состояния, когда изменение произошло в реальной жизни или действительно в реальной жизни (используется приложением), которое может отличаться от времени записи. В большинстве случаев достаточно одного измерения, а второе измерение не требуется.

В любом случае временная функциональность влияет на вашу базу данных. Как вы заявили: ", который теперь в их первичных ключах включает период действия". Итак, как вы моделируете личность сущности? Я предпочитаю использовать surrogate keys. В таком случае это означает, что:

  • один идентификатор для объекта
  • один идентификатор для объекта в базе данных (ряд)
  • временных столбцы

Первичный ключ для таблицы это идентификатор объекта.Каждый объект имеет одну или несколько (1-n) записей в таблице, идентифицированных идентификатором объекта. Связывание таблиц основано на идентификаторе объекта. Поскольку временные записи умножают объем данных, стандартные отношения не работают. Стандартное соотношение 1-n может стать отношением x * 1-y * n.

Как вы это решаете? Стандартным подходом было бы введение таблицы сопоставления, но это не естественный подход. Для редактирования одной таблицы (например, изменения места жительства) вам также потребуется обновить/вставить таблицу сопоставления, которая является странной для каждого программиста.

Другой подход заключается не в использовании таблицы сопоставления. В этом случае вы не можете использовать ссылочную целостность и внешние ключи, каждая таблица действует изолированно, связь между одной таблицей и другими должна быть выполнена вручную, а не с функциями JPA.

Функциональность инициализации объектов базы данных должна быть в пределах объектов (как в структуре DAO Fusion). Я бы не поместил его в службу. Если вы передадите его в DAO или используете Активный шаблон записи, вам.

Я знаю, что мой ответ не предоставляет вам «готовые к использованию» рамки. Вы находитесь в очень сложной области, из моего опыта ресурсов для этого сценария использования очень сложно найти. Спасибо за ваш вопрос! Но в любом случае я надеюсь, что помог вам в вашем дизайне.

В этом ответе вы найдете справочник "Developing Time-ориентированных баз данных приложений в SQL", см https://stackoverflow.com/a/800516/734687

Update: Пример

  • Вопрос: Допустим, что у меня есть Таблица PERSON, у которой есть суррогатный ключ, который является полем с именем «id». Каждая таблица ссылок в этот момент будет иметь этот идентификатор в качестве ограничения внешнего ключа. Если теперь добавить временные столбцы, я должен изменить первичный ключ на «id + from_date + to_date». Перед изменением первичного ключа я должен сначала удалить каждое внешнее ограничение каждой таблицы ссылок в эту ссылочную таблицу (Person). Я прав? Полагаю, это то, что вы имеете в виду с помощью суррогатного ключа. ID - это сгенерированный ключ, который может быть сгенерирован последовательностью. Бизнес-ключ таблицы Person - это SSN.
  • Ответ: Не совсем. SSN будет естественным ключом, который я не использую для идентичности objcet. Также «id + from_date + to_date» будет composite key, чего я бы тоже избегал. Если вы посмотрите на example, у вас будет две таблицы, человек и место жительства, и в нашем примере мы скажем, что у нас есть 1-я связь с резиденцией иностранного ключа. Теперь мы добавляем временные поля для каждой таблицы. Да, мы бросаем все ограничения внешнего ключа. Человек получит 2 идентификатора, один идентификатор для идентификации строки (назовите его ROW_ID), один идентификатор, чтобы идентифицировать самого человека (назовите его ENTIDY_ID) индексом этого идентификатора. То же самое для человека. Конечно, ваш подход тоже сработает, но в этом случае у вас будут операции, которые изменяют ROW_ID (когда вы закрываете временной интервал), чего я бы избегал.

Чтобы продлить example реализованный с предположениями выше (2 таблицы, 1-п):

  • запрос, чтобы показать все записи в базе данных (все данные действия и запись - он же технический - включая информацию):

    SELECT * FROM Person p, Residence r 
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON   // JOIN
  • запрос, чтобы скрыть запись - aka technical - information. Это показывает все значения validy-Changes сущностей.

    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
  • запрос для отображения фактических значений.

    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

Как вы можете видеть, что я никогда не использую ROW_ID. Замените [сейчас] временной отметкой, чтобы вернуться во времени.

Update, чтобы отразить ваше обновление
Я бы рекомендовал следующую модель данных:

ввести "PlaysInTeam" стол:

  • ID
  • ID команды (внешний ключ к команде)
  • ID Player (внешний ключ для игрока)
  • ValidFrom
  • ValidTo

Когда вы список игроков команды, вы должны запросить с датой, для которых соотношение справедливо и должно быть в [ValdFrom, ValidTo)

Для создания команды временного I имеют два подхода;

подход 1: Ввести «Сезон» таблицу, которая моделирует действительность в течение сезона

  • ID
  • Сезон Название (например, лето 2011).
  • С (возможно, не нужно, потому что каждый один знает, когда сезон)
  • К (возможно, не нужно, потому что каждый знает, когда сезон)

Разделить стол команды. У вас будут поля, принадлежащие команде, и которые не являются релевантными по времени (имя, адрес и т. Д.) И поля, которые относятся к сезону (выигрыш, проигрыш, ..). В этом случае я бы использовал Team и TeamInSeason. PlaysInTeam может связать с TeamInSeason вместо команды (должен быть рассмотрен, - я позволил бы она указывала на команду)

TeamInSeason

  • ID
  • ID команды
  • ID Сезон
  • Win
  • Потеря
  • ...

Подход 2: Не моделируйте сезон явно. Разделите таблицу команд. У вас будут поля, которые принадлежат команде, и которые не являются релевантными по времени (имя, адрес и т. Д.) И поля, которые относятся к времени (выигрыш, потеря, ..). В этом случае я бы использовал Team и TeamInterval. TeamInterval будет иметь поля «от» и «до» для интервала.PlaysInTeam может связать с TeamInterval вместо команды (я позволил бы его команда)

TeamInterval

  • ID
  • ID команды
  • От
  • Чтобы
  • Win
  • Потеря
  • ...

В обоих подходах: если вам не нужна отдельная таблица команд в течение определенного времени, не разделяйте ее.

+0

Предположим, что у меня есть таблица PERSON, у которой есть суррогатный ключ, который является полем с именем «id». Каждая таблица ссылок в этот момент будет иметь этот идентификатор в качестве ограничения внешнего ключа. Если теперь добавить временные столбцы, я должен изменить первичный ключ на «id + from_date + to_date». Перед изменением первичного ключа я должен сначала удалить каждое внешнее ограничение каждой таблицы ссылок в эту ссылочную таблицу (Person). Я прав? Полагаю, это то, что вы имеете в виду с помощью суррогатного ключа. ID - это сгенерированный ключ, который может быть сгенерирован последовательностью. Бизнес-ключ таблицы Person - это SSN. –

+0

Обновлен ответ с нашими комментариями. – ChrLipp

1

Кажется, что вы не можете сделать это с JPA, так как предполагается, что имя таблицы и целая схема статичны.

Лучшим вариантом может быть, чтобы сделать это через JDBC (например, с использованием шаблона DAO)

Если производительность является проблемой, если мы не говорим о десятках миллионов записей, я сомневаюсь, что динамически создавать классы & скомпилировать его &, тогда загрузка будет лучше.

Другим вариантом может быть использование представлений (если вы должны использовать JPA) может быть как-то абстрактной таблицы (сопоставьте @Entity (name = "myView"), тогда вам придется динамически обновлять/заменять представление как в CREATE OR REPLACE VIEW usernameView AS SELECT * FROM prefix_sessionId

, например, вы могли бы написать один вид сказать:

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 дие те ayude)

2

Не совсем уверен, что вы имеете в виду, но EclipseLink имеет полную поддержку истории. Вы можете включить HistoryPolicy в ClassDescriptor через @DescriptorCustomizer.

+2

Разница заключается в том, что временный подход работает с одной таблицей вместо таблицы истории и что EclipseLink поддерживает только одно измерение вместо двух. Но в любом случае, спасибо за информацию. Я не знал этой возможности. – ChrLipp

1

в DAO Fusion, отслеживание объекта в обоих временных рамках (срок действия и интервал записи) осуществляется путем упаковки этого объекта на BitemporalWrapper.

bitemporal reference documentation представляет пример с обычным Order объектом, обернутым BitemporalOrder объектом. BitemporalOrder сопоставляется с отдельной таблицей базы данных с столбцами для срока действия и интервала записи и ссылкой внешнего ключа на Order (через @ManyToOne) для каждой строки таблицы.

В документации также указывается, что каждая битемпольная обертка (например, BitemporalOrder) представляет один элемент в битрепольной цепочке записи. Следовательно, вам нужен какой-то объект более высокого уровня, который содержит битмпоральную коллекцию оберток, например. Customer объект, который содержит @OneToMany Collection<BitemporalOrder> orders.

Так что, если вам нужен «логический дочерний» объект (например Order или Player) для bitemporally отслеживаются, и его «логический родительский» объект (например Customer или Team), чтобы быть bitemporally отслеживаются, а также, вы должны предоставить битемпоральные обертки для обоих. У вас будет BitemporalPlayer и BitemporalTeam. BitemporalTeam может объявить @OneToMany Collection<BitemporalPlayer> players. Но вам нужен какой-то объект более высокого уровня для содержания @OneToMany Collection<BitemporalTeam> teams, как упоминалось выше. Например, для примера можно создать объект Game, который содержит BitemporalTeam.

Однако, если вам не нужен интервал записи, и вам нужен только интервал действия (например, не битпоральное, но одновременное отслеживание ваших объектов), наилучшим вариантом является сворачивание собственной пользовательской реализации.