Spring mongo soft delete - java

I need to implement soft delete functionality(Maintain a boolean field in table and filter all query based on this).
Below link has solution for hibernate only.
Handling soft-deletes with Spring JPA
Since my application is very old, I don't want to change each existing query. I am looking for solution like one place change in spring data classes.
Spring mongo data version: 1.5.0.RELEASE

Add Boolean Field active to every class which is mapped with Collection
set the same true for all valid Documents and false for non valid documnets
private Boolean active = Boolean.TRUE;
and can chnage your Query to
Long countByActiveTrueAndAccountStatusNot(AccountStatus status);

First Step. Override default methods like as findAll(), findById(), exists().... For this you should override mongoTemplate, it simple).
Add to your entities field "deletedAt":
#Document("costAreas")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#Builder
public class User{
#Id
String id;
String name;
LocalDateTime deletedAt;
}
PS: Filed "deletedAt" contains the date of deletion (if this field is null then document wasn't deleted).
Create CustomMongoTemplate:
import com.mongodb.client.MongoClient;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;
public class CustomMongoTemplate extends MongoTemplate {
public CustomMongoTemplate(MongoTemplate mongoTemplate) {
super(mongoTemplate.getMongoDatabaseFactory());
}
public CustomMongoTemplate(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
}
public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory) {
super(mongoDbFactory);
}
public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter) {
super(mongoDbFactory, mongoConverter);
}
#Override
public <T> List<T> find(Query query, Class<T> entityClass, String collectionName) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(collectionName, "CollectionName must not be null!");
Assert.notNull(entityClass, "EntityClass must not be null!");
query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));
return super.find(query, entityClass, collectionName);
}
#Nullable
#Override
public <T> T findById(Object id, Class<T> entityClass, String collectionName) {
T t = super.findById(id, entityClass, collectionName);
try {
Field field = entityClass.getDeclaredField("deletedAt");
field.setAccessible(Boolean.TRUE);
if (Objects.nonNull(field.get(t))) {
return null;
}
} catch (NoSuchFieldException | IllegalAccessException ignored) {
}
return t;
}
#Nullable
#Override
public <T> T findOne(Query query, Class<T> entityClass, String collectionName) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(entityClass, "EntityClass must not be null!");
Assert.notNull(collectionName, "CollectionName must not be null!");
query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));
return super.findOne(query, entityClass, collectionName);
}
#Override
#SuppressWarnings("ConstantConditions")
public boolean exists(Query query, #Nullable Class<?> entityClass, String collectionName) {
if (query == null) {
throw new InvalidDataAccessApiUsageException("Query passed in to exist can't be null");
}
query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));
return super.exists(query, entityClass, collectionName);
}
// You can also override ```delete()``` method, but I decided to not to do this
// Maybe here should add other methods: count, findAndModify and ect. It depends which methods you going to use.
}
Then create Bean in configuration class:
#Configuration
public class MyConfiguration {
//...
#Bean(name = "mongoTemplate")
CustomMongoTemplate customMongoTemplate(MongoDatabaseFactory databaseFactory, MappingMongoConverter converter) {
return new CustomMongoTemplate(databaseFactory, converter);
}
//...
}
And allow Spring to override default MongoTemplate bean. Add next thing to your application.properties file:
spring.main.allow-bean-definition-overriding=true
Replace delete() with set deletedAt:
// Deletion method
// Before
User = userRepository.findById(id);
userRepository.delete(user);
// Now
User = userRepository.findById(id);
user.setDeletedAt(LocalDateTime.now());
userRepository.save(user);
Second Step. Implement soft delete for method in Repositories (generated by JPA) like as findAllByEmail(String email), existsByNameAndUsername(String name, String username)....
Resource: https://blog.rpuch.com/2019/10/27/spring-data-mongo-soft-delete-repositories.html
SoftDeleteMongoQueryLookupStrategy
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.lang.reflect.Method;
public class SoftDeleteMongoQueryLookupStrategy implements QueryLookupStrategy {
private final QueryLookupStrategy strategy;
private final MongoOperations mongoOperations;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public SoftDeleteMongoQueryLookupStrategy(QueryLookupStrategy strategy,
MongoOperations mongoOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.strategy = strategy;
this.mongoOperations = mongoOperations;
this.evaluationContextProvider = evaluationContextProvider;
}
#Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, factory, namedQueries);
// revert to the standard behavior if requested
if (method.getAnnotation(SeesSoftlyDeletedRecords.class) != null) {
return repositoryQuery;
}
if (!(repositoryQuery instanceof PartTreeMongoQuery)) {
return repositoryQuery;
}
PartTreeMongoQuery partTreeQuery = (PartTreeMongoQuery) repositoryQuery;
return new SoftDeletePartTreeMongoQuery(partTreeQuery);
}
private Criteria notDeleted() {
return new Criteria().andOperator(
Criteria.where("deletedAt").exists(false)
);
}
private class SoftDeletePartTreeMongoQuery extends PartTreeMongoQuery {
SoftDeletePartTreeMongoQuery(PartTreeMongoQuery partTreeQuery) {
super(partTreeQuery.getQueryMethod(), mongoOperations, new SpelExpressionParser(), evaluationContextProvider);
}
#Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
Query query = super.createQuery(accessor);
return withNotDeleted(query);
}
#Override
protected Query createCountQuery(ConvertingParameterAccessor accessor) {
Query query = super.createCountQuery(accessor);
return withNotDeleted(query);
}
private Query withNotDeleted(Query query) {
return query.addCriteria(notDeleted());
}
}
}
SeesSoftlyDeletedRecords (if you marked method annotation then method will ignore soft-deletion)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface SeesSoftlyDeletedRecords {
}
SoftDeleteMongoRepositoryFactory
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import java.util.Optional;
public class SoftDeleteMongoRepositoryFactory extends MongoRepositoryFactory {
private final MongoOperations mongoOperations;
public SoftDeleteMongoRepositoryFactory(MongoOperations mongoOperations) {
super(mongoOperations);
this.mongoOperations = mongoOperations;
}
#Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Optional<QueryLookupStrategy> optStrategy = super.getQueryLookupStrategy(key,
evaluationContextProvider);
return Optional.of(createSoftDeleteQueryLookupStrategy(optStrategy.get(), evaluationContextProvider));
}
private SoftDeleteMongoQueryLookupStrategy createSoftDeleteQueryLookupStrategy(QueryLookupStrategy strategy,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return new SoftDeleteMongoQueryLookupStrategy(strategy, mongoOperations, evaluationContextProvider);
}
}
SoftDeleteMongoRepositoryFactoryBean
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import java.io.Serializable;
public class SoftDeleteMongoRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends MongoRepositoryFactoryBean<T, S, ID> {
public SoftDeleteMongoRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}
#Override
protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
return new SoftDeleteMongoRepositoryFactory(operations);
}
}
Add it to configuration
#Configuration
#EnableMongoRepositories(basePackages = {"path to package with your repositories"}, repositoryFactoryBeanClass = SoftDeleteMongoRepositoryFactoryBean.class)
public class MyConfiguration {
//...
}
Hope it will help someone)

Related

Why doesn't Spring's Qualifier annotation have the ability to resolve properties?

In Spring 5, I am able to use a properties file to inject values into fields using the Values annotation. I assumed that the same can be done using the Qualifier annotation, but it doesn't work, instead I have to do the following
import java.lang.annotation.Annotation;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.stereotype.Component;
#Component
public class AutowireCandidateResolverConfigurer implements BeanFactoryPostProcessor {
private static class EnvironmentAwareQualifierAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {
private static class ResolvedQualifier implements Qualifier {
private final String value;
ResolvedQualifier(String value) { this.value = value; }
#Override
public String value() { return this.value; }
#Override
public Class<? extends Annotation> annotationType() { return Qualifier.class; }
}
#Override
protected boolean checkQualifier(BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
if (annotation instanceof Qualifier) {
Qualifier qualifier = (Qualifier) annotation;
if (qualifier.value().startsWith("${") && qualifier.value().endsWith("}")) {
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) this.getBeanFactory();
ResolvedQualifier resolvedQualifier = new ResolvedQualifier(bf.resolveEmbeddedValue(qualifier.value()));
return super.checkQualifier(bdHolder, resolvedQualifier, typeConverter);
}
}
return super.checkQualifier(bdHolder, annotation, typeConverter);
}
}
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) beanFactory;
bf.setAutowireCandidateResolver(new EnvironmentAwareQualifierAnnotationAutowireCandidateResolver());
}
}
This is something I found here: How to read Qualifier from property file in spring boot?
I am very new to Spring, so I might be missing something, but isn't this an issue?

Spring Boot: Type cannot be null; nested exception is java.lang.IllegalArgumentException: Type cannot be null when running Stored Procedue

Good morning, I am new to Spring Boot, and I am performing a rest service that must invoke a procedure stored in the database, the question is that you receive the mobile and must return a code and result, as shown below:
This is my code:
Main Class
package com.app.validacion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Controller
package com.app.validacion.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.app.validacion.dao.DriverBonificadosRepository;
import com.app.validacion.entity.RespuestaVo;
#RestController
public class DriverBonificadosController {
#Autowired
private DriverBonificadosRepository dao;
#GetMapping("/service/{movil}")
public RespuestaVo ConsultarMovil(#PathVariable String movil) {
return dao.validarClienteBonifiado(movil);
}
}
Repository
package com.app.validacion.dao;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.CrudRepository;
import com.app.validacion.entity.DriverBonificados;
import com.app.validacion.entity.RespuestaVo;
public interface DriverBonificadosRepository extends CrudRepository<DriverBonificados, Integer> {
#Procedure(procedureName="ValidacionClienteBonificado")
RespuestaVo validarClienteBonifiado(String pMovil);
}
My entity
import javax.persistence.NamedStoredProcedureQueries;
import javax.persistence.NamedStoredProcedureQuery;
import javax.persistence.ParameterMode;
import javax.persistence.StoredProcedureParameter;
import javax.persistence.Table;
#NamedStoredProcedureQueries({
#NamedStoredProcedureQuery(
name="SPValidationClienteBonus4G",
procedureName="ValidacionClienteBonificado",
parameters = {
#StoredProcedureParameter(mode=ParameterMode.IN, name="p_movil",type=String.class),
#StoredProcedureParameter(mode=ParameterMode.OUT, name="code",type=String.class),
#StoredProcedureParameter(mode=ParameterMode.OUT, name="result",type=String.class),
})
})
#Entity
#Table
public class DriverBonificados {
#Id
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getMovil() {
return movil;
}
public void setMovil(String movil) {
this.movil = movil;
}
public String getContador() {
return contador;
}
public void setContador(String contador) {
this.contador = contador;
}
public Date getFecha_driver() {
return fecha_driver;
}
public void setFecha_driver(Date fecha_driver) {
this.fecha_driver = fecha_driver;
}
public Date getFecha_alta() {
return fecha_alta;
}
public void setFecha_alta(Date fecha_alta) {
this.fecha_alta = fecha_alta;
}
public Date getFecha_fin() {
return fecha_fin;
}
public void setFecha_fin(Date fecha_fin) {
this.fecha_fin = fecha_fin;
}
public Date getCodigo_transaccion() {
return codigo_transaccion;
}
public void setCodigo_transaccion(Date codigo_transaccion) {
this.codigo_transaccion = codigo_transaccion;
}
private String movil;
private String contador;
private Date fecha_driver;
private Date fecha_alta;
private Date fecha_fin;
private Date codigo_transaccion;
My Class RespuestaVo
package com.app.validacion.entity;
public class RespuestaVo {
private String code;
private String result;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
And I get the following error (the mobile parameter must be received as a String, since in the database it is found as Varchar):
Anyone have an idea how this problem could be solved? I need to consult via Stored Procedue if or if
UPDATE
Using #Query and modifying the code as follows:
package com.app.validacion.dao;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import com.app.validacion.entity.DriverBonificados;
import com.app.validacion.entity.RespuestaVo;
public interface DriverBonificadosRepository extends CrudRepository<DriverBonificados, Integer> {
#Query(nativeQuery = true,value = "call ValidacionClienteBonificado(:movil)")
RespuestaVo validarClienteBonifiado(#Param("movil") String pMovil);
}
I get the following error:
org.springframework.core.convert.ConverterNotFoundException: No
converter found capable of converting from type
[org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap]
to type [com.app.validacion.entity.RespuestaVo] at
org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:321)
~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at
org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:194)
~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at
org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:174)
~[spring-core-5.2.1.RELEASE.jar:5.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor$ProjectingConverter.convert(ResultProcessor.java:297)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor$ChainingConverter.lambda$and$0(ResultProcessor.java:217)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor$ChainingConverter.convert(ResultProcessor.java:228)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.repository.query.ResultProcessor.processResult(ResultProcessor.java:170)
~[spring-data-commons-2.2.1.RELEASE.jar:2.2.1.RELEASE] at
org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:157)
~[spring-data-jpa-2.2.1.RELEASE.jar:2.2.1.RELEASE]
SOLVED
I managed to solve my problem, using the #Query annotation, and building an interface for the response I was going to receive, in these cases with 2 methods (according to the number of parameters that I will receive), and With this I got my answer in a Json, I leave the interface code below:
public interface RespuestaVo {
String getCode();
String getResult();
}
I recommend using #Query to run Stored Procedue with Spring Boot
Try this -
#GetMapping("/service/{movil}")
public RespuestaVo ConsultarMovil(#PathVariable("movil") String movil) {
return dao.validarClienteBonifiado(movil);
}

Pattern to implement generic CRUD Controller

I have a bunch of tables for which I need to provide standard CRUD interface. Every time I have to expose a new table, I follow the below pattern.
public interface EntityWithId<TDbEntity> extends Serializable {
public TDbEntity entityId();
}
#Entity
public class DbEntityName implements EntityWithId<Long> {
#Id private Long id;
#Override public Long entityId() {return id;}
// other fields follow
}
public class EntityName {
private Long id;
// other fields follow
// create Entity from DbEntity
public EntityName(DbEntityName dbItem) { ... }
// get DbEntity from Entity
public DbEntityName toDb() { ... }
}
#Repository
public interface DbEntityNameRepository extends CrudRepository<DbEntityName, Long> { }
public interface CrudService<TDbEntity extends EntityWithId<ID>, ID> {
CrudRepository<TDbEntity, ID> getCrudRepository();
// provide default implementation of all CRUD operations here like the below one
default TDbEntity save(TDbEntity entity) { return getCrudRepository().save(entity); }
}
public interface DbEntityNameService extends CrudService<DbEntityName, Long> {
}
#Service
public class DbEntityNameServiceImpl implements DbEntityNameService {
#lombok.Getter #Autowired DbEntityNameRepository crudRepository;
}
#RestController
#RequestMapping("/api/v1/dbservice")
public class EntityNameController {
#Autowired DbEntityNameService dbService;
#PostMapping("/{EntityName}") // this should be replaced by the actual name of the entity
public Long save(#RequestBody EntityName msg) {
return dbService.save(msg.toDb()).entityId();
}
// implement other CRUD end points
}
EntityWithId<T> interface and CrudService<TDbEntity extends EntityWithId<ID>, ID> are defined only once for the system. They provide mechanism to get rid of repeat code in accessing the repo.
As you will notice, the only real code needs to be done to add the fields in the Entity and the DB Entity, and their conversion. Also, I need to roll a new Controller out for each new table.
Question: How can I structure the controller code, in a way that I can inherit the functionality from a base CRUD controller.
Note that, in my real code not all entities are for simple CRUD, and the current structure provides easy way to extend the services
In a nutshell, I am looking for some pattern that will help me provide something like below, where I have a generic Base class, and I can create a subclass with minimal code to expose the controller's end point. Needless to say, the below code will not work as-is to provide the functionality I am looking for.
class BaseController<TEntity, TDbEntity, TId> {
CrudService<TDbEntity, TId> dbService;
#GetMapping("/{TEntity}/{id}")
public TEntity getById(#PathVariable TId id) {
return new TEntity(dbService.getById(id));
}
#PostMapping("/{TEntity}")
public Long save(#RequestBody TEntity msg) {
return dbService.save(msg.toDb()).entityId();
}
}
class EntityNameController : BaseController<EntityName, DbEntityName, Long> {
}
Feel free to provide other suggestions too. My intention is to reduce repeated code in the controller - which is primarily creating the CRUD function, associating it with a CRUD endpoint and invoking the underlying service to do the real work.
EDIT: I understand that I can write a custom annotation processor to generate the standard CRUD functions (almost like how CrudRepository works), but that is not the direction I want to go.
Just to clarify, the intention here is that the standard functionality (like CRUD) can be coded once and for all in a base controller which will expose it, freeing up the child controller to take care of other non-standard work.
I would think for the dbService you could use something like
public interface CrudService<T, ID> {
T findByName(String name);
Set<T> findAll();
T findById(ID id);
T save(T object);
void delete(T object);
void deleteById(ID id);
}
public interface EntityNameService extends CrudService<EntityName, Long> {
}
public class EntityNameServiceImpl implements EntityNameService {
#Inject
private DbEntityNameRepository repository;
// implement all your repo code here
}
And your Base Controller could start like
public class BaseController {
#Autowired
private EntityNameService service;
public String getEntityName(String name) {
service.findByName(name);
}
#PostMapping("/{EntityName}") // this should be replaced by the actual name of the entity
public Long save(#PathVariable String EntityName) {
getEntityName(EntityName);
return dbService.save(msg.toDb()).entityId();
}
}
This was an attempt at getting rid of some boiler plate. The idea was that the business logic would sit in the service and not in the RestController or Repository.
A service could be reused and unit tested well.
QueryDSL with SpringData is your friend:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
A base repo.
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
#NoRepositoryBean
public interface BaseRepo<T , ID > extends PagingAndSortingRepository<T, ID>, QuerydslPredicateExecutor<T> {}
The real repository that can access very large tables:
import com.querydsl.core.types.Predicate;
import static java.lang.System.out;
import refactor.BaseRepo;
public interface MyEntityRepository extends BaseRepo<MyEntity, String> {
#Override
default long count(Predicate predicate){
//counts on very large tables take forever. Optionally add this
return 0;
}
#Override
default long count(){
//counts on very large tables take forever. Optionally add this
return 0;
}
}
The base service:
import com.querydsl.core.types.Predicate;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
#RequiredArgsConstructor
public class BaseService<T, ID> {
final BaseRepo<T, ID> repo;
public Page<T> findAll(Predicate predicate, Pageable pageable) {
return repo.findAll(predicate, pageable);
}
public Iterable<T> findAllWithoutMeta(Predicate predicate, Pageable pageable) {
return repo.findAll(predicate, pageable);
}
public Iterable<T> findAll() {
return repo.findAll();
}
public T save(T vendor) {
return repo.save(vendor);
}
public T update(T vendor) {
return repo.save(vendor);
}
public void delete(ID id) {
repo.deleteById(id);
}
public boolean exists(ID id) {
return repo.findById(id).isPresent();
}
public Optional<T> getById(ID id) {
return repo.findById(id);
}
}
The real service:
import com.querydsl.core.types.Predicate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
#Service
public class MyService extends BaseService<MyEntity, String>{
public MyService(MyEntityRepository repo) {
super(repo);
}
#Override
public Page<MyEntity> findAll(Predicate predicate, Pageable pageable) {
return super.findAll(predicate, pageable);
}
}
I decided not to genrify my RestContoller and only write what I code I needed for the CRUD operations I needed. (In some cases delete and put operations are not needed or wanted for example)
This is an implementation of a HATEOAS RESTful API. Investing in a HATEOAS design is not for everyone and every application. This can be substituted by a plain rest controller.
The get here can filter on all the fields in the repository. So you can get http://localhost/api/v1/myapi?name=YourName&age=30
import com.querydsl.core.types.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.querydsl.binding.QuerydslPredicate;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping(path = "/api/v1/myapi", produces = MediaTypes.HAL_JSON_VALUE)
public class MyApiController {
private final MyService service;
private final EntityLinks eLinks;
MyApiController(MyService service, EntityLinks eLinks) {
this.service = service;
this.eLinks = eLinks;
}
#GetMapping
#Transactional(readOnly = true)
ResponseEntity<Resources<Resource<MyEntity>>> findAll(#QuerydslPredicate(root = MyEntity.class) Predicate predicate, Pageable pageable) {
return new ResponseEntity(toResources(service.findAllWithoutMeta(predicate,pageable)), HttpStatus.OK);
}
#GetMapping(value = "/{id}")
ResponseEntity<Resource<MyEntity>> findOne(#PathVariable String id) {
final Optional<MyEntity> findById = service.getById(id);
if (!findById.isPresent()) {
return null;//fixme ResponseEntity.notFound(assembler.);
}
return ResponseEntity.ok(toResource(findById.get()));
}
private Resources<Resource<MyEntity>> toResources(Iterable<MyEntity> customers) {
List<Resource<MyEntity>> customerResources = new ArrayList<>();
for (MyEntity l : customers) {
customerResources.add(toResource(l));
}
return new Resources<>(customerResources);//, selfLink);
}
private Resource<MyEntity> toResource(MyEntity customer) {
Link selfLink = linkTo(methodOn(CallLoggingController.class).findOne(customer.getId())).withSelfRel();
return new Resource<>(customer, selfLink);
}
}
My advice is do not to be obsessed with generic code. Copy and paste is better than super generic code imho.

DAO and Spring Autowired

I tried to create an abstract Dao. I use Spring + Hibernate.
Here's my code.
Main class with configuration:
package ru.makaek.growbox.api;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
#ComponentScan(value = "ru.makaek.growbox")
#EnableAutoConfiguration(exclude = HibernateJpaAutoConfiguration.class)
#EnableTransactionManagement
#SpringBootApplication
public class Application {
#Autowired
private Environment env;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public DataSource getDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getRequiredProperty("datasource.driver"));
dataSource.setUrl(env.getRequiredProperty("datasource.url"));
dataSource.setUsername(env.getRequiredProperty("datasource.username"));
dataSource.setPassword(env.getRequiredProperty("datasource.password"));
return dataSource;
}
#Bean
public LocalSessionFactoryBean getSessionFactory() {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(getDataSource());
sessionFactory.setPackagesToScan(new String[]{"ru.makaek.growbox"});
return sessionFactory;
}
#Bean
public HibernateTransactionManager getTransactionManager(SessionFactory sessionFactory) {
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(sessionFactory);
return txManager;
}
}
Rest controller
package ru.makaek.growbox.api.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.makaek.growbox.api.model.data.entities.Device;
import ru.makaek.growbox.api.service.IStructureService;
#RestController
public class DeviceController extends AbstractController {
#Autowired
IStructureService structureService;
#RequestMapping(value = "/devices", method = RequestMethod.POST)
public Answer addDevice(#RequestBody Device device) {
structureService.addDevice(device);
return ok("Device has been added");
}
#RequestMapping(value = "/devices", method = RequestMethod.GET)
public Answer getDevices() {
return ok(structureService.getDevices());
}
#RequestMapping(value = "/devices/{deviceId}", method = RequestMethod.GET)
public Answer getDevice(#PathVariable Long deviceId) {
return ok(structureService.getDevice(deviceId));
}
}
Service layer. Interface
package ru.makaek.growbox.api.service;
import ru.makaek.growbox.api.model.data.entities.Device;
import java.util.List;
public interface IStructureService {
void addDevice(Device device);
List<Device> getDevices();
Device getDevice(Long deviceId);
}
Service layer. Implementation
package ru.makaek.growbox.api.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.makaek.growbox.api.model.data.dao.base.IDao;
import ru.makaek.growbox.api.model.data.entities.Device;
import java.util.List;
#Service
#Transactional
public class StructureService implements IStructureService {
IDao<Device> deviceDao;
#Autowired
public void setDao(IDao<Device> dao) {
deviceDao = dao;
dao.setClazz(Device.class);
}
#Override
public void addDevice(Device device) {
deviceDao.create(device);
}
#Override
public List<Device> getDevices() {
return deviceDao.findAll();
}
#Override
public Device getDevice(Long deviceId) {
return deviceDao.findOne(deviceId);
}
}
Entity
package ru.makaek.growbox.api.model.data.entities;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
#Entity(name = "devices")
#Data public class Device extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
DAO. Interface
package ru.makaek.growbox.api.model.data.dao.base;
import ru.makaek.growbox.api.model.data.entities.BaseEntity;
import java.util.List;
public interface IDao<T extends BaseEntity> {
T findOne(final long id);
void setClazz(Class<T> clazz);
List<T> findAll();
void create(final T entity);
T update(final T entity);
void delete(final T entity);
void deleteById(final long entityId);
}
Abstract DAO
package ru.makaek.growbox.api.model.data.dao.base;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import ru.makaek.growbox.api.model.data.entities.BaseEntity;
import ru.makaek.growbox.api.util.GBException;
import java.util.List;
public abstract class AbstractDao<T extends BaseEntity> implements IDao<T> {
private Class<T> clazz;
#Autowired
private SessionFactory sessionFactory;
public final void setClazz(Class<T> clazz) {
this.clazz = clazz;
}
public T findOne(long id) {
try {
return (T) getCurrentSession().get(clazz, id);
} catch (Exception e) {
throw new GBException.InternalError(e.getMessage());
}
}
public List<T> findAll() {
try {
return getCurrentSession().createQuery("from " + clazz.getName()).list();
} catch (Exception e) {
throw new GBException.InternalError(e.getMessage());
}
}
public void create(T entity) {
try {
getCurrentSession().persist(entity);
} catch (Exception e) {
throw new GBException.InternalError(e.getMessage());
}
}
public T update(T entity) {
try {
return (T) getCurrentSession().merge(entity);
} catch (Exception e) {
throw new GBException.InternalError(e.getMessage());
}
}
public void delete(T entity) {
try {
getCurrentSession().delete(entity);
} catch (Exception e) {
throw new GBException.InternalError(e.getMessage());
}
}
public void deleteById(long entityId) {
try {
T entity = findOne(entityId);
delete(entity);
} catch (Exception e) {
throw new GBException.InternalError(e.getMessage());
}
}
protected final Session getCurrentSession() {
return sessionFactory.getCurrentSession();
}
}
DAO. Implementation
package ru.makaek.growbox.api.model.data.dao;
import org.springframework.stereotype.Repository;
import ru.makaek.growbox.api.model.data.dao.base.AbstractDao;
import ru.makaek.growbox.api.model.data.entities.Device;
#Repository
public class DeviceDao extends AbstractDao<Device> {
}
I have one trouble. When I call GET http://host:port/devices API method I have null in the clazz variable in the AbstractDao.findAll() method. When I was debugging the code i found one interesting thing: in the service layer method deviceDao.getClazz() returned needed clazz (not null). But in method AbstractDao.findAll() I have null in clazz variable. Why? Please help.
Sorry for my English and formulation. I'm new in this site, Spring and English
You are overcomplicating things. Because you are using Spring Boot it is possible to just create generic interface that extends CrudRepository and add the methods you need and are not already present in there.
Take a look here https://docs.spring.io/spring-data/data-commons/docs/1.6.1.RELEASE/reference/html/repositories.html

Hibernate doesn't save entity, only if I double save

I'm developing an application using Vaadin and I am having some trouble:
When I try to save and object with Hibernate, nothing appears in my table unless i double save it.
Code from my class:
if (referenciacao==null){
referenciacao = (Referenciacao)referenciacaoSession.newEntity();
}
referenciacao.setMedico((Medico)cbbMedico.getValue());
referenciacao.setOrigem((Origem)cbbOrigem.getValue());
referenciacao.setReferenciacaotipo((ReferenciacaoTipo)cbbReferenciacaoTipo.getValue());
referenciacao=referenciacaoSession.saveAndFlush(referenciacao);
My Referenciacao Entity:
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
/**
*
* #author pepatusco
*/
#SuppressWarnings("serial")
#Entity
#Table(name = "referenciacao")
public class Referenciacao extends BaseBean implements Serializable {
public static final String PROPERTY_ID = "id";
public static final String PROPERTY_MEDICO = "medico";
public static final String PROPERTY_ORIGEM = "origem";
public static final String PROPERTY_REFTIPO = "referenciacaoTipo";
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "medico_id")
private Medico medico;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "origem_id")
private Origem origem;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "referenciacao_tipo_id")
private ReferenciacaoTipo referenciacaoTipo;
public Medico getMedico() {
return medico;
}
public void setMedico(Medico medico) {
this.medico = medico;
}
public Origem getOrigem() {
return origem;
}
public void setOrigem(Origem origem) {
this.origem = origem;
}
public ReferenciacaoTipo getReferenciacaotipo() {
return referenciacaoTipo;
}
public void setReferenciacaotipo(ReferenciacaoTipo referenciacaoTipo) {
this.referenciacaoTipo = referenciacaoTipo;
}
public Referenciacao() {
//this.id = null;
}
}
My Referenciacao DAO:
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import pt.app.dpn.base.common.beans.entity.Medico;
import pt.app.dpn.base.common.beans.entity.Origem;
import pt.app.dpn.base.common.beans.entity.Referenciacao;
import pt.app.dpn.base.common.beans.entity.ReferenciacaoTipo;
/**
*
* #author pepatusco
*/
#Repository
#Transactional
public interface ReferenciacaoRepository extends JpaRepository<Referenciacao, Long> {
List<Referenciacao> findByMedico(Medico medico);
List<Referenciacao> findByOrigem(Origem origem);
List<Referenciacao> findByReferenciacaoTipo(ReferenciacaoTipo referenciacaoTipo);
Referenciacao findByMedicoAndOrigemAndReferenciacaoTipo(Medico medico, Origem origem, ReferenciacaoTipo referenciacaoTipo);
Referenciacao findById(Long id);
}
My ReferenciacaoSession:
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.enterprise.inject.Default;
import javax.inject.Inject;
import javax.transaction.Transactional;
import pt.app.dpn.base.common.beans.dao.ReferenciacaoRepository;
import pt.app.dpn.base.common.beans.entity.BaseBean;
import pt.app.dpn.base.common.beans.entity.Medico;
import pt.app.dpn.base.common.beans.entity.Origem;
import pt.app.dpn.base.common.beans.entity.Referenciacao;
import pt.app.dpn.base.common.beans.entity.ReferenciacaoTipo;
/**
*
* #author pepatusco
*
*/
#Default
#Stateless
#Local(ReferenciacaoSessionLocal.class)
public class ReferenciacaoSession implements ReferenciacaoSessionLocal, Serializable {
#Inject
private ReferenciacaoRepository referenciacaoRepository;
#Override
public void flush() {
referenciacaoRepository.flush();
}
#Transactional
#Override
public <S extends Referenciacao> S saveAndFlush(S s) {
return referenciacaoRepository.saveAndFlush(s);
}
#Override
public Long getCount() {
return referenciacaoRepository.count();
}
#Override
public List<BaseBean> findAll() {
List<BaseBean> listBaseBean = new ArrayList<>();
referenciacaoRepository.findAll().forEach((referenciacao) -> {
listBaseBean.add(referenciacao);
});
return listBaseBean;
}
#Transactional
#Override
public void save(Object o) {
referenciacaoRepository.save((Referenciacao)o);
referenciacaoRepository.flush();
}
#Override
public void remove(Object o) {
referenciacaoRepository.delete((Referenciacao)o);
}
#Override
public Object newEntity() {
return new Referenciacao();
}
#Override
public List<Referenciacao> findByMedico(Medico medico) {
return referenciacaoRepository.findByMedico(medico);
}
#Override
public List<Referenciacao> findByOrigem(Origem origem) {
return referenciacaoRepository.findByOrigem(origem);
}
#Override
public Referenciacao findById(Long id) {
return referenciacaoRepository.findById(id);
}
#Override
public List<Referenciacao> findByReferenciacaoTipo(ReferenciacaoTipo referenciacaoTipo) {
return referenciacaoRepository.findByReferenciacaoTipo(referenciacaoTipo);
}
#Override
public Referenciacao findByMedicoAndOrigemAndReferenciacaoTipo(Medico medico, Origem origem, ReferenciacaoTipo referenciacaoTipo) {
return referenciacaoRepository.findByMedicoAndOrigemAndReferenciacaoTipo(medico, origem, referenciacaoTipo);
}
}
Can anyone tell me why, when I do save, nothing appears in the table but when I do save again it saves a Referenciacao in the table?
I can't see the reason for this behavior, so let me at least try to help to debug this thing.
I see multiple possible things that might be going on:
The repository for your entity doesn't get called on the first try, for some reason outside of the code you showed us.
The repository does not store your entity.
Something goes haywire with your transactions, causing you not to see the changes (I'm actually betting on this one).
In order to rule out (1) put a breakpoint or a logging statement next to the referenciacaoRepository.save and referenciacaoRepository.saveAndFlush statements to ensure, that code path gets executed.
In order to rule out (2) activate SQL logging, to see if SQL statements actually get executed as expected. Obviously, you should see insert statements getting executed.
To tackle the third one, activate transaction logging to see when transactions start and end. There should be a transaction getting started before the save and getting commited afterward. Verify that this actually happens and that there are no other transactions running.
Let us know if you were able to solve your problem.
Side note: Why do you have ReferenciacaoSession? It really doesn't do anything and seems to be perfectly replaceable by the repository.

Categories

Resources