Extend Spring Data Repository - spring-boot

I would like to introduce a <T> T findOrCreate(Supplier<Optional<T>> finder, Supplier<T> factory) to all of my repositories.
So created a new Interface
#NoRepositoryBean
public interface ExtendedJpaRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
T findOrCreate(Supplier<Optional<T>> finder, Supplier<T> factory);
}
.
public class ExtendedJpaRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements ExtendedJpaRepository<T, ID> {
private final JpaEntityInformation entityInformation;
private final EntityManager entityManager;
public ExtendedJpaRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityInformation = entityInformation;
this.entityManager = entityManager;
}
#Override
public T findOrCreate(Supplier<Optional<T>> finder, Supplier<T> factory) {
throw new NotImplementedException("No implemented yet");
}
}
Then I use this interface in my concrete repositories, e.g. RecipeIngredientRepository:
public interface RecipeIngredientRepository extends ExtendedJpaRepository<RecipeIngredient, Long> {}
When I finally inject the repository to my service I get the following exception:
java.lang.IllegalStateException: Failed to load ApplicationContext
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'recipeIngredientRepository': Invocation of init method failed; nested exception is org.springframework.data.mapping.PropertyReferenceException: No property find found for type RecipeIngredient! Did you mean 'id'?
It is searching for a find property in my entitiy RecipeIngredient. I did not want it to do this. I think this is related to JPA Query Methods. So I changed the name from findOrCreate to xxx to Bypass any query method detection - without success. It searches for a xxx property then.
What does make spring data look for this property?
I'm using org.springframework.boot:spring-boot-starter-data-jpa.

You need to specify your customized repository implementation via #EnableJpaRepositories(repositoryBaseClass = ExtendedJpaRepositoryImpl.class).
Take a look at the reference docs: Adding custom behavior to all repositories.

Adding to #md911de answer:
So what you can is to define a generic interface which has the base method that you want to have in all of your repositories:
#NoRepositoryBean
interface BaseGenericReactiveMongoRepository<T> :
ReactiveMongoRepository<T, String> {
fun patch(id: String, fields: Map<String, Any>): Mono<T>
}
Then you need to implement this and inform spring to use the implementation class for implementing the interface.
class SimpleBaseGenericReactiveMongoRepository<ENTITY>(
private val entityInformation: MappingMongoEntityInformation<ENTITY, String>,
private val template: ReactiveMongoTemplate
) : SimpleReactiveMongoRepository<ENTITY, String>(entityInformation, template),
BaseGenericReactiveMongoRepository<ENTITY> {
private val eventPublisher: ApplicationEventPublisher?
init {
val context = template.converter.mappingContext as MongoMappingContext
val indexCreator = MongoPersistentEntityIndexCreator(context) { collectionName ->
IndexOperationsAdapter.blocking(template.indexOps(collectionName))
}
eventPublisher = MongoMappingEventPublisher(indexCreator)
}
override fun patch(id: String, fields: Map<String, Any>): Mono<ENTITY> {
val collection = entityInformation.collectionName
val query = Query(Criteria.where("_id").`is`(id))
val document = Document()
return findById(id)
.flatMap { entity ->
maybeEmitEvent(BeforeConvertEvent<ENTITY>(entity, collection))
document.putAll(fields)
val update = Update()
fields
.filter { entry ->
!hashSetOf("_id", "createdAt", "createdBy", "modifiedAt", "modifiedBy").contains(entry.key)
}
.forEach { entry -> update.set(entry.key, entry.value) }
maybeEmitEvent(BeforeSaveEvent<ENTITY>(entity, document, collection))
template.updateFirst(query, update, collection)
}
.then(findById(id)).map { entity ->
maybeEmitEvent(AfterSaveEvent<ENTITY>(entity, document, collection))
entity
}
}
private fun <T> maybeEmitEvent(event: MongoMappingEvent<T>) {
eventPublisher?.publishEvent(event)
}
}
And the last part is to inform spring data.
#Configuration
#EnableReactiveMongoRepositories(
basePackages = ["**.repository"],
repositoryBaseClass = SimpleBaseGenericReactiveMongoRepository::class
)
class MongoConfiguration
Now you can use the interface as a base interface for your repository and have the functionality for your domain.
interface BookRepository : BaseMongoRepository<Book> {
findByNameContainingIgnoreCaseAndVisibileIsTrue(name:String): Flux<Book>
}
If you need a working example, you are welcome to check my medium:
https://medium.com/#ghahremani/extending-default-spring-data-repository-methods-patch-example-a23c07c35bf9

Related

Is there a way to override JPA saveAll method to used customized UPSERT query?

I am trying to override the JpaRepository saveAll method to use the custom UPSERT query in java SpringBoot. Is it possible?
As it's only one repository you can create a custom repository like this. I assume that the Entity name is User:
Your interface with only this saveAll Method
interface CustomizedUserRepository {
void savAllWithUpsert(Iterator<User> entities);
}
Then you have to implement the interface
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void savAllWithUpsert(Iterator<User> entities) {
// Your custom implementation
}
}
The most important part of the class name that corresponds to the fragment interface is the Impl postfix.
And finally use but all together:
interface UserRepository extends JpaRepository<User, Long>, CustomizedUserRepository {
}
Please also read the full docuementaion: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.single-repository-behavior
I used JdbcTemplate (NamedParameterJdbcTemplate)
//Bean
#Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
JdbcTemplate template = new JdbcTemplate(hikariDataSource);
template.setQueryTimeout(Integer.parseInt(queryTimeout));
return new NamedParameterJdbcTemplate(template);
}
Then
//Autowire NamedParameterJdbcTemplate
MapSqlParameterSource[] paramsArray =
mapperClass.mapDTOstoSqlParameterSource(items);
namedParameterJdbcTemplate.batchUpdate(SQL_qUERY, paramsArray);
Then
//Mapper class
public static MapSqlParameterSource[]
mapDTOstoSqlParameterSource(List<ItemDTO> items) {
List<MapSqlParameterSource> params = new ArrayList<>();
for (ItemDTO obj : items) {
MapSqlParameterSource source = new MapSqlParameterSource();
source.addValue("queryPara1", obj.getID());
source.addValue("queryPara2", obj.getSomething());
params.add(source);
}
return params.toArray(MapSqlParameterSource[]::new);
}

#RepositoryRestResource changes url every time the application is restarted

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.

Spring + MongoDB tag #Query with $group not working

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

How to use Spring Data JPA methods returning a Stream in a try-with-resources block in Kotlin?

So I want to create a Spring Boot with Spring Data JPA project using Kotlin and lets say I have a Person entity. Lets say like this:
#Entity
public class Person {
private #GeneratedValue #Id Long id;
private String name;
#OneToMany
private List<Person> friends;
…
}
I would create the following interface to be able to use Try-with-Resources and a Stream<Person>.
public interface PersonRepository extends Repository<Person, Long> {
#Query("select p from Person p")
Stream<Person> findAllStream();
}
So normally in my service I would do this:
#Service
class MyService {
#Autowired PersonRepository repository;
List<String> foo() {
try(Stream<Person> stream = repository.findAllStream()) {
return stream.flatMap(p -> p.getFriends().stream())
.map(f -> f.getName())
.collect(Collectors.toList());
}
}
}
Now if you want to do this in Kotlin (The IntelliJ converter doesn't produce valid code). I suppose you would normally do something like:
class MyService #Autowired constructor(val personRepository: PersonRepository) {
fun foo() {
val list = personRepository.findAllStream()
.use {{p -> p.friends.stream()}.map {f -> f.name}}
}
}
Only you cant do that since there is no #use method on stream and you cant call #stream() from a List. So is there any way to do this?
Well, Java 8 support is not yet complete in Kotlin. So you can just declare use on your side like this
inline fun <A : AutoCloseable, R> A.use(block: (A) -> R): R {
try {
return block(this)
} finally {
close()
}
}
The other alternative is to declare it directly on Stream
inline fun <T, R> Stream<T>.use(block: (Stream<T>) -> R): R {
try {
return block(this)
} finally {
close()
}
}
UPD
If you are new to Kotlin you have to notice that extensions are resolved statically:
Extensions do not actually modify classes they extend. By defining an extension, you do not insert new members into a class, but merely make new functions callable with the dot-notation on instances of this class.
See more http://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statically

Spring data neo4j embedded property MappingException

I am fairly new to Neo4J; I am developing a project for learning purposes on which I am facing an issue that I am not managing to solve. My model might be somewhat relational DB influenced, but design issues aside, I believe however that what I am attempting should technically be done.
I have a NodeEntity Foo with an nested object Bar, converted to- and from String via ConversionService. In effect, Bar contains only one single String field, making the mapping trivial.
#NodeEntity
public class Foo {
#GraphId
private Long id;
#Indexed
private Bar bar;
...
}
public class Bar {
private String value;
...
}
When returning from a fairly simple Cypher query defined as follows on my repository:
#RepositoryRestResource(...)
public interface FooRepository
extends PagingAndSortingRepository<Foo, Long> {
...
#Query ("MATCH (foo) RETURN foo.bar")
Iterable<Bar> listBars ();
...
}
Conversion is configured as follows:
#Configuration
#ComponentScan(value = "de.h7r.playground.sd.neo4j",
excludeFilters = #ComponentScan.Filter({ Configuration.class }))
public class PKanbanConfiguration {
#Bean
public ConversionServiceFactoryBean conversionService ()
throws Exception {
final ConversionServiceFactoryBean csfb = new ConversionServiceFactoryBean ();
csfb.setConverters (getConverters ());
return csfb;
}
private Set<Converter> getConverters () {
return Sets.newHashSet (new BarConverter.ToString (), new BarConverter.FromString ());
}
}
Where the code for BarConverter is as follows.
public class BarConverter {
public static class FromString <S extends String, P extends Bar>
implements Converter<S, P> {
#Override
public P convert (final S source) {
// sets value into new instance of Bar and returns
}
}
public static class ToString <P extends Bar, S extends String>
implements Converter<P, S> {
#Override
public S convert (final P source) {
// gets value from Bar and returns
}
}
}
I am getting the following exception.
org.springframework.data.mapping.model.MappingException: Unknown persistent entity test.domain.Bar
at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:178)
...
Bar is indeed not a persitent entity nor should in my understanding be one. I grasp that this might have something to do with the defined return type of listBars; on the other hand, the repository if of Foos, so I had expected it to work. I would very much not like to fetch the whole set of nodes and then filter only the nested objects; the same way I would not like to have Bar replaced by String on Foo, since their type might have semantic relevance.
I am a bit lost as to how to returning all the property values for the existing nodes, specially since this query works as expected from neo4j-shell, so I see this as a pure Spring mapping issue.
I can add any further information that might prove helpful upon request.
Any help is very much appreciated.

Resources