Как реализовать временную таблицу с помощью 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.

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

4 ответов


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

Я не знал фреймворков "DAO Fusion", они предоставляют интересную информацию и ссылки, спасибо за предоставление этой информации. Особенно страница шаблон и страница аспектах здорово!

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

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

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

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

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

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

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

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

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

Обновление: Пример

  • вопрос: предположим, что у меня есть таблица PERSON, у которой есть суррогатный ключ, который является полем с именем "id". Каждая ссылочная таблица на данный момент будет иметь "ID" как ограничение внешнего ключа. Если я добавлю временные столбцы, мне придется изменить первичный ключ на "id+from_date+to_date". Прежде чем изменять первичный ключ, я должен сначала удалить все внешние ограничения каждой ссылочной таблицы в эту ссылочную таблицу (Person). Я прав? Я полагаю, что вы имеете в виду суррогатный ключ. ID-это сгенерированный ключ, который может быть сгенерирован последовательностью. Бизнес-ключ таблицы Person-это SSN.
  • ответ: не совсем. ПЛА будет естественный ключ, который я не использую для objcet личности. Также "id+from_date+to_date" будет составной ключ, которого я бы тоже избегал. Если вы посмотрите на пример у вас будет две таблицы: person и residence, и для нашего примера скажем, что у нас есть 1-n отношения с резиденцией внешнего ключа. Теперь мы добавляем временные поля в каждую таблицу. Да, мы отбрасываем все ограничения внешнего ключа. Человек получит 2 идентификатора, один идентификатор для идентификации строки (назовите его ROW_ID), один идентификатор чтобы идентифицировать самого человека (назовите его ENTIDY_ID) с индексом на этом идентификаторе. Же для человека. Конечно, ваш подход тоже будет работать, но в этом случае у вас будут операции, которые изменяют ROW_ID (когда вы закрываете временной интервал), чего я бы избегал.

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

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

    SELECT * FROM Person p, Residence r
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON          // JOIN 
  • запрос, чтобы скрыть запись-он же техническая информация. Это показывает все валидные изменения сущностей.

    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)

для создания команды temporal у меня есть два подходит;

подход 1: Представьте таблицу "сезон", которая моделирует действительность для сезона

  • ID
  • название сезона (например. Лето 2011 года)
  • от (возможно, не обязательно, потому что каждый знает, когда сезон)
  • To (возможно, не обязательно, потому что каждый знает, когда сезон)

разделить таблицу команды. У вас будут поля, которые принадлежат команде и которые не относятся ко времени (имя, адрес. ,..) и поля, которые актуальны для сезона (win, loss, ..). В этом случае я бы использовал Team и TeamInSeason. PlaysInTeam может ссылаться на TeamInSeason вместо Team (следует учитывать - я бы позволил ему указать на Team)

TeamInSeason

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

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

TeamInterval

  • ID
  • ID Команда
  • С
  • до
  • Win
  • потеря
  • ...

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


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


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

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

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

другой вариант может использовать представления (Если вы должны использовать JPA) может быть чтобы как-то абстрагировать таблицу (сопоставить @Entity(name="myView"), тогда вам придется динамически обновлять/заменять представление, как в CREATE или REPLACE VIEW usernameView как 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)


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

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

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

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

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