Статьи

Java: использование шаблона спецификации с JPA

Эта статья представляет собой введение в использование шаблона спецификации в Java. Мы также увидим, как мы можем комбинировать классические спецификации с запросами JPA Criteria для извлечения объектов из реляционной базы данных.

В этом посте мы будем использовать следующий класс Poll в качестве примера сущности для создания спецификаций. Это представляет опрос, который имеет дату начала и окончания. В промежутке между этими двумя датами пользователи могут голосовать за разные варианты. Опрос также может быть заблокирован администратором до достижения конечной даты. В этом случае будет установлена ​​дата блокировки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Entity
public class Poll { 
 
  @Id
  @GeneratedValue
  private long id;
 
  private DateTime startDate; 
  private DateTime endDate;
  private DateTime lockDate;
 
  @OneToMany(cascade = CascadeType.ALL)
  private List<Vote> votes = new ArrayList<>();
 
}

Для лучшей читабельности я пропустил геттеры, сеттеры, аннотации JPA для отображения экземпляров Joda DateTime и полей, которые не нужны в этом примере (например, вопрос, задаваемый в опросе).

Теперь предположим, что у нас есть два ограничения, которые мы хотим реализовать:

  • В настоящее время выполняется опрос, если он не заблокирован и если startDate <now <endDate
  • Опрос популярен, если он содержит более 100 голосов и не заблокирован

Мы могли бы начать с добавления соответствующих методов в опрос, например: poll.isCurrentlyRunning (). В качестве альтернативы мы можем использовать сервисный метод, такой как pollService.isCurrentlyRunning (poll). Однако мы также хотим иметь возможность запрашивать базу данных, чтобы получить все текущие опросы. Поэтому мы можем добавить метод DAO или репозитория, например pollRepository.findAllCurrentlyRunningPolls ().

Если мы будем следовать этим путем, мы реализуем ограничение isCurrentlyRunning два раза в двух разных местах. Все становится хуже, если мы хотим объединить ограничения. Что если мы хотим запросить в базе данных список всех популярных опросов, которые в настоящее время проводятся?

Это где образец спецификации пригодится. При использовании шаблона спецификации мы перемещаем бизнес-правила в дополнительные классы, называемые спецификациями.

Чтобы начать работу со спецификациями, мы создадим простой интерфейс и абстрактный класс:

1
2
3
4
5
public interface Specification<T> {  
  boolean isSatisfiedBy(T t);  
  Predicate toPredicate(Root<T> root, CriteriaBuilder cb);
  Class<T> getType();
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
abstract public class AbstractSpecification<T> implements Specification<T> {
  @Override
  public boolean isSatisfiedBy(T t) {
    throw new NotImplementedException();
  }  
 
  @Override
  public Predicate toPredicate(Root<T> poll, CriteriaBuilder cb) {
    throw new NotImplementedException();
  }
 
  @Override
  public Class<T> getType() {
    ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
    return (Class<T>) type.getActualTypeArguments()[0];
  }
}

Пожалуйста, на мгновение проигнорируйте класс AbstractSpecification <T> с загадочным методом getType () (мы вернемся к нему позже).

Центральной частью спецификации является метод isSatisfiedBy (), который используется для проверки соответствия объекта спецификации. toPredicate () — это дополнительный метод, который мы используем в этом примере для возврата ограничения в виде экземпляра javax.persistence.criteria.Predicate, который можно использовать для запроса базы данных.

Для каждого ограничения мы создаем новый класс спецификации, который расширяет AbstractSpecification <T> и реализует isSatisfiedBy () и toPredicate ().

Реализация спецификации для проверки, запущен ли в данный момент опрос, выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class IsCurrentlyRunning extends AbstractSpecification<Poll> {
 
  @Override
  public boolean isSatisfiedBy(Poll poll) {
    return poll.getStartDate().isBeforeNow() 
        && poll.getEndDate().isAfterNow() 
        && poll.getLockDate() == null;
  }
 
  @Override
  public Predicate toPredicate(Root<Poll> poll, CriteriaBuilder cb) {
    DateTime now = new DateTime();
    return cb.and(
      cb.lessThan(poll.get(Poll_.startDate), now),
      cb.greaterThan(poll.get(Poll_.endDate), now),
      cb.isNull(poll.get(Poll_.lockDate))
    );
  }
}

В isSatisfiedBy () мы проверяем, соответствует ли переданный объект ограничению. В toPredicate () мы создаем предикат, используя JPA CriteriaBuilder. Мы будем использовать полученный экземпляр Predicate позже для создания CriteriaQuery для запросов к базе данных.

Спецификация для проверки популярности опроса выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class IsPopular extends AbstractSpecification<Poll> {
 
  @Override
  public boolean isSatisfiedBy(Poll poll) {
    return poll.getLockDate() == null && poll.getVotes().size() > 100;
  }  
 
  @Override
  public Predicate toPredicate(Root<Poll> poll, CriteriaBuilder cb) {
    return cb.and(
      cb.isNull(poll.get(Poll_.lockDate)),
      cb.greaterThan(cb.size(poll.get(Poll_.votes)), 5)
    );
  }
}

Если мы теперь хотим проверить, соответствует ли экземпляр Poll одному из этих ограничений, мы можем использовать наши недавно созданные спецификации:

1
2
boolean isPopular = new IsPopular().isSatisfiedBy(poll);
boolean isCurrentlyRunning = new IsCurrentlyRunning().isSatisfiedBy(poll);

Для запросов к базе данных нам нужно расширить наш DAO / репозиторий для поддержки спецификаций. Это может выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class PollRepository {
 
  private EntityManager entityManager = ...
 
  public <T> List<T> findAllBySpecification(Specification<T> specification) {
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
 
    // use specification.getType() to create a Root<T> instance
    CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(specification.getType());
    Root<T> root = criteriaQuery.from(specification.getType());
 
    // get predicate from specification
    Predicate predicate = specification.toPredicate(root, criteriaBuilder);
 
    // set predicate and execute query
    criteriaQuery.where(predicate);
    return entityManager.createQuery(criteriaQuery).getResultList();
  }
}

Здесь мы наконец используем метод getType (), реализованный в AbstractSpecification <T> для создания экземпляров CriteriaQuery <T> и Root <T>. getType () возвращает универсальный тип экземпляра AbstractSpecification <T>, определенного подклассом. Для IsPopular и IsCurrentlyRunning возвращает класс Poll. Без getType () нам пришлось бы создавать экземпляры CriteriaQuery <T> и Root <T> внутри toPredicate () каждой создаваемой нами спецификации. Так что это всего лишь маленький помощник для уменьшения кода котельной пластины внутри спецификаций. Не стесняйтесь заменить это своей собственной реализацией, если вы придумаете лучшие подходы.

Теперь мы можем использовать наш репозиторий для запроса к базе данных опросов, которые соответствуют определенной спецификации:

1
2
List<Poll> popularPolls = pollRepository.findAllBySpecification(new IsPopular());
List<Poll> currentlyRunningPolls = pollRepository.findAllBySpecification(new IsCurrentlyRunning());

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

Однако остается один вопрос: как объединить два или более ограничений? Например, мы хотели бы запросить базу данных для всех популярных опросов, которые все еще работают.

Ответом на это является вариация шаблона составного проекта, называемая составными спецификациями. Используя составную спецификацию, мы можем комбинировать спецификации разными способами.

Чтобы запросить базу данных для всех запущенных и популярных пулов, нам нужно объединить isCurrentlyRunning со спецификацией isPopular, используя логику и операцию. Давайте создадим другую спецификацию для этого. Мы называем это AndSpecification:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AndSpecification<T> extends AbstractSpecification<T> {
 
  private Specification<T> first;
  private Specification<T> second;
 
  public AndSpecification(Specification<T> first, Specification<T> second) {
    this.first = first;
    this.second = second;
  }
 
  @Override
  public boolean isSatisfiedBy(T t) {
    return first.isSatisfiedBy(t) && second.isSatisfiedBy(t);
  }
 
  @Override
  public Predicate toPredicate(Root<T> root, CriteriaBuilder cb) {
    return cb.and(
      first.toPredicate(root, cb), 
      second.toPredicate(root, cb)
    );
  }
 
  @Override
  public Class<T> getType() {
    return first.getType();
  }
}

AndSpecification создается из двух других спецификаций. В isSatisfiedBy () и toPredicate () мы возвращаем результат обеих спецификаций, объединенных логикой и операцией.

Мы можем использовать нашу новую спецификацию следующим образом:

1
2
Specification<Poll> popularAndRunning = new AndSpecification<>(new IsPopular(), new IsCurrentlyRunning());
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);

Чтобы улучшить читабельность, мы можем добавить метод and () в интерфейс спецификации:

1
2
3
4
5
6
public interface Specification<T> {
 
  Specification<T> and(Specification<T> other);
 
  // other methods
}

и реализовать его в нашей абстрактной реализации:

1
2
3
4
5
6
7
8
9
abstract public class AbstractSpecification<T> implements Specification<T> {
 
  @Override
  public Specification<T> and(Specification<T> other) {
    return new AndSpecification<>(this, other);
  }
 
  // other methods
}

Теперь мы можем связать несколько спецификаций с помощью метода and ():

1
2
3
Specification<Poll> popularAndRunning = new IsPopular().and(new IsCurrentlyRunning());
boolean isPopularAndRunning = popularAndRunning.isSatisfiedBy(poll);
List<Poll> polls = myRepository.findAllBySpecification(popularAndRunning);

При необходимости мы можем легко расширить это с помощью других составных спецификаций (например, OrSpecification или NotSpecification).

Вывод

При использовании шаблона спецификации мы перемещаем бизнес-правила в отдельные классы спецификации. Эти классы спецификаций могут быть легко объединены с помощью составных спецификаций. В целом, спецификации улучшают возможность повторного использования и ремонтопригодности. Кроме того, технические характеристики могут быть легко протестированы. Для более подробной информации о шаблоне спецификации я рекомендую эту статью Эрика Эванса и Мартина Фаулера.

  • Вы можете найти источник этого примера проекта на GitHub .