Query Services
Overview
Query services provide security-filtered data access using the SecuritySpecifications helper class. Security filtering is implemented in extended (*Ex) classes to preserve JHipster code generation compatibility.
Architecture
Design Principles
-
Helper class over inheritance - Use
SecuritySpecificationshelper methods instead of base class inheritance -
Type-safe paths - Use JPA metamodel functions for compile-time path validation
-
Extended classes - Security methods go in
*QueryServiceExclasses, not generated classes -
Selective filtering - Apply only the security dimensions each entity needs
JHipster Compatibility
JHipster regenerates <Entity>QueryService classes. To preserve custom security code:
-
Generated classes extend JHipster’s
QueryService<ENTITY> -
Custom security methods go in
<Entity>QueryServiceEx extends <Entity>QueryService -
REST controllers inject and use the
*Exclass for secured operations
SecuritySpecifications Helper
The SecuritySpecifications class provides static methods to apply security filters to JPA specifications.
Function-Based Methods (Recommended)
Use function-based methods for type-safe path resolution:
// Apply organisation filter with type-safe path
public static <T> Specification<T> applyOrgFilter(
Specification<T> baseSpec,
SecurityDimensionService securityService,
AccessLevel accessLevel,
Function<Root<T>, Expression<Long>> orgIdPath)
// Apply person filter with type-safe path
public static <T> Specification<T> applyPersonFilter(
Specification<T> baseSpec,
SecurityDimensionService securityService,
Function<Root<T>, Expression<Long>> personIdPath)
For entities requiring both organisation AND person filtering, simply chain the methods:
Specification<EventParticipant> spec = createSpecification(criteria);
spec = SecuritySpecifications.applyOrgFilter(spec, securityService, AccessLevel.READ, ORG_PATH);
spec = SecuritySpecifications.applyPersonFilter(spec, securityService, PERSON_PATH);
Path Functions
Define path functions using JPA static metamodels:
// Direct relationship: Event -> organiser -> id
Function<Root<Event>, Expression<Long>> eventOrgPath =
root -> root.join(Event_.organiser, JoinType.LEFT).get(Organisation_.id);
// Transitive relationship: EventParticipant -> event -> organiser -> id
Function<Root<EventParticipant>, Expression<Long>> participantOrgPath =
root -> root.join(EventParticipant_.event, JoinType.LEFT)
.join(Event_.organiser, JoinType.LEFT)
.get(Organisation_.id);
// Deep transitive: Race -> eventRaceType -> event -> organiser -> id
Function<Root<Race>, Expression<Long>> raceOrgPath =
root -> root.join(Race_.eventRaceType, JoinType.LEFT)
.join(EventRaceType_.event, JoinType.LEFT)
.join(Event_.organiser, JoinType.LEFT)
.get(Organisation_.id);
Implementation Pattern
Organisation-Scoped Entity (Event)
@Service
@Transactional(readOnly = true)
public class EventQueryServiceEx extends EventQueryService {
@Autowired
private SecurityDimensionService securityDimensionService;
@Autowired
private EventRepository eventRepository;
@Autowired
private EventMapper eventMapper;
// Define org path as constant for reuse
private static final Function<Root<Event>, Expression<Long>> ORG_PATH =
root -> root.join(Event_.organiser, JoinType.LEFT).get(Organisation_.id);
/**
* Find events with security filtering applied.
* Only returns events from organisations the user has access to.
*/
public Page<EventDTO> findByCriteriaSecure(EventCriteria criteria, Pageable page) {
Specification<Event> spec = createSpecification(criteria);
spec = SecuritySpecifications.applyOrgFilter(
spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
return eventRepository.findAll(spec, page).map(eventMapper::toDto);
}
/**
* Find single event with security filtering.
*/
public Optional<EventDTO> findOneSecure(Long id) {
Specification<Event> spec = (root, query, cb) -> cb.equal(root.get(Event_.id), id);
spec = SecuritySpecifications.applyOrgFilter(
spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
return eventRepository.findOne(spec).map(eventMapper::toDto);
}
}
Composite-Scoped Entity (EventParticipant)
For entities requiring both organisation AND person access:
@Service
@Transactional(readOnly = true)
public class EventParticipantQueryServiceEx extends EventParticipantQueryService {
@Autowired
private SecurityDimensionService securityDimensionService;
// Org path: EventParticipant -> event -> organiser -> id
private static final Function<Root<EventParticipant>, Expression<Long>> ORG_PATH =
root -> root.join(EventParticipant_.event, JoinType.LEFT)
.join(Event_.organiser, JoinType.LEFT)
.get(Organisation_.id);
// Person path: EventParticipant -> person -> id
private static final Function<Root<EventParticipant>, Expression<Long>> PERSON_PATH =
root -> root.join(EventParticipant_.person, JoinType.LEFT).get(Person_.id);
/**
* Find participants with composite security filtering.
* User must have access to BOTH the organisation AND the person.
*/
public Page<EventParticipantDTO> findByCriteriaSecure(
EventParticipantCriteria criteria, Pageable page) {
Specification<EventParticipant> spec = createSpecification(criteria);
spec = SecuritySpecifications.applyOrgFilter(
spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
spec = SecuritySpecifications.applyPersonFilter(spec, securityDimensionService, PERSON_PATH);
return eventParticipantRepository.findAll(spec, page)
.map(eventParticipantMapper::toDto);
}
}
Transitive Path Entity (Race)
For entities with deep relationship chains:
@Service
@Transactional(readOnly = true)
public class RaceQueryServiceEx extends RaceQueryService {
@Autowired
private SecurityDimensionService securityDimensionService;
// Deep path: Race -> eventRaceType -> event -> organiser -> id
private static final Function<Root<Race>, Expression<Long>> ORG_PATH =
root -> root.join(Race_.eventRaceType, JoinType.LEFT)
.join(EventRaceType_.event, JoinType.LEFT)
.join(Event_.organiser, JoinType.LEFT)
.get(Organisation_.id);
public Page<RaceDTO> findByCriteriaSecure(RaceCriteria criteria, Pageable page) {
Specification<Race> spec = createSpecification(criteria);
spec = SecuritySpecifications.applyOrgFilter(
spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
return raceRepository.findAll(spec, page).map(raceMapper::toDto);
}
}
Entity Path Reference
Common entity paths to organisation/person:
| Entity | Organisation Path | Person Path |
|---|---|---|
Event |
|
N/A |
EventParticipant |
|
|
Race |
|
N/A |
EventCategory |
|
N/A |
Membership |
|
|
REST Controller Integration
Controllers should inject and use the *Ex class:
@RestController
@RequestMapping("/api/events")
public class EventResource {
private final EventQueryServiceEx eventQueryServiceEx; // Use Ex class
@GetMapping
public ResponseEntity<List<EventDTO>> getAllEvents(EventCriteria criteria, Pageable page) {
// Use secured method
Page<EventDTO> result = eventQueryServiceEx.findByCriteriaSecure(criteria, page);
return ResponseEntity.ok()
.headers(PaginationUtil.generatePaginationHttpHeaders(result, "/api/events"))
.body(result.getContent());
}
@GetMapping("/{id}")
public ResponseEntity<EventDTO> getEvent(@PathVariable Long id) {
return eventQueryServiceEx.findOneSecure(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
Deprecated: SecuredQueryService Base Class
|
The The base class approach had several drawbacks:
|
Next Steps
-
Implement Service Layer
-
Add REST Controllers
-
Write Integration Tests