28 April, 2012

Собственная реализация методов в Spring Data JPA

Очевидно, что мы не всегда можем воспользоваться автоматической генерацией кода, предоставляемой Spring Data JPA. Например, у нас слишком сложный запрос, или нам необходимо вызвать процедуру в базе данных, либо у нас сложная бизнес-логика. Рассмотрим следующий пример - например, нам нужна функциональность уникального счётчика, который мы решили реализовать с помощью последовательности (sequence). Сначала определим интерфейс, в котором опишем все методы, которые мы будем реализовывать самостоятельно. В нашем случае, это будет только один метод:
public interface UserRepositoryCustom {
    /**
     * Returns next unique id.
     *
     * @return next unique id.
     */
    Integer getNextUniqueId();
}

Затем обновим объявление репозитория, чтобы он унаследовал новый интерфейс UserRepositoryCustom

public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
   ...
}

Теперь напишем реализацию метода:
public class UserRepositoryImpl implements UserRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Integer getNextUniqueId() {

        // When using Hibernate via JPA native queries fails with mapping exception, so just use Hibernate directly:
        Session session = (Session) entityManager.getDelegate();
        SQLQuery nativeQuery = session.createSQLQuery("SELECT \"nextval\"('unique_id_seq') ");
        List<BigInteger> list = nativeQuery.list();
        if (list.isEmpty()) {
            throw new IncorrectResultSizeDataAccessException(1);
        }

        BigInteger result = list.get(0);

        return result.intValue();
    }
}
И, наконец, укажем Spring Data JPA, чтобы в качестве класса для прокси использовался наш класс с реализацией собственных методов. Для этого нам нужна ещё одна секция repositories в конфигурационном файле:
    <repositories base-package="[base.repository.package]"/>

    <repositories base-package="[base.repository.package]">
        <repository id="userRepository" custom-impl-ref="userRepositoryImpl"/>
    </repositories>

    <beans:bean id="userRepositoryImpl" class="...UserRepositoryImpl"/>
Вот и всё.

24 April, 2012

Ищем с помощью Spring Data JPA

Рассмотрим подробнее одну из наиболее полезных вещей в Spring Data JPA - генерация JPQL-запросов на основе имени метода. Spring Data JPA умеет автоматически генерировать запросы используя для подсказки название метода. Например, метод User.findByLoginAndPassword сгенерирует примерно следующий код:
    FROM User u where u.login = :login and password = :password
Вообще Spring Data JPA пытается быть умным, поэтому реализация findBy{...} методов ищется следующим образом:
  1. Сначала смотрится аннотация @Query на объявлении метода, если она есть, то используется.
  2. Затем смотрится аннотация @NamedQuery с именем вида Entity.findMethodName, для вышеприведённого случая это будет User.findByLoginAndPassword.
  3. Если ничего не нашли. то по сигнатуре метода генерируется запрос.
У @Query следующие плюсы:
  1. Позволяет не засорять объявление доменной сущности.
  2. Сильно помогает, если у нас в запросе есть неявные джойны, потому что для таких запросов Spring Data JPA не умеет корректно генерировать запрос SELECT COUNT(*), который нужен в тех случаях, когда метод должен вернуть Page.
Очевидно, что при использовании запросов нам необходимо каким-то образом указывать параметры для запросов. Для этого есть аннотация @Param:
    @Query("select u from User u where u.login = :login and u.password = :password")
    Page<User> findByLoginAndPassword(@Param("login") String login, @Param("password") String password);
Кроме того, Spring Data JPA поддерживает отличную концепцию спецификаций. Спецификации позволяют делать составлять сложные запросы из набора простых. Для поддержки спецификаций необходимо объявить метод в репозитории:
    Page<User> findAll(Specification<User> spec, Pageable pageable);
Спецификация по сути является фильтром и позволяет комбинирование фильтров, что даёт мощный инструмент для построения запросов. Пример использования спецификаций: Объявляем наши спецификации:
        public static Specification<User> firstNameOrLastNameOrLoginLike(final String search) {
            return new Specification<User>() {
                @Override
                public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {

                    Predicate loginPredicate = builder.like(root.get(User_.login), search);
                    Predicate firstnamePredicate = builder.like(root.get(User_.firstname), search);
                    Predicate lastnamePredicate = builder.like(root.get(User_.lastname), search);

                    return builder.or(loginPredicate, firstnamePredicate, lastnamePredicate);
                }
            };
        }

        public static Specification<User> hasRole(final Role role) {
            return new Specification<User>() {
                @Override
                public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                    return builder.equal(root.get(User_.role), role);
                }
            };
        }
И в нашем сервисе комбинируем их:

        public Page<User> searchUser(Role role, String search, Pageable pageable) {
            Specifications<User> mainSpec = where(hasRole(role));

            // уточняем запрос, если была передана строка для поиска
            if (StringUtils.isNotBlank(search)) {
                mainSpec = mainSpec.and(firstNameOrLastNameOrLoginLike(search));
            } 

            return userRepository.findAll(mainSpec, pageable);
        }

20 April, 2012

Использование BeanPostProcessor на примере журналирования

Сегодня я хочу рассказать, как можно сделать инициализацию логгера в классе с использованием аннотаций и BeanPostProcessor Очень часто мы инициализируем логгер следующим образом:
public class MyClass {
    private static final Logger LOG = LoggerFactory.getLogger(MyClass.class);
}
Я покажу, как сделать, чтобы можно было писать вот так:
    @Log
    private Logger LOG;
Первым делом нам нужно объявить аннотацию:
@Retention(RUNTIME)
@Target(FIELD)
@Documented
public @interface Log {
    String category() default "";
}
А вторым делом, написать собственный BeanPostProcessor, который бы устанавливал нам логгер:
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

@Component
public class LoggerPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(final Object bean, final String beanName) {
        ReflectionUtils.doWithFields(bean.getClass(), new FieldProcessor(bean, beanName), new LoggerFieldFilter());
        return bean;
    }

    private static class FieldProcessor implements ReflectionUtils.FieldCallback {
        private final Object bean;
        private final String beanName;

        private FieldProcessor(Object bean, String beanName) {
            this.bean = bean;
            this.beanName = beanName;
        }

        @Override
        public void doWith(Field field) throws IllegalAccessException {
            Log loggerAnnot = field.getAnnotation(Log.class);

            // Sanity check if annotation is on the field with correct type.
            if (field.getType().equals(org.slf4j.Logger.class)) {
                // As user can override logger category - check if it was done.
                String loggerCategory = loggerAnnot.category();
                if (StringUtils.isBlank(loggerCategory)) {
                    // use default category instead.
                    loggerCategory = bean.getClass().getName();
                }
                Logger logger = LoggerFactory.getLogger(loggerCategory);
                ReflectionUtils.makeAccessible(field);
                field.set(bean, logger);
            } else {
                throw new IllegalArgumentException(
                    "Unable to set logger on field '" + field.getName() + "' in bean '" + beanName +
                        "': field should have class " + Logger.class.getName());
            }
        }
    }

    private static class LoggerFieldFilter implements ReflectionUtils.FieldFilter {
        @Override
        public boolean matches(Field field) {
            Log logger = field.getAnnotation(Log.class);
            return null != logger;
        }
    }
}
Если вы используете не sfl4j, а, например, log4j, или commons-logging, то нужно немного поправить код внутри метода doWith Попутно, данный код показывает пример использования класса org.springframework.util.ReflectionUtils.

Преобразуем строку в дату

Казалось бы, есть простейшая задача - преобразовать строковое представление даты в объект класса java.util.Date. Как оказалось, иногда использование DateFormat не помогает. В случае, если строка - это заголовок Date из письма, то нам нужно использовать javax.mail.internet.MailDateFormat для преобразования такой строки.
String dateStr = ...
Date parsedDate = new MailDateFormat().parse(dateStr);