Как реализовать временную таблицу с помощью JPA?
Я хотел бы знать, как реализовать временные таблицы в JPA 2 с EclipseLink. По временному я имею в виду таблицы, которые определяют срок действия.
Одна из проблем, с которой я столкнулась, заключается в том, что ссылки на таблицы больше не имеют ограничений внешних ключей для ссылочных таблиц (временных таблиц) из-за характера ссылочных таблиц, которые теперь в их первичных ключах включают период действия.
- Как мне сопоставить отношения моих сущностей?
- Будет ли это означать, что мои сущности больше не могут иметь отношения к этим действительным объектам?
- Если ответственность за инициализацию этих отношений теперь выполняется мной вручную в какой-то службе или специализированном DAO?
Единственное, что я нашел, это фреймворк под названием DAO Fusion, который имеет дело с этим.
- Есть ли другие способы решить эту проблему?
- Не могли бы вы предоставить пример или ресурсы по этому вопросу (JPA с временными базами данных)?
Вот вымышленный пример модели данных и ее классов. Он начинается как простая модель, которая не имеет дело с временными аспектами:
1-й сценарий: не временная модель
Модель данных:
![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
.
2-й сценарий: временная модель
Модель данных:
![Temporal Data Model]()
Как вы можете видеть, нам пришлось отказаться от первичного ключа и определить другой, который включает даты (период). Также нам пришлось отказаться от уникальных ограничений, потому что теперь их можно повторить в таблице. Теперь таблица может содержать текущие записи, а также историю.
Вещи становятся довольно уродливыми, если нам также нужно сделать команду временной, в этом случае нам нужно будет сбросить ограничение внешнего ключа, чтобы таблица Player
имела значение Team
. Проблема в том, как бы вы моделировали это в Java и JPA.
Обратите внимание, что идентификатор является суррогатным ключом. Но теперь ключи суррогата должны включать дату, потому что если они этого не сделают, это не позволит хранить более одной " версии" того же объекта (во время временной шкалы).
Ответы
Ответ 1
Мне очень интересна эта тема. Я работаю уже несколько лет в разработке приложений, которые используют эти шаблоны, идея появилась в нашем случае из немецкой дипломной работы.
Я не знал рамки DAO Fusion, они предоставляют интересную информацию и ссылки, спасибо за предоставление этой информации. Особенно шаблонная страница и страница аспектов великолепны!
На ваши вопросы: нет, я не могу указать другие сайты, примеры или рамки. Я боюсь, что вам придется использовать либо инфраструктуру DAO Fusion, либо реализовать эту функцию самостоятельно. Вы должны различать, какую функциональность вам действительно нужно. Говорить в терминах структуры DAO Fusion: нужны ли вам как "действительные временные", так и "рекордные временные"? Записывайте временные состояния, когда изменение применяется к вашей базе данных (обычно используется для проблем аудита), действительные временные состояния, когда изменение произошло в реальной жизни или действительно в реальной жизни (используется приложением), которое может отличаться от времени записи. В большинстве случаев достаточно одного измерения, а второе измерение не требуется.
В любом случае временная функциональность влияет на вашу базу данных. Как вы заявили: "в настоящее время их первичными ключами являются период действия". Итак, как вы моделируете личность сущности? Я предпочитаю использовать суррогатные ключи. В этом случае это означает:
- один идентификатор для объекта
- один идентификатор объекта в базе данных (строка)
- временные столбцы
Первичным ключом для таблицы является идентификатор объекта. Каждый объект имеет одну или несколько (1-n) записей в таблице, идентифицированных идентификатором объекта. Связывание таблиц основано на идентификаторе объекта. Поскольку временные записи умножают объем данных, стандартные отношения не работают. Стандартное отношение 1-n может стать отношением x * 1-y * n.
Как вы решаете это? Стандартным подходом было бы введение таблицы сопоставления, но это не естественный подход. Для редактирования одной таблицы (например, изменения места жительства) вам также потребуется обновить/вставить таблицу сопоставления, которая является странной для каждого программиста.
Другим подходом было бы не использовать таблицу сопоставления. В этом случае вы не можете использовать ссылочную целостность и внешние ключи, каждая таблица действует изолированно, связь между одной таблицей и другими должна быть выполнена вручную, а не с функциями JPA.
Функциональность инициализации объектов базы данных должна быть внутри объектов (как в структуре DAO Fusion). Я бы не поместил его в службу. Если вы передадите его в DAO или используете шаблон Active Record, вам придется.
Я знаю, что мой ответ не предоставляет вам "готовые к использованию" рамки. Вы находитесь в очень сложной области, из моего опыта ресурсов для этого сценария использования очень сложно найти. Спасибо за ваш вопрос! Но в любом случае я надеюсь, что помог вам в вашем дизайне.
В этом ответе вы найдете справочник "Разработка ориентированных на время приложений баз данных в SQL", см. fooobar.com/questions/120435/...
Обновление: пример
- Вопрос: скажем, что у меня есть таблица PERSON, у которой есть суррогатный ключ, который является полем с именем "id". Каждая таблица ссылок в этот момент будет иметь этот идентификатор в качестве ограничения внешнего ключа. Если теперь добавить временные столбцы, я должен изменить первичный ключ на "id + from_date + to_date". Перед изменением первичного ключа я должен сначала удалить каждое внешнее ограничение каждой таблицы ссылок в эту ссылочную таблицу (Person). Я прав? Я верю, что вы имеете в виду суррогатный ключ. ID - это сгенерированный ключ, который может быть сгенерирован последовательностью. Бизнес-ключ таблицы Person - это SSN.
- Ответ: Не совсем. SSN будет естественным ключом, который я не использую для идентичности objcet. Также "id + from_date + to_date" будет составным ключом, чего я также избегу. Если вы посмотрите на exampleу вас будет две таблицы, человек и место жительства, и в нашем примере мы говорим, что у нас есть 1-я связь с резиденцией иностранного ключа.
Теперь мы добавляем временные поля для каждой таблицы. Да, мы бросаем все ограничения внешнего ключа. Человек получит 2 идентификатора, один идентификатор для идентификации строки (назовите его ROW_ID), один идентификатор, чтобы идентифицировать самого человека (назовите его ENTIDY_ID) индексом этого идентификатора. То же самое для человека. Конечно, ваш подход тоже будет работать, но в этом случае у вас будут операции, которые изменяют ROW_ID (когда вы закрываете временной интервал), чего я бы избегал.
Чтобы расширить example, реализованный с предположениями выше (2 таблицы, 1-n):
-
запрос для отображения всех записей в базе данных (вся информация о достоверности и запись - ака-техническая информация включена):
SELECT * FROM Person p, Residence r
WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON // JOIN
-
запрос, чтобы скрыть запись - ака техническая информация. Это показывает все значения 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. Замените [сейчас] временной отметкой, чтобы вернуться назад.
Обновить, чтобы отразить ваше обновление
Я бы рекомендовал следующую модель данных:
Ввести таблицу "PlaysInTeam":
- ID
- Команда ID (внешний ключ для команды)
- ID Player (внешний ключ для игрока)
- ValidFrom
- ValidTo
Когда вы указываете игроков команды, вы должны запросить дату, для которой отношение действительно, и должно быть в [ValdFrom, ValidTo)
Для создания временной команды у меня есть два подхода:
Подход 1:
Ввести таблицу "Сезон", которая моделирует срок действия сезона
- ID
- Название сезона (например, лето 2011)
- От (возможно, не обязательно, потому что каждый знает, когда сезон)
- To (возможно, не обязательно, потому что каждый знает, когда сезон)
Разделите таблицу команд. У вас будут поля, принадлежащие команде, и которые не являются релевантными по времени (имя, адрес и т.д.) И поля, которые относятся к сезону (выигрыш, проигрыш,..). В этом случае я бы использовал Team и TeamInSeason. PlaysInTeam может ссылаться на TeamInSeason вместо Team (должен быть рассмотрен - я бы хотел указать на команду)
TeamInSeason
- ID
- ID Team
- Сезон идентификаторов
- Win
- Потеря
- ...
Подход 2:
Не моделируйте сезон явно. Разделите таблицу команд. У вас будут поля, которые принадлежат команде, и которые не являются релевантными по времени (имя, адрес и т.д.) И поля, которые относятся к времени (выигрыш, потеря,..). В этом случае я бы использовал Team и TeamInterval. TeamInterval будет иметь поля "от" и "до" для интервала. PlaysInTeam может ссылаться на TeamInterval вместо Team (я бы отпустил его в Team)
TeamInterval
- ID
- ID Team
- С
- Для
- Win
- Потеря
- ...
В обоих подходах: если вам не нужна отдельная таблица команд в поле времени, не разделяющее время, не разделяйте.
Ответ 2
Не совсем понятно, что вы имеете в виду, но EclipseLink имеет полную поддержку истории. Вы можете включить HistoryPolicy в ClassDescriptor через @DescriptorCustomizer.
Ответ 3
Кажется, вы не можете сделать это с JPA, так как он предполагает, что имя таблицы и целая схема статичны.
Лучшим вариантом может быть использование JDBC (например, с использованием шаблона DAO)
Если производительность является проблемой, если мы не говорим о десятках миллионов записей, я сомневаюсь, что динамическое создание классов и их компиляция, а затем загрузка будет лучше.
Другим вариантом может быть использование представлений (если вы должны использовать JPA) может быть каким-то образом абстрагировать таблицу (сопоставьте @Entity (name= "myView" ), тогда вам придется динамически обновлять/заменять представление как в CREATE ИЛИ 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 que te ayude)
Ответ 4
в DAO Fusion, отслеживание объекта в обоих временных рамках (срок действия и интервал записи) реализуется путем переноса этого объекта на BitemporalWrapper
.
В справочной документации приведен пример с регулярным Order
сущностью, обернутой сущностью BitemporalOrder
. BitemporalOrder
сопоставляется с отдельной таблицей базы данных с столбцами для достоверности и интервала записи и ссылкой внешнего ключа на Order
(через @ManyToOne
) для каждой строки таблицы.
В документации также указывается, что каждая битпольная обертка (например, BitemporalOrder
) представляет один элемент в цепочке битпоральной записи. Следовательно, вам нужен какой-то объект более высокого уровня, который содержит битмпоральную коллекцию оберток, например. Customer
, который содержит @OneToMany Collection<BitemporalOrder> orders
.
Итак, если вам нужен "логический дочерний" объект (например, Order
или Player
), который будет отслеживаться по методу "бит" , а его "логическая родительская" сущность (например, Customer
или Team
) должна быть отслежена Кроме того, вам необходимо предоставить bitemporal wrappers для обоих. У вас будут BitemporalPlayer
и BitemporalTeam
. BitemporalTeam
может объявить @OneToMany Collection<BitemporalPlayer> players
. Но вам нужен какой-то объект более высокого уровня для содержания @OneToMany Collection<BitemporalTeam> teams
, как указано выше. Для
Например, вы можете создать объект Game
, содержащий коллекцию BitemporalTeam
.
Однако, если вам не нужен интервал записи, и вам нужен только интервал действия (например, не битпопольное, но однопользовательское отслеживание ваших объектов), лучше всего выполнить собственную собственную реализацию.