Скорая Компьютерная Помощь г. Калуга

Полный спектр компьютерных услуг!

Здравствуйте, гость ( Вход | Регистрация )

> Внимание!

  • Вся информация, расположенная в данном и других разделах форума получена из открытых источников (интернет-ресурсы, средства массовой информации, печатные издания и т.п.) и/или добавлена самими пользователями. Администрация форума предоставляет его участникам площадку для общения / размещения файлов / статей и т.п. и не несет ответственности за содержание сообщений, а также за возможное нарушение авторских, смежных и каких-либо иных прав, которое может повлечь за собой информация, содержащаяся в сообщениях.
Ремонт компьютеров в калуге Рекламное место сдается
 
Ответить в эту темуОткрыть новую тему
Decker
сообщение 1.11.2011, 10:55
Сообщение #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

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form.




То есть при сериализации перечисления всегда преобразуются в строковую форму, а числовое значение игнорируется. Вуаля!



А теперь немного практики. Для начала, определим модель данных для нашего примера:



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, после чего обновляем числовые значения экземпляров класса перечисления. Собственно, на этом всё. Теперь мы спокойно можем:



  1. Хранить перечисления как Java Enum и объявлять содержащие их поля как @Enumerated(EnumType.ORDINAL)
  2. Автоматически контролировать синхронизацию справочников в коде и базе данных
  3. Не заботиться о порядке объявления идентификаторов в коде и соответствия их идентификаторам в базе данных
  4. Выполнять удобные запросы к базе данных, содержащие доступ к значениям перечислений по их строковому названию
  5. ...
  6. PROFIT!


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



Данный подход используется нами (Open Source Technologies) в достаточно крупных системах (от полумиллиона строк исходного кода и больше) с распределённой сервис-ориентированной архитектурой на базе JMS и очень хорошо себя показал — как в части удобства использования, так и в части надёжности. Чего и вам желаю smile.gif

Original source: habrahabr.ru (comments, light).

Читать дальше


--------------------

Вернуться в начало страницы
 
+Ответить с цитированием данного сообщения

Ответить в эту темуОткрыть новую тему
1 чел. читают эту тему (гостей: 1, скрытых пользователей: 0)
Пользователей: 0

 

Рекламное место сдается Рекламное место сдается
Текстовая версия Сейчас: 11.4.2025, 22:37
Рейтинг@Mail.ru
Яндекс.Метрика Яндекс цитирования