Как стать автором
Обновить

Комментарии 23

У Вас невероятно познавательный сайт!:)
А нам он другой и не нужен на текущий момент :)
Еще бы под EclipseLink бы кто-нибудь адаптировал :-)
Дык, тут особых Hibernate-специфичных моментов и нет, а те что есть отвечают только за листинг базы. Используйте вместо Hibernate Criteria — Criteria API из JPA 2.0.
спасибо, полезный пост!
если кто-то удалит или изменит значение в enum'е, то как это синхронизируетс с базой?
(честно говоря с явой знаком плохо и статью детально не осилил, но узнать про этот момент очень интересно)
сам новичок, но по моему достаточно обыкновенного обновления… я так делал с HSQL. если не прав — поправьте, пожалуйста
UPD: у меня небольшое приложение было, на сильной базе думаю будет какой нибудь TimeLimit или MemoryExc
Никак. Тут тоже самое, что и с фикстурами. Напишите — миграции — всё будет отлично.
В базе никак, но если добавить в код проверку на совпадение множества значений в коде и в базе, то приложение может при запуске ругаться на несинхронизацию базы и, таким образом, минимизировать геморрой.
О, спасибо за интересное решение!
Я наверное не понимаю, но зачем столько кода, что бы хранить в поле БД одну цифру?
Одну? У вас в системе один справочник с единственным значением? Я вам искренне завидую :-) У нас их несколько десятков с кучей значений в каждом. И работать с ними таким способом — очевиднее, проще, удобнее и надёжнее с точки зрения страховки от ошибок.
Эм, а разве нельзя создавать Enum-ы при запуске приложения, я так понимаю, их заполняют из словаря в БД? А если нужно использовать их в запросе, так делать это через биндинг значений?
Если создавать их при запуске приложения, тогда их нельзя будет использовать из кода. Смысл данной затеи в частности в том, чтобы можно было просто написать

session.createCriteria(Person.class).add(Restrictions.eq("status", Status.NEW)

а не морочиться с JOIN'ами вроде вот такого:

session.createCriteria(Person.class).createAlias("status", "st").add(Restrictions.eq("status_id", "id")).add(Restrictions.eq("st.id", "NEW")


Не говоря уже про постоянную выборку по строковому идентификатору, не защищённому от опечаток. Обращение же к enum'ам валидируется во время компиляции, в этом огромный плюс и это защищает от тонн ошибок и опечаток.

По сути же этот код и делает выборку из базы при подключении и биндинг enum'ов к идентификаторам из базы данных.
Эм, я правильно понял, что вы объявляете enum-ы с их значениями в коде, а потом заполняете их поле ordinal из БД?

А в запросе использовать Status.NEW.ordinal()?
Да. Разве что Status.NEW.ordinal() писать совсем не обязательно, можно просто Status.NEW — Hibernate (и JPA в целом) отлично это переваривает, причём внутри оно использует не метод ordinal(), а getEnumConstants() — это внезапно выяснилось в ходе отладки, после чего была добавлена замена массива $VALUES.

Кроме того, данный код сохраняет возможность обращения с Enum'ами произвольным образом — как выборок по индексам из getEnumConstants(), так и valueOf() и всего остального их функционала. Чем и удобен — no side effects at all.
Т.е. почему нельзя использовать Status.NEW.ordinal() по полю status_id?
Почему нельзя? Можно :-) Только зачем писать лишний вызов метода, удлинняющий код, когда логика JPA/Hibernate спокойно скушает и без него?
прошу прощения, во втором Restrictions.eq() в варианте с JOIN'ом конечно же st.code, а не st.id:
session.createCriteria(Person.class).createAlias("status", "st").add(Restrictions.eq("status_id", "id")).add(Restrictions.eq("st.code", "NEW")
проходил случайно мимо, никак не могу не оставить комментарий :)

все последущее, несмотря на категоричность является исключительно личным мнением (хоть и проверенным во многих проектах) не претендующим на истину в последней инстанции

1.) никогда никогда никогда не используйте ordinals в «перeчислениях» в Яве. Это очень опасно, так как значения (literals) часто меняются. Если где-то используется порядковое значение как ключ, то это очень плохой знак, можно даже сказать, плохой дизайн системы. Гораздо лучше использовать в перичислениях собственные идентификаторы, основанные на предметной области, хотя бы строковые. Пример:

public enum CustomerStatusType implements MyEnum {
ACTIVE("ACT", "Active"),
DELETED("DEL", "Deleted"),
SUSPENDED("SUS", "Suspended");

private String id;
private String description;

private CustomerStatusType(String id, String description) {
this.id = id;
this.description = description;
}

@Override public String getId() {return this.id;}
@Override public String getDescription() {return this.description;}
public static CustomerStatusType getInstanceById(String id) {...}
}


Если этот энум расширить, к примеру добавить до ACTIVE статус CREATED(«CRT», «Created»), то порядковое значение «старых» элементов изменится, но не их идентификаторы. При использовании порядкового значения (ordinal) как идентификатора, произойдет неименуемый крах. Несомненно лучше использовать getId и getInstanceById.

2) EclipseLink и Hibernate позволяют использовать так называемые CustomMapper (UserType). С их помощью можно добится «прозрачного» писания и считывания идентификаторов в базу. К тому же иногда бывает, что нужно сохранять не одно значение, а список. Желательно этот список сохранять как список идентификаторов в одной строке, а не как foreign keys в n-to-m-relation-таблице.

Короткий пример использования, насчет реализации должен к сожалению отослать к документации Hibernate или EclipseLink, там есть примеры.

@Entity
public class Customer {

@Type(type = "example.EnumerationIdMapper", 
parameters = { @Parameter(name = "classname", value = "example.CustomerStatusType") })
@Column(name = "status", nullable = false)
private CustomerStatusType status;

@Type(type = "example.EnumerationIdListMapper", 
parameters = { @Parameter(name = "classname", value = "example.CustomerStatusType") })
@Column(name = "statuses", nullable = true)
private List<CustomerStatusType> statuses;

}

/**
* пример 
*/
public class EnumerationIdMapper implements UserType, ParameterizedType
...
    protected Class myEnum = null;
    protected Method getInstanceById =null;

    /**
     * @see org.hibernate.usertype.UserType#nullSafeGet(ResultSet, java.lang.String[],
     *      java.lang.Object)
     */
    public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner)
            throws HibernateException, SQLException {
        MyEnum targetObject = null;
        try {
            String id = resultSet.getString(names[0]);
            if (!resultSet.wasNull()) {
                targetObject = (MyEnum) this.getInstanceById.invoke(
                        myEnum, new Object[] { id.trim() });
            }
       catch (Exception e) {
         // do something...
        }
        return targetObject;
    }

    /**
     * @see org.hibernate.usertype.UserType#nullSafeSet(PreparedStatement, java.lang.Object, int)
     */
    public void nullSafeSet(PreparedStatement statement, Object value, int index)
            throws HibernateException, SQLException {
        if (value == null) {
            statement.setNull(index, Types.VARCHAR);
        } else {
            statement.setString(index, ((MyEnum) value).getId());
        }
    }

    /**
     * @see org.hibernate.usertype.ParameterizedType#setParameterValues(java.util.Properties)
     */
    public void setParameterValues(Properties properties) {
        if (properties == null) {
            return;
        }
        String className = properties.getProperty("classname");
        if (className == null || className.length() == 0) {
            return;
        }
        try {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            Class<?> clazz = cl.loadClass(className);
            this.myEnum = (Class<T>) clazz.asSubclass(Enum.class);
            this.getInstanceById = myEnum.getMethod("getInstanceById",
                    new Class[] { String.class });
        } catch (ClassNotFoundException cnfe) {
            throw new HibernateException("class not found", cnfe);
        }
    }

...
}



Ну и плюс можно использовать JPQL.:

Query query = em.createQuery("select c from Customer c where c.status:=statusValue");
query.setParameter("statusValue", CustomerStatusType.ACTIVE);


Успехов и приятных экспериментов!
Чего то я не могу понять, почему у меня не происходит инъекция энума в Set
public enum AccountRole {
        // ordinal соответсвенно 10, 20, 30
	ROOT, USERMANAGER, USERVIEWER
}

@Entity
@Table(name = "ACCOUNT_GROUP")
public class AccountGroup {
	
	@Id
	@Column(name = "AG_ID", nullable = false, unique = true)
	private Integer groupId;
	
	@Column(name = "AG_NAME", nullable = false, unique = true, length = 32)
	private String groupName;

	@Enumerated(EnumType.ORDINAL)
	@ElementCollection(targetClass = AccountRole.class) 
	@CollectionTable(name = "GROUP_ROLE", 
		joinColumns = @JoinColumn(name = "AG_ID"))
	@Column(name = "ROLE_ID") 
	private Set<AccountRole> roles = new HashSet<>();

...

}


Создаю группу
		AccountGroup group = new AccountGroup();
		group.setGroupName("ABCDE");
		group.setGroupId(101);
		group.getRoles().add(AccountRole.ROOT);
		group.getRoles().add(AccountRole.USERMANAGER);
		accountGroupRepository.save(group);


В БД сохраняется все как надо… в таблицу GROUP_ROLE падает две строки — [101;10] и [101,20]

При чтении же мне выдается InvocationException и пишет, что не может найти enum c ordinal 10 и 20.

На что можно обратить внимание, подскажите пожалуйста?
Поиски проблемы привели меня к классу org.hibernate.type.EnumType

в котором enumClass.getEnumConstants() возвращает массив со всего тремя элементами с индексами 0,1 и 2 и он же используется хибернейтом дальше

private Enum[] enumsByOrdinal() {
			if ( enumsByOrdinal == null ) {
				enumsByOrdinal = enumClass.getEnumConstants();
				if ( enumsByOrdinal == null ) {
					throw new HibernateException( "Failed to init enum values" );
				}
			}
			return enumsByOrdinal;
		}


А дальше в методе идет проверка по размеру массива, естественно в нем всего 3 элемента, а ordinal имею значения 10,20 и 30.
Кроме того, выборка проиходит по индексу в массиве, а не ordinal
private Enum fromOrdinal(int ordinal) {
			final Enum[] enumsByOrdinal = enumsByOrdinal();
			if ( ordinal < 0 || ordinal >= enumsByOrdinal.length ) {
				throw new IllegalArgumentException(
						String.format(
								"Unknown ordinal value [%s] for enum class [%s]",
								ordinal,
								enumClass.getName()
						)
				);
			}
			return enumsByOrdinal[ordinal];

		}


Что нужно переделать, чтобы этот массив соответствовал ordinal?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории