Validation API: проверка объекта на уникальность при помощи @Unique

Автор PremaServices - Понедельник, 24 Июнь 2013. В рубриках: Validation API, Hibernate

Реализация собственного класса валидации

Проверка нового объекта на уникальность при сохранении в базу данных - одна из самых необходимых проверок. Однако, в Hibernate через Validation API она по умолчанию не реализована по ряду вполне допустимых причин. Вследствие этого, такая проверка стандартно выполняется уже на более низком уровне при взаимодействии фреймворка с СУБД и в случае нарушения целостности базы данных бросает исключение HibernateException.

Конечно, это не слишком удобно извлекать информацию из исключения и пытаться понять, почему оно произошло: то ли целостность нарушена при сохранении, то ли ещё много и много различных других причин...

В хорошем приложении нарушение целостности лучше фиксировать заранее и посылать пользователю красивое сообщение об ошибке, а не HibernateException, чтобы он мог поправить ситуацию. Это можно реализовать обычными дополнительными методами, но более привлекательно и удобно создать специальную аннотацию, тем более если в приложении во всю используется Validation API. Этот подход реализован в данной статье.


Постановка задачи

В любом учебном заведении бывают сессии. Так и в заочной школе астрологии они проходят два раза в год. Для их регистрации в базе данных существуют объекты Session. И первое, что делает менеджер, когда руководством назначаются даты очередной сессии, - это вводит новый объект в базу данных, а потом регистрирует тех студентов, которые собираются приехать. Итак у нас есть сущность (для простоты я опущу методы и те члены класса, которые не участвуют в нашем примере):

package ru.premaservices.astroved.student.pojo;
...
@Entity
@Table(name="SESSIONS") @Unique(names = {"name", "startDate"}) @SessionConstraint
public class Session implements Comparable<Session> {
 	
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name="ID")
	private Integer id;
 	
	@NotBlank 
	@Column(name="NAME", length=70, nullable = false, unique = true)
	private String name;
 	
	@NotNull @Future
	@Column(name="START_DATE", nullable = false, unique = true)
	@Temporal(value=TemporalType.DATE)
	private Date startDate;
 	
	@NotNull @Future
	@Column(name="FINAL_DATE", nullable = false)
	@Temporal(value=TemporalType.DATE)
	private Date finalDate;	
	...
}

Как видно из примера, для проверки уникальности мы ввели новую аннотацию @Unique, которая имеет один параметр names, в нем мы перечислим те поля сущности Session, которые необходимо проверить на уникальность. Идентификатор объекта при сохранении задается в БД автоматически, поэтому, его проверять нет нужды. Главное, чтобы объект имел уникальное наименование и дату начала, то есть не может быть несколько сессий, начинающихся в одну и ту же дату. Логично.

Также видно, что дополнительно мы проверяем наименование на непустое значение, а обе даты, как начала, так и окончания сессии должны быть в будущем. Это делается стандартными аннотациями Hibernate Viladation API.


Аннотация

Аннотация @Unique проста для понимания и  выглядит так:

package ru.premaservices.extention.spring.hibernate.validation.constraints;
...
@Documented
@Constraint(validatedBy = {UniqueValidator.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Unique {
	String message() default "{ru.premaservices.extention.spring.hibernate.validation.constraints.Unique.message}";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
	String[] names();
}

Метод names возвращает массив. Надо сказать, что в данном случае мы реализуем подход, когда все переменные объекта Session, указанные в аннотации, будут проверяться на уникальность отдельно друг от друга, хотя в рамках универсальности такой аннотации важно также предусматривать возможность проверки и составных значений. Но я предполагаю, что такие проверки, если они где-то понадобятся, можно делать другой аннотацией, скажем @UniqueComplex, не валить все в одну кучу.


Сервис проверки

Теперь создадим универсальный класс, который позволил бы нам осуществлять непосредственную проверку на целостность.

package ru.premaservices.extention.spring.hibernate.validation.dao;
...
@Repository
public class ValidationManager {
 
	@Autowired
        private SessionFactory sessionFactory;	
	...
 	
	@Transactional(readOnly = true)
	public boolean validateUnique (Class<?> clazz, Serializable id) {
		Session session = sessionFactory.getCurrentSession();
		return session.get(clazz, id) == null ? true : false;
	}
 	
	@Transactional(readOnly = true)
	public boolean validateUnique (Class<?> clazz, String name, Object value) {
		Session session = sessionFactory.getCurrentSession();
		Criteria criteria = session.createCriteria(clazz);
		criteria.add(Restrictions.eq(name, value));
		return criteria.uniqueResult() == null ? true : false;
	}
 	
}

Реализация через Criteria API. Заметим, что этот подход работает в связке со Spring, то есть предполагается, что SessionFactory и параметры доступа к БД описаны в конфигурации Spring.

Первый метод проверяет на уникальность идентификатор, а второй - тот, которым мы воспользуемся далее. Он проверяет любые другие переменные объекта.


Класс валидации

Наконец, сам класс проверки, где для извлечения нужных значений пользуемся Reflection API.

package ru.premaservices.extention.spring.hibernate.validation.constraints;
...
public class UniqueValidator implements ConstraintValidator<Unique, Object> {
 
	@Autowired
	private ValidationManager manager;
	 
	private String[] names;
 	
	@Override
	public void initialize(Unique annotation) {
		names = annotation.names();
	}
 
	@Override
	public boolean isValid(Object object, ConstraintValidatorContext context) {
 		
		boolean result = true;
 		
		if (manager == null || object == null) return true;
 		
		Class<?> c = object.getClass(); 
		Object property = null;
 		
		ConstraintViolationBuilder builder = context.buildConstraintViolationWithTemplate("{ru.premaservices.astroved.student.validation.Unique." + c.getSimpleName() + ".message}");
 		
		for (String name : names) {
 		
			try {	
				String m = "get" + String.valueOf(name.charAt(0)).toUpperCase() + name.substring(1);		
				Method method = c.getDeclaredMethod(m);
				property = method.invoke(object);			
			}
			catch (Exception e) {
				e.printStackTrace();
				return false;
			}
 				
			boolean r = manager.validateUnique(c, name, property);
			if (!r) {	
				builder.addNode(name).addConstraintViolation();
			}
			result &= r;
 		
		}
 			
		return result;
	}
 
}

В этом же классе, кроме самой проверки, мы подготавливаем сообщения об ошибке, которые увидит пользователь, через настройку объекта ConstraintViolationBuilder. Поскольку наша аннотация может быть использована для проверки любого объекта, то шаблон сообщения берется в зависимости от класса этого объекта. И ошибка подставляется в каждое поле, которое потерпело неудачу.


Сообщение об ошибке

Осталось только задать русский текст сообщения об ошибке, который будет отослан пользователю. Прописываем в файле ValidationMessages.propepties:

ru.premaservices.extention.spring.hibernate.validation.constraints.Unique.Session.message=Сессия с таким значением уже существует в базе данных


И контроллер Spring для наглядности

В котором вызываются все проверки, включая и только что нами созданную.

@RequestMapping(value = SESSION_FORM_REQUEST, method = RequestMethod.POST)
	public ModelAndView addSession (@ModelAttribute("session") @Valid Session s, BindingResult result) { //AJAX
 		
		ModelAndView model;
 		
		if (result.hasErrors()) {
			//назад, все сообщения об ошибках в модели объекта result и передаются клиенту
			model = new ModelAndView(SESSION_FORM_JSP);
			model.addAllObjects(result.getModel());
		}
		else {
			//сохраняем объект в БД
		}	
		return model;
	}

Запускает проверку аннотация @Valid. На этом пока всё.

  • 0.0/5 оценка (0 голосов)
  • Тэги: Validation API, аннотация, проверка данных

Об авторе

Комментарии (0)

Оставить комментарий

Вы комментируете как Гость.