Как реализовать временную таблицу с использованием JPA? - PullRequest
25 голосов
/ 03 марта 2012

Я хотел бы знать, как реализовать временные таблицы в 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

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

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

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

Ответы [ 4 ]

7 голосов
/ 05 марта 2012

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

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

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

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

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

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

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

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

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

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

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

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

  • Вопрос: Допустим, у меня есть таблица PERSON, у которой есть суррогатный ключ, который является полем с именем "id". Каждая ссылочная таблица в этот момент будет иметь этот «идентификатор» в качестве ограничения внешнего ключа. Если я добавлю временные столбцы, я должен изменить первичный ключ на «id + from_date + to_date». Перед изменением первичного ключа мне нужно было бы сначала отбросить все внешние ограничения каждой ссылочной таблицы в эту ссылочную таблицу (Person). Я прав? Я полагаю, что вы имеете в виду суррогатный ключ. ID - это сгенерированный ключ, который может быть сгенерирован последовательностью. Бизнес-ключ таблицы Person - это SSN.
  • Ответ: Не совсем. SSN будет естественным ключом, который я не использую для идентификации личности. Также "id + from_date + to_date" будет составным ключом , которого я также избегал бы. Если вы посмотрите на пример , у вас будет две таблицы: человек и место жительства, и в нашем примере мы скажем, что у нас есть отношение 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 игрока (внешний ключ к игроку)
  • ValidFrom
  • ValidTo

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

Для того, чтобы сделать команду временной, у меня есть два подхода;

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

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

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

TeamInSeason

  • ID
  • ID Team
  • ID Сезон
  • Win
  • Потеря
  • ...

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

TeamInterval

  • ID
  • ID Team
  • С
  • К
  • Win
  • Потеря
  • ...

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

2 голосов
/ 05 марта 2012

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

1 голос
/ 01 апреля 2012

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

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

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

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

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

1 голос
/ 03 марта 2012

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

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

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

Другой вариант может заключаться в использовании представлений (если вы должны использовать JPA), возможно, чтобы каким-то образом абстрагировать таблицу (отобразить @Entity (name = "myView"), тогда вам придется динамически обновлять / заменять представление, как в CREATE ORЗАМЕНИТЕ ПРОСМОТР 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)

...