如何使用 JPA 实现时态表?
Posted
技术标签:
【中文标题】如何使用 JPA 实现时态表?【英文标题】:How to implement a temporal table using JPA? 【发布时间】:2012-03-21 11:47:54 【问题描述】:我想知道如何使用 EclipseLink 在 JPA 2 中实现 temporal tables。我所说的时间是指定义有效期的表格。
我面临的一个问题是引用表不能再对被引用表(临时表)有外键约束,因为被引用表的性质现在它们的主键包括有效期。
如何映射我的实体之间的关系? 这是否意味着我的实体不能再与那些有效时间实体建立关系? 现在应该由我在某种服务或专门的 DAO 中手动执行初始化这些关系的职责吗?我发现的唯一东西是一个名为 DAO Fusion 的框架,它可以处理这个问题。
还有其他方法可以解决这个问题吗? 您能否提供有关此主题的示例或资源(带有时态数据库的 JPA)?这是一个数据模型及其类的虚构示例。它从一个不需要处理时间方面的简单模型开始:
第一个场景:非时间模型
数据模型:
团队:
@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
开始。
第二个场景:时间模型
数据模型:
如您所见,我们必须删除主键并定义另一个包含日期(句点)的主键。我们还必须删除唯一约束,因为现在它们可以在表中重复。现在该表可以包含当前条目以及历史记录。
如果我们还必须让 Team 临时化,事情会变得非常糟糕,在这种情况下,我们需要将 Player
表必须的外键约束删除到 Team
。问题是如何在 Java 和 JPA 中对其进行建模。
请注意,ID 是代理键。但现在代理键必须包含日期,因为如果不包含日期,则不允许存储同一实体的多个“version”(在时间轴期间)。
【问题讨论】:
1) 您是用什么工具绘制图表的? 2) 一个时间维度足以满足您的要求,DAOFusion 模式和我的答案(基于这些模式)在我看来都过大了 3) 您是否更喜欢仅将时间方面添加到 Player 的解决方案,或者您更喜欢它两个表 4)你的最后一段是错误的。代理键永远不会包含其他字段。在这种情况下,您将有两个代理键。 @ChrLipp 1) Sparx Enterprise Architect 2) 我同意。 3)我需要一个解决方案,为两个表添加时间。 4)我不同意这不是代理键。我认为它是一个代理键,因为: 1. 在添加临时列之前,它是一个代理键,它是一个没有业务意义的键。例如 Player 的业务键是“team_id”和“number”,而 Team 的业务键是“name”。当它们没有时间列时,它们都有自己的代理键“id”。问题是当我添加不再起作用的时间列时。同一个条目可以在同一个表中出现多次。 这就是为什么代理键“id”本身不能再只有一列了,因为它是同一个条目,但在不同的时间线中跟踪,所以为了让同一个条目出现更多不止一次,我可以将以下内容添加为主键“id+validstart”或“id+validend”或“id+validstart+validend”。为了方便起见,我在 Java 映射中选择了最后一个选项,其中我有一个定义句点的“间隔”对象,因此为了在 JPA 中映射它,我将“间隔”作为 EmbeddedId 添加到 Id。 你可以在en.wikipedia.org/wiki/Surrogate_key看到这个"由于数据库中可能有多个对象对应一个代理,我们不能使用代理作为主键;需要另一个属性,除了代理,以唯一标识每个对象。” 明白你的意思。错误是我的英语不好,我认为代理键是“生成的而不是自然的键”。现在我知道它是实体键而不是主键。所以我的回答是:你不需要它。检查(并评论)我更新的答案。只需提供一个生成的密钥,它是没有validstart 或validend 的主键。 【参考方案1】:您似乎无法使用 JPA 来执行此操作,因为它假定表名和整个架构是静态的。
最好的选择是通过 JDBC 来实现(例如使用 DAO 模式)
如果性能是问题,除非我们谈论的是数千万条记录,否则我怀疑动态创建类并编译它然后加载它会更好。
另一个选项可能是使用视图(如果您必须使用 JPA)可能是以某种方式抽象表(映射 @Entity(name="myView"),那么您必须动态更新/替换视图,如CREATE OR 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.
【讨论】:
【参考方案2】:在DAO Fusion 中,通过BitemporalWrapper
包装该实体来实现在两个时间线(有效性和记录间隔)中跟踪实体。
bitemporal reference documentation 提供了一个示例,其中常规Order
实体被BitemporalOrder
实体包装。 BitemporalOrder
映射到一个单独的数据库表,列用于有效性和记录间隔,以及对每个表行的 Order
(通过 @ManyToOne
)的外键引用。
文档还指出每个双时态包装器(例如BitemporalOrder
)代表双时态记录链中的一个项目。因此,您需要一些包含双时间包装集合的更高级别的实体,例如包含@OneToMany Collection<BitemporalOrder> orders
的Customer
实体。
因此,如果您需要对“逻辑子”实体(例如 Order
或 Player
)进行双向跟踪,并对其“逻辑父”实体(例如 Customer
或 Team
)进行双向跟踪同样,您需要为两者提供双时间包装器。您将拥有BitemporalPlayer
和BitemporalTeam
。 BitemporalTeam
可以声明 @OneToMany Collection<BitemporalPlayer> players
。但是如上所述,您需要一些更高级别的实体来包含@OneToMany Collection<BitemporalTeam> teams
。为了
例如,您可以创建一个包含BitemporalTeam
集合的Game
实体。
但是,如果您不需要记录间隔而只需要有效间隔(例如,不是对实体进行双时域跟踪,而是单时域跟踪),那么最好的办法是推出您自己的自定义实现。
【讨论】:
【参考方案3】:我对这个话题很感兴趣。我多年来一直致力于开发使用这些模式的应用程序,在我们的案例中,这个想法来自德国文凭论文。
我不知道“DAO Fusion”框架,它们提供了有趣的信息和链接,感谢您提供这些信息。尤其是pattern page 和aspects page 很棒!
对于您的问题:不,我不能指出其他网站、示例或框架。恐怕您必须使用DAO Fusion框架或自己实现此功能。您必须区分您真正需要哪种功能。用“DAO Fusion”框架来说:需要“valid temporal”和“record temporal”吗?当更改应用于您的数据库时记录时间状态(通常用于审计问题),当更改发生在现实生活中或在现实生活中有效(由应用程序使用)时的有效时间状态可能与记录时间不同。在大多数情况下,一维就足够了,而第二维则不需要。
无论如何,时间功能都会对您的数据库产生影响。正如您所说:“现在他们的主键包括有效期”。那么如何对实体的身份进行建模呢?我更喜欢使用surrogate keys。在这种情况下,这意味着:
实体的一个 ID 数据库中对象的一个 id(行) 时间列表的主键是对象 id。每个实体在表中都有一个或多个 (1-n) 条目,由对象 id 标识。表之间的链接基于实体 ID。由于时间条目乘以数据量,标准关系不起作用。标准的 1-n 关系可能会变成 x*1-y*n 关系。
你如何解决这个问题?标准方法是引入一个映射表,但这不是一种自然的方法。仅仅为了编辑一个表(例如发生住所变化),您还必须更新/插入映射表,这对每个程序员来说都是陌生的。
另一种方法是不使用映射表。在这种情况下,您不能使用参照完整性和外键,每个表都是独立的,从一个表到另一个表的链接必须手动实现,而不是使用 JPA 功能。
初始化数据库对象的功能应该在对象内部(如在 DAO Fusion 框架中)。我不会把它放在服务中。是否将其提供给 DAO 或使用 Active Record 模式取决于您。
我知道我的回答并未为您提供“即用型”框架。你处于一个非常复杂的领域,从我的经验资源到这个使用场景都很难找到。谢谢你的提问!但无论如何,我希望我对你的设计有所帮助。
在此答案中,您将找到参考书“Developing Time-Oriented Database Applications in SQL”,请参阅https://***.com/a/800516/734687
更新:示例
问题:假设我有一个 PERSON 表,它有一个代理键,它是一个名为“id”的字段。此时每个引用表都将具有该“ID”作为外键约束。如果我现在添加时间列,我必须将主键更改为“id+from_date+to_date”。在更改主键之前,我必须首先将每个引用表的每个外部约束都删除到这个被引用的表(Person)。我对吗?我相信这就是您对代理键的意思。 ID 是可以由序列生成的生成密钥。 Person 表的业务键是 SSN。 回答:不完全是。 SSN 将是一个自然密钥,我不将其用于对象身份。 “id+from_date+to_date”也是composite key,我也会避免。如果您查看example,您将有两张表,即person 和residence,在我们的示例中,假设我们与外键residence 有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
用于隐藏记录(也称为技术)信息的查询。这显示了实体的所有有效更改。
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] 替换为时间戳以返回时间。
更新以反映您的更新 我会推荐以下数据模型:
引入“PlaysInTeam”表:
身份证 ID 团队(团队的外键) ID 播放器(播放器的外键) 有效期自 有效期至当您列出球队的球员时,您必须查询关系有效的日期并且必须在 [ValdFrom, ValidTo)
为了让团队暂时化,我有两种方法;
方法一: 引入一个模拟季节有效性的“季节”表
身份证 季节名称(例如 2011 年夏季) 从(也许没有必要,因为每个人都知道季节是什么时候) 到(也许没必要,因为每个人都知道什么时候到)拆分团队表。您将拥有属于球队但与时间无关的字段(姓名、地址……)和与赛季时间相关的字段(胜利、失败……)。在这种情况下,我会使用 Team 和 TeamInSeason。 PlaysInTeam 可以链接到 TeamInSeason 而不是 Team(必须考虑 - 我会让它指向 Team)
团队赛季
身份证 ID 团队 ID 季节 赢 损失 ...方法 2: 不要明确地模拟季节。拆分团队表。您将拥有属于团队且与时间无关的字段(姓名、地址……)和与时间相关的字段(胜利、失败……)。在这种情况下,我会使用 Team 和 TeamInterval。 TeamInterval 将具有用于间隔的字段“from”和“to”。 PlaysInTeam 可以链接到 TeamInterval 而不是 Team(我会让它加入 Team)
团队间隔
身份证 ID 团队 发件人 到 赢 损失 ...在这两种方法中:如果您不需要为没有时间相关字段的单独团队表,请不要拆分。
【讨论】:
假设我有一个 PERSON 表,它有一个代理键,它是一个名为“id”的字段。此时每个引用表都将具有该“ID”作为外键约束。如果我现在添加时间列,我必须将主键更改为“id+from_date+to_date”。在更改主键之前,我必须首先将每个引用表的每个外部约束都删除到这个被引用的表(Person)。我对吗?我相信这就是您对代理键的意思。 ID 是可以由序列生成的生成密钥。 Person表的业务键是SSN。 用我们的 cmets 更新了答案。【参考方案4】:不完全确定您的意思,但 EclipseLink 完全支持历史记录。您可以通过 @DescriptorCustomizer 在 ClassDescriptor 上启用 HistoryPolicy。
【讨论】:
不同之处在于时间方法使用一个表而不是历史表,并且 EclipseLink 只支持一维而不是二维。但无论如何,谢谢你的信息。我不知道这个功能。以上是关于如何使用 JPA 实现时态表?的主要内容,如果未能解决你的问题,请参考以下文章