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

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

Related

How do you use a RepositoryItemWriter to delete objects

I have configured a RepositoryItemWriter so that I can use it to delete a number of products. Here is what my basic configuration looks like:
#Repository
public interface ProductRepository extends CrudRepository<Product, Integer>{
}
#Bean(name = "productRepositoryItemWriter")
RepositoryItemWriter<Product> productRepositoryWriter(ProductRepository repository) {
RepositoryItemWriter<Product> writer = new RepositoryItemWriter<Product>();
writer.setRepository(repository);
writer.setMethodName("deleteAllById");
return writer;
}
I am assuming I should call write on the product Ids. But I'm not sure if that's correct.
How should I use the repositoryItemWriter? Is the setMethod correct?

Extend Spring Data Repository

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

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

Criteria in spring data

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.

How to use Entity Framework context with dependency injection?

I'm trying to setup a base Repository class that can use the Entity Framework edmx model context. The problem I'm having is that I need to find an interface that the EF EDMX object context implements so I can pass to the constructor via dependency injections. I've got around this before by using a DataFactory that creates it and stores it in the HttpContext but that kills the ability to unit test. Any help would be appreciated. Thanks!
public abstract class BaseRepository<T> where T : EntityObject
{
private MyDataModelContext _dataContext;
private ObjectSet<T> dbset;
protected BaseRepository(IObjectContext dataContext)
{
_dataContext = dataContext;
dbset = _dataContext.CreateObjectSet<T>();
}
.....
I've always created a DataContextFactory that passes my own interface to the Context, and passed that to my repositories like so:
The context interface:
public IMyDataContext {
// One per table in the database
IDbSet<Class1> Class1s { get;set; }
// etc
// The standard methods from EF you'll use
void Add( object Entity );
void Attach( object Entity );
void Delete( object Entity );
void SaveChanges();
}
The context factory:
public class MyDataContextFactory : IMyDataContextFactory {
public IMyDataContext GetContext() {
// TODO: Use the service locator pattern to avoid the direct instanciation
return new MyDataContext();
}
}
The context factory interface:
public interface IMyDataContextFactory {
IMyDataContext GetContext();
}
The repository:
public class MyClass1Repository {
private readonly IMyDataContextFactory factory;
public MyClass1Repository( IMyDataContextFactory Factory ) {
// TODO: check for null
this.factory = Factory;
}
public List<MyClass1> GetAll() {
using ( IMyDataContext db = this.factory.GetContext() ) {
return db.Class1s.ToList();
}
}
// TODO: Other methods that get stuff
}
Then when I want to test the repository, I pass in a fake IMyDataContextFactory that returns a fake IMyDataContext from GetContext().
In time I notice duplication in repositories, and can push certain methods into the base repository: GetAll(), Save(), GetById() sometimes if I have consistent primary keys, etc.

Resources