Strange behaviour when integrate Spring JPA Data and Spring Cache - spring

When I integrate Spring JPA Data and Spring Cache, there is a strange behaviour I can't explain.
I am using Spring Boot to setup my demo project. The code is below.
My config bean:
#Configuration
public class AppConfig {
#Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("Person");
}
}
My entity bean.
#Entity
public class Person implements Serializable {
private static final long serialVersionUID = 2263555241909370718L;
#Id
#GeneratedValue
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
My JPA interface. I overwrite some methods from JpaRepository and add #cachable annotation.
public interface PersonRepository extends JpaRepository<Person, Long> {
#Override
#CacheEvict(value = "Person", allEntries = true)
public void delete(Long id);
#Cacheable("Person")
public Person findByName(String name);
#Override
#Query("select p from Person p where p.id = :id + 1L")
#Cacheable("Person")
public Person findOne(#Param("id") Long id);
}
My unit test class
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = SpringDataDemoApplication.class)
public class SpringDataDemoApplicationTests {
#Autowired
private PersonRepository personRepository;
#Autowired
private CacheManager cacheManager;
#Before
public void setup() {
Person p1 = new Person();
p1.setName("Chris");
personRepository.save(p1);
}
#Test
public void test2() {
JpaRepository<Person, Long> jpaRepository = personRepository;
Person p = personRepository.findOne(0L);
Assert.assertNotNull(p);
p = personRepository.findOne(0L);
Assert.assertNotNull(p);
System.err.println("---------------------------------------");
p = jpaRepository.findOne(0L);
Assert.assertNotNull(p);
p = jpaRepository.findOne(0L);
Assert.assertNotNull(p);
}
}
The output is very strange.
Hibernate: insert into person (id, name) values (default, ?)
Hibernate: select person0_.id as id1_0_, person0_.name as name2_0_ from person person0_ where person0_.id=?+1
---------------------------------------
Hibernate: select person0_.id as id1_0_, person0_.name as name2_0_ from person person0_ where person0_.id=?+1
Hibernate: select person0_.id as id1_0_, person0_.name as name2_0_ from person person0_ where person0_.id=?+1
It should only print out one sql statement for my expect. The jpaRepository.findOne(0L) doesn't use the cache object.
The cache annotation is not working after I assign the PersonRepository interface to its parent interface, JpaRepository.
These 2 variables are exactly point to the same reference, even it a proxy object. Why call the same reference's method causing 2 difference result?
I also notice that the #Query annotation is working well. Both the JpaRepository and PersonRepository references use the customized SQL.
I guess there maybe some differences between how Spring Cache and Spring JPA Data generate the proxy advisor. Is that possible a bug here?

Add #EnableCaching to your configuration:
#EnableCaching
#Configuration
public class AppConfig {
#Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("Person");
}
}
Declaring the cache annotations does not automatically trigger their actions per se, you should declaratively enable the Caching behavior by using EnableCaching annotation. One of the advantages of this approach is you can disable it by removing only one configuration line rather than all the annotations in your code.

I think I find the reason why this happened. I add some aop code to track what method be called in the repository.
#Aspect
#Configuration
public class AppConfig {
#Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
#AfterReturning("execution(* org..*Repository.*(..))")
public void logServiceAccess(JoinPoint joinPoint) {
Arrays.asList(joinPoint.getTarget().getClass().getMethods()) //
.stream() //
.filter(m -> m.getName().startsWith("findOne")) //
.forEach(m -> System.err.println(m));
System.err.println("Completed: " + joinPoint);
}
}
The output is
public final java.lang.Object com.sun.proxy.$Proxy66.findOne(java.io.Serializable)
public final org.chris.demo.domain.Person com.sun.proxy.$Proxy66.findOne(java.lang.Long)
Spring container proxy 2 findOne methods with different arguments. I think that caused by generic code as all generic code will be removed after compile.
When I use the parent interface call the method, it call the public final java.lang.Object com.sun.proxy.$Proxy66.findOne(java.io.Serializable)
method. And on that method, there is not way to add #cacheable annotation.
I don't know whether there is way to focus Spring container generate only one findOne method when there is a sub-interface overwrite the method by generic programming.

Related

AOP. Hibernate EventListeners

I am trying to use Hibernate event listeners with AOP. My code:
#EntityListeners(MyEntityListener.class)
#NoArgsConstructor
#Getter
#Setter
#ToString
#Entity
#Table(name = "some_table", uniqueConstraints = #UniqueConstraints(columnNames = "code", name = "uc_some_table_code")
public class MyEntity {
#Column(name = "code", nullable = false)
private String code;
#Column(name = "description")
private String description;
}
public class MyEntityListener {
#AnnotationForAudit(name = "EVENT1")
#PostPersist
private postCreate(MyEntity myEntity) {}
#AnnotationForAudit(name= "EVENT2")
#PreUpdate
private void preUpdate(MyEntity myEntity) {}
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface AnnotationForAudit {
String name() default "";
}
#AllArgsConstructor
#Service
public class MyEntityServiceImpl implements MyEntityService {
private final MyEntityRepository repository;
private final MyEntityMapper mapper;
#Override
#Transactional
public MyEntity create(MyEntityModel model) {
var entity = mapper.fromModel(model);
return mapper.toModel(repository.save(entity));
}
}
#Aspect
#Component
public class AuditEventAspect {
#Pointcut("#annotation(annotationForAudit)")
public void callAnnotatedmethod(AnnotationForAudit annotation) {}
#before(value = "callAnnotatedmethod(annotation)", argnames="joinPoint,annotation")
public void beforeCallAnnotatedmethod(JoinPoint joinPoint, AnnotationForAudit annotation) {
System.out.println("do something...");
}
}
I want to intercept the Hibernate event when saving or editing an entity, but this configuration does not work. I explicitly declared the listener as a bean in application configuration - and no, it does not work anyway. But if I inject it into the service and call it methods, then the aspect works.
Background: Spring AOP is based on JDK dynamic proxies for interfaces or CGLIB proxies for class types. Proxies rely on sub-classing, and in Java subclasses or other classes other than the declaring class do not have access to private methods. Therefore, you also cannot intercept private methods using proxy-based AOP technologies.
Solution: Make your listener method public, then it works.
Alternative: If your really need to intercept private methods, you have to switch to a more powerful AOP technology like AspectJ. AspectJ does not work proxy-based, so there you can intercept private methods.

Unable to enable hibernate filter in spring EntityManager using spring aop

I'm trying to enable a hibernate filter through spring EntityManager by tyring to pointcut a service implementation method annotated with custom annotation #TenantAware and add #Around advise to that method. I want to enable custom filter which adds a differentiator where tenant_id = :tenantId on all entities that extend a BaseEntity. Hence I created the custom annotation and using it on #Transactional methods where it is required. It is intercepting the method successfully but the variable values when I log them are showing up empty and neither is the filter being set.
The project is a spring-boot 2 application and I'm using spring aop for creating the aspect. I'm using Hibernate 5 as the JPA implementation provider.
Load time weaving of the SimpleJpaRepository.class is not possible since it does not expose a noarg constructor.
This is my TenantFilterAdvisor class.
package org.foo.bar.advisors;
#Aspect
#Slf4j
#Component
public class TenantFilterAdvisor {
#PersistenceContext
private EntityManager entityManager;
public TenantFilterAdvisor() {
log.debug("###########################################################################");
log.debug("###################### Tenant Advisor Filter Started ######################");
log.debug("###########################################################################");
}
#Pointcut(value = "#annotation(org.foo.bar.TenantAware)")
public void methodAnnotatedWithTenantAware() {
}
#Pointcut(value = "execution(public * * (..))")
public void allPublicMethods() {
}
#Around(value = "methodAnnotatedWithTenantAware() && allPublicMethods()")
public Object enableTenantFilter(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.debug("###########################################################################");
log.debug("###################### Before enabling tenant filter ######################");
log.debug("###########################################################################");
if (null != entityManager) {
log.debug("Tenant filter name: ", "tenantFilter");
log.debug("Tenant filter property: ", "tenantId");
log.debug("Setting tenant id to: ", new Long(10));
Session session = entityManager.unwrap(Session.class);
Filter filter = session.enableFilter("tenantFilter");
filter.setParameter("tenantId", new Long(10));
}
Object result = proceedingJoinPoint.proceed();
// Code to disable the hibernate filter goes here.
log.debug("###########################################################################");
log.debug("###################### After disabling tenant filter ######################");
log.debug("###########################################################################");
return result;
}
}
The relevant part of service interface and implementation class is
public interface InventoryService {
Inventory getInventoryById(Long id);
}
#Service
public class InventoryServiceImpl implements InventoryService {
#Autowired
private InventoryRepository repo;
#Override
#Transactional
#TenantAware
public Inventory getInventoryById(Long id) {
LOG.debug("getInventoryById() called with: id = {}", id);
final Optional<Inventory> inventoryOp = repo.findById(id);
if (inventoryOp.isPresent()) {
return inventoryOp.get();
} else {
throw new InventoryNotFoundException(String.format(MESSAGE_INVENTORY_NOT_FOUND_FOR_ID, id));
}
}
}
The repository interface is
#Repository
#Transactional(readOnly = true)
public interface InventoryRepository extends BaseRepository<Inventory, Long> {
}
The BaseRepository interface extends JpaRepository.
And the aspect configuration class is
#Configuration
#ComponentScan(basePackages = {"org.foo.bar.advisors"})
#EnableAspectJAutoProxy(proxyTargetClass = true)
public class AOPConfig {
}
And finally the relevant MappedSuperClass which is inherited by other classes has the filter defined as
#Getter
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#MappedSuperclass
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#FilterDef(
name = "tenantFilter",
parameters = #ParamDef(name = "tenantId", type = "long")
)
#Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public abstract class BaseTransactionalEntity extends BaseEntity {
#Column(name = "tenant_id", nullable = false)
private Long tenantId;
}
Here is the cutom annotation class if you need the detail
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
#Inherited
public #interface TenantAware {
}
I need the hibernate filter to be enabled in session and disabled after the proceeding join point completes the execution. But it is not so. What am I missing?
As explained in the Hibernate Reference Guide filters only apply to entity queries not to direct fetching. In your code you are doing a direct fetch through findById which translates to entityManager.find and is thus a direct fetch.
You could override the Spring JPA repository and reimplement the findById to be an entity query instead of a direct fetch, to workaround this issue.
An alternative (and proven to be working) way without AOP is using TransactionManagerCustomizers:
#Configuration
public class HibernateFilterConfig {
#Bean
#ConditionalOnMissingBean
public PlatformTransactionManager transactionManager(
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
JpaTransactionManager transactionManager = new JpaTransactionManager() {
#Override
#NonNull
protected EntityManager createEntityManagerForTransaction() {
final EntityManager entityManager = super.createEntityManagerForTransaction();
Session session = entityManager.unwrap(Session.class);
session.enableFilter("tenantFilter").setParameter("tenantId", new Long(10));
return entityManager;
}
};
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
}

Entity can still be found after being deleted

I'm working with Spring Data Neo4j 4 and have the following user entity
#NodeEntity
public class User{
private Long id;
private String username;
//Getter, Setter
}
Using the Neo4j GraphRepository, i first create the user in one transaction and later delete him in a second transaction.
Working with the standalone Neo4j server on localhost:7474 i get no result when running "MATCH (n) return n" but when i run the findOne(Long id) method of the GraphRepository using the id of the User i just deleted, i get the user, i just deleted returned.
Is there some kind of behavior involved i don't understand?
Regards
Urr4
Edit:
My application class
#SpringBootApplication(scanBasePackages = {/.../})
#EnableNeo4jRepositories(basePackages = {/.../})
#EnableTransactionManagement
public class MyApplication extends Neo4jConfiguration {
public static void main(String[] args) {
SpringApplication.run(TSApplication.class, args);
}
#Override
#Bean
public Neo4jServer neo4jServer() {
return new RemoteServer(/.../);
}
#Override
#Bean
public SessionFactory getSessionFactory() {
return new SessionFactory("/.../);
}
}
After Michaels comment, i've googled a bit and added the following to my Controller:
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class)
Afterwards it worked - Thank you all :)

Issue with transactions in multiple services (Spring Framework/JTA): org.hibernate.ObjectDeletedException: deleted instance passed to merge

I receive the following exception during program execution:
org.hibernate.ObjectDeletedException: deleted instance passed to merge: [ns.entity.Category#<null>]; nested exception is java.lang.IllegalArgumentException: org.hibernate.ObjectDeletedException: deleted instance passed to merge: [ns.entity.Category#<null>]
The following code throws exception:
importer.foo();
Importer service:
#Service
#Transactional
public class Importer {
#Autowired
private UserService userService;
#Autowired
private CategoryService categoryService;
#Transactional
public void foo() {
User user = userService.findByLogin("max");
categoryService.delete(user.getCategories());
}
}
UserService (uses CrudRepository):
#Service
#Repository
#Transactional
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository repository;
#Override
#Transactional(readOnly = true)
public User findById(Long userId) {
return repository.findOne(userId);
}
}
CategoryService (uses CrudRepository):
#Service
#Repository
#Transactional
public class CategoryServiceImpl implements CategoryService {
#Autowired
private CategoryRepository repository;
#Override
#Transactional
public void delete(Set<Category> categories) {
repository.delete(categories);
}
}
The following code snippet in CategoryServiceImpl.delete() works without exception:
for (Category category : categories) {
Category newCat = findById(category.getCategoryId());
if (newCat != null) {
delete(newCat);
}
}
From what I understand two different transactions are used (one read only and one for deletion). Is it possible to re-use the transaction for all calls? Removing (readOnly = true) from UserServiceImpl.findById() does not help.
I thought that just one transaction should be used for all three methods (Importer.foo(), UserServiceImpl.findById(), CategoryServiceImpl.delete()) according to Spring documentation.

How to add cache feature in Spring Data JPA CRUDRepository

I want to add "Cacheable" annotation in findOne method, and evict the cache when delete or happen methods happened.
How can I do that ?
virsir, there is one more way if you use Spring Data JPA (using just interfaces). Here what I have done, genereic dao for similar structured entities:
public interface CachingDao<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
#Cacheable(value = "myCache")
T findOne(ID id);
#Cacheable(value = "myCache")
List<T> findAll();
#Cacheable(value = "myCache")
Page<T> findAll(Pageable pageable);
....
#CacheEvict(value = "myCache", allEntries = true)
<S extends T> S save(S entity);
....
#CacheEvict(value = "myCache", allEntries = true)
void delete(ID id);
}
I think basically #seven's answer is correct, but with 2 missing points:
We cannot define a generic interface, I'm afraid we have to declare every concrete interface separately since annotation cannot be inherited and we need to have different cache names for each repository.
save and delete should be CachePut, and findAll should be both Cacheable and CacheEvict
public interface CacheRepository extends CrudRepository<T, String> {
#Cacheable("cacheName")
T findOne(String name);
#Cacheable("cacheName")
#CacheEvict(value = "cacheName", allEntries = true)
Iterable<T> findAll();
#Override
#CachePut("cacheName")
T save(T entity);
#Override
#CacheEvict("cacheName")
void delete(String name);
}
Reference
I solved the this in the following way and its working fine
public interface BookRepositoryCustom {
Book findOne(Long id);
}
public class BookRepositoryImpl extends SimpleJpaRepository<Book,Long> implements BookRepositoryCustom {
#Inject
public BookRepositoryImpl(EntityManager entityManager) {
super(Book.class, entityManager);
}
#Cacheable(value = "books", key = "#id")
public Book findOne(Long id) {
return super.findOne(id);
}
}
public interface BookRepository extends JpaRepository<Book,Long>, BookRepositoryCustom {
}
Try provide MyCRUDRepository (an interface and an implementation) as explained here: Adding custom behaviour to all repositories. Then you can override and add annotations for these methods:
findOne(ID id)
delete(T entity)
delete(Iterable<? extends T> entities)
deleteAll()
delete(ID id)

Resources