Наивное использование порядкового номера 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.