Hibernate template translation - java

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!

Related

Spring Boot datasource doesn't go to postProcessAfterInitialization method of BeanPostProcessor implementation

I am trying to make a proxy for my dataSource to supervise database queries, but the following is never invoked with bean data source object and bean instanceof DataSource is always false:
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Clock;
import java.util.List;
import javax.sql.DataSource;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.metrics.MetricsEndpoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.talan.dbmonitor.config.RabbitMQProperties;
import com.talan.dbmonitor.listeners.QueryListener;
import com.talan.dbmonitor.service.MapperService;
import lombok.extern.slf4j.Slf4j;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
#Configuration
#ConditionalOnProperty(value="enable.dbmonitor", havingValue="true", matchIfMissing= false)
#Slf4j
public class DatasourceProxyBeanPostProcessor implements BeanPostProcessor {
#Autowired
private AmqpTemplate amqpTemplate;
#Autowired
private Clock clock;
#Autowired
private RabbitMQProperties rabbitMQProperties;
#Autowired
private MetricsEndpoint metricsendpoint;
#Autowired
private ObjectMapper objectMapper;
#Autowired
private MapperService mapperService;
#Value("#{T(java.util.Arrays).asList('${dbmonitor.tables.ignore.list:}')}")
List<String> tablesIgnoreList;
#Override
public Object postProcessBeforeInitialization(final Object bean, final String beanName) {
return bean;
}
#Override
public Object postProcessAfterInitialization(final Object bean, final String beanName) {
System.out.println(beanName+" : "+bean.getClass().toString());
System.out.println((bean instanceof DataSource));
if (bean instanceof DataSource && !(bean instanceof ProxyDataSource)) {
ProxyFactory factory = new ProxyFactory(bean);
factory.setProxyTargetClass(true);
factory.addAdvice(new ProxyDataSourceInterceptor((DataSource) bean, amqpTemplate,
rabbitMQProperties, metricsendpoint, objectMapper, mapperService, tablesIgnoreList ,clock
));
return factory.getProxy() == null ? factory.getProxy() : bean;
}
return bean;
}
private static class ProxyDataSourceInterceptor implements MethodInterceptor {
private final DataSource dataSource;
public ProxyDataSourceInterceptor(final DataSource dataSource, AmqpTemplate amqpTemplate,
RabbitMQProperties rabbitMQProperties,MetricsEndpoint metricsendpoint,ObjectMapper objectMapper,MapperService mapperService,
List<String> tablesIgnoreList ,Clock clock
) {
super();
QueryListener qListener = new QueryListener(amqpTemplate, rabbitMQProperties,metricsendpoint,objectMapper, mapperService, tablesIgnoreList , clock
);
String datasourceUrl = null;
try (Connection datasourceConnection = dataSource.getConnection()){
datasourceUrl = datasourceConnection.getMetaData().getURL();
} catch (SQLException e) {
log.warn(e.getMessage());
}
finally {
this.dataSource = ProxyDataSourceBuilder.create(dataSource).name(datasourceUrl).countQuery()
.listener(qListener).build();
}
}
#Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method proxyMethod = ReflectionUtils.findMethod(dataSource.getClass(), invocation.getMethod().getName());
if (proxyMethod != null) {
return proxyMethod.invoke(dataSource, invocation.getArguments());
}
return invocation.proceed();
}
}
}
Datasource settings:
spring.datasource.platform = postgres
spring.datasource.url= jdbc:postgresql://${DB_SERVER_HOST:localhost}:${DB_SERVER_PORT:5432}/${DB_NAME:polaris_db}
spring.datasource.username= ${DB_USER:postgres}
spring.datasource.password= ${DB_PASSWORD:yosra}
spring.datasource.driverClassName= org.postgresql.Driver
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
Spring boot version: 2.3.7.RELEASE
The problem is this instruction in postProcessAfterInitialization is never verified, so my proxy is never invoked
bean instanceof DataSource && !(bean instanceof ProxyDataSource)
There are lots of ready-to-use solutions to inspect database queries. You don't have to write it yourself.
I don't know what is the ProxyDatasource in your domain model. But I think that you misunderstood the concept of the BeanPostProcessor. The postProcessAfterInitialization might receive a bean that has already been proxied by another BeanPostProccessor. Normally you should memoize the bean names on the postProcessBeforeInitialization stage. And then you can wrap them in postProcessAfterInitialization.

Dynamically update the #value annotated fields in spring

I am trying to dynamically update the #value annotated fields in my application.
First of all, this application has a custom property source, with source being a Map<Object, String>.
A timer is enabled to update the values after a minute interval.
package com.test.dynamic.config;
import java.util.Date;
import java.util.Map;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.StringUtils;
public class CustomPropertySorce extends EnumerablePropertySource<Map<String, Object>> {
public CustomPropertySorce(String name, Map<String, Object> source) {
super(name, source);
new java.util.Timer().schedule(new java.util.TimerTask() {
#Override
public void run() {
source.put("prop1", "yoyo-modified");
source.put("prop2", new Date().getTime());
System.out.println("Updated Source :" + source);
}
}, 60000);
}
#Override
public String[] getPropertyNames() {
// TODO Auto-generated method stub
return StringUtils.toStringArray(this.source.keySet());
}
#Override
public Object getProperty(String name) {
// TODO Auto-generated method stub
return this.source.get(name);
}
}
Initial values of source Map<String, Object> is supplied from the PropertySourceLocator. (This is not the real scenario, but I am trying to recreate the logic used here)
package com.test.dynamic.config;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
public class CustomPropertySourceLocator implements PropertySourceLocator {
#Override
public PropertySource<?> locate(Environment environment) {
Map<String, Object> source=new HashMap<String,Object>(){{put("prop1","yoyo");put("prop2",new Date().getTime());}};
return new CustomPropertySorce("custom_source",source);
}
}
RestController class where I inject these properties using #Value is given below.
environment.getProperty("prop1"); is supplying updated value, but not the #value annotated fields.
I also tried to inject a new property source updatedMap using the addFirst method of environment.propertySources() assuming that it will take precedence over the others. But that effort also went futile. any clue is much appreciated.
package com.test.dynamic.config.controller;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DataController {
#Value("${prop1}")
private String propertyOne;
#Value("${prop2}")
private Long propertyTwo;
#Autowired
private ConfigurableEnvironment environment;
#GetMapping("/p1")
private String getProp1() {
System.out.println("~~~~>"+environment.getPropertySources());
environment.getPropertySources().forEach(ps -> {
if(ps.containsProperty("prop1") || ps.containsProperty("prop2")) {
System.out.println("*******************************************************");
System.out.println(ps.getName());
System.out.println(ps.getProperty("prop1"));
System.out.println(ps.getProperty("prop2"));
System.out.println("*******************************************************");
}
});
// env.get
return propertyOne;
// return environment.getProperty("prop1");
}
#GetMapping("/p2")
private Long getProp2() {
System.out.println("~~~~>"+environment.getPropertySources());
// env.get
return propertyTwo;
// return environment.getProperty("prop1");
}
#GetMapping("/update")
public String updateProperty() {
Map<String, Object> updatedProperties = new HashMap<>();
updatedProperties.put("prop1", "Property one modified");
MapPropertySource mapPropSource = new MapPropertySource("updatedMap", updatedProperties);
environment.getPropertySources().addFirst(mapPropSource);
return environment.getPropertySources().toString();
}
}
If you think this is not the right way of injecting values to a RestController, please let me know. All possible alternate suggestions/best practices are accepted.
Thank you #flaxel. I used #RefreshScope to resolve this issue.
Posting the solution here if it helps someone with the same query.
In this particular case, I applied #RefreshScope on my Controller to refresh the bean with new values.
You can refer to this link before applying #RefreshScope to your bean.
It is the spring boot actuator that facilitates this refresh mechanism. So in order for this to work, you must have actuator in your classpath.
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: "${springboot_version}"
Then as discussed earlier, add RefreshScope to the bean that needs to be refreshed.
Finally, invoke the actuator/refresh endpoint to trigger the refresh.
If you want to programmatically do it, Autowire an instance of RefreshEndpoint class to your bean and invoke the refresh() method in it.
[Note: You don’t have to strictly follow this approach, but I am giving a clue that it can be Autowired]
#RefreshScope
#RestController
public class DataController {
#Value("${prop1}")
private String prop1;
#Autowired
private RefreshEndpoint refreshEndpoint;
#GetMapping("/p1")
public String getProp1(){
return prop1;
}
#getMappig("/refresh")
public void refresh(){
refreshEndpoint.refresh();
}
}
**************** MORE (if you are developing a library) ********************
What if you are developing a library and you have to get the RefreshEndpoint instance from the current ApplicationContext?
Simply Autowiring RefreshEndpoint may give you a null reference. Instead, you can get hold of the current ApplicationContext by the method given below. And use the ApplicationContext to get the RefreshEndpoint instance to invoke the refresh() method on it.
public class LocalApplicationContextFetcher implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
private static ApplicationContext ctx;
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ctx = applicationContext;
}
public static ApplicationContext getCtx() {
return ctx;
}
public static void refresh(){
ctx.getBean(RefreshEndpoint.class).refresh();
}
}
Finally, add this class to the spring.factories to get invoked by spring.
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.x.y.z.LocalApplicationContextFetcher

Java spring mvc using hibernate to do update

I started to develop APIs for iOS app using Java Spring MVC two months ago.
I'll explain my question with an example for clarification purpose.
Let's say I want to update a user's name.
My mySQL user table has columns: id, user_id, email, display_name.
My approach was:
define user:
User.java:
package bean;
public class User {
String displayName;
String email;
String userId;
getters/setters...
}
2.define a DAO:
UserDao.java:
package dao;
import bean.StandardResponse;
public interface UserDao {
public StandardResponse updateUserName(String user_id,String userName);
}
UserDaoImpl.java:
package implement;
import bean.User;
import common.DatabaseConnect;
public UserDaoImpl implements UserDao {
private DatabaseConnect dbConnect;
public UserDaoImpl(DatabaseConnect dbConnect) {
this.dbConnect = dbConnect;
}
public StandardResponse updateUserName(userId,userName) {
if ((userId == null || userId.isEmpty()) ||(userName == null || userName.isEmpty())) return new StandardResponse("Error","Parameters not set!");
String sql = null;
Statement smt = dbConnect.createDataBaseConnectResourse();
StandardResponse result = new StandardResponse("Error","Fail to update the record!");
sql = "update User set user_name="+userName+" where user_id='"+userId+"'";
int updateResult = 0;
try {
updateResult = smt.executeUpdate(sql);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
dbConnect.closeDatabaseConnectResource();
}
if (updateResult == 1) {
result = new StandardResponse("Success","The record has been updated!");
}
return result;
}
}
3.controller
import org.springframework.stereotype.Controller;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import bean.StandardResponse;
import bean.User;
import common.DatabaseConnect;
import common.SpringApplicationContext;
import dao.UserDao;
import implement.UserDAOImpl;
#Controller
#RestController
#RequestMapping("user")
public class UserController {
DatabaseConnect dbConnect = SpringApplicationContext.getApplicationContext().getBean("databaseConnect", DatabaseConnect.class);
UserDao uiObject = new UserDaoImpl(dbConnect);
#RequestMapping(value = "/updateUserName", method = RequestMethod.POST)
public StandardResponse updateUserName(HttpServletRequest request, HttpServletResponse reponses, Model model) {
StandardResponse srObject = uiObject.updateUserName(request.getparameter("userId"),request.getParameter("userName"));
return srObject;
}
}
I just put the crucial classes here. I believe you can understand what I am doing here. So if someone access the URL:****/user/updateUserName providing the userId and userName, he can update the user name of that record. It is functionalbe.
I used the same approach and finished the whole project. It is working. Then, I asked an experienced programmer to look at my code since I figured out all these based on my own understanding. I would like to know how did they do in industry. He gave me some valuable comments.
The whole structure is wrong. I shouldn't have logics in my dao. I should at least have dao, service and action layers. dao only handles database access, service handles all the logic and action handels communication and decide which service to call.
It is very bad approach to hand written SQL in the code. I should use Hibernate.
I should use control inverse and dependency injection.(I am not dealing with this in this post.)
So I started to update my codes to use Hibernate.
define user:
User.java:
package model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
#Entity
#Table(name="User")
public class User {
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
#Column(name = "user_id")
private String userId;
#Column(name = "user_name")
private String displayName;
#Column(name = "email")
private String emai;
all getters/setters...
}
dao:
UserDao.java:
package dao;
import model.User;
import java.util.List;
public interface UserDAO {
public void updateUser(User p);
other methods....
}
UserDaoImpl.java
package dao;
import model.User;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import model.User;
#Repository
public class PersonDAOImpl implements PersonDAO {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sf){
this.sessionFactory = sf;
}
#Override
public void updatePerson(Person p) {
Session session = this.sessionFactory.getCurrentSession();
session.update(p);
}
implementations of other methods....
}
service:
UserService.java:
package service;
import java.util.List;
import model.User;
public interface UserService {
public void updateUser(User p);
other methods....
}
UserServiceImpl.java:
package service;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import dao.UserDAO;
import model.User;
#Service
public class UserServiceImpl implements UserService {
private UserDAO userDAO;
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
#Override
#Transactional
public void updateUser(User p) {
this.userDAO.updateUser(p);
}
implementation of other methods...
}
Now, I just need to write a class to handle the request and call this updateUser service. The whole structure looks better than mine. However, the request won't gave me the whole user object. All I can get is user_id and user_name. I am also not allowed to put logics in dao. So I have to query the table first to get the whole user object and then update the user_name. Comparing to what I used to do, one SQL handles the update. Now using Hibernate, I need 1 query and 1 update.
I found a solution on StackOverflow that I can use HQL to do this in one step. But I though the purpose for using Hibernate is to free us from writing the SQL. If I need to write HQL, why don't I just keep using the writing SQL approach.
So is there a way to do update with Hibernate without query the table first in this structure? Or this is a trade-off for better structure?
I am sorry that I used a really long example for this question. But I couldn't think of other ways to explain the whole story clearly.
So is there a way to do update with Hibernate without query the table first in this structure? Or this is a trade-off for better structure?
Thereis NO tradeoff, you can do updates with HQL(Hibernate Query Language) as well like how you do in SQL.
You can look at the following code:
UserDAOImpl class:
#Repository
//you are calling this in ur code as PersonDAOImpl..change it to UserDAOImpl
public class UserDAOImpl implements UserDAO {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sf){
this.sessionFactory = sf;
}
#Override
public int updateUser(String userId, String userName) {
Session session = this.sessionFactory.getCurrentSession();
Query query = session.createQuery("update User set userName =:userName where userId = :userName ");
query.setString("userName", userName);
query.setString(userName, userName);
int result = query.executeUpdate();
return result;
}
}
UserServiceImpl class:
#Service
public class UserServiceImpl implements UserService {
private UserDAO userDAO;
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
#Override
#Transactional
public void updateUserName(String userId, String userName) {
if(userId !=null && userName != null) {
int result = this.userDAO.updateUser(userId, userName);
if(result==0) //userid not available {
//if userid is NOT available, what to do? check your requirement and handle properly
}
}
} else {
//throw exception
}
}
implementation of other methods...
}
Controller Class:
#Controller
#RestController
#RequestMapping("user")
public class UserController {
#Autowired
private UserService userService;
#RequestMapping(value = "/updateUserName", method = RequestMethod.POST)
public StandardResponse updateUserName(HttpServletRequest request, HttpServletResponse reponses, Model model) {
StandardResponse srObject = userService.updateUserName(request.getparameter("userId"),request.getParameter("userName"));
return srObject;
}
}

Spring mongo soft delete

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)

Spring Data - Overriding default methods for some repositories

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.

Categories

Resources