QueryDslMongoRepository Projection - mongodb

I am using spring-data for mongodb with querydsl.
I have a repository
public interface DocumentRepository extends MongoRepository<Document, String> ,QueryDslPredicateExecutor<Document> {}
and an entity
#QueryEntity
public class Document {
private String id;
private String name;
private String description;
private boolean locked;
private String message;
}
I need to load a list of documents with id and name informations.
So only id and name should be loaded and set in my entity.
I think query projection is the right word for it.
Is this supported?
In addition I need to implement some lazy loading logic.
Is there anything like "skip" and "limit" features in a repository?

There's quite a few aspects to this, as it is - unfortunately - not a single question but multiple ones.
For the projection you can simply use the fields attribute of the #Query annotation:
interface DocumentRepository extends MongoRepository<Document, String>, QuerydslPredicateExecutor<Document> {
#Query(value = "{}", fields = "{ 'id' : 1, 'name' : 1 }")
List<Document> findDocumentsProjected();
}
You can combine this with the query derivation mechanism (by not setting query), with pagination (see below) and even a dedicated projection type in the return clause (e.g. a DocumentExcerpt with only id and name fields).
Pagination is fully supported on the repository abstraction. You already get findAll(Pageable) and a Querydsl specific version of the method by extending the base interfaces. You can also use the pagination API in finder methods adding a Pageable as parameter and returning a Page
Page<Document> findByDescriptionLike(String description, Pageable pageable)
See more on that in the reference documentation.

Projection
For all I know projections are not supported by the default Spring Data repositories. If you want to make sure only the projection is sent from the DB to your application (e.g. for performance reasons) you will have to implement the corresponding query yourself. Adding custom methods to extensions of the standard repo should not be too much effort.
If you just want to hide the content of certain fields from some client calling your application, you would typically use another set of entity objects with a suitable mapping in between. Using the same POJO for different levels of detail is always confusing as you will not know if a field is actually null or if the value was just suppressed in a certain context.
Pagination
I am currently not able to test any code, but according to the documentation of QueryDslPredicateExecutor the method findAll(predicate, pageable) should be what you want:
it returns a Page object that is a regular Iterable for your Document
you have to pass it a Pageable for which you can e.g. use a PageRequest; initializing it for known values of skip and limit should be trivial

I also found this approach for JPA
Spring Data JPA and Querydsl to fetch subset of columns using bean/constructor projection
I am currently trying to implement this for MongoDB.

According to the Answer of this -> Question <- I implemeted following solution.
Entity
#QueryEntity
public class Document extends AbstractObject {
}
Custom QuerydslMongoRepository
public interface CustomQuerydslMongoRepository<T extends AbstractObject,ID extends Serializable> extends MongoRepository<T, ID> ,QueryDslPredicateExecutor<T>{
Page<T> findAll(Predicate predicate, Pageable pageable,Path... paths);
Page<T> findAll(Predicate predicate, Pageable pageable,List<Path> projections);
}
Custom QuerydslMongoRepository Implementation
public class CustomQuerydslMongoRepositoryImpl<T extends AbstractObject,ID extends Serializable> extends QueryDslMongoRepository<T,ID> implements CustomQuerydslMongoRepository<T,ID> {
//All instance variables are available in super, but they are private
private static final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE;
private final EntityPath<T> path;
private final PathBuilder<T> pathBuilder;
private final MongoOperations mongoOperations;
public CustomQuerydslMongoRepositoryImpl(MongoEntityInformation<T, ID> entityInformation, MongoOperations mongoOperations) {
this(entityInformation, mongoOperations,DEFAULT_ENTITY_PATH_RESOLVER);
}
public CustomQuerydslMongoRepositoryImpl(MongoEntityInformation<T, ID> entityInformation, MongoOperations mongoOperations, EntityPathResolver resolver) {
super(entityInformation, mongoOperations, resolver);
this.path=resolver.createPath(entityInformation.getJavaType());
this.pathBuilder = new PathBuilder<T>(path.getType(), path.getMetadata());
this.mongoOperations=mongoOperations;
}
#Override
public Page<T> findAll( Predicate predicate, Pageable pageable,Path... paths) {
Class<T> domainType = getEntityInformation().getJavaType();
MongodbQuery<T> query = new SpringDataMongodbQuery<T>(mongoOperations, domainType);
long total = query.count();
List<T> content = total > pageable.getOffset() ? query.where(predicate).list(paths) : Collections.<T>emptyList();
return new PageImpl<T>(content, pageable, total);
}
#Override
public Page<T> findAll(Predicate predicate, Pageable pageable, List<Path> projections) {
Class<T> domainType = getEntityInformation().getJavaType();
MongodbQuery<T> query = new SpringDataMongodbQuery<T>(mongoOperations, domainType);
long total = query.count();
List<T> content = total > pageable.getOffset() ? query.where(predicate).list(projections.toArray(new Path[0])) : Collections.<T>emptyList();
return new PageImpl<T>(content, pageable, total);
}
}
Custom Repository Factory
public class CustomQueryDslMongodbRepositoryFactoryBean<R extends QueryDslMongoRepository<T, I>, T, I extends Serializable> extends MongoRepositoryFactoryBean<R, T, I> {
#Override
protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
return new CustomQueryDslMongodbRepositoryFactory<T,I>(operations);
}
public static class CustomQueryDslMongodbRepositoryFactory<T, I extends Serializable> extends MongoRepositoryFactory {
private MongoOperations operations;
public CustomQueryDslMongodbRepositoryFactory(MongoOperations mongoOperations) {
super(mongoOperations);
this.operations = mongoOperations;
}
#SuppressWarnings({ "rawtypes", "unchecked" })
protected Object getTargetRepository(RepositoryMetadata metadata) {
return new CustomQuerydslMongoRepositoryImpl(getEntityInformation(metadata.getDomainType()), operations);
}
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return CustomQuerydslMongoRepository.class;
}
}
}
Entity Repository
public interface DocumentRepository extends CustomQuerydslMongoRepository<Document, String>{
}
Usage in Service
#Autowired
DocumentRepository repository;
public List<Document> getAllDocumentsForListing(){
return repository.findAll( QDocument.document.id.isNotEmpty().and(QDocument.document.version.isNotNull()), new PageRequest(0, 10),QDocument.document.name,QDocument.document.version).getContent();
}

Related

Dynamic injection using #SpringBean in wicket

I have a form that based on collected information generates a report. I have multiple sources from which to generate reports, but the form for them is the same. I tried to implement strategy pattern using an interface implementing report generator services, but that led to wicket complaining about serialization issues of various parts of the report generator. I would like to solve this without duplicating the code contained in the form, but I have not been able to find information on dynamic injection with #SpringBean.
Here is a rough mock up of what I have
public class ReportForm extends Panel {
private IReportGenerator reportGenerator;
public ReportForm(String id, IReportGenerator reportGenerator) {
super(id);
this.reportGenerator = reportGenerator;
final Form<Void> form = new Form<Void>("form");
this.add(form);
...
form.add(new AjaxButton("button1") {
private static final long serialVersionUID = 1L;
#Override
protected void onSubmit(AjaxRequestTarget target)
{
byte[] report = reportGenerator.getReport(...);
...
}
});
}
}
If I do it this way, wicket tries to serialize the concrete instance of reportGenerator. If I annotate the reportGenerator property with #SpringBean I receive Concrete bean could not be received from the application context for class: IReportGenerator
Edit: I have reworked implementations of IRerportGenerator to be able to annotate them with #Component and now I when I use #SpringBean annotation I get More than one bean of type [IReportGenerator] found, you have to specify the name of the bean (#SpringBean(name="foo")) or (#Named("foo") if using #javax.inject classes) in order to resolve this conflict. Which is exactly what I don't want to do.
I think the behavior you're trying to achieve can be done with a slight workaround, by introducing a Spring bean that holds all IReportGenerator instances:
#Component
public class ReportGeneratorHolder {
private final List<IReportGenerator> reportGenerators;
#Autowired
public ReportGeneratorHolder(List<IReportGenerator> reportGenerators) {
this.reportGenerators = reportGenerators;
}
public Optional<IReportGenerator> getReportGenerator(Class<? extends IReportGenerator> reportGeneratorClass) {
return reportGenerators.stream()
.filter(reportGeneratorClass::isAssignableFrom)
.findAny();
}
}
You can then inject this class into your Wicket page, and pass the desired class as a constructor-parameter. Depending on your Spring configuration you might need to introduce an interface for this as well.
public class ReportForm extends Panel {
#SpringBean
private ReportGeneratorHolder reportGeneratorHolder;
public ReportForm(String id, Class<? extends IReportGenerator> reportGeneratorClass) {
super(id);
IReportGenerator reportGenerator = reportGeneratorHolder
.getReportGenerator(reportGeneratorClass)
.orElseThrow(IllegalStateException::new);
// Form logic omitted for brevity
}
}
As far as I am able to find, looking through documentation and even the source for wicket #SpringBean annotation, this isn't possible. The closest I got is with explicitly creating a proxy for a Spring bean based on class passed. As described in 13.2.4 Using proxies from the wicket-spring project chapter in Wicket in Action.
public class ReportForm extends Panel {
private IReportGenerator reportGenerator;
private Class<? extends IReportGenerator> classType;
private static ISpringContextLocator CTX_LOCATOR = new ISpringContextLocator() {
public ApplicationContext getSpringContext() {
return ((MyApplication)MyApplication.get()).getApplicationContext();
}
};
public ReportForm(String id, Class<? extends IReportGenerator> classType) {
super(id);
this.classType = classType;
final Form<Void> form = new Form<Void>("form");
this.add(form);
...
form.add(new AjaxButton("button1") {
private static final long serialVersionUID = 1L;
#Override
protected void onSubmit(AjaxRequestTarget target)
{
byte[] report = getReportGenerator().getReport(...);
...
}
});
}
private <T> T createProxy(Class<T> classType) {
return (T) LazyInitProxyFactory.createProxy(classType, new
SpringBeanLocator(classType, CTX_LOCATOR));
}
private IReportGenerator getReportGenerator() {
if (reportGenerator = null) {
reportGenerator = createProxy(classType);
}
return reportGenerator;
}
}

Query for multiple values of the same property with queryDSL and Spring Data JPA

Is there a way to query for multiple values of the same property with Spring DataREST JPA and querydsl? I am not sure what the format of the query URL should be and if I need extra customization in my bindings. I couldn't find anything in documentation. If I have a "student" table in my database with a "major" column with corresponding Student entity I would assume that querying for all students which have "math" and "science" majors would look like http://localhost:8080/students?major=math&major=science. However in this query only the first part is being taken and major=science is ignored
Below example customizes Querydsl web support to perform collection in operation. URI /students?major=sword&major=magic searches for students with major in ["sword", "magic"].
Entity and repository
public class Student {
private Long id;
private String name;
private String major;
}
public interface StudentRepos extends PagingAndSortingRepository<Student, Long>,
QuerydslPredicateExecutor<Student>,
QuerydslBinderCustomizer<QStudent> {
#Override
default void customize(QuerydslBindings bindings, QStudent root) {
bindings.bind(root.major)
.all((path, value) -> Optional.of(path.in(value)));
}
}
Test data
new Student("Arthur", "sword");
new Student("Merlin", "magic");
new Student("Lancelot", "lance");
Controller
#RestController
#RequestMapping("/students")
#RequiredArgsConstructor
public class StudentController {
private final StudentRepos studentRepos;
#GetMapping
ResponseEntity<List<Student>> getAll(Predicate predicate) {
Iterable<Student> students = studentRepos.findAll(predicate);
return ResponseEntity.ok(StreamSupport.stream(students.spliterator(), false)
.collect(Collectors.toList()));
}
}
Test case
#Test
#SneakyThrows
public void queryAll() {
mockMvc.perform(get("/students"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$", hasSize(3)))
.andDo(print());
}
#Test
#SneakyThrows
void querySingleValue() {
mockMvc.perform(get("/students?major=sword"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].name").value("Arthur"))
.andExpect(jsonPath("$[0].major").value("sword"))
.andDo(print());
}
#Test
#SneakyThrows
void queryMultiValue() {
mockMvc.perform(get("/students?major=sword&major=magic"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("Arthur"))
.andExpect(jsonPath("$[0].major").value("sword"))
.andExpect(jsonPath("$[1].name").value("Merlin"))
.andExpect(jsonPath("$[1].major").value("magic"))
.andDo(print());
}
The full Spring Boot application is in Github

overwrite findAll() method of QuerydslPredicateExecutor

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
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);
}
}

Spring Data MongoDB No property get found for type at org.springframework.data.mapping.PropertyPath

I am using Spring Data MongodB 1.4.2.Release version. For Spring Data MongoDB, I have created the custom repository interface and implementation in one location and create custom query function getUsersName(Users users).
However I am still getting below exception:
Caused by: org.springframework.data.mapping.PropertyReferenceException:
No property get found for type Users! at org.springframework.data.mapping.PropertyPath. (PropertyPath.java:75) at
org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:327) at
org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:359) at
org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:359) at
org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:307) at
org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:270) at
org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:241) at
org.springframework.data.repository.query.parser.Part.(Part.java:76) at
org.springframework.data.repository.query.parser.PartTree$OrPart.(PartTree.java:201) at
org.springframework.data.repository.query.parser.PartTree$Predicate.buildTree(PartTree.java:291) at
org.springframework.data.repository.query.parser.PartTree$Predicate.(PartTree.java:271) at
org.springframework.data.repository.query.parser.PartTree.(PartTree.java:80) at
org.springframework.data.mongodb.repository.query.PartTreeMongoQuery.(PartTreeMongoQuery.java:47)
Below is my Spring Data MongoDB structure:
/* Users Domain Object */
#Document(collection = "users")
public class Users {
#Id
private ObjectId id;
#Field ("last_name")
private String last_name;
#Field ("first_name")
private String first_name;
public String getLast_name() {
return last_name;
}
public void setLast_name(String last_name) {
this.last_name = last_name;
}
public String getFirst_name() {
return first_name;
}
public void setFirst_name(String first_name) {
this.first_name = first_name;
}
}
/* UsersRepository.java main interface */
#Repository
public interface UsersRepository extends MongoRepository<Users,String>, UsersRepositoryCustom {
List findUsersById(String id);
}
/* UsersRepositoryCustom.java custom interface */
#Repository
public interface UsersRepositoryCustom {
List<Users> getUsersName(Users users);
}
/* UsersRepositoryImpl.java custom interface implementation */
#Component
public class UsersRepositoryImpl implements UsersRepositoryCustom {
#Autowired
MongoOperations mongoOperations;
#Override
public List<Users> getUsersName(Users users) {
return mongoOperations.find(
Query.query(Criteria.where("first_name").is(users.getFirst_name()).and("last_name").is(users.getLast_name())), Users.class);
}
/* Mongo Test function inside Spring JUnit Test class calling custom function with main UsersRepository interface */
#Autowired
private UsersRepository usersRepository;
#Test
public void getUsersName() {
Users users = new Users();
users.setFirst_name("James");`enter code here`
users.setLast_name("Oliver");
List<Users> usersDetails = usersRepository.getUsersName(users);
System.out.println("users List" + usersDetails.size());
Assert.assertTrue(usersDetails.size() > 0);
}
The query method declaration in your repository interface is invalid. As clearly stated in the reference documentation, query methods need to start with get…By, read_By, find…By or query…by.
With custom repositories, there shouldn't be a need for method naming conventions as Oliver stated. I have mine working with a method named updateMessageCount
Having said that, I can't see the problem with the code provided here.
I resolved this issue with the help of this post here, where I wasn't naming my Impl class correctly :
No property found for type error when try to create custom repository with Spring Data JPA

Is there a way to eager fetch a lazy relationship through the Predicate API in QueryDSL?

I am using the QueryDslPredicateExecutor from Spring Data JPA project, and I am facing the need to eager fetch a lazy relation. I know that I can use a native JPA-QL query in the Repository interface, or even used the JPAQLQuery from Query DSL, but I was intrigued if this is even possible in order to facilitate building queries for future needs.
I had a similar problem where I had to fetch join a Collection while using Predicates and QueryDslPredicateExecutor.
What I did was to create a custom repository implementation to add a method that allowed me to define the entities that should be fetched.
Don't be daunted by the amount of code in here, it's actually very simple and you will need to do very few changes to use it on your application
This is the interface of the custom repository
#NoRepositoryBean
public interface JoinFetchCapableRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, QueryDslPredicateExecutor<T> {
Page<T> findAll(Predicate predicate, Pageable pageable, JoinDescriptor... joinDescriptors);
}
JoinDescriptor
public class JoinDescriptor {
public final EntityPath path;
public final JoinType type;
private JoinDescriptor(EntityPath path, JoinType type) {
this.path = path;
this.type = type;
}
public static JoinDescriptor innerJoin(EntityPath path) {
return new JoinDescriptor(path, JoinType.INNERJOIN);
}
public static JoinDescriptor join(EntityPath path) {
return new JoinDescriptor(path, JoinType.JOIN);
}
public static JoinDescriptor leftJoin(EntityPath path) {
return new JoinDescriptor(path, JoinType.LEFTJOIN);
}
public static JoinDescriptor rightJoin(EntityPath path) {
return new JoinDescriptor(path, JoinType.RIGHTJOIN);
}
public static JoinDescriptor fullJoin(EntityPath path) {
return new JoinDescriptor(path, JoinType.FULLJOIN);
}
}
Implementation of the custom repository
public class JoinFetchCapableRepositoryImpl <T, ID extends Serializable> extends QueryDslJpaRepository<T, ID> implements JoinFetchCapableRepository<T, ID> {
private static final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE;
private final EntityPath<T> path;
private final PathBuilder<T> builder;
private final Querydsl querydsl;
public JoinFetchCapableRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
this(entityInformation, entityManager, DEFAULT_ENTITY_PATH_RESOLVER);
}
public JoinFetchCapableRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager, EntityPathResolver resolver) {
super(entityInformation, entityManager, resolver);
this.path = resolver.createPath(entityInformation.getJavaType());
this.builder = new PathBuilder<>(path.getType(), path.getMetadata());
this.querydsl = new Querydsl(entityManager, builder);
}
#Override
public Page<T> findAll(Predicate predicate, Pageable pageable, JoinDescriptor... joinDescriptors) {
JPQLQuery countQuery = createQuery(predicate);
JPQLQuery query = querydsl.applyPagination(pageable, createFetchQuery(predicate, joinDescriptors));
Long total = countQuery.count();
List<T> content = total > pageable.getOffset() ? query.list(path) : Collections.<T> emptyList();
return new PageImpl<>(content, pageable, total);
}
protected JPQLQuery createFetchQuery(Predicate predicate, JoinDescriptor... joinDescriptors) {
JPQLQuery query = querydsl.createQuery(path);
for(JoinDescriptor joinDescriptor: joinDescriptors)
join(joinDescriptor, query);
return query.where(predicate);
}
private JPQLQuery join(JoinDescriptor joinDescriptor, JPQLQuery query) {
switch(joinDescriptor.type) {
case DEFAULT:
throw new IllegalArgumentException("cross join not supported");
case INNERJOIN:
query.innerJoin(joinDescriptor.path);
break;
case JOIN:
query.join(joinDescriptor.path);
break;
case LEFTJOIN:
query.leftJoin(joinDescriptor.path);
break;
case RIGHTJOIN:
query.rightJoin(joinDescriptor.path);
break;
case FULLJOIN:
query.fullJoin(joinDescriptor.path);
break;
}
return query.fetch();
}
}
Factory to create the custom repositories, replacing the default QueryDslJpaRepository
public class JoinFetchCapableQueryDslJpaRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable>
extends JpaRepositoryFactoryBean<R, T, I> {
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
return new JoinFetchCapableQueryDslJpaRepositoryFactory(entityManager);
}
private static class JoinFetchCapableQueryDslJpaRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {
private EntityManager entityManager;
public JoinFetchCapableQueryDslJpaRepositoryFactory(EntityManager entityManager) {
super(entityManager);
this.entityManager = entityManager;
}
protected Object getTargetRepository(RepositoryMetadata metadata) {
return new JoinFetchCapableRepositoryImpl<>(getEntityInformation(metadata.getDomainType()), entityManager);
}
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return JoinFetchCapableRepository.class;
}
}
}
Last step is to change the jpa configuration so it uses this factory instead of the default one:
<jpa:repositories base-package="com.mycompany.repository"
entity-manager-factory-ref="entityManagerFactory"
factory-class="com.mycompany.utils.spring.data.JoinFetchCapableQueryDslJpaRepositoryFactoryBean" />
Then you can use it from your service layer like this:
public Page<ETicket> list(ETicketSearch eTicket, Pageable pageable) {
return eticketRepository.findAll(like(eTicket), pageable, JoinDescriptor.leftJoin(QETicket.eTicket.order));
}
By using JoinDescriptor you will be able to specify what you want to join based on your service needs.
I was able to do this thanks to the Murali's response here: Spring Data JPA and Querydsl to fetch subset of columns using bean/constructor projection Please take a look.
Spring data has introduced JPA Entity Graph support. Beware that is does not currently work with graphs that are traversed via EmbeddedIds.