I have a base Repository, say IBaseRepository which is
public interface IBaseRepository<T extends BaseEntity<PK>, PK extends Serializable>
extends JpaRepository<T, PK>, JpaSpecificationExecutor<T> {
}
now every repository class, for example UserRepository extends from this base repository. How can I add a general method like
T findOne(String filter, Map<String, Object> params);
for all inherited classes so that calling
Map<String,Object> params = new HashMap<String,Object>();
params.put("username","Lord");
params.put("locked",Status.LOCKED);
userRepo.findeOne("username = :username AND status = :locked",params);
return me a single record with dynamic where clause.
You can do the Following
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import java.io.Serializable;
import java.util.Map;
/**
* Created by shazi on 1/11/2017.
*/
#NoRepositoryBean
public interface IBaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
T findOne(String filter, Map<String, Object> params);
}
And Implement it as follows.
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.io.Serializable;
import java.util.Map;
/**
* Created by shazi on 1/11/2017.
*/
public class BaseRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> implements IBaseRepository<T, ID> {
private final EntityManager entityManager;
private final JpaEntityInformation entityInformation;
public BaseRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
this.entityInformation = entityInformation;
}
#Override
public T findOne(String filter, Map<String, Object> params) {
final String jpql = "FROM " + entityInformation.getEntityName() + " WHERE " + filter;
Query query = entityManager.createQuery(jpql);
for (Map.Entry<String, Object> value:params.entrySet()) {
query.setParameter(value.getKey(), value.getValue());
}
return (T) query.getSingleResult();
}
}
And configure it as follows
#Configuration
#EnableJpaRepositories(repositoryBaseClass = BaseRepositoryImpl.class)
#EnableTransactionManagement
public class RepoConfig {
or in XML
<repositories base-class="….BaseRepositoryImpl" />
Finally you can use it as follows;
User found = userRepository.findOne("name = :name", Collections.singletonMap("name", "name"));
But you have to make sure that your query WHERE is such that the Query will always return 1 result only. See this post
Related
I am having trouble with generic abstract classes and multiple parameters. In brief, I have a database table with a composite key defined by ProductAttributeEntity.class and ProductAttributeEntityPk.class, where ProductAttributeEntityPk.class is the composite key. I am struggling to pass ProductAttributeEntityPk.class into the abstract class for the getById method, which gives me, “Class must either be declared abstract or implement abstract method” error.
How can I get around this problem?
PS: I am new to java
Abstract DAO class (https://pastebin.com/n2FBQCg2)
package com.beetlehand.model.dao;
import javax.persistence.EntityManager;
import javax.persistence.Persistence;
import java.util.List;
import java.util.Map;
public abstract class AbstractDao<T, S> {
protected EntityManager entityManager;
public EntityManager getEntityManager() {
if(this.entityManager == null) {
this.entityManager = Persistence
.createEntityManagerFactory("NewPersistenceUnit")
.createEntityManager();
}
return this.entityManager;
}
public abstract T getById(S id);
public abstract T getOneBy(Map<String, Object> params);
public abstract List<T> getBy(String predicates, Map<String, Object> params);
public abstract List<T> getBy(String predicates, Map<String, Object> params, String orderBy);
public abstract List<T> getBy(String predicates, Map<String, Object> params, String orderBy
, Integer limit);
public abstract List<T> getBy(String predicates, Map<String, Object> params, String orderBy
, Integer limit, Integer offset);
public abstract Long count();
public abstract Long countBy(String predicates, Map<String, Object> params);
}
DAO class (https://pastebin.com/tkMYLVZE)
package com.beetlehand.model.dao;
import com.beetlehand.model.ProductAttributeEntity;
import com.beetlehand.model.ProductAttributeEntityPk;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ProductAttributeDao extends AbstractDao<ProductAttributeEntity, ProductAttributeEntityPk> {
public ProductAttributeEntity getById(ProductAttributeEntityPk id) {
if (id == null) return null;
return getEntityManager().find(ProductAttributeEntity.class, id);
}
/** other methods implementation **/
}
Entity classes (https://pastebin.com/5YySBt7X, https://pastebin.com/8rwD4kec)
package com.beetlehand.model;
import javax.persistence.*;
#Entity
#Table(name = "product_attribute", schema = "beetlehand", catalog = "")
#IdClass(ProductAttributeEntityPK.class)
public class ProductAttributeEntity {
private int productId;
private int attributeValueId;
/** getter, setters, equals and hashcode implementation **/
}
package com.beetlehand.model;
import javax.persistence.Column;
import javax.persistence.Id;
import java.io.Serializable;
public class ProductAttributeEntityPK implements Serializable {
private int productId;
private int attributeValueId;
/** getter, setters, equals and hashcode implementation **/
}
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.
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)
I am just staring with spring-data and spring-data-rest and I really want to take advantage of what these tools have to offer. For the most case the base functionality is perfect for my use case however there are some cases where I need to customize the underlying functionality quite a bit, and selectively assign some repositories to inherit the customized functionality I am after.
To explain the problem a bit better, in spring-data there are 2 possible interfaces which you can inherit functionality from, CrudRepository or PagingAndSortingRepository. I want to add a third called lets say PesimisticRepository
All the PesimisticRepository does is to handle the notion of a deleted #Entity differently. A deleted entity is one where its deleted property is NOT NULL. This means that an #Entity which can be handled by a PesimisticRepository has to have a deleted property.
All this is possible, I have actually implemented this a couple of years ago. (You can check it out here in case you are interested)
My current attempt using spring-data is the following:
An extension of the PagingAndSortingRepository
package com.existanze.xxx.datastore.repositories;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.io.Serializable;
#NoRepositoryBean
public interface PesimisticRepository<T,ID extends Serializable> extends PagingAndSortingRepository<T,ID> {
}
For which I provide a default implementation extending JPARepository
package com.existanze.xxx.datastore.repositories;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.io.Serializable;
import java.util.Date;
public class JpaPesimisticRepository<T,ID extends Serializable> extends SimpleJpaRepository<T,ID> implements PesimisticRepository<T,ID> {
private final EntityManager entityManager;
public JpaPesimisticRepository(Class<T> domainClass, EntityManager em) {
super(domainClass, em);
this.entityManager = em;
}
#Override
#Transactional
public Page<T> findAll(Specification<T> spec, Pageable pageable) {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
CriteriaQuery<T> criteriaQuery = cb.createQuery(getDomainClass());
Root<T> from = criteriaQuery.from(this.getDomainClass());
Predicate deleted = cb.equal(from.get("deleted"), cb.nullLiteral(Date.class));
criteriaQuery.select(from).where(deleted);
TypedQuery<T> query = this.entityManager.createQuery(criteriaQuery);
return pageable == null ? new PageImpl<T>(query.getResultList()) : readPage(query, pageable, spec);
}
}
And then for any bean for which I wish to handle the deletion using the pessimistic method I define it as such
package com.existanze.xxx.datastore.repositories;
import com.existanze.xxx.domain.Phone;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
#RepositoryRestResource
public interface PhoneRepository extends PesimisticRepository<Phone,Integer> {
}
It is important to explain why I wish to override these methods instead of providing custom ones, like findAllButDeleted. The reason is because I also want the pessimistic delete to trickle down to spring-data-rest. So that the HTTP endpoints generated will not need any form of customization.
This seems to work only for the findAll method. However for the rest of the methods the current exception is thrown.
$ curl http://localhost:8881/phones/23
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 500 </title>
</head>
<body>
<h2>HTTP ERROR: 500</h2>
<p>Problem accessing /phones/23. Reason:
<pre> org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: object is not an instance of declaring class; nested exception is java.lang.IllegalArgumentException: object is not an instance of declaring class</pre></p>
<hr /><i><small>Powered by Jetty://</small></i>
</body>
</html>
Furthermore, I have read the documentation which allows you to change the default JpaRepository for all Repositories, but again I need to do this on a per repository basis.
I hope I have been descriptive enough. Please let me know in the comments section if there is something that needs better explanation.
You can create a custom repository, something like this:
package com.brunocesar.custom.repository.support;
import java.io.Serializable;
import javax.persistence.EntityManager;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import com.brunocesar.custom.entity.CustomAbstractEntity;
#NoRepositoryBean
public interface CustomGenericRepository<E extends CustomAbstractEntity, PK extends Serializable> extends
JpaRepository<E, PK>, JpaSpecificationExecutor<E> {
EntityManager getEntityManager();
}
package com.brunocesar.custom.repository.support.impl;
import java.io.Serializable;
import java.util.Calendar;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import com.brunocesar.custom.entity.CustomAbstractEntity;
import com.brunocesar.custom.repository.support.CustomGenericRepository;
#Transactional(readOnly = true)
public class CustomGenericRepositoryImpl<E extends CustomAbstractEntity, PK extends Serializable> extends
SimpleJpaRepository<E, PK> implements CustomGenericRepository<E, PK> {
private final EntityManager entityManager;
private final JpaEntityInformation<E, ?> entityInformation;
public CustomGenericRepositoryImpl(final JpaEntityInformation<E, ?> entityInformation,
final EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
this.entityInformation = entityInformation;
}
#Override
#Transactional
public void delete(final E entity) {
Assert.notNull(entity, "Entity object must not be null!");
entity.setChangeDate(Calendar.getInstance().getTime());
entity.setDeleted(true);
}
#Override
public List<E> findAll() {
return super.findAll(this.isRemoved());
}
#Override
public E findOne(final PK pk) {
return this.findOne(this.isRemovedByID(pk));
}
private Specification<E> isRemoved() {
return new Specification<E>() {
#Override
public Predicate toPredicate(final Root<E> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.isFalse(root.<Boolean> get("deleted"));
}
};
}
private Specification<E> isRemovedByID(final PK pk) {
return new Specification<E>() {
#Override
public Predicate toPredicate(Root<E> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
final Predicate id = cb.equal(root.get("id"), pk);
final Predicate hidden = cb.isFalse(root.<Boolean> get("deleted"));
return cb.and(id, hidden);
}
};
}
#Override
public EntityManager getEntityManager() {
return this.entityManager;
}
protected JpaEntityInformation<E, ?> getEntityInformation() {
return this.entityInformation;
}
}
You will need too a custom factory bean to setup your custom repositories. Look like this:
package com.brunocesar.custom.repository.support.factory;
import java.io.Serializable;
import javax.persistence.EntityManager;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import com.brunocesar.custom.repository.support.impl.CustomGenericRepositoryImpl;
public class CustomGenericRepositoryFactoryBean<T extends JpaRepository<S, ID>, S, ID extends Serializable> extends
JpaRepositoryFactoryBean<T, S, ID> {
#Override
protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) {
return new RepositoryFactory(entityManager);
}
private static class RepositoryFactory extends JpaRepositoryFactory {
public RepositoryFactory(final EntityManager entityManager) {
super(entityManager);
}
#Override
#SuppressWarnings({"unchecked", "rawtypes"})
protected <T, ID extends Serializable> SimpleJpaRepository<?, ?> getTargetRepository(
final RepositoryMetadata metadata, final EntityManager entityManager) {
final JpaEntityInformation<?, Serializable> entityInformation = this.getEntityInformation(metadata
.getDomainType());
return new CustomGenericRepositoryImpl(entityInformation, entityManager);
}
#Override
protected Class<?> getRepositoryBaseClass(final RepositoryMetadata metadata) {
return CustomGenericRepositoryImpl.class;
}
}
}
And finally, the application context configuration.
Common repositories configuration look like this:
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:dataSource-ref="dataSource" p:jpaProperties-ref="jpaProperties" p:jpaVendorAdapter-ref="jpaVendorAdapter"/>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" />
<jpa:repositories base-package="com.brunocesar.repository"
transaction-manager-ref="transactionManager" entity-manager-factory-ref="entityManagerFactory" />
And custom, like this (you can or not use separeted EMF and transaction manager):
<bean id="entityManagerFactoryCustom" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:dataSource-ref="dataSource" p:jpaProperties-ref="jpaProperties" p:jpaVendorAdapter-ref="jpaVendorAdapter"/>
<bean id="transactionManagerCustom" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactoryCustom" />
<jpa:repositories base-package="com.brunocesar.custom.repository,com.brunocesar.custom.repository.support"
factory-class="com.brunocesar.custom.repository.support.factory.CustomGenericRepositoryFactoryBean"
transaction-manager-ref="transactionManagerCustom" entity-manager-factory-ref="entityManagerFactoryCustom" />
Example 1, using JpaRepository:
package com.brunocesar.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.brunocesar.entity.CommonEntity;
#Repository
public interface CommonRepository extends JpaRepository<CommonEntity, Long> {
}
Example 2, using custom repository:
package com.brunocesar.custom.repository;
import org.springframework.stereotype.Repository;
import com.brunocesar.custom.entity.CustomEntity;
import com.brunocesar.custom.repository.support.CustomGenericRepository;
#Repository
public interface CustomRepository extends CustomGenericRepository<CustomEntity, Long> {
}
This is part of what I usually do. If you need, I can create a basic application as an example.
I am in need of a custom constraint which validates that a value is unique. I have been searching the internet for a long time to find a good example. The one I found is using hibernate template which is not recommended anymore so I was wondering if anyone had the expertise to "translate" this code so that it doesn't use the templates. I have tried to do it myself but I don't understand what the getter and setter are for. Here is the code which I got from this helpful guide.
package com.omgo.security.domain.validator;
import java.io.Serializable;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateTemplate;
public class UniqueIDValidator implements ConstraintValidator<Unique, Serializable> {
HibernateTemplate hibernateTemplate;
private Class<?> entityClass;
private String uniqueField;
public void initialize(Unique unique) {
entityClass = unique.entity();
uniqueField = unique.property();
}
public boolean isValid(Serializable property, ConstraintValidatorContext cvContext) {
String query = String.format("from %s where %s = ? ", entityClass.getName(), uniqueField);
List<?> list = hibernateTemplate.find(query, property);
return list != null && list.size() > 0;
}
public SessionFactory getSessionFactory() {
return hibernateTemplate != null ? hibernateTemplate.getSessionFactory() : null;
}
#Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
}
Thank you for your help.
/D
Update
After changing the code I am getting this error:
org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here
So I'm thinking that maybe I haven't injected the SessionFactory properly?
UniqueIdValidator.java:
package testapp.mis.validator;
import java.io.Serializable;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional; // Added in update 2
public class UniqueIdValidator implements ConstraintValidator<Unique, Serializable> {
#Autowired(required=true)
private SessionFactory sessionFactory;
private Class<?> entityClass;
private String uniqueField;
public void initialize(Unique unique) {
entityClass = unique.entity();
uniqueField = unique.property();
}
#Transactional //Added in update 2 and validation works after that
public boolean isValid(Serializable property, ConstraintValidatorContext cvContext) {
Criteria crit = sessionFactory.getCurrentSession().createCriteria(entityClass);
crit.add(Restrictions.eq(uniqueField, property));
return crit.list().isEmpty();
}
Update 2
I got it working by adding #Transactional in the code.
That uses plain HQL, you can achieve something simpler by using the Criteria API, something like:
public boolean isValid(Serializable property, ConstraintValidatorContext cvContext) {
Criteria crit = sessionFactory.getCurrentSession().createCriteria(entityClass);
crit.add(Restrictions.eq(uniqueField, property));
return crit.list().isEmpty();
}
That should check for uniqueness.
Instead of injecting the hibernate template, just inject the SessionFactory which you also should have configured in your spring context.
Hope you find it useful!