Наивное использование порядкового номера java-перечислений в контексте орфографической катастрофы JPA в доменной модели

Я хотел бы описать неприятную проблему, возникающую при наивном использовании перечислений Java в контексте сущностей JPA. Давайте посмотрим, как эта проблема может возникнуть.

Сначала модель домена:

Скажи у меня есть 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.

Для начала предположим, что приложение начинает свою жизнь со следующего перечисления:

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 enum помещает эту новую константу / значение enum в качестве первой:

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

Очевидно, что модель предметной области сейчас серьезно скомпрометирована!

Мой вопрос заключается в том, каковы возможные стратегии, позволяющие избежать орфографической катастрофы в домене, как в случае выше (кроме очевидного решения для размещения STRIKE_THROUGH константа enum после ITALIC)?

редактировать 1: Очевидно, я не хочу хранить строки (см. EnumType.STRING) в моей базе данных по очевидным причинам производительности, т. е. производительность поиска и хранения данных будет серьезно затронута!

4 ответа

Вы должны переопределить ваш enum как ниже.

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

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

И реализовать тип пользователя Hibernate, чтобы сохранить code,

Я не понимаю, почему люди считают имена перечислений более надежными, чем их порядковые номера. На самом деле, есть много веских причин для переименования перечислений (исправление опечаток, изменение имен из-за политики или политкорректности и т. Д.), Но я не вижу веских причин для их переупорядочения.

Происходит как переименование, так и переупорядочение, и единственное, что может помочь, - это тест. К сожалению, лучший тест, который я могу придумать, провалится при любых изменениях. К счастью, тест может сказать, что произошло, а затем легко исправить.

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();
}

Аннотация Enumerated также знает свойство, которое определяет тип EnumType. Существуют два типа: EnumType.ORDINAL а также EnumType.STRING, ORDINAL является значением по умолчанию.

Итак, если вы делаете это следующим образом

    @Enumerated(EnumType.STRING)

вы увидите имена перечислений в столбце БД (а не порядковые номера). Конечно, теперь вы уязвимы для изменения имени в вашем перечислении. Ты должен умереть одной смертью, но я думаю, имена лучше.

Существует опция (EnumType.STRING), чтобы использовать фактическое имя значения перечисления (String, возвращаемое { name() } вместо порядкового номера. Таким образом, вы можете реорганизовать значения перечисления, но затем вы будете привязаны к именам значения перечисления.

Идеальным решением было бы иметь возможность декларативно указать реализации JPA использовать произвольное свойство enum в качестве идентификатора базы данных. Но AFAIK, что это не предусмотрено в текущих спецификациях JPA, было бы здорово иметь такую ​​функцию в будущих спецификациях JPA.

Ответ Саджана показывает, как реализовать это, используя особенность Hibernate.

Другие вопросы по тегам