← Назад к вопросам
Что такое Specification в Spring Data JPA?
2.0 Middle🔥 191 комментариев
#Spring Boot и Spring Data
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Specification в Spring Data JPA: динамические запросы
Specification — это паттерн в Spring Data JPA для построения динамических SQL запросов без необходимости писать строки SQL или создавать множество методов репозитория. Это реализация Specification паттерна из Domain-Driven Design, который инкапсулирует логику запроса к базе данных.
Основная идея
Вместо того, чтобы писать метод репозитория для каждого возможного набора фильтров:
// ❌ ПЛОХО: explosion of query methods
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
List<User> findByAge(Integer age);
List<User> findByNameAndAge(String name, Integer age);
List<User> findByNameAndAgeAndCity(String name, Integer age, String city);
List<User> findByNameOrAgeAndCity(String name, Integer age, String city);
// ... и так далее для всех комбинаций
}
✅ Правильно: Specifications
// Один универсальный метод
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// Всего один метод для всех случаев!
}
Синтаксис и использование
Шаг 1: Реализуй интерфейс JpaSpecificationExecutor
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// JpaSpecificationExecutor даёт методы:
// Optional<User> findOne(Specification<User> spec)
// List<User> findAll(Specification<User> spec)
// Page<User> findAll(Specification<User> spec, Pageable pageable)
// long count(Specification<User> spec)
}
Шаг 2: Создай Specification
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.*;
public class UserSpecifications {
// Простая спецификация: по имени
public static Specification<User> hasName(String name) {
return (root, query, cb) -> {
// root - сущность User
// query - объект CriteriaQuery
// cb - CriteriaBuilder (для создания условий)
if (name == null) {
return cb.conjunction(); // true условие
}
return cb.equal(root.get("name"), name);
};
}
// Спецификация: возраст больше чем
public static Specification<User> ageGreaterThan(Integer age) {
return (root, query, cb) -> {
if (age == null) {
return cb.conjunction();
}
return cb.greaterThan(root.get("age"), age);
};
}
// Спецификация: город
public static Specification<User> fromCity(String city) {
return (root, query, cb) -> {
if (city == null) {
return cb.conjunction();
}
return cb.equal(root.get("city"), city);
};
}
}
Шаг 3: Используй Specification в запросах
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Простой поиск
public List<User> findUsersByName(String name) {
return userRepository.findAll(
UserSpecifications.hasName(name)
);
}
// Комбинирование спецификаций (AND)
public List<User> findUsersByNameAndCity(String name, String city) {
Specification<User> spec =
UserSpecifications.hasName(name)
.and(UserSpecifications.fromCity(city));
return userRepository.findAll(spec);
}
// Комбинирование спецификаций (OR)
public List<User> findYoungOrFromCity(Integer age, String city) {
Specification<User> spec =
UserSpecifications.ageGreaterThan(age)
.or(UserSpecifications.fromCity(city));
return userRepository.findAll(spec);
}
// С пагинацией
public Page<User> searchUsers(String name, Integer age, String city, int page) {
Specification<User> spec =
Specification.where(UserSpecifications.hasName(name))
.and(UserSpecifications.ageGreaterThan(age))
.and(UserSpecifications.fromCity(city));
Pageable pageable = PageRequest.of(page, 20);
return userRepository.findAll(spec, pageable);
}
}
Примеры сложных условий
Like поиск
public class UserSpecifications {
public static Specification<User> nameLike(String pattern) {
return (root, query, cb) -> {
if (pattern == null || pattern.isEmpty()) {
return cb.conjunction();
}
return cb.like(
cb.lower(root.get("name")),
"%" + pattern.toLowerCase() + "%"
);
};
}
}
// Использование:
List<User> results = userRepository.findAll(
UserSpecifications.nameLike("john")
); // Найдёт: john, John, JOHN, johnny и т.д.
Диапазон значений
public class UserSpecifications {
public static Specification<User> ageBetween(Integer minAge, Integer maxAge) {
return (root, query, cb) -> {
if (minAge == null && maxAge == null) {
return cb.conjunction();
}
List<Predicate> predicates = new ArrayList<>();
if (minAge != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), minAge));
}
if (maxAge != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("age"), maxAge));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
// Использование:
List<User> users = userRepository.findAll(
UserSpecifications.ageBetween(18, 65)
);
Работа с объединениями (JOIN)
@Entity
public class User {
@Id
private Long id;
private String name;
@ManyToMany
private List<Role> roles; // Связь многие-ко-многим
}
@Entity
public class Role {
@Id
private Long id;
private String name;
}
// Specification с JOIN
public class UserSpecifications {
public static Specification<User> hasRole(String roleName) {
return (root, query, cb) -> {
if (roleName == null) {
return cb.conjunction();
}
// Создаём JOIN
Join<User, Role> roleJoin = root.join("roles", JoinType.INNER);
return cb.equal(roleJoin.get("name"), roleName);
};
}
}
// SQL эквивалент:
// SELECT DISTINCT u FROM User u
// INNER JOIN u.roles r
// WHERE r.name = 'ADMIN'
Сложная бизнес-логика
public class OrderSpecifications {
public static Specification<Order> isPending() {
return (root, query, cb) ->
cb.equal(root.get("status"), OrderStatus.PENDING);
}
public static Specification<Order> isOverdue() {
return (root, query, cb) ->
cb.lessThan(root.get("dueDate"), LocalDate.now());
}
public static Specification<Order> totalGreaterThan(BigDecimal amount) {
return (root, query, cb) -> {
if (amount == null) {
return cb.conjunction();
}
return cb.greaterThan(root.get("total"), amount);
};
}
}
// Комплексный запрос: просроченные неоплаченные заказы
List<Order> overdueOrders = orderRepository.findAll(
Specification.where(OrderSpecifications.isPending())
.and(OrderSpecifications.isOverdue())
.and(OrderSpecifications.totalGreaterThan(new BigDecimal("100.00")))
);
Реальный пример: поисковый фильтр
// DTO для фильтра
public class UserSearchFilter {
private String name;
private Integer minAge;
private Integer maxAge;
private String city;
private String roleName;
}
// Service с универсальным поиском
@Service
public class UserService {
private final UserRepository userRepository;
public Page<UserDTO> search(UserSearchFilter filter, Pageable pageable) {
// Строим спецификацию динамически
Specification<User> spec = Specification.where(null);
if (filter.getName() != null && !filter.getName().isEmpty()) {
spec = spec.and(UserSpecifications.nameLike(filter.getName()));
}
if (filter.getMinAge() != null || filter.getMaxAge() != null) {
spec = spec.and(
UserSpecifications.ageBetween(
filter.getMinAge(),
filter.getMaxAge()
)
);
}
if (filter.getCity() != null && !filter.getCity().isEmpty()) {
spec = spec.and(UserSpecifications.fromCity(filter.getCity()));
}
if (filter.getRoleName() != null && !filter.getRoleName().isEmpty()) {
spec = spec.and(UserSpecifications.hasRole(filter.getRoleName()));
}
return userRepository.findAll(spec, pageable)
.map(UserMapper::toDTO);
}
}
// REST контроллер
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
@GetMapping("/search")
public ResponseEntity<Page<UserDTO>> search(
@RequestParam(required = false) String name,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
@RequestParam(required = false) String city,
@RequestParam(required = false) String role,
@PageableDefault(size = 20) Pageable pageable) {
UserSearchFilter filter = new UserSearchFilter();
filter.setName(name);
filter.setMinAge(minAge);
filter.setMaxAge(maxAge);
filter.setCity(city);
filter.setRoleName(role);
Page<UserDTO> results = userService.search(filter, pageable);
return ResponseEntity.ok(results);
}
}
// Использование:
// GET /api/v1/users/search?name=john&minAge=25&maxAge=40&city=NYC
Лучшие практики
1. Null-safety в спецификациях
// ✅ ПРАВИЛЬНО
public static Specification<User> hasName(String name) {
return (root, query, cb) -> {
if (name == null) {
return cb.conjunction(); // true условие, не фильтруем
}
return cb.equal(root.get("name"), name);
};
}
// ❌ НЕПРАВИЛЬНО
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
cb.equal(root.get("name"), name); // NPE если name = null
}
2. Отделение спецификаций в отдельный класс
// ✅ Хорошая структура
src/
domain/
User.java
repository/
UserRepository.java
specification/
UserSpecifications.java
service/
UserService.java
3. Используй Specification.where() для удобства
// ✅ Читаемо
Specification<User> spec = Specification.where(
UserSpecifications.hasName(name)
).and(UserSpecifications.fromCity(city));
// ❌ Менее читаемо
Specification<User> spec =
UserSpecifications.hasName(name)
.and(UserSpecifications.fromCity(city));
4. Тестирование спецификаций
@SpringBootTest
class UserSpecificationsTest {
@Autowired
private UserRepository userRepository;
@Test
void testNameSpecification() {
// Подготовка
userRepository.save(new User("John", 30));
userRepository.save(new User("Jane", 25));
// Выполнение
List<User> results = userRepository.findAll(
UserSpecifications.hasName("John")
);
// Проверка
assertEquals(1, results.size());
assertEquals("John", results.get(0).getName());
}
}
Заключение
Specification в Spring Data JPA:
- Устраняет explosion of query methods
- Позволяет создавать динамические запросы
- Реализует паттерн Specification из DDD
- Поддерживает сложные условия, JOIN, пагинацию
- Тестируется легко
- Производительна — генерирует оптимальные SQL запросы