在域模型中JPA拼写灾难的上下文中天真地使用java枚举'序数

时间:2013-09-25 11:09:48

标签: java hibernate jpa enums ordinals

我想描述一下在JPA实体中天真地使用Java枚举时出现的令人讨厌的问题。我们来看看这个问题是如何发生的。

首先是域名模型

假设我有一个Text JPA实体代表一段文字(小说,新闻文章等)。这是JPA实体:

@Entity
public class Text {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @Version
    @Column(name = "version")
    private Integer version;

    private String content;

    @Enumerated
    @ElementCollection
    private Set<Style> styles;

    //Setters and getters omitted.

对于Text的实例,可以应用一种或多种样式,例如斜体,粗体等。该样式表示为java enum。

首先,我们假设应用程序以以下枚举开始生命

public enum Style {
  BOLD, ITALIC
}

然后,下面的测试将在关系数据库中插入以下行:

集成测试:

@Test
@Rollback(value=false)
public void inEarlyLifePersist() {
    Text text =new  Text();
    text.setContent("This is my beautiful novel...");
    text.setStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    text.persist();
}

文本表格中的数据:

# id, content, version
11, This is my beautiful novel..., 0

* text_style表中的数据:*

# text, styles
11, 0
11, 1

然后,稍后,一些不明智的开发人员决定向我们的STRIKE_THROUGH枚举添加新样式Style,将此新枚举常量/值作为第一个之一:

public enum Style {
    STRIKE_THROUGH, BOLD, ITALIC
}

然后是new record is inserted in DB as follows

    @Test
    @Rollback(value=false)
    public void afterChangeToEnumPersist() {
        Text text =new  Text();
        text.setContent("This is my beautiful short story...");
        text.setStyles(EnumSet.of(Style.STRIKE_THROUGH, Style.BOLD));
        text.persist();
    }

在文字表格中:

# id, content, version
14, This is my beautiful short story..., 0

和*在text_style表中:*

# text, styles
14, 0
14, 1

显然,域名模型现在严重受损!

我的问题是,如上所述,在域中避免拼写灾难的可能策略是什么 (除了放置{{1}的明显解决方案之外}枚举常量 STRIKE_THROUGH后)?

编辑1 :显然我不想在数据库中存储字符串(请参阅ITALIC),原因很明显,即数据检索和存储性能会受到严重影响!

4 个答案:

答案 0 :(得分:3)

您需要重新定义enum,如下所示。

public enum Style {
    STRIKE_THROUGH(2), BOLD(0), ITALIC(1)

    Style(int code){
      this.code=code;
    }
}

并实施Hibernate User type以保留code

答案 1 :(得分:1)

有一个选项(EnumType.STRING)来使用枚举值的实际名称(由{name()}返回的字符串而不是序数。这样你可以重新组织你的枚举值,但是你被绑定到枚举值的名称。

理想的解决方案是能够声明性地告诉JPA实现使用枚举的任意属性作为数据库标识符。但AFAIK,它在当前的JPA规范中没有提供,在未来的JPA规范中有这样的功能会很棒。

Sajan的回答展示了如何使用特定于Hibernate的功能来实现它。

答案 2 :(得分:1)

Enumerated注释也知道指定EnumType的属性。存在两种类型:EnumType.ORDINALEnumType.STRING。 ORDINAL是默认值。

所以,如果你按照以下方式进行

    @Enumerated(EnumType.STRING)

您将在DB列中看到枚举名称(而不是序数)。当然,您现在容易受到枚举中名称更改的影响。你必死一死,但我认为,名字更好。

答案 3 :(得分:1)

我不明白为什么人们发现枚举名称比其序数更可靠。实际上,有很多很好的理由来重命名枚举(修正错别字,因政治或政治正确性而改名等),但我看不出有任何理由重新安排它们。

重命名和重新排序都会发生,唯一可以帮助的是测试。不幸的是,我能想到的最佳测试将在任何变化时失败。幸运的是,测试可以判断发生了什么,然后很容易修复。

public void testE1IsStable() {
    assertEnumUnchanged(E1.class, 4, "bec419c8380dbe9ec3b86a7023a55107");
}

public void testE2IsStable() {
    assertEnumUnchanged(E2.class, 3, "1e89e93c6cbdbb7311b814c19d682548");
}

private void assertEnumUnchanged(Class<? extends Enum<?>> enumClass, int expectedCount, String expectedHash) {
    final Object[] enumConstants = enumClass.getEnumConstants();
    if (expectedCount < enumConstants.length) {
        final Object[] shortened = Arrays.copyOf(enumConstants, expectedCount);
        assertEquals("Enum constants may be only appended! Ask balteo!",
            expectedHash, hashAsString(shortened));
        fail("An enum constant has been added! This test needs to be updated. Ask balteo!");
    } else if (expectedCount > enumConstants.length) {
        fail("Enum constants must not be removed! Ask balteo!");
    } else {
        assertEquals("Enum constants must not be reordered! If they get renamed, this test must be updated. Ask balteo!",
            expectedHash, hashAsString(enumConstants));
    }
}

private String hashAsString(Object[] enumConstants) {
    final Hasher hasher = Hashing.md5().newHasher();
    for (final Object o : enumConstants) hasher.putUnencodedChars(o.toString());
    return hasher.hash().toString();
}