I have Spring Application with a repository
interface EventRepository extends JpaRepository<Event, Long>, QueryByExampleExecutor<Event> { }
Event e = new Event();
e.setTest('ABC');
eventRepository.findAll(Example.of(e), pageable);
Is working great and I am almost there.
But I need to restrict to a date range between 'from' and 'to'
I have seen some post that it is not working with QBE but this was in 2015.
I have created a Range object but I don't know how to apply it.
http://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Range.html
I can not use the default spring way like
#Transactional
interface EventRepository extends JpaRepository<Event, Long>, QueryByExampleExecutor<Event> {
def findBetween(Date lower, Date upper)
}
because I have a bunch of dynamic search parameters.
You can extends from JpaSpecificationExecutor<T> too and get the Predicate with QueryByExamplePredicateBuilder from Example<T>.
Keeping in mind that birthdateField belongs to the Event entity.
public Specification<Event> getSpecFromDatesAndExample(
LocalDateTime from, LocalDateTime to, Example<Event> example) {
return (Specification<Event>) (root, query, builder) -> {
final List<Predicate> predicates = new ArrayList<>();
if (from != null) {
predicates.add(builder.greaterThan(root.get("birthdateField"), from));
}
if (to != null) {
predicates.add(builder.lessThan(root.get("birthdateField"), to));
}
predicates.add(QueryByExamplePredicateBuilder.getPredicate(root, builder, example));
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
};
}
And use Specification in findAll with JpaSpecificationExecutor.
List<Event> events = eventRepository.findAll(getSpecFromDatesAndExample(from, to, Example.of(Event)));
Related
I have a repository interface that extends JpaRepository and a NameRepositoryCustom.
My repository is annotated with #RepositoryRestRessource(collectionResourceRel="pathname", path="pathname").
The problem I have is that every second restart of my application the URL of the repository gets changed so I can't find the exposed data of the repository under the URL I defined and some features like the search of the repository aren't exposed in the API anymore either.
The "NameRepositroyCustom" is used for a search function which uses another Repository to implement Specification with JPA Criteria Api for a searchbar in my frontend.
Does anybody have a solution for this? The only repository annotated as #RepositoryRestRessource is the main repository that implements all the others. The NameRepositorySpec is annotated with #Repository, could this maybe be the cause?
Edit: I implemented the code as an example to clarify the relations between the mentioned classes and interfaces.
This is the basic repository related to the entity persisted in the database:
#RepositoryRestResource(collectionRessourceRel = "enitynames", path = "entitynames")
public interface EntitynameRepository extends JpaRepository<Entityname, Long>, EntitynameRepositoryCustom{
//custom methods in here
}
This is the custom repository:
public interface EntitynameRepositoryCustom {
Page<Entityname> search(String exampleParam1, String exampleParam2, Pageable pageable);
}
This is the implementation of the custom repository:
public class EntitynameRepositoryCustomImpl implements EntitynameRepositoryCustom{
#Autowired
EntityManager em;
#Autowired
EntitynameRepositorySpec entitynameRepositorySpec;
Specification<Entityname> querySpecification = null;
#Override
public Page<Entityname> search(String exampleParam1, String exampleParam2, Pageable pageable) {
//Code here uses the criteria builder and Specification to generate a custom query with optional parameters
CriteriaBuilder cb= em.getCriteriaBuilder();
CriteriaQuery<Entityname> cq = cb.createQuery(Entityname.class);
//Code below is done for every passed in parameter
if(exampleParam1 != null){
Specification<Entityname> param1Specification = EntitynameSpecification.likeParam1(exampleParam1);
querySpecification = Specification.where(param1Specification);
} else {
return null;
}
return entitynameRepositorySpec.findAll(specification, pageable);
}
}
This is the specification repository:
public interface EntitynameRepositorySpec extends JpaRepository<Entityname, Long>, JpaSpecificationExecutor<Entityname>{
}
And this is the implementation of the specification:
public class EntitynameSpecification {
public static Specification<Entityname> likeExampleParam1(String exampleParam1){
if(exampleParam1 == null){
return null;
}
return(root, query, cb) -> {
reutrn cb.like(root.get("fieldname"), "%"+ exampleParam1 + "%");
};
}
}
The URL of the repository gets changed to a part of the entity name compared to my example it would be something like: entityname has URL: /entityname
if the bug occurs the URL changes to /name.
I want to get the List<Map<String,Object>>returned by query like this:
[{production:{the object}, avgscore:123}, {production:{other object},avgscore:456}, ...]
I can get it by this :
public interface ProductionRepository extends PagingAndSortingRepository<Production, Long> {
#Query("SELECT p as productions,avg(c.score) as avgscore FROM ......")
List<Map<String,Object>> findSorted(......);
}
but now I try to use Specification, and I done the specification:
Specification specification = new Specification() {
#Override
public Predicate toPredicate(Root root1, CriteriaQuery query, CriteriaBuilder cb) {
.......
.......
if (date2 != null) {
Predicate endTimeP = cb.lessThanOrEqualTo(root.get(Production_.productDate), date2);
predicates.add(endTimeP);
}
query.groupBy(root.get(Production_.id)).orderBy(cb.desc(cb.avg(commentJoin.get(Comment_.score))));
Predicate[] predicates1 = new Predicate[predicates.size()];
predicates.toArray(predicates1);
query.multiselect(root.alias("production"),
cb.avg(commentJoin.get(Comment_.score)).as(Double.class).alias("avgscore"));
return cb.and(predicates1);
}
and then I use this to findAll:
return specificationRepo.findAll(specification);
the repository is:
#Repository
public interface SpecificationRepository extends PagingAndSortingRepository<Production, Long>,
JpaSpecificationExecutor {
List<Map<String,Object>> findAll(Specification specification);
}
BUT, the findAll() only return List<Production> , no matter how I change the return type, it always return List<production> , not List<Map<String,Object>> I wanted.
it it bind to the Production in PagingAndSortingRepository<Production, Long>. If I delete this and only have JpaSpecificationExecutor, spring will not seem it as a #Repository; The Production position must be a #Entity, I can not change it to a custom java thing having Production and avgscore.
I have searched long time, but still didn't find how to solve.
NOTE: Go down in order to see the edited message.
I'm trying to imitate this query:
db.sentiments.aggregate([
{"$group" : {_id:{theme_id:"$theme",sentiment_id:"$sentiment"}, count:{$sum:1}}},
{"$sort":{"_id.theme_id":1}} ])
This is the code that I had generated in order to imitate it:
#RepositoryRestResource(collectionResourceRel = "sentiments", path = "sentiments")
public interface SentimentsRepository extends MongoRepository<Sentiments, String> {
Long countByTheme(#Param("theme") String theme);
#Query(value ="[\n" +
" {\"$group\" : {_id:{theme_id:\"$theme\",sentiment_id:\"$sentiment\"}, count:{$sum:1}}},\n" +
"\t{\"$sort\":{\"_id.theme_id\":1}}\n" +
"]",count = true)
List<Object> comptarSentiments();
}
Well this code is returning me this error:
"exception": "org.springframework.data.mongodb.UncategorizedMongoDbException",
"message": "Can't canonicalize query: BadValue unknown operator: $group; nested exception is com.mongodb.MongoException: Can't canonicalize query: BadValue unknown operator: $group",
Actually I'm a begginer in what refers to the use of Spring so I'm very lost, does any one know what should I do?
Thanks and sorry for my bad english, not my native language.
[EDIT]----------------------------------------
Just as the comment wrote by Shawn Clark It's not possible to do it this way, in order to achieve that you will need to create a customRepository.
What's the difference between Spring Data's MongoTemplate and MongoRepository?
I have been trying to do it this way but something doesn't seem to be correct, here is my new code:
#RepositoryRestResource(collectionResourceRel = "sentiments", path = "sentiments")
public interface SentimentsRepository extends CrudRepository<Sentiments, String>, CustomSentimentsRepository {
//Other methods...
}
public interface CustomSentimentsRepository {
List<CountResult> yourCustomMethod();
class CountResult{
String theme;
String sentiment;
int total;
}
}
public class SentimentsRepositoryImpl implements CustomSentimentsRepository {
private final MongoOperations operations;
#Autowired
public SentimentsRepositoryImpl(MongoOperations operations) {
Assert.notNull(operations, "MongoOperations must not be null!");
this.operations = operations;
}
#Override
public List<CountResult> yourCustomMethod(){
Aggregation agg = Aggregation.newAggregation(
Aggregation.group("theme","sentiment").count().as("total"),
Aggregation.project("theme","sentiment").and("total").previousOperation(),
Aggregation.sort(Sort.Direction.DESC, "theme")
);
//Convert the aggregation result into a List
AggregationResults<CountResult> groupResults
= operations.aggregate(agg,"sentiments", CountResult.class);
//List<CountResult> result = groupResults.getMappedResults();
return groupResults.getMappedResults();
}
}
I'm not even able to debbug this code and I'm always getting a 404.
Based on the information I have found you can't do that complex of a #Query on a MongoRepository method. In this case you would want to create a class and implement your comptarSentiments() method using the mongoTemplate to query the data store with your aggregate function. Then create a controller class that exposes a REST endpoint and have it call the repository.
Once you get to doing complex queries in Mongo you lose the ease of #RepositoryRestResource and have to go back to wiring the REST endpoint to the repository yourself.
Spring Data REST : custom query for MongoDB repository
Implementing custom methods of Spring Data repository and exposing them through REST
I finally managed to solve the problem, seems like it was related with the controller and the type of the atribute "total" from the innerClass CountResult, it needs to be a String (this is very important, otherwise the Aggregation.project will fail). Here goes the final code:
public interface CustomSentimentsRepository {
List<CountResult> myCountGroupByThemeAndSentiment();
class CountResult{
public String theme;
public String sentiment;
public String total;
}
}
public class SentimentsRepositoryImpl implements CustomSentimentsRepository {
private final MongoTemplate mongoTemplate;
#Autowired
public SentimentsRepositoryImpl(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
#Override
public List<CountResult> myCountGroupByThemeAndSentiment(){
Aggregation agg = Aggregation.newAggregation(
Aggregation.group("theme","sentiment").count().as("total"),
Aggregation.project("theme","sentiment").andInclude("total"),
Aggregation.sort(Sort.Direction.ASC,"theme","sentiment")
);
AggregationResults<CountResult> groupResults
= mongoTemplate.aggregate(agg,"sentiments", CountResult.class);
return groupResults.getMappedResults();
}
}
#RepositoryRestResource(collectionResourceRel = "sentiments", path = "sentiments")
public interface SentimentsRepository extends CrudRepository<Sentiments, String>, CustomSentimentsRepository {
//Other methods
}
#RestController
#RequestMapping(value = "sentiments/search")
public class ChartsController {
#Autowired
private SentimentsRepository sentimentsRepository;
#RequestMapping(value = "myCountGroupByThemeAndSentiment", method = RequestMethod.GET)
public ResponseEntity<?> yourCustomMethod() {
List<?> count=sentimentsRepository.myCountGroupByThemeAndSentiment();
return new ResponseEntity(count, HttpStatus.OK);
}
}
You can use #Aggrgation available in spring data mongodb 2.2.X versions:
#Aggregation(pipeline = {"{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }", "{ '$sort' : { 'lastname' : -1 } }"}) List<PersonAggregate> groupByLastnameAnd(String property);
I'm working on a web application using angular js, spring mvc and spring jpa data.
I'm wondering if there is something similar to criteria and detachedcriteria(hibernate) to build advanced queries with spring jpa data.
Nothing stops you from still using Criteria
#Repository
public interface FooRepository extends JpaRepository<Foo, Long>, FooRepositoryCustom {
}
interface FooRepositoryCustom {
public List<Foo> findByBar(Bar bar);
}
class FooRepositoryImpl implements FooRepositoryCustom {
#PersistenceContext
protected EntityManager em;
#Transactional(readOnly = true)
public List<Foo> findByBar(Bar bar) {
Criteria crit = em.unwrap(Session.class).createCriteria(Foo.class);
crit.add(Restrictions.eq("name", bar.getName()));
...
crit.setResultTransformer(DetachedCriteria.DISTINCT_ROOT_ENTITY);
List<Foo> foos = crit.list();
return foos;
}
}
Yes, you can use Specifications, which basically uses the Criteria API (obviously, since Spring Data JPA is just a wrapper around JPA).
you can use Query Dsl
, it is less verbose than Specification, here is a blog containing both Specification and QueryDsl.
You can use Criteria with Spring Data, you don't need a Custom Repository, You could use JpaSpecificationExecutor, here an example:
Your repository:
#Repository("yourRepository")
public interface YourRepository extends JpaRepository, JpaSpecificationExecutor
{
}
Your Service
#Override
public List<YourModel> yourDataModel getAllEntitiesByAttr(String attrValue){
List<YourModel> yourDataModel = null;
try {
Specification specification=getAndSpecByAttribute("attribute",attrValue);
List list = userRepository.findAll(specification);
yourDataModel =orikaMapper.mapAsList(list, YourModel.class);
} catch (Exception e) {
throw e;
}
return yourDataModel;
}
private Specification getAndSpecByAttribute(String attribute, String valueAttribute){
return new Specification() {
#Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
Path path = root.get(attribute);
return cb.equal(path, valueAttribute);
};
};
}
It is enough.
If I have a repository setup like the following, making use of Spring Data REST, I can access the data at /receipts and see all data. However, I want to only return data for the user. I have a custom finder "findByStorer" which would do this. How would I get Spring Data REST to use this and get the storer value from the user rather than specifying a query parameter?
#RepositoryRestResource(collectionResourceRel = "receipts", path = "receipts")
public interface ReceiptRepository extends PagingAndSortingRepository<Receipt, BigDecimal> {
#Query
public Page<Receipt> findByStorer(String storer, Pageable pageable);
}
I haven't implemented any security yet, so this question is more theory at the moment than practice.
Thanks.
Building on #rpr's answer:
You should be able to reference properties of the joined entity (Storer). In your example if you have Receipt -> Storer -> User you can query the Receipts where Storer.user has a value injected from the Security Context.
#PreAuthorize("isFullyAuthenticated && (#userName==principal.username)")
Page<Receipt> findByStorer_User(#Param("userName") String userName)
For example, given a Repositoryfor SomeEntity you could override findAll method with a custom #Query filtering by attribute ownerwith value of`#{principal.username}
#RepositoryRestResource(path = "some-entities", collectionResourceRel = "some-entities", itemResourceRel = "some-entity")
interface SomeEntityRepository extends PagingAndSortingRepository<SomeEntity, String> {
#Override
#RestResource(exported = true)
#Query("select someEntity from SomeEntity someEntity where someEntity.owner = ?#{principal.username}")
Iterable<SomeResource> findAll();
}
If you use Spring Security you can use this approach:
#PreAuthorize("isFullyAuthenticated() && (#userName == principal.username)")
public List<User> findByUserName(#Param("userName")String userName);
This issue is a tipical cross-cutting concern so I tried apply AOP. Define Advice and update the args (String storer), as explain at: https://stackoverflow.com/a/46353783/1203628
#Aspect
#Transactional
#Component
public class FilterProjectsAspect {
#Pointcut("execution(* com.xxx.ReceiptRepository.findByStorer(..))")
public void projectFindAll() {
}
#Around("projectFindAll()")
public Object filterProjectsByUser(final ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
String storer=(String) args[i];
// Find storer by user
args[i]=storer; //Update args
}
return pjp.proceed(args);
}
}