Reputation: 6288
My goal is to add a dynamic Predicate
to the findAll
method of QuerydslPredicateExecutor
. This should be used to filter entities based on the organization of the currently active user.
I'm using Spring Data together with Spring Data REST to get the REST API out of the box, i.e. I have no dedicated REST service where I can intercept the incoming data and modify it.
By extending a SimpleJpaRepository
and registering it with @EnableJpaRepositories
it is possible to overwrite a method and change its default behavior. I wanted to do this, but my Repository
interfaces are implementing QuerydslPredicateExecutor
and this does not seem to work.
My failed approach started as:
public class CustomizedJpaRepositoryIml<T, ID extends Serializable> extends
SimpleJpaRepository<T, ID> {
private EntityManager entityManager;
@Autowired
public CustomizedJpaRepositoryIml(JpaEntityInformation<T, ?>
entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
}
but obviously this extension does not provide the method to be overwritten. I debugged how the implementing QuerydslJpaPredicateExecutor
is wired, but this is rather complex and I see no way of plugging in here something easily.
Another idea was to use a filter intercepting the URL call and adding parameters but this does not sound nice.
I could also override the controller path for the finder with a @BasePathAwareController
, but this would mean to do this for all entities I have and not in a single place.
Any ideas to achieve my goal? maybe there are also completely different options possible to achieve my goal of add additional filtering to the Querydsl Predicate
Upvotes: 3
Views: 3034
Reputation: 6288
I found a way in the meanwhile. It requires to provide an own implementation of QuerydslPredicateExecutor
. But this is not made easy in Spring Data. The answer is motivated by https://stackoverflow.com/a/53960209/3351474, but in the meanwhile a constructor has changed in newer Spring Data, why this cannot be taken 1:1.
I use a different example as in my question, but with this solution you have complete freedom also to add and append any Predicate
. As an example I take here a customized Querydsl implementation using always the creationDate
of an entity as sort criteria if nothing is is passed. I assume in this example that this column exists in some @MappedSuperClass
for all entities. Use generated static metadata in real life instead the hard coded string "creationDate".
As first the wrapped delegating all CustomQuerydslJpaRepositoryIml
delegating all methods to the QuerydslJpaPredicateExecutor
:
/**
* Customized Querydsl JPA repository to apply custom filtering and sorting logic.
*
*/
public class CustomQuerydslJpaRepositoryIml<T> implements QuerydslPredicateExecutor<T> {
private final QuerydslJpaPredicateExecutor querydslPredicateExecutor;
public CustomQuerydslJpaRepositoryIml(QuerydslJpaPredicateExecutor querydslPredicateExecutor) {
this.querydslPredicateExecutor = querydslPredicateExecutor;
}
private Sort applyDefaultOrder(Sort sort) {
if (sort.isUnsorted()) {
return Sort.by("creationDate").ascending();
}
return sort;
}
private Pageable applyDefaultOrder(Pageable pageable) {
if (pageable.getSort().isUnsorted()) {
Sort defaultSort = Sort.by(AuditableEntity_.CREATION_DATE).ascending();
pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), defaultSort);
}
return pageable;
}
@Override
public Optional<T> findOne(Predicate predicate) {
return querydslPredicateExecutor.findOne(predicate);
}
@Override
public List<T> findAll(Predicate predicate) {
return querydslPredicateExecutor.findAll(predicate);
}
@Override
public List<T> findAll(Predicate predicate, Sort sort) {
return querydslPredicateExecutor.findAll(predicate, applyDefaultOrder(sort));
}
@Override
public List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders) {
return querydslPredicateExecutor.findAll(predicate, orders);
}
@Override
public List<T> findAll(OrderSpecifier<?>... orders) {
return querydslPredicateExecutor.findAll(orders);
}
@Override
public Page<T> findAll(Predicate predicate, Pageable pageable) {
return querydslPredicateExecutor.findAll(predicate, applyDefaultOrder(pageable));
}
@Override
public long count(Predicate predicate) {
return querydslPredicateExecutor.count(predicate);
}
@Override
public boolean exists(Predicate predicate) {
return querydslPredicateExecutor.exists(predicate);
}
}
Next the CustomJpaRepositoryFactory
doing the magic and providing the Querydsl wrapper class instead of the default one. The default one is passed as parameter and wrapped.
/**
* Custom JpaRepositoryFactory allowing to support a custom QuerydslJpaRepository.
*
*/
public class CustomJpaRepositoryFactory extends JpaRepositoryFactory {
/**
* Creates a new {@link JpaRepositoryFactory}.
*
* @param entityManager must not be {@literal null}
*/
public CustomJpaRepositoryFactory(EntityManager entityManager) {
super(entityManager);
}
@Override
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
final RepositoryComposition.RepositoryFragments[] modifiedFragments = {RepositoryComposition.RepositoryFragments.empty()};
RepositoryComposition.RepositoryFragments fragments = super.getRepositoryFragments(metadata);
// because QuerydslJpaPredicateExecutor is using som internal classes only a wrapper can be used.
fragments.stream().forEach(
f -> {
if (f.getImplementation().isPresent() &&
QuerydslJpaPredicateExecutor.class.isAssignableFrom(f.getImplementation().get().getClass())) {
modifiedFragments[0] = modifiedFragments[0].append(RepositoryFragment.implemented(
new CustomQuerydslJpaRepositoryIml((QuerydslJpaPredicateExecutor) f.getImplementation().get())));
} else {
modifiedFragments[0].append(f);
}
}
);
return modifiedFragments[0];
}
}
Finally the CustomJpaRepositoryFactoryBean
. This must be registered with the Spring Boot application, to make Spring aware where to get the repository implementations from, e.g. with:
@SpringBootApplication
@EnableJpaRepositories(basePackages = "your.package",
repositoryFactoryBeanClass = CustomJpaRepositoryFactoryBean.class)
...
Here now the class:
public class CustomJpaRepositoryFactoryBean<T extends Repository<S, I>, S, I> extends JpaRepositoryFactoryBean<T, S, I> {
/**
* Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface.
*
* @param repositoryInterface must not be {@literal null}.
*/
public CustomJpaRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
return new CustomJpaRepositoryFactory(entityManager);
}
}
Upvotes: 3