Spring Data Rest: #RepositoryRestController and projections [duplicate] - java

Is it possible to specify projection when calling data repository method directly? Here's repository code - note I would not like to expose it via REST, instead I would like to be able to call it from a service or controller:
#RepositoryRestResource(exported = false)
public interface UsersRepository extends PagingAndSortingRepository<User, Long> {
#Query(value = "SELECT u FROM User u WHERE ....")
public Page<User> findEmployeeUsers(Pageable p);
}
Then in a controller I do this:
#PreAuthorize(value = "hasRole('ROLE_ADMIN')")
#RequestMapping(value = "/users/employee")
public Page<User> listEmployees(Pageable pageable) {
return usersRepository.findEmployeeUsers(pageable);
}
Is there any way to specify projection for findEmployeeUsers method when it is called directly like above?
I realise that the code above might look odd for someone... it would be possible to expose the repository via REST and put the #PreAuthorize thing in the repository. Thought controller is the more right place to do security checks - it is more natural as well as simpler to test.
So, can projection thing be somehow passed into a repository method called directly?

No it's not, especially as projections are usually applied to the result of a query execution on a case by case basis. Thus they're currently designed to be selectively applied to domain types.
As of the latest Spring Data Fowler release train GA release the projection infrastructure can be used programmatically in Spring MVC controllers. Simply declare a Spring bean for SpelAwareProxyProjectionFactory:
#Configuration
class SomeConfig {
#Bean
public SpelAwareProxyProjectionFactory projectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
}
Then inject it into your controller and use it:
#Controller
class SampleController {
private final ProjectionFactory projectionFactory;
#Autowired
public SampleController(ProjectionFactory projectionFactory) {
this.projectionFactory = projectionFactory;
}
#PreAuthorize(value = "hasRole('ROLE_ADMIN')")
#RequestMapping(value = "/users/employee")
public Page<?> listEmployees(Pageable pageable) {
return usersRepository.findEmployeeUsers(pageable).//
map(user -> projectionFactory.createProjection(Projection.class, user);
}
}
See how as of the latest release Page has a map(…) method that can be used to transform the page content on the fly. We use a JDK 8 lambda to provide a conversion step using the ProjectionFactory.

Additionally to the #Oliver's answer, if you want to lookup the Projections by name as SpringDataRest does (instead of hardwired them in your controller), this is what you have to do:
Inject RepositoryRestConfiguration into your controller. This bean gives you access to a class called ProjectionDefinitions (see, getProjectionConfiguration()) which acts a projection metadata directory.
Using ProjectionDefinitions you can retrieve Projection Classes given their names and their associated bound classes.
Later, you can use the method detailed by #Oliver to create the projections instances ...
This is a small Controller that implements what I describe:
#RestController
#RequestMapping("students")
public class StudentController {
/**
* {#link StudentController} logger.
*/
private static final Logger logger =
LoggerFactory.getLogger(StudentController.class);
/**
* Projections Factory.
*/
private ProjectionFactory p8nFactory;
/**
* Projections Directory.
*/
private ProjectionDefinitions p8nDefs;
/**
* {#link Student} repository.
*/
private StudentRepository repo;
/**
* Class Constructor.
*
* #param repoConfig
* {#code RepositoryRestConfiguration} bean
* #param p8nFactory
* Factory used to create projections
* #param repo
* {#link StudentRepository} instance
*/
#Autowired
public StudentController(
RepositoryRestConfiguration repoConfig,
ProjectionFactory p8nFactory,
StudentRepository repo
) {
super();
this.p8nFactory = p8nFactory;
this.p8nDefs = repoConfig.getProjectionConfiguration();
this.repo = repo;
}
...
/**
* Retrieves all persisted students.
*
* #param projection
* (Optional) Name of the projection to be applied to
* students retrieved from the persistence layer
* #return
* {#code ResponseEntity} whose content can be a list of Students
* or a projected view of them
*/
#GetMapping(path = "", produces = APPLICATION_JSON_VALUE)
public ResponseEntity<Object> retrieveAll(
#RequestParam(required = false) String projection
) {
Class<?> type; // Kind of Projection to be applied
List<?> rawData; // Raw Entity Students
List<?> pjData; // Projected students (if applies)
rawData = this.repo.findAll();
pjData = rawData;
if (projection != null) {
type = this.p8nDefs.getProjectionType(Student.class, projection);
pjData = rawData
.stream()
.map(s -> this.p8nFactory.createProjection(type, s))
.collect(Collectors.toList());
}
return new ResponseEntity<>(pjData, HttpStatus.OK);
}
}

It can be easily done in the lates Spring Data Rest releases!
All you need to do is to:
pass projection name as request param
`/api/users/search/findEmployeeUsers?projection=userView`
return PagedModel<PersistentEntityResource> instead of Page<User> from your service method;
Done!
and I assume you want to call this service method from your custom controller, in this case you need to return ResponseEntity<PagedModel<PersistentEntityResource>> from your controller method.
Don't want it pageable? Simply return ResponseEntity<CollectionModel<PersistentEntityResource>> instead.
Also check out example for single resoure projection.
Spring Data Rest takes care of applying #Projections to PersistentEntityResources on api requests, it's just like you keep exposing your #RestResource from #RepositoryRestResource; same behaviour for projections, keeping same naming convention, basically same URI (for current example).
Your service method with a bit of bussiness logic might look like:
#Override
#Transactional(readOnly = true)
public PagedModel<PersistentEntityResource> listEmployees(Pageable pageable, PersistentEntityResourceAssembler resourceAssembler) {
Page<User> users = userRepository.findEmployeeUsers(pageable);
List<User> entities = users.getContent();
entities.forEach(user -> user.setOnVacation(isUserOnVacationNow(user)));
CollectionModel<PersistentEntityResource> collectionModel = resourceAssembler.toCollectionModel(entities);
return PagedModel.of(collectionModel.getContent(), new PagedModel.PageMetadata(
users.getSize(),
users.getNumber(),
users.getTotalElements(),
users.getTotalPages()));
}
and your controller method might look like this:
#BasePathAwareController
public class UsersController {
#GetMapping(value = "/users/search/findEmployeeUsers")
ResponseEntity<PagedModel<PersistentEntityResource>> findEmployeeUsers(Pageable pageable,
PersistentEntityResourceAssembler resourceAssembler) {
return ResponseEntity.status(HttpStatus.OK)
.body(userService.listEmployees(pageable, resourceAssembler));
}
}
I'm using spring-boot-starter-data-rest:2.3.4.RELEASE with spring-data-rest-webmvc:3.3.4.RELEASE and spring-data-rest-webmvc:3.3.4.RELEASE as dependencies, configuring it as parent of my pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Related

Spring QueryDsl pagination filter by ACL permissions

Let's assume the following Spring JPA based repository with QueryDsl support.
#Repository
public interface TeamRepository extends JpaRepository<Team, Long>, QuerydslPredicateExecutor<Team> {
}
The application uses Access Control Lists (ACL) in service layer for checking permission for individual resources using #PreAuthorize(hasPermission(#id, 'Team', 'READ') for example.
I want to allow a user to request all teams for which he has read permission. I tried to use
#PostFilter(hasPermission(filterObject, 'READ'), that works pretty good as long as I use Iterable<Team> findAll(Predicate predicate). But when I try to make use of pagination, #PostFilter seems to throw an exception.
java.lang.IllegalArgumentException: Filter target must be a collection, array, or stream type, but was Page 1 of 0 containing UNKNOWN instances
The official Spring Security Reference Documentation recommends to write a custom query using #Query which supports pagination.
How could I write such a complex query which supports QueryDsl's Predicate, Pagination and filtering based on permissions?
Approach 03/24/20
In another forum I came across the following QueryDsl based approach: Instead of a native or custom query, the ACL tables are mapped as #Immutable JPA entities, thus generating Q classes and using them to filter for permissions manually.
#Entity
#Immutable
#Table(name = "acl_object_identity")
public class AclObjectIdentity implements Serializable {
...
}
How could you do this using a custom repository, extending QueryDslRepositorySupport, so that the part of the query that checks permissions is automatically appended and hidden inside of a custom repository implementation?
Based on this approach I have developed a possibility which is more a dirty workaround than a solution.
The approach is to add an additional permission filter to existing predicates, for example those generated by web support. For this, the ACL tables must first be mapped as #Immutable JPA entities so that QueryDsl can generate the corresponding Q classes.
Such predicates to which an ACL Permission Filter should be appended are marked with the following annotation.
public Page<PostDTO> findAll(#QueryDslAclPermission(root = Post.class, permission = "READ") Predicate predicate, Pageable pageable) {
...
}
This annotation holds primarily meta information about the domain type that are required for building the filter query.
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.PARAMETER, ElementType.TYPE})
public #interface QueryDslAclPermission {
Class<?> root();
String permission();
String identifier() default "id";
}
The actual filter query is generated and appended using the the following class and Spring's AOP Module.
#Aspect
#Component
public class QueryDslAclPermissionAspect {
private PermissionFactory permissionFactory;
#Autowired
public QueryDslAclPermissionAspect(PermissionFactory permissionFactory) {
this.permissionFactory = permissionFactory;
}
#Around(value = "execution(* *(.., #QueryDslAclPermission (*), ..))")
public Object addPermissionFilter(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Parameter[] parameters = method.getParameters();
Object[] arguments = joinPoint.getArgs();
for(int index = 0; index < parameters.length; ++index) {
if(parameters[index].getType().equals(Predicate.class) &&
parameters[index].isAnnotationPresent(QueryDslAclPermission.class)) {
Predicate predicate = (Predicate) arguments[index];
QueryDslAclPermission aclPermission = parameters[index].getAnnotation(QueryDslAclPermission.class);
arguments[index] = addPermissionFilter(predicate, aclPermission);
}
}
return joinPoint.proceed(arguments);
}
private Predicate addPermissionFilter(Predicate predicate, QueryDslAclPermission aclPermission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(null == authentication || !authentication.isAuthenticated()) {
throw new IllegalStateException("Permission filtering not possible for unauthenticated principal");
}
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
PrincipalSid principalSid = new PrincipalSid(userDetails.getUsername());
NumberPath<Long> idPath = new PathBuilderFactory().create(aclPermission.root())
.getNumber(aclPermission.identifier(), Long.class);
return idPath.in(selectPermitted(aclPermission.root(), principalSid,
permissionFactory.buildFromName(aclPermission.permission()))).and(predicate);
}
private JPQLQuery<Long> selectPermitted(Class<?> targetType, PrincipalSid sid, Permission permission) {
return selectAclEntry(targetType, sid, permission)
.select(QAclEntry.aclEntry.aclObjectIdentity.objectIdIdentity);
}
private JPQLQuery<AclEntry> selectAclEntry(Class<?> targetType, PrincipalSid sid, Permission permission) {
return new JPAQuery<AclEntry>().from(QAclEntry.aclEntry)
.where(QAclEntry.aclEntry.aclObjectIdentity.id.in(selectAclObjectIdentity(targetType)
.select(QAclObjectIdentity.aclObjectIdentity.id))
.and(QAclEntry.aclEntry.aclSid.id.eq(selectAclSid(sid).select(QAclSid.aclSid.id)))
.and(QAclEntry.aclEntry.mask.eq(permission.getMask())));
}
private JPQLQuery<AclObjectIdentity> selectAclObjectIdentity(Class<?> targetType) {
return new JPAQuery<AclObjectIdentity>().from(QAclObjectIdentity.aclObjectIdentity)
.where(QAclObjectIdentity.aclObjectIdentity.objectIdClass.id.eq(selectAclClass(targetType)
.select(QAclClass.aclClass.id)));
}
private JPQLQuery<AclSid> selectAclSid(PrincipalSid sid) {
return new JPAQuery<AclSid>().from(QAclSid.aclSid)
.where(QAclSid.aclSid.sid.eq(sid.getPrincipal()));
}
private JPQLQuery<AclClass> selectAclClass(Class<?> targetType) {
return new JPAQuery<AclClass>().from(QAclClass.aclClass)
.where(QAclClass.aclClass.className.eq(targetType.getSimpleName()));
}
}
Edit 09/20/2022
A more generic approach based on JPA's Specification<T> and a custom repository implementation can be found in my GitHub Gist. It is decoupled from QueryDsl.

QueryDsl web query on the key of a Map field

Overview
Given
Spring Data JPA, Spring Data Rest, QueryDsl
a Meetup entity
with a Map<String,String> properties field
persisted in a MEETUP_PROPERTY table as an #ElementCollection
a MeetupRepository
that extends QueryDslPredicateExecutor<Meetup>
I'd expect
A web query of
GET /api/meetup?properties[aKey]=aValue
to return only Meetups with a property entry that has the specified key and value: aKey=aValue.
However, that's not working for me.
What am I missing?
Tried
Simple Fields
Simple fields work, like name and description:
GET /api/meetup?name=whatever
Collection fields work, like participants:
GET /api/meetup?participants.name=whatever
But not this Map field.
Customize QueryDsl bindings
I've tried customizing the binding by having the repository
extend QuerydslBinderCustomizer<QMeetup>
and overriding the
customize(QuerydslBindings bindings, QMeetup meetup)
method, but while the customize() method is being hit, the binding code inside the lambda is not.
EDIT: Learned that's because QuerydslBindings means of evaluating the query parameter do not let it match up against the pathSpecs map it's internally holding - which has your custom bindings in it.
Some Specifics
Meetup.properties field
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "MEETUP_PROPERTY", joinColumns = #JoinColumn(name = "MEETUP_ID"))
#MapKeyColumn(name = "KEY")
#Column(name = "VALUE", length = 2048)
private Map<String, String> properties = new HashMap<>();
customized querydsl binding
EDIT: See above; turns out, this was doing nothing for my code.
public interface MeetupRepository extends PagingAndSortingRepository<Meetup, Long>,
QueryDslPredicateExecutor<Meetup>,
QuerydslBinderCustomizer<QMeetup> {
#Override
default void customize(QuerydslBindings bindings, QMeetup meetup) {
bindings.bind(meetup.properties).first((path, value) -> {
BooleanBuilder builder = new BooleanBuilder();
for (String key : value.keySet()) {
builder.and(path.containsKey(key).and(path.get(key).eq(value.get(key))));
}
return builder;
});
}
Additional Findings
QuerydslPredicateBuilder.getPredicate() asks QuerydslBindings.getPropertyPath() to try 2 ways to return a path from so it can make a predicate that QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess() can use.
1 is to look in the customized bindings. I don't see any way to express a map query there
2 is to default to Spring's bean paths. Same expression problem there. How do you express a map?
So it looks impossible to get QuerydslPredicateBuilder.getPredicate() to automatically create a predicate.
Fine - I can do it manually, if I can hook into QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
HOW can I override that class, or replace the bean? It's instantiated and returned as a bean in the RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean declaration.
I can override that bean by declaring my own repoRequestArgumentResolver bean, but it doesn't get used.
It gets overridden by RepositoryRestMvcConfigurations. I can't force it by setting it #Primary or #Ordered(HIGHEST_PRECEDENCE).
I can force it by explicitly component-scanning RepositoryRestMvcConfiguration.class, but that also messes up Spring Boot's autoconfiguration because it causes
RepositoryRestMvcConfiguration's bean declarations to be processed
before any auto-configuration runs. Among other things, that results in responses that are serialized by Jackson in unwanted ways.
The Question
Well - looks like the support I expected just isn't there.
So the question becomes:
HOW do I correctly override the repoRequestArgumentResolver bean?
BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver is awkwardly non-public. :/
Replace the Bean
Implement ApplicationContextAware
This is how I replaced the bean in the application context.
It feels a little hacky. I'd love to hear a better way to do this.
#Configuration
public class CustomQuerydslHandlerMethodArgumentResolverConfig implements ApplicationContextAware {
/**
* This class is originally the class that instantiated QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver and placed it into the Spring Application Context
* as a {#link RootResourceInformationHandlerMethodArgumentResolver} by the name of 'repoRequestArgumentResolver'.<br/>
* By injecting this bean, we can let {#link #meetupApiRepoRequestArgumentResolver} delegate as much as possible to the original code in that bean.
*/
private final RepositoryRestMvcConfiguration repositoryRestMvcConfiguration;
#Autowired
public CustomQuerydslHandlerMethodArgumentResolverConfig(RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
this.repositoryRestMvcConfiguration = repositoryRestMvcConfiguration;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((GenericApplicationContext) applicationContext).getBeanFactory();
beanFactory.destroySingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME);
beanFactory.registerSingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME,
meetupApiRepoRequestArgumentResolver(applicationContext, repositoryRestMvcConfiguration));
}
/**
* This code is mostly copied from {#link RepositoryRestMvcConfiguration#repoRequestArgumentResolver()}, except the if clause checking if the QueryDsl library is
* present has been removed, since we're counting on it anyway.<br/>
* That means that if that code changes in the future, we're going to need to alter this code... :/
*/
#Bean
public RootResourceInformationHandlerMethodArgumentResolver meetupApiRepoRequestArgumentResolver(ApplicationContext applicationContext,
RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(repositoryRestMvcConfiguration.defaultConversionService(),
factory.getEntityPathResolver());
return new CustomQuerydslHandlerMethodArgumentResolver(repositoryRestMvcConfiguration.repositories(),
repositoryRestMvcConfiguration.repositoryInvokerFactory(repositoryRestMvcConfiguration.defaultConversionService()),
repositoryRestMvcConfiguration.resourceMetadataHandlerMethodArgumentResolver(),
predicateBuilder, factory);
}
}
Create a Map-searching predicate from http params
Extend RootResourceInformationHandlerMethodArgumentResolver
And these are the snippets of code that create my own Map-searching predicate based on the http query parameters.
Again - would love to know a better way.
The postProcess method calls:
predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();
just before the predicate reference is passed into the QuerydslRepositoryInvokerAdapter constructor and returned.
Here is that addCustomMapPredicates method:
private BooleanBuilder addCustomMapPredicates(MultiValueMap<String, String> parameters, Predicate predicate, Class<?> domainType) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
parameters.keySet()
.stream()
.filter(s -> s.contains("[") && matches(s) && s.endsWith("]"))
.collect(Collectors.toList())
.forEach(paramKey -> {
String property = paramKey.substring(0, paramKey.indexOf("["));
if (ReflectionUtils.findField(domainType, property) == null) {
LOGGER.warn("Skipping predicate matching on [%s]. It is not a known field on domainType %s", property, domainType.getName());
return;
}
String key = paramKey.substring(paramKey.indexOf("[") + 1, paramKey.indexOf("]"));
parameters.get(paramKey).forEach(value -> {
if (!StringUtils.hasLength(value)) {
booleanBuilder.or(matchesProperty(key, null));
} else {
booleanBuilder.or(matchesProperty(key, value));
}
});
});
return booleanBuilder.and(predicate);
}
static boolean matches(String key) {
return PATTERN.matcher(key).matches();
}
And the pattern:
/**
* disallow a . or ] from preceding a [
*/
private static final Pattern PATTERN = Pattern.compile(".*[^.]\\[.*[^\\[]");
I spent a few days looking into how to do this. In the end I just went with manually adding to the predicate. This solution feels simple and elegant.
So you access the map via
GET /api/meetup?properties.aKey=aValue
On the controller I injected the request parameters and the predicate.
public List<Meetup> getMeetupList(#QuerydslPredicate(root = Meetup.class) Predicate predicate,
#RequestParam Map<String, String> allRequestParams,
Pageable page) {
Predicate builder = createPredicateQuery(predicate, allRequestParams);
return meetupRepo.findAll(builder, page);
}
I then just simply parsed the query parameters and added contains
private static final String PREFIX = "properties.";
private BooleanBuilder createPredicateQuery(Predicate predicate, Map<String, String> allRequestParams) {
BooleanBuilder builder = new BooleanBuilder();
builder.and(predicate);
allRequestParams.entrySet().stream()
.filter(e -> e.getKey().startsWith(PREFIX))
.forEach(e -> {
var key = e.getKey().substring(PREFIX.length());
builder.and(QMeetup.meetup.properties.contains(key, e.getValue()));
});
return builder;
}

Spring Boot and JPA Repository -- how to filter a GET by ID

I'm rewriting an application, this time using a RESTful interface from Spring. I'm presuming that server-side authorization is best. That is:
Supppose user 1 works this REST repository. He/she accesses mysite.com/heroes/1 and gets the (id = 1) hero from the hero table.
User 2 doesn't have rights to see the (id = 1) hero, but could craft a cURL statement to try anyway. I claim the server should prevent user 2 from accessing the (id = 1) hero.
I believe that the server can extract a JWT payload that gives me the user name or password (I put it in there). From that payload the server fetches the user's account and knows what heroes he/she is entitled to see.
I have already accomplished this goal through services and DAO classes. However, the Spring Boot and JPA tutorials I see promote using CrudRepository implementations to reduce coding. I'd like to know how to do my filtering using this technology.
Here is an example from the web:
#RepositoryRestResource(collectionResourceRel = "heroes", path = "heroes")
public interface HeroRepository extends CrudRepository<Hero, Long> {
}
When mysite.com/heroes/1 is accessed it automagically returns the data from hero (id = 1). I'd like to instruct it to let me choose which ID values to permit. That is, at runtime a query parameter is provided to it through code.
As a test I provided this code:
#RepositoryRestResource(collectionResourceRel = "heroes", path = "heroes")
public interface HeroRepository extends CrudRepository<Hero, Long> {
#Query ("from Hero h where id in (1, 3, 5)")
public Hero get();
}
However, it doesn't block mysite.com/heroes/2 from returning the (id = 2) hero.
How do I get to my desired goal?
Thanks, Jerome.
UPDATE 5/13, 5:50 PM
My request is being misunderstood, so I further explain my intent.
Users 1 and 2 are ordinary users, accessing their accounts.
Each user must be confined to his/her own account.
A user can't cheat by crafting requests for other peoples' data.
Thus the need for the server to extract a user ID, or such, from a JWT token and apply it in code to whatever causes the /heroes query to work.
My original example originated with this tutorial. In it the only Java classes are Hero and HeroRepository. There are no explicit classes for DAO, services or controllers. The included Spring libraries let all of the /heroes fetching occur without further coding.
Thanks again for all of your interest and help. Jerome.
You can create a custom #Query, that uses informations (here: id) of the logged in user. With this solution an user have only access to an entity with the same id as he has.
#Override
#Query("SELECT h FROM Hero h WHERE h.id=?1 AND h.id=?#{principal.id}")
public Hero findOne(Long id);
You need to enable SpEl for #Query (link) and create an custom UserDetailsService (link) with custom UserDetails, that contains the id of the user, so you can do principal.id.
In the same way you should secure the findAll() method.
I have created HeroRepository to resolve all the queries up to my understanding.
I'd like to instruct it to let me choose which ID values to permit.
You can achieve the same using.
List<Hero> findByIdIn(List<Long> ids);
Or, if you prefer Query
#Query("SELECT H FROM Hero H WHERE H.id IN :ids")
List<Hero> alternativeFindByIdIn(#Param("ids") List<Long> ids);
it doesn't block mysite.com/heroes/2 from returning the (id = 2) hero.
I cannot see your Controller/Service methods, so I am assuming that findOne() is being called. You can prevent it using..
// Disallow everybody to use findOne()
default Hero findOne(Long id) {
throw new RuntimeException("Forbidden !!");
}
OR, if you want more control over your method invocations, you can also use #PreAuthorize from spring-security.
// Authorization based method call
#PreAuthorize("hasRole('ADMIN')")
Optional<Hero> findById(Long id);
Summary
public interface HeroRepository extends CrudRepository<Hero, Long> {
// Disallow everybody to use findOne()
default Hero findOne(Long id) {
throw new RuntimeException("Forbidden !!");
}
// If u want to pass ids as a list
List<Hero> findByIdIn(List<Long> ids);
// Alternative to above one
#Query("SELECT H FROM Hero H WHERE H.id IN :ids")
List<Hero> alternativeFindByIdIn(#Param("ids") List<Long> ids);
// Authorization based method call
#PreAuthorize("hasRole('ADMIN')")
Optional<Hero> findById(Long id);
}
PS: Note that I am returning Optional<Hero> from the method. Optional.empty() will be returned if query produces no results. This will force us to check if the value is present before doing any operation, thereby avoiding NullPointerException.
use this code for Controller : -
#RestController
#RequestMapping("/cities")
public class CityController {
private static final Logger logger = LoggerFactory.getLogger(CityController.class);
#Autowired
private CityService cityService;
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public RestResponse find(#PathVariable("id") Long id) {
.
.
}
use below code for Repo :-
public interface CityRepo extends JpaRepository<FCity, Long> {
#Query("select e from FCity e where e.cityId = :id")
FCity findOne(#Param("id") Long id);
}
use below code for service :-
#Service
#Transactional
public class CityService {
#Autowired(required = true)
private CityRepo cityRepo;
public FCity findOne(Long id) {
return cityRepo.findOne(id);
}
}

Sorting by parent entity using Specifications

I’m dealing with an issue which to my understanding looks unsupported on Spring Data JPA.
I got a grid (using JqGrid plugin for jQuery) on the view which sends parameters to the server, they are parsed and then a dynamic query generated through Specifications is executed.
The issue comes when I want to order a column which doesn’t belong to the root entity.
Eg. Transaction, Card and Account are my entities and grid displays last4digits as a way for the user to identify the card. As you can imagine last4digits belongs to Card. I query transactions per account.
Using specifications I can filter by that attribute, joining tables and so on but sorting fails as findAll() implementation assumes properties from Sort class belongs to the root entity.
Code example:
JQGridRule panFirst6DigitsRule = FilterUtils.findSearchOrFilterRule(settings, Card_.panFirst6Digits.getName());
JQGridRule panLast4DigitsRule = FilterUtils.findSearchOrFilterRule(settings, Card_.panLast4Digits.getName());
if(panFirst6DigitsRule != null) {
filterPan1 = TransactionSpecs.withPanFirst6Digits(panFirst6DigitsRule.getData(),
panFirst6DigitsRule.getOp(), gridGroupOp);
}
if(panLast4DigitsRule != null) {
filterPan2 = TransactionSpecs.withPanLast4Digits(panLast4DigitsRule.getData(),
panLast4DigitsRule.getOp(), gridGroupOp);
}
Specification<Transaction> joinSpec = TransactionSpecs.withAccountId(account.getAccountId());
Specification<Transaction> activeSpec = BaseSpecs.withEntityStatus(true);
Page<Transaction> results = transactionRepository.findAll(
Specifications.where(joinSpec).and(filterSpec).and(filterPan1).and(filterPan2).and(activeSpec), springPageable);
springPageable variable contains a Sort for last4Digits column generated this way*:
List<Order> sortOrders = new ArrayList<Order>();
Order sortOrder = new Order(Direction.ASC, "panLast4Digits");
sortOrders.add(sortOrder);
sort = new Sort(sortOrders);
*There are missing code parsing parameters and creating more Order objects
Does someone know how to implement that kind of sort over an attribute which belongs to a parent entity/class?
Thanks in advance
Version 1.4.3 for Spring-data-jpa and 4.2.8 for Hibernate
EDIT
Showing how Specification for panLast4Digits is generated
public static Specification<Transaction> withPanLast4Digits(final String panLast4Digits, final JQGridSearchOp op, final JQGridGroupOp whereOp) {
Specification<Transaction> joinSpec = new Specification<Transaction>() {
#Override
public Predicate toPredicate(Root<Transaction> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Transaction, Card> join = joinCards(root, JoinType.INNER);
return FilterUtils.buildPredicate(cb, join.get(Card_.panLast4Digits), op, panLast4Digits, null, whereOp);
}
};
return joinSpec;
}
private static Join<Transaction, Card> joinCards(Root<Transaction> root, JoinType joinType) {
Join<Transaction, Card> join = getJoin(root, Transaction_.parentCard, joinType);
// only join if not already joined
if (join == null) {
join = root.join(Transaction_.parentCard, joinType);
}
return join;
}
protected static <C, T> Join<C, T> getJoin(Root<C> root, Attribute<? super C, T> attribute, JoinType joinType) {
Set<Join<C, ?>> joins = root.getJoins();
for (Join<C, ?> join : joins) {
if (join.getAttribute().equals(attribute) && join.getJoinType().equals(joinType)) {
return (Join<C, T>) join;
}
}
return null;
}
Also I have updated to spring-data-jpa 1.6.0 and hibernate 4.3.5
the attribute for Sorting is "yourChildentity.attribute"
In your Case you can use the PagingAndSortingRepository this way:
let's assume you have two entities : an Account and a Card
#Entity
public class Account{
// Autogeneration and Ill just assume that your id is type long
private Long id;
#ManyToOne
#JoinColumn(name="CARD_ID")
private Card creditCard;
//getters and setters
}
#Entity
public class Card{
//Id and other attributes.
private String panLast4Digits;
//getters and Setters
}
Repository interface :
#Repository
public interface AccountRepository extends PagingAndSortingRepository<Account, Long>,
JpaSpecificationExecutor<Account>{
}
Service Layer :
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface AccountService{
//you can specify other arguments the one that you want to filter by
Page<Account> filter(Pageable pageable);
}
Service Implementation:
#Service
public calss AccountServiceImpl implements AccountService{
#Resource//or #Autowired
private AccountRepository repository;
#Override
public Page<Account> filter(Pageable pageable){
//Filter using Specifications if you have other arguments passed in the signature of the method.
return repository.findAll(pageable);//if you have specifications than return repository.findAll(yourspecification,pageable);
}
Now the call to service throw an endpoint or a Controller:
just a mthod to see how to sort throw child entity parameter :
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort.Direction;
// method
#Resource
private AccountService service;
public Page<Account> consumeMyService(){
// 0 : for Page 1
// 12 for page size
// Soting throw Child enntiy Account , by attribute panLast4Digits
PageRequest pageable = new PageRequest(0,
12, Direction.ASC, "mycard.panLast4Digits");
Page<Account> service.filter(pageable);
}
You must register you beans by configuring Jpa:repositories for the repository interfaces, and context:component-scan for service implementation
this answer may be useful too.

Filtering database rows with spring-data-jpa and spring-mvc

I have a spring-mvc project that is using spring-data-jpa for data access. I have a domain object called Travel which I want to allow the end-user to apply a number of filters to it.
For that, I've implemented the following controller:
#Autowired
private TravelRepository travelRep;
#RequestMapping("/search")
public ModelAndView search(
#RequestParam(required= false, defaultValue="") String lastName,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page<Travel> travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject("page", page);
mav.addObject("lastName", lastName);
return mav;
}
This works fine: The user has a form with a lastName input box which can be used to filter the Travels.
Beyond lastName, my Travel domain object has a lot more attributes by which I'd like to filter. I think that if these attributes were all strings then I could add them as #RequestParams and add a spring-data-jpa method to query by these. For instance I'd add a method findByLastNameLikeAndFirstNameLikeAndShipNameLike.
However, I don't know how should I do it when I need to filter for foreign keys. So my Travel has a period attribute that is a foreign key to the Period domain object, which I need to have it as a dropdown for the user to select the Period.
What I want to do is when the period is null I want to retrieve all travels filtered by the lastName and when the period is not null I want to retrieve all travels for this period filtered by the lastName.
I know that this can be done if I implement two methods in my repository and use an if to my controller:
public ModelAndView search(
#RequestParam(required= false, defaultValue="") String lastName,
#RequestParam(required= false, defaultValue=null) Period period,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page travels = null;
if(period==null) {
travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
} else {
travels = travelRep.findByPeriodAndLastNameLike(period,"%"+lastName+"%", pageable);
}
mav.addObject("page", page);
mav.addObject("period", period);
mav.addObject("lastName", lastName);
return mav;
}
Is there a way to do this without using the if ? My Travel has not only the period but also other attributes that need to be filtered using dropdowns !! As you can understand, the complexity would be exponentially increased when I need to use more dropdowns because all the combinations'd need to be considered :(
Update 03/12/13: Continuing from M. Deinum's excelent answer, and after actually implementing it, I'd like to provide some comments for completeness of the question/asnwer:
Instead of implementing JpaSpecificationExecutor you should implement JpaSpecificationExecutor<Travel> to avoid type check warnings.
Please take a look at kostja's excellent answer to this question
Really dynamic JPA CriteriaBuilder
since you will need to implement this if you want to have correct filters.
The best documentation I was able to find for the Criteria API was http://www.ibm.com/developerworks/library/j-typesafejpa/. This is a rather long read but I totally recommend it - after reading it most of my questions for Root and CriteriaBuilder were answered :)
Reusing the Travel object was not possible because it contained various other objects (who also contained other objects) which I needed to search for using Like - instead I used a TravelSearch object that contained the fields I needed to search for.
Update 10/05/15: As per #priyank's request, here's how I implemented the TravelSearch object:
public class TravelSearch {
private String lastName;
private School school;
private Period period;
private String companyName;
private TravelTypeEnum travelType;
private TravelStatusEnum travelStatus;
// Setters + Getters
}
This object was used by TravelSpecification (most of the code is domain specific but I'm leaving it there as an example):
public class TravelSpecification implements Specification<Travel> {
private TravelSearch criteria;
public TravelSpecification(TravelSearch ts) {
criteria= ts;
}
#Override
public Predicate toPredicate(Root<Travel> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
Join<Travel, Candidacy> o = root.join(Travel_.candidacy);
Path<Candidacy> candidacy = root.get(Travel_.candidacy);
Path<Student> student = candidacy.get(Candidacy_.student);
Path<String> lastName = student.get(Student_.lastName);
Path<School> school = student.get(Student_.school);
Path<Period> period = candidacy.get(Candidacy_.period);
Path<TravelStatusEnum> travelStatus = root.get(Travel_.travelStatus);
Path<TravelTypeEnum> travelType = root.get(Travel_.travelType);
Path<Company> company = root.get(Travel_.company);
Path<String> companyName = company.get(Company_.name);
final List<Predicate> predicates = new ArrayList<Predicate>();
if(criteria.getSchool()!=null) {
predicates.add(cb.equal(school, criteria.getSchool()));
}
if(criteria.getCompanyName()!=null) {
predicates.add(cb.like(companyName, "%"+criteria.getCompanyName()+"%"));
}
if(criteria.getPeriod()!=null) {
predicates.add(cb.equal(period, criteria.getPeriod()));
}
if(criteria.getTravelStatus()!=null) {
predicates.add(cb.equal(travelStatus, criteria.getTravelStatus()));
}
if(criteria.getTravelType()!=null) {
predicates.add(cb.equal(travelType, criteria.getTravelType()));
}
if(criteria.getLastName()!=null ) {
predicates.add(cb.like(lastName, "%"+criteria.getLastName()+"%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
}
Finally, here's my search method:
#RequestMapping("/search")
public ModelAndView search(
#ModelAttribute TravelSearch travelSearch,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
TravelSpecification tspec = new TravelSpecification(travelSearch);
Page<Travel> travels = travelRep.findAll(tspec, pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject(travelSearch);
mav.addObject("page", page);
mav.addObject("schools", schoolRep.findAll() );
mav.addObject("periods", periodRep.findAll() );
mav.addObject("travelTypes", TravelTypeEnum.values());
mav.addObject("travelStatuses", TravelStatusEnum.values());
return mav;
}
Hope I helped!
For starters you should stop using #RequestParam and put all your search fields in an object (maybe reuse the Travel object for that). Then you have 2 options which you could use to dynamically build a query
Use the JpaSpecificationExecutor and write a Specification
Use the QueryDslPredicateExecutor and use QueryDSL to write a predicate.
Using JpaSpecificationExecutor
First add the JpaSpecificationExecutor to your TravelRepository this will give you a findAll(Specification) method and you can remove your custom finder methods.
public interface TravelRepository extends JpaRepository<Travel, Long>, JpaSpecificationExecutor<Travel> {}
Then you can create a method in your repository which uses a Specification which basically builds the query. See the Spring Data JPA documentation for this.
The only thing you need to do is create a class which implements Specification and which builds the query based on the fields which are available. The query is build using the JPA Criteria API link.
public class TravelSpecification implements Specification<Travel> {
private final Travel criteria;
public TravelSpecification(Travel criteria) {
this.criteria=criteria;
}
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// create query/predicate here.
}
}
And finally you need to modify your controller to use the new findAll method (I took the liberty to clean it up a little).
#RequestMapping("/search")
public String search(#ModelAttribute Travel search, Pageable pageable, Model model) {
Specification<Travel> spec = new TravelSpecification(search);
Page<Travel> travels = travelRep.findAll(spec, pageable);
model.addObject("page", new PageWrapper(travels, "/search"));
return "travels/list";
}
Using QueryDslPredicateExecutor
First add the QueryDslPredicateExecutor to your TravelRepository this will give you a findAll(Predicate) method and you can remove your custom finder methods.
public interface TravelRepository extends JpaRepository<Travel, Long>, QueryDslPredicateExecutor<Travel> {}
Next you would implement a service method which would use the Travel object to build a predicate using QueryDSL.
#Service
#Transactional
public class TravelService {
private final TravelRepository travels;
public TravelService(TravelRepository travels) {
this.travels=travels;
}
public Iterable<Travel> search(Travel criteria) {
BooleanExpression predicate = QTravel.travel...
return travels.findAll(predicate);
}
}
See also this bog post.

Categories

Resources