如何使用 JPA 实现时态表?

我想知道如何使用 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;
    }


}

测试类:

@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 成为临时的,那么事情就会变得非常丑陋,在这种情况下,我们需要删除该表必须的外键约束。问题是你如何在Java和JPA中建模。PlayerTeam

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


答案 1

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

我不知道“DAO Fusion”框架,它们提供了有趣的信息和链接,感谢您提供此信息。特别是模式页面方面页面很棒!

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

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

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

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

您如何解决这个问题?标准方法是引入映射表,但这不是一种自然而然的方法。只是为了编辑一个表(例如,发生驻地更改),您还必须更新/插入映射表,这对每个程序员来说都是奇怪的。

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

初始化数据库对象的功能应该在对象中(如在 DAO 融合框架中)。我不会把它放在服务中。如果您将其放入DAO或使用活动记录模式取决于您。

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

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

更新:示例

  • 问:假设我有一个 PERSON 表,它有一个代理键,这是一个名为“id”的字段。此时,每个引用表都将该“ID”作为外键约束。如果我现在添加时态列,则必须将主键更改为“id+ from_date + to_date”。在更改主键之前,我必须首先将每个引用表的每个外部约束都删除到此引用的表(Person)中。我说的对吗?我相信这就是你对代理密钥的意思。ID 是可以由序列生成的生成密钥。“人员”表的业务键是 SSN。
  • 答:不完全是。SSN将是一个自然的密钥,我不使用它来识别objcet身份。此外,“id+from_date+to_date”将是一个复合键,我也会避免。如果您看一下这个例子,你会有两个表,人与居住地,在我们的例子中,假设我们与一个外国键住所有1-n关系。现在,我们在每个表上添加时态字段。是的,我们删除每个外键约束。Person将获得2个ID,一个ID用于标识行(称为ROW_ID),一个ID用于标识该人本身(称为ENTIDY_ID),该ID在该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 播放器(玩家的外键)
  • 有效起价
  • 有效期至

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

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

方法 1:引入一个“季节”表,该表模拟季节的有效性

  • 编号
  • 季节名称(例如。2011年夏季)
  • 从(也许没有必要,因为每个人都知道季节是什么时候)
  • 到(也许没有必要,因为每个人都知道季节是什么时候)

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

TeamInSeason

  • 编号
  • 身份识别团队
  • ID 季节
  • 损失
  • ...

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

团队介入

  • 编号
  • 身份识别团队
  • 损失
  • ...

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


答案 2

不完全确定你的意思,但EclipseLink完全支持历史。您可以通过@DescriptorCustomizer在类描述符上启用历史记录策略。


推荐