如何在 Hibernate 中使用增量管理多对多关系的版本历史记录?
How to manage version history for Many-To-Many relationship using deltas in Hibernate?
我们正在开发一个系统,一个人可以将文件发送给另一个人,一个文件可以有多个附件,如下所示。
Document {
Set<Attachment> attachments;
}
如果 X 将文档 (Doc1,Ver1) 发送给 Y,而 Y 编辑了该文档,那么我们必须创建该文档的新版本 (Doc1,Ver2),这样 X 的发送框就不会反映所做的更改由 Y.
在我们的系统中将有数百万个文档,每个文档可以有数百个附件。此外,一份文件可以在一段时间内通过 n 个人。
我们应该能够获取任何文档版本及其当时的附件列表,所以我必须维护文档的版本,所以我立即想到的 table 结构是下面那个。
Document - id primary key, ver_id primary key
Attachment - id, doc_id foreign key, doc_ver_id foreign key
但是这个结构的问题是,如果 X 正在向 Y 发送带有 100 个附件的文档并且 Y 做了一些小的修改,那么我必须创建一个新版本并复制新版本的所有附件,其中大部分与之前版本中的相同,因为我们将拥有数百万个文档,并且每个文档都将通过 n 个人,此模型将导致非常大的附件 table,其中包含大量冗余数据。
所以我们想到了另一种附件结构,如下所示。
Document - id primary key, ver_id primary key
Attachment - id, doc_id, attached_ver_id, detached_version_id
但是我无法用这种结构为 Document 创建一个休眠实体,所以我的问题是,是否有任何其他 table 结构可以更好地解决这个问题,减少冗余,是否可能为上述 table 结构创建休眠映射。
背景
当需要保留历史数据时,通常有两种可能的方法:
方法一:克隆
创建新条目时,将从最新的现有条目复制其详细信息。
方法 #2:三角洲
第一个条目的详细信息已存储。每个后续条目都存储了对先前版本的更改。
Pros/Cons:
方法 #1 通常更简单、更快速,因为可以直接查找任何记录的详细信息,而无需构建它们。但是方法#2 使用较少的存储空间。 (可能值得注意的是,根据我的经验,方法 #1 总是更可取,因为检索的简单性和速度通常比存储更重要)。
问什么?
我的理解是您开始使用方法 #1,但现在更喜欢方法 #2。
回答
But I couldn't create a hibernate entity for Document with this
structure, so my question is, is there any other table structure which
is better equipped to solve this problem with less redundancy and is
it possible to create a hibernate mapping for the above table
structure.
完全有可能为此数据库结构创建实体 - 每个实体当然只是数据库 table 的 Hibernate class 表示。建议在文档版本和附件之间添加映射 table:
Document - id primary key, ver_id primary key, ...
Attachment - id primary key, ...
DocumentAttachmentDelta - doc_id, doc_ver_id, attachment_id, added_or_removed_flag
这里 DocumentAttachmentDelta
是一个显式定义的映射 table,@ManyToOne 关系链接到标识文档版本和附件的主键。它有一个额外的布尔标志,用于指定是否为此版本的文档删除或添加附件。因此,从上面来看,对于文档的第一个版本,将添加其所有初始附件,但对于后续版本,将仅存储增量,这可能是添加或删除。
实体详细信息(根据评论中的请求)
@Entity
class Document {
/* ...other fields... */
@OneToMany(cascade = CascadeType.ALL, mappedBy = "document", orphanRemoval = true)
List<DocumentAttachmentDelta> documentAttachmentDeltas;
}
@Entity
class Attachment {
/* ...other fields... */
@OneToMany(cascade = CascadeType.ALL, mappedBy = "attachment", orphanRemoval = true)
List<DocumentAttachmentDelta> documentAttachmentDeltas;
}
@Entity
class DocumentAttachmentDeltas {
/* ...other fields... */
@ManyToOne
Document document;
@ManyToOne
Attachment attachment;
}
或者您将 Document 和 Attachment 之间的关系定义为双方的 @ManyToMany 并确保在 Java 方回调所有附件的文档具有相同的 id 并且仅在 ver_id.
如果我真的想使用增量,我会使用以下模型。
@Entity
public class Document {
@Id
private String id;
@Lob
private byte [] firstVersion;
@OneToMany(mappedBy = "document")
private final Set<Attachment> attachments = Sets.newHashSet();
}
连同附件实体如下
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = { "document_id", "version" })
})
@Check(constraints = "(delta is null and previous_version_id is null) or (delta is not null and previous_version_id is not null)")
public class Attachment {
@Id
private Long id;
@Column(nullable = false, name = "version")
private Long version;
@Lob
@Column(name = "delta")
private byte [] delta;
@JoinColumn(name = "document_id")
@ManyToOne(optional = false)
private Document document;
@JoinColumn(name = "previous_version_id")
@ManyToOne(optional = true)
private Attachment previousVersion;
}
这样,您就拥有了用户创建的文档的原始版本。然后,每个附件引用文档的先前版本,反映增量字段中的变化。当用户向用户发送文档的一个版本(实际上只是一个附件)时,您在 person/user 实体和附件之间添加多对多。这样,可以重建文档的发送版本及其所有前身。
由于文档只能有一个初始版本,我会考虑部分唯一约束(例如 Postgres 中的部分唯一索引)的 possibility强制每个文档只有一个附件,没有增量也没有父级。但是,这不能在 Hibernate 中建模。
检查约束强制没有父版本的版本也没有增量,因为它恰好是文档实体中包含的文档的第一个版本。在这个模型中,"version" 字段不是绝对必要的,但是当你想为文档的特定版本命名并强制它在每个文档中是唯一的时,它可能很有用(参见我的 unique-constraint注释)。
不过,我可能会按如下方式解决这个问题:
@Entity
public class Document {
@Id
private String id;
@OneToMany(mappedBy = "document")
private final Set<Attachment> attachments = Sets.newHashSet();
}
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = { "document_id", "version" })
})
public class Attachment {
@Id
private Long id;
@Column(nullable = false, name = "version")
private Long version;
@Lob
@Column(name = "content")
private byte [] content;
@JoinColumn(name = "document_id")
@ManyToOne(optional = false)
private Document document;
@JoinColumn(name = "previous_version_id")
@ManyToOne(optional = true)
private Attachment previousVersion;
}
当 previous_version_id 为 null 时,我仍然需要文档的部分唯一索引,以确保每个文档只有一个初始版本。
使用这两个 os 解决方案,您可以避免任何文件克隆。对于第一个(使用增量),您可以在 space 上节省一点,因为您只存储每个文档第一个版本的完整文档内容。但是,使用第二个版本,您可以更轻松地检索任何特定版本。
我们正在开发一个系统,一个人可以将文件发送给另一个人,一个文件可以有多个附件,如下所示。
Document {
Set<Attachment> attachments;
}
如果 X 将文档 (Doc1,Ver1) 发送给 Y,而 Y 编辑了该文档,那么我们必须创建该文档的新版本 (Doc1,Ver2),这样 X 的发送框就不会反映所做的更改由 Y.
在我们的系统中将有数百万个文档,每个文档可以有数百个附件。此外,一份文件可以在一段时间内通过 n 个人。
我们应该能够获取任何文档版本及其当时的附件列表,所以我必须维护文档的版本,所以我立即想到的 table 结构是下面那个。
Document - id primary key, ver_id primary key
Attachment - id, doc_id foreign key, doc_ver_id foreign key
但是这个结构的问题是,如果 X 正在向 Y 发送带有 100 个附件的文档并且 Y 做了一些小的修改,那么我必须创建一个新版本并复制新版本的所有附件,其中大部分与之前版本中的相同,因为我们将拥有数百万个文档,并且每个文档都将通过 n 个人,此模型将导致非常大的附件 table,其中包含大量冗余数据。
所以我们想到了另一种附件结构,如下所示。
Document - id primary key, ver_id primary key
Attachment - id, doc_id, attached_ver_id, detached_version_id
但是我无法用这种结构为 Document 创建一个休眠实体,所以我的问题是,是否有任何其他 table 结构可以更好地解决这个问题,减少冗余,是否可能为上述 table 结构创建休眠映射。
背景
当需要保留历史数据时,通常有两种可能的方法:
方法一:克隆
创建新条目时,将从最新的现有条目复制其详细信息。
方法 #2:三角洲
第一个条目的详细信息已存储。每个后续条目都存储了对先前版本的更改。
Pros/Cons:
方法 #1 通常更简单、更快速,因为可以直接查找任何记录的详细信息,而无需构建它们。但是方法#2 使用较少的存储空间。 (可能值得注意的是,根据我的经验,方法 #1 总是更可取,因为检索的简单性和速度通常比存储更重要)。
问什么?
我的理解是您开始使用方法 #1,但现在更喜欢方法 #2。
回答
But I couldn't create a hibernate entity for Document with this structure, so my question is, is there any other table structure which is better equipped to solve this problem with less redundancy and is it possible to create a hibernate mapping for the above table structure.
完全有可能为此数据库结构创建实体 - 每个实体当然只是数据库 table 的 Hibernate class 表示。建议在文档版本和附件之间添加映射 table:
Document - id primary key, ver_id primary key, ...
Attachment - id primary key, ...
DocumentAttachmentDelta - doc_id, doc_ver_id, attachment_id, added_or_removed_flag
这里 DocumentAttachmentDelta
是一个显式定义的映射 table,@ManyToOne 关系链接到标识文档版本和附件的主键。它有一个额外的布尔标志,用于指定是否为此版本的文档删除或添加附件。因此,从上面来看,对于文档的第一个版本,将添加其所有初始附件,但对于后续版本,将仅存储增量,这可能是添加或删除。
实体详细信息(根据评论中的请求)
@Entity
class Document {
/* ...other fields... */
@OneToMany(cascade = CascadeType.ALL, mappedBy = "document", orphanRemoval = true)
List<DocumentAttachmentDelta> documentAttachmentDeltas;
}
@Entity
class Attachment {
/* ...other fields... */
@OneToMany(cascade = CascadeType.ALL, mappedBy = "attachment", orphanRemoval = true)
List<DocumentAttachmentDelta> documentAttachmentDeltas;
}
@Entity
class DocumentAttachmentDeltas {
/* ...other fields... */
@ManyToOne
Document document;
@ManyToOne
Attachment attachment;
}
或者您将 Document 和 Attachment 之间的关系定义为双方的 @ManyToMany 并确保在 Java 方回调所有附件的文档具有相同的 id 并且仅在 ver_id.
如果我真的想使用增量,我会使用以下模型。
@Entity
public class Document {
@Id
private String id;
@Lob
private byte [] firstVersion;
@OneToMany(mappedBy = "document")
private final Set<Attachment> attachments = Sets.newHashSet();
}
连同附件实体如下
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = { "document_id", "version" })
})
@Check(constraints = "(delta is null and previous_version_id is null) or (delta is not null and previous_version_id is not null)")
public class Attachment {
@Id
private Long id;
@Column(nullable = false, name = "version")
private Long version;
@Lob
@Column(name = "delta")
private byte [] delta;
@JoinColumn(name = "document_id")
@ManyToOne(optional = false)
private Document document;
@JoinColumn(name = "previous_version_id")
@ManyToOne(optional = true)
private Attachment previousVersion;
}
这样,您就拥有了用户创建的文档的原始版本。然后,每个附件引用文档的先前版本,反映增量字段中的变化。当用户向用户发送文档的一个版本(实际上只是一个附件)时,您在 person/user 实体和附件之间添加多对多。这样,可以重建文档的发送版本及其所有前身。
由于文档只能有一个初始版本,我会考虑部分唯一约束(例如 Postgres 中的部分唯一索引)的 possibility强制每个文档只有一个附件,没有增量也没有父级。但是,这不能在 Hibernate 中建模。
检查约束强制没有父版本的版本也没有增量,因为它恰好是文档实体中包含的文档的第一个版本。在这个模型中,"version" 字段不是绝对必要的,但是当你想为文档的特定版本命名并强制它在每个文档中是唯一的时,它可能很有用(参见我的 unique-constraint注释)。
不过,我可能会按如下方式解决这个问题:
@Entity
public class Document {
@Id
private String id;
@OneToMany(mappedBy = "document")
private final Set<Attachment> attachments = Sets.newHashSet();
}
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = { "document_id", "version" })
})
public class Attachment {
@Id
private Long id;
@Column(nullable = false, name = "version")
private Long version;
@Lob
@Column(name = "content")
private byte [] content;
@JoinColumn(name = "document_id")
@ManyToOne(optional = false)
private Document document;
@JoinColumn(name = "previous_version_id")
@ManyToOne(optional = true)
private Attachment previousVersion;
}
当 previous_version_id 为 null 时,我仍然需要文档的部分唯一索引,以确保每个文档只有一个初始版本。
使用这两个 os 解决方案,您可以避免任何文件克隆。对于第一个(使用增量),您可以在 space 上节省一点,因为您只存储每个文档第一个版本的完整文档内容。但是,使用第二个版本,您可以更轻松地检索任何特定版本。