java - 如何使用 JPA 实现时态表?

标签 java jpa-2.0 eclipselink temporal-database

我想知道如何实现temporal tables在带有 EclipseLink 的 JPA 2 中。我所说的时间是指定义有效期的表。

我面临的一个问题是,由于被引用表的性质,现在它们的主键包括有效期,因此引用表不能再对被引用表(临时表)具有外键约束。

  • 我将如何映射实体之间的关系?
  • 这是否意味着我的实体不能再与那些有效时间实体建立关系?
  • 初始化这些关系的责任现在应该由我在某种服务或专门的 DAO 中手动完成吗?

  • 我唯一发现的是一个名为 DAO Fusion 的框架。处理这个。
  • 有没有其他方法可以解决这个问题?
  • 您能否提供有关此主题的示例或资源(带有时态数据库的 JPA)?

  • 这是一个数据模型及其类的虚构示例。它从一个不需要处理时间方面的简单模型开始:

    第一个场景:非时间模型

    数据模型 :
    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;
        }
    
    
    }
    

    测试类(class):
    @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

    如您所见,我们不得不删除主键并定义另一个包含日期(句点)的主键。我们还必须删除唯一约束,因为现在它们可以在表中重复。现在该表可以包含当前条目和历史记录。

    如果我们还必须让 Team 临时化,事情就会变得非常糟糕,在这种情况下,我们需要删除 Player 的外键约束。表要Team .问题是您将如何在 Java 和 JPA 中对其进行建模。

    请注意,ID 是一个代理键。但是现在代理键必须包含日期,因为如果不包含,则不允许存储同一实体的多个“ 版本 ”(在时间轴内)。

    最佳答案

    我对这个话题很感兴趣。我在开发使用这些模式的应用程序方面工作了几年,这个想法来自我们的德国文凭论文。

    我不知道“DAO Fusion”框架,它们提供了有趣的信息和链接,感谢您提供这些信息。特别是pattern pageaspects page很棒!

    对于您的问题:不,我无法指出其他站点、示例或框架。恐怕您必须使用DAO Fusion框架或自己实现此功能。您必须区分您真正需要哪种功能。用“DAO Fusion”框架来说:你需要“有效时间”和“记录时间”吗?当更改应用于您的数据库时记录时间状态(通常用于审计问题),当更改发生在现实生活中或在现实生活中有效(由应用程序使用)时,有效的时间状态可能与记录时间不同。在大多数情况下,一维就足够了,不需要第二维。

    无论如何,时间功能会对您的数据库产生影响。正如您所说:“现在他们的主键包括有效期”。那么如何对实体的身份进行建模呢?我更喜欢 surrogate keys 的用法.在这种情况下,这意味着:

  • 实体的一个 ID
  • 数据库中对象的一个​​ ID(行)
  • 时间列

  • 表的主键是对象 ID。每个实体在表中都有一个或多个 (1-n) 个条目,由对象 ID 标识。表之间的链接基于实体 ID。由于时间条目使数据量成倍增加,因此标准关系不起作用。标准的 1-n 关系可能会变成 x*1-y*n 关系。

    你如何解决这个问题?标准方法是引入映射表,但这不是自然的方法。仅仅为了编辑一张表(例如发生住所变化),您还必须更新/插入映射表,这对每个程序员来说都很奇怪。

    另一种方法是不使用映射表。在这种情况下,您不能使用参照完整性和外键,每个表都是独立的,从一个表到其他表的链接必须手动实现,而不是使用 JPA 功能。

    初始化数据库对象的功能应该在对象内(如在 DAO Fusion 框架中)。我不会把它放在服务中。如果您将其放入 DAO 或使用 Active Record Pattern 取决于您。

    我知道我的回答没有为您提供“随时可用”的框架。你在一个非常复杂的领域,从我的经验资源到这个使用场景都很难找到。感谢您的提问!但无论如何,我希望我能在你的设计中帮助你。

    在此答案中,您将找到引用书“在 SQL 中开发面向时间的数据库应用程序”,请参阅 https://stackoverflow.com/a/800516/734687

    更新:示例
  • 问题:假设我有一个 PERSON 表,它有一个名为“id”的字段的代理键。此时的每个引用表都将该“ID”作为外键约束。如果我现在添加时间列,我必须将主键更改为“id+from_date+to_date”。在更改主键之前,我必须首先将每个引用表的每个外部约束删除到这个被引用表(人)。我对吗?我相信这就是您对代理键的意思。 ID 是可以由序列生成的生成 key 。 Person表的业务键是SSN。
  • 答:不完全是。 SSN 将是一个自然 key ,我不用于对象身份。 “id+from_date+to_date”也是 composite key ,我也会避免。如果你看 example您将有两个表,人员和住所,对于我们的示例,假设我们与外键住所具有 1-n 关系。
    现在我们在每个表上添加时间字段。是的,我们删除了所有外键约束。 Person 将获得 2 个 ID,一个 ID 用于标识行(称为 ROW_ID),一个 ID 用于标识人员本身(称为 ENTIDY_ID),并带有该 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。用时间戳替换 [now] 以回到过去。

    更新以反射(reflect)您的更新
    我会推荐以下数据模型:

    引入一个“PlaysInTeam”表:
  • 编号
  • ID Team(团队外键)
  • ID播放器(播放器的外键)
  • 有效期从
  • 有效期至

  • 当您列出球队的球员时,您必须查询关系有效且必须在 [ValdFrom, ValidTo) 中的日期

    为了使团队临时化,我有两种方法;

    方法一:
    引入一个“季节”表来模拟一个季节的有效性
  • 编号
  • 季节名称(例如 2011 年夏季)
  • 来自(可能没有必要,因为每个人都知道什么时候是季节)
  • 到(可能没有必要,因为每个人都知道是什么时候)

  • 拆分团队表。您将拥有属于团队且与时间无关的字段(姓名、地址等)以及与某个赛季时间相关的字段(赢、输...)。在这种情况下,我会使用 Team 和 TeamInSeason。 PlaysInTeam 可以链接到 TeamInSeason 而不是 Team(必须考虑 - 我会让它指向 Team)

    季军
  • 编号
  • ID团队
  • ID季
  • 亏损
  • ...

  • 方法二:
    不要明确地模拟季节。拆分团队表。您将拥有属于团队且与时间无关的字段(姓名、地址等)和与时间相关的字段(赢、输...)。在这种情况下,我会使用 Team 和 TeamInterval。 TeamInterval 将具有用于间隔的字段“from”和“to”。 PlaysInTeam 可以链接到 TeamInterval 而不是 Team(我会让它在 Team 上)

    团队间隔
  • 编号
  • ID团队
  • 来自
  • 亏损
  • ...

  • 在这两种方法中:如果您不需要单独的团队表来处理与时间无关的字段,请不要拆分。

    关于java - 如何使用 JPA 实现时态表?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9542366/

    相关文章:

    java - 如何查看已安装的 Karaf 功能的内部情况

    java - Codingbat 练习 BMI 结果

    java - 对子类使用 JPA 2.0 @Inheritance 注释是否有意义?

    java - JPA/Hibernate 删除实体中的所有内容

    java - 使用 JPA 从子对象级联到父对象

    java - 尝试提取嵌套 iframe 的 src 属性时出现陈旧元素引用异常

    java - 中间件 API 的最佳实践是什么?

    java - JPA 和 derby 空指针异常

    hibernate - 如何在 JPA 2.1 中进行批量更新、删除、插入时调用回调方法,如@PreUpdate、@PrePersist、@PreRemove?

    java - 线程 "main"javax.xml.bind.PropertyException : name: eclipselink. 媒体类型值中的异常:application/json