![]() |
Здравствуйте, гость ( Вход | Регистрация )
![]() ![]() |
![]() |
![]()
Сообщение
#1
|
|
![]() Администратор ![]() ![]() ![]() ![]() ![]() Группа: Главные администраторы Сообщений: 14349 Регистрация: 12.10.2007 Из: Twilight Zone Пользователь №: 1 ![]() |
Наверняка многие из вас сталкивались с проблемой хранения перечислений в базе данных, возникающей при попытке реализации удобного способа работы с разного рода служебными справочниками — статусами, типами объектов и так далее.
Суть её очень проста: если хранить перечисления как сущности (@Entity), то с ними получается крайне неудобно работать, база данных нагружается лишними запросами даже несмотря на кэширование, а сами запросы к БД усложняются лишними JOIN'ами. Если же перечисление определять как enum, то с ними становится удобно работать, но возникает проблема синхронизации с базой данных и отслеживания ошибок таковой синхронизации. Особенно актуально это в том случае, когда поле, содержащее перечисление, аннотировано как @Enumerated(EnumType.ORDINAL) — всё мгновенно ломается при смене порядка объявления значений. Если же мы храним значения в строковом виде — как @Enumerated(EnumType.STRING) — возникает проблема скорости доступа, так как индексы по строковым полям менее эффективны и занимают больше места. Более того, вне зависимости от способа хранения значения поля при отсутствии в базе данных таблицы со списком допустимых значений мы никак не защищены от некорректных или устаревших данных и, как следствие, проблем. Однако сама идея хранения в базе данных заманчива простотой построения запросов вручную, очень ценной при отладке ПО или решении сложных ситуаций. Когда можно написать не просто SELECT id, title FROM product WHERE status = 5, а, скажем, SELECT id, title FROM product JOIN status ON status.id = product.status_id WHERE status.code = 'NEW' — это очень ценно. В том числе и тем, что мы всегда можем быть уверены в том, что status_id содержит корректное значение, если поставим FOREIGN KEY. На самом деле, существует очень простое и изящное решение этой проблемы, убивающее сразу всех зайцев. Решение это основывается на простом хаке, который, хоть и хак, не привносит никаких сайд-эффектов. Как известно, перечисления в Java — всего лишь синтаксический сахар, внутри представленый всё теми же экземплярами классов, порождённых от java.lang.Enum. И вот как раз в последнем есть чудесное поле ordinal, объявленное как private, которое и хранит значение, возвращаемое методом ordinal() и используемое ORM для помещения в базу. Нам всего лишь надо прочитать из справочника в базе данных актуальный идентификатор элемента перечисления и поместить его в это поле. Тогда мы сможем использовать штатным образом EnumType.ORDINAL для хранения в базе с быстрым и удобным доступом, сохраняя таким образом все прелести собственно Enum'ов в Java, и не иметь проблем с синхронизацией идентификаторов и их актуальностью. Тут может показаться, что такой подход рождает проблемы с сериализацией объектов, однако это не так, ибо спецификация платформы Java дословно говорит нам следующее: 1.12. Serialization of Enum Constants То есть при сериализации перечисления всегда преобразуются в строковую форму, а числовое значение игнорируется. Вуаля! А теперь немного практики. Для начала, определим модель данных для нашего примера: CREATE SEQUENCE status_id; CREATE SEQUENCE product_id; CREATE TABLE status ( id INTEGER NOT NULL DEFAULT NEXT VALUE FOR status_id, code CHARACTER VARYING (32) NOT NULL, CONSTRAINT status_pk PRIMARY KEY (id), CONSTRAINT status_unq1 UNIQUE KEY (code) ); INSERT INTO status (code) VALUES ('NEW'); INSERT INTO status (code) VALUES ('ACTIVE'); INSERT INTO status (code) VALUES ('DELETED'); CREATE TABLE product ( id INTEGER NOT NULL DEFAULT NEXT VALUE FOR product_id, status_id INTEGER NOT NULL, title CHARACTER VARYING (128) NOT NULL, CONSTRAINT product_pk PRIMARY KEY (id), CONSTRAINT product_unq1 UNIQUE KEY (title), CONSTRAINT product_fk1 FOREIGN KEY (status_id) REFERENCES status (id) ON UPDATE CASCADE ON DELETE RESTRICT ); CREATE INDEX product_fki1 ON product (status_id); Теперь опишем эту же схему данных на Java. Обратите внимание, что в данном случае определяется и перечисление, и класс сущности для справочника. Чтобы избежать повторения однообразного кода, справочники для перечислений наследуются от SystemDictionary. Также обратите внимание на аннотацию @MappedEnum, которая будет нами использоваться в дальнейшем, чтобы определять, какие перечисления отражены на базу данных. public enum Status { NEW, ACTIVE, DELETED } @Retention(value = RetentionPolicy.RUNTIME) public @interface MappedEnum { Class<? extends Enum> enumClass(); } @MappedSuperclass public class SystemDictionary { @Id @GeneratedValue(generator = "entityIdGenerator") @Column(name = "id", nullable = false, unique = true) private Integer id; @Column(name = "code", nullable = false, unique = true, length = 32) private String code; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } } @Entity @Table(name = "status") @SequenceGenerator(name = "entityIdGenerator", sequenceName = "status_id") @MappedEnum(enumClass = Status.class) public class StatusEx extends SystemDictionary { } @Entity @Table(name = "product") @SequenceGenerator(name = "entityIdGenerator", sequenceName = "product_id") public class Product { @Id @GeneratedValue(generator = "entityIdGenerator") @Column(name = "id", nullable = false, unique = true) private Integer id; @Column(name = "status_id", nullable = false, unique = false) @Enumerated(EnumType.ORDINAL) private Status status; @Column(name = "title", nullable = false, unique = true) private String title; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Status getStatus() { return status; } public void setStatus(Status status) { this.status = status; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } Теперь нам всего лишь осталось прочитать значения из базы данных и записать их в поле ordinal, а также не забыть обновить ещё и массив values, чтобы можно было получать экземпляры перечисления по индексу из getEnumConstants() — это не только используется тем же Hibernate при работе с перечислениями, но и просто местами очень удобно. Сделать это можно сразу после инициализации подключения к базе данных использованием примерно такого кода: public interface SessionAction { void run(Session session); } public class EnumLoader implements SessionAction { @Override public void run(Session session) { Iterator<PersistentClass> mappingList = configuration.getClassMappings(); while (mappingList.hasNext()) { PersistentClass mapping = mappingList.next(); Class<?> clazz = mapping.getMappedClass(); if (!SystemDictionary.class.isAssignableFrom(clazz)) continue; if (!clazz.isAnnotationPresent(MappedEnum.class)) continue; MappedEnum mappedEnum = clazz.getAnnotation(MappedEnum.class); updateEnumIdentifiers(session, mappedEnum.enumClass(), (Class<SystemDictionary>) clazz); } } private void updateEnumIdentifiers( Session session, Class<? extends Enum> enumClass, Class<? extends SystemDictionary> entityClass) { List<SystemDictionary> valueList = (List<SystemDictionary>) session.createCriteria(entityClass).list(); int maxId = 0; Enum[] constants = enumClass.getEnumConstants(); Iterator<SystemDictionary> valueIterator = valueList.iterator(); while (valueIterator.hasNext()) { SystemDictionary value = valueIterator.next(); int valueId = value.getId().intValue(); setEnumOrdinal(Enum.valueOf(enumClass, value.getCode()), valueId); if (valueId > maxId) maxId = valueId; } Object valuesArray = Array.newInstance(enumClass, maxId + 1); for (Enum value : constants) Array.set(valuesArray, value.ordinal(), value); Field field; try { field = enumClass.getDeclaredField("$VALUES"); field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(null, valuesArray); } catch (Exception ex) { throw new Exception("Can't update values array: ", ex); } } private void setEnumOrdinal(Enum object, int ordinal) { Field field; try { field = object.getClass().getSuperclass().getDeclaredField("ordinal"); field.setAccessible(true); field.set(object, ordinal); } catch (Exception ex) { throw new Exception("Can't update enum ordinal: " + ex); } } } Как видно, мы просто получаем из Hibernate полный список классов, отражённых на базу данных, отбираем из них все, наследуемые от объявленного выше SystemDictionary и, одновременно, содержащие аннотацию @MappedEnum, после чего обновляем числовые значения экземпляров класса перечисления. Собственно, на этом всё. Теперь мы спокойно можем:
Для достижения полного дзена можно (и нужно) добавить также проверку того, что в базе данных не содержится лишних значений, то есть что таблица-справочник и объявление enum в коде синхронизированы. Данный подход используется нами (Open Source Technologies) в достаточно крупных системах (от полумиллиона строк исходного кода и больше) с распределённой сервис-ориентированной архитектурой на базе JMS и очень хорошо себя показал — как в части удобства использования, так и в части надёжности. Чего и вам желаю ![]() Original source: habrahabr.ru (comments, light). Читать дальше -------------------- |
|
|
![]() ![]() |
Текстовая версия | Сейчас: 11.4.2025, 22:37 | |