在运行时扩展JPA实体数据

5
我需要允许客户端用户在运行时扩展JPA实体中包含的数据。换句话说,我需要在实体表中在运行时添加一个“虚拟列”。此“虚拟列”仅适用于特定的“数据行”,并且可能有相当多的这些“虚拟列”。因此,我不想在数据库中创建实际的附加列,而是想利用表示这些“虚拟列”的额外实体。
例如,考虑以下情况。我有一个名为“Company”的实体,其中有一个标记为“Owner”的字段,其中包含对“Company”的所有者的引用。在运行时,客户端用户决定属于特定“Owner”的所有“Companies”都应具有名为“ContactDetails”的额外字段。
我的初步设计使用了两个额外的实体来完成此操作。第一个实体基本上表示“虚拟列”,并包含诸如字段名称和预期值类型的信息。其他实体表示实际数据,并将实体行连接到“虚拟列”。例如,第一个实体可能包含数据“ContactDetails”,而第二个实体则包含说“555-5555”。
这是正确的执行方式吗?是否有更好的选择?还有,最容易的方法是在加载原始实体时“自动”加载这些数据?我希望我的DAO调用与其“扩展”一起返回实体。
5个回答

3
也许一个更简单的选择是为每个公司添加一个CLOB列,并将扩展名作为XML存储。与您的解决方案相比,这里有一组不同的权衡,但只要额外的数据不需要SQL访问(没有索引、fkeys等),它可能会比您现在做的更简单。
这也意味着,如果您对额外的数据有一些花哨的逻辑,您需要以不同的方式实现它。例如,如果您需要所有可能的扩展类型列表,您需要单独维护它。或者如果您需要搜索功能(按电话号码查找客户),您将需要lucene或类似的解决方案。
如果您感兴趣,我可以详细说明。
编辑:
为了启用搜索,您需要像lucene这样的东西,它是一个在任意数据上进行自由文本搜索的优秀引擎。还有hibernate-search,它使用注释等将lucene直接集成到hibernate中-我没有使用过,但我听说它很好。
获取/写入/访问数据时,您基本上在处理 XML,因此任何 XML 技术都应适用。最好的方法实际上取决于内容和如何使用它。我建议研究 XPath 进行数据访问,并可能研究定义自己的 hibernate usertype,以便所有访问都封装到类中而不仅仅是普通字符串。

谢谢您的回答,我非常有兴趣听更多。我甚至没有考虑过这样的选项。额外的数据不需要是 SQL 可访问的,但应该是可搜索的。我不确定如何能够“自动”将此数据加载到实体中[以便我可以调用 company.getExtensions().get("someField");],考虑到实体有时会通过 Hibernate 的获取进行加载? - Zecrates

1

我遇到的问题比我预期的要多,因此我决定降低第一次迭代的要求。我目前正在尝试仅允许在整个“公司”实体上使用这样的扩展,换句话说,我放弃了整个所有者的要求。因此,问题可以重新表述为“如何在运行时向实体添加虚拟列(在另一个实体中的条目,像额外的列)?”

我的当前实现如下(过滤掉不相关的部分):

@Entity
class Company {
  // The set of Extension definitions, for example "Location"
  @Transient
  public Set<Extension> getExtensions { .. }

  // The actual entry, for example "Atlanta"
  @OneToMany(fetch = FetchType.EAGER)
  @JoinColumn(name = "companyId")
  public Set<ExtensionEntry> getExtensionEntries { .. }
}

@Entity
class Extension {
  public String getLabel() { .. }
  public ValueType getValueType() { .. } // String, Boolean, Date, etc.
}

@Entity
class ExtensionEntry {
  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "extensionId")
  public Extension getExtension() { .. }

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "companyId", insertable = false, updatable = false)
  public Company getCompany() { .. }

  public String getValueAsString() { .. }
}

现有的实现允许我加载一个公司实体,Hibernate将确保所有其扩展条目也被加载,并且我可以访问对应于这些扩展条目扩展。换句话说,如果我想在网页上显示此附加信息,我可以按以下方式访问所有必需的信息:

Company company = findCompany();
for (ExtensionEntry extensionEntry : company.getExtensionEntries()) {
  String label = extensionEntry.getExtension().getLabel();
  String value = extensionEntry.getValueAsString();
}

然而,这里有许多问题。首先,当使用FetchType.EAGER与@OneToMany时,Hibernate使用外连接,因此会返回重复的公司(每个ExtensionEntry一个)。可以通过使用Criteria.DISTINCT_ROOT_ENTITY来解决这个问题,但这反过来又会导致我的分页出现错误,因此这不是一个可接受的答案。另外一个选择是将FetchType更改为LAZY,但这意味着我将始终需要“手动”加载ExtensionEntries。就我所知,例如,如果我加载100个Companies的列表,我必须循环查询每个公司,生成100个SQL语句,这在性能上是不可接受的。

另一个问题是,理想情况下,我希望在加载公司时加载所有的扩展。我的意思是说,我希望@Transient getter命名为getExtensions(),以返回任何公司的所有扩展。问题在于,公司和扩展之间没有外键关系,因为扩展不适用于任何单个公司实例,而是适用于所有公司。目前,我可以通过像下面展示的代码来解决这个问题,但是当访问引用实体时(例如,如果我有一个实体Employee,它引用了Company,则通过employee.getCompany()检索到的Company将不会加载扩展):
List<Company> companies = findAllCompanies();
List<Extension> extensions = findAllExtensions();
for (Company company : companies) {
  // Extensions are the same for all Companies, but I need them client side
  company.setExtensions(extensions); 
}

目前我就在这个位置,不知道如何继续以解决这些问题。 我想我的整个设计可能有缺陷,但我不确定还有其他什么方法可以尝试。

欢迎提出任何想法和建议!


0

在我看来,使用EAV模式是一个糟糕的选择,因为它会导致性能问题和报表生成问题(需要进行多个连接操作)。在寻找解决方案时,我在这里找到了其他的东西:http://www.infoq.com/articles/hibernate-custom-fields


0
使用模式装饰器并将实体隐藏在decoratorClass中。

1
嗨,我不太确定你的意思,或者说,我知道什么是装饰器模式,但我不确定它如何帮助解决我的问题。您能详细说明一下吗? - Zecrates

0

关于公司、合作伙伴和客户的示例实际上是多态性的一个很好的应用,这可以通过JPA的继承来支持:您将有以下3种策略可供选择:单表、每个类一张表和联接。您的描述听起来更像联接策略,但不一定。

您还可以考虑只有一对一(或零)关系。然后,您需要为虚拟列的每个值都有这样的关系,因为它们的值代表不同的实体。因此,您将与合作伙伴实体建立关系,并与客户实体建立另一个关系,其中任何一个、两个或都可以为空。


这个例子事后看来很糟糕。我不关心我的设计中的继承部分,而是想找到一种允许用户在运行时创建“虚拟列”的方法。我不确定我是否理解通过创建客户和合作伙伴实体来帮助实现这一点,因为我仍然需要提供一种方法让用户创建“虚拟列”,只不过这次是在合作伙伴和客户实体上。 - Zecrates

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接