Announcement Announcement Module
Collapse
No announcement yet.
Spring Data JPA - Infinite loop when updating, but not saving an auditable object Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Spring Data JPA - Infinite loop when updating, but not saving an auditable object

    I'm using hades 2.0.3 and hibernate 3.6.6. I've got a uni directional relationship from an entity that extends AbstractAuditable to my user entity.

    The behavior I'm seeing is that on @PrePersist everything works fine. But on @PreUpdate, the AuditingEntityListener gets stuck in an infinite loop until I get a StackOverFlow. Specifically, the act of retrieving the user from the userDAO triggers the @PreUpdate. The userDAO is just an interface extending GenericDAO

    Below is my auditaware implementation:

    Code:
    public class AuditorAwareBean implements AuditorAware<User> {
    
        //~ Instance fields ----------------------------------------------------------------------------
    
        @Resource private UserDAO UserDAO;
    
        //~ Methods ------------------------------------------------------------------------------------
    
        /** @see  org.synyx.hades.domain.auditing.AuditorAware#getCurrentAuditor() */
        @Override public User getCurrentAuditor() {
    
            String id = SecurityContextHolder.getContext().getAuthentication()
                    .getName()
                    ;
    
    // This sets off an infinte loop during update only
            return userDAO.findByUserId(id);
    
        }
    }

  • #2
    I have the same problem but..

    Hi,

    I got the sampe problem yesterday in my project when I use a declared query:

    Code:
            public SystemUser getCurrentAuditor() {
    		SecurityContext secureContext = SecurityContextHolder.getContext();
    		Authentication authentication = secureContext.getAuthentication();
    		Object principal = authentication.getPrincipal();  
      
    		UserDetails userDetails = (UserDetails) principal;  
    		
    		SystemUser systemUser = systemUserDAO.findByUsername(userDetails.getUsername()); 
    		
    		return systemUser;
    	}

    But when I changed it to findOne from the interface JPARepository I no longer got the StackOverflow

    Code:
      public SystemUser getCurrentAuditor() {
    		SecurityContext secureContext = SecurityContextHolder.getContext();
    		Authentication authentication = secureContext.getAuthentication();
    		Object principal = authentication.getPrincipal();  
      
    		UserDetails userDetails = (UserDetails) principal;  
    		
    		SystemUser systemUser = systemUserDAO.findOne(1); 
    		
    		return systemUser;
    	}
    Hope this helps in some way, I've tried both 1.1.0.M1 and 1.0.1.RELEASE

    //Evo 1

    Comment


    • #3
      Thanks for the tip. Unfortunately I'm still on hades so don't have access to that method. I'll dig through the spring-data-jpa source to see if anything jumps out at me as different between those two methods.

      Comment


      • #4
        The Hades' equivalent for findOne(…) is readByPrimaryKey(…) in case you might want to try that. I suspect a query being triggered to recursively trigger flushing and thus the invocation of the postUpdate callback. We have customers using finders in their AuditorAware implementation, so what OR mapper are you using? Might be an implementation detail of the one chosen.

        Comment


        • #5
          Hello Oliver,

          I'm using hibernate 3.6.6 and get the same behavior Evo1 described. When I change the method to readByPrimaryKey - everything works. I don't like that solution because I would thus have to store that value as the Principal rather then a userId.

          Comment


          • #6
            Here is some more info

            Thx for the reply,

            I'm using Hibernate. I slimmed it down to a smaller project that get the same error and post the relevant details for it.

            This is my dao-config:
            Code:
              
                <bean class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" id="dataSource">
                    <property name="driverClassName" value="${database.driverClassName}"/>
                    <property name="url" value="${database.url}"/>
                    <property name="username" value="${database.username}"/>
                    <property name="password" value="${database.password}"/>
                </bean>
            
                <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager">
                    <property name="entityManagerFactory" ref="entityManagerFactory"/>
                </bean>
               
                <tx:annotation-driven transaction-manager="transactionManager" />
            
                <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
                    <property name="dataSource" ref="dataSource"/>
                    <property name="jpaVendorAdapter">
            			<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            				<property name="showSql" value="true" />
            				<property name="generateDdl" value="true" />
            			</bean>
            		</property>
            		<property name="jpaProperties">
            			<props>
            				<prop key="hibernate.show_sql">true</prop>
            				<prop key="hibernate.format_sql">true</prop>
            				<prop key="hibernate.hbm2ddl.auto">create-drop</prop>
            			</props>
            		</property>
                </bean>
            This is my repository-config
            Code:
                    <repositories base-package="se.testproject" />
              
            	<beans:bean id="auditorAware" class="se.testproject.AuditorAwareImpl" >
            		<beans:constructor-arg ref="userAccountDAO">
            		</beans:constructor-arg>
            	</beans:bean>
            	
            	<auditing auditor-aware-ref="auditorAware"  />
            persistence.xml and orm.xml
            Code:
            <persistence ..>
            
                <persistence-unit name="persistenceUnit" transaction-type="RESOURCE_LOCAL">
                </persistence-unit>
            </persistence>
            
            
            <entity-mappings ..>
              <persistence-unit-metadata>
                 <persistence-unit-defaults>
                     <entity-listeners>
                          <entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener" />
                     </entity-listeners>
                  </persistence-unit-defaults>
               </persistence-unit-metadata>
            </entity-mappings>
            The entities
            Code:
            package se.testproject.useraccount;
            
            @Entity
            public class UserAccount {
            	private int userAccountId;
                    private String name;
               
            	@Id
                    @GeneratedValue(strategy=GenerationType.AUTO)
                    public Integer getUserAccountId() {
            		return userAccountId;
            	}
            	public void setUserAccountId(int userAccountId) {
            		this.userAccountId = userAccountId;
            	}
            
            	@NotNull
                    @Size(min = 1)
            	public String getName() {
            		return name;
            	}
            
            	public void setName(String name) {
            		this.name = name;
            	}
            }
            
            package se.testproject.country;
            
            @Entity
            public class Country implements Auditable<UserAccount, Integer>{
            	private Integer countryId;
                    private String name;
            
            	private UserAccount createdBy;
            	private DateTime createdDate;
            	private UserAccount lastModifiedBy;
            	private DateTime lastModifiedDate;
            	
                    @Id
                    @GeneratedValue(strategy=GenerationType.AUTO)
                    public Integer getCountryId() {
                	     return countryId;
                    }
            
                    public void setCountryId(Integer countryId) {
                	    	this.countryId = countryId;
                	}
            
                	@NotNull
                	@Size(min = 1)
            	public String getName() {
            		return name;
            	}
            
            	public void setName(String name) {
            		this.name = name;
            	}
            	
            	@OneToOne
            	@JoinColumn(updatable=false)
            	public UserAccount getCreatedBy() {
            		return createdBy;
            	}
            
            	@Override
            	public void setCreatedBy(UserAccount createdBy) {
            		this.createdBy = createdBy;
            
            	}
            
            	@Override
            	@Type(type="org.joda.time.contrib.hibernate.PersistentDateTime")
            	@Column(updatable=false)
            	public DateTime getCreatedDate() {
            		return null == createdDate ? null : new DateTime(createdDate);
            	}
            
            	@Override
            	public void setCreatedDate(DateTime createdDate) {
            		this.createdDate = null == createdDate ? null : new DateTime(createdDate);
            	}
            
            	@Override
            	@OneToOne
            	public UserAccount getLastModifiedBy() {
            		return lastModifiedBy;
            	}
            
            	@Override
            	public void setLastModifiedBy(UserAccount lastModifiedBy) {
            		this.lastModifiedBy = lastModifiedBy;
            	}
            
            	@Override
            	@Type(type="org.joda.time.contrib.hibernate.PersistentDateTime")
            	public DateTime getLastModifiedDate() {
            		return null == lastModifiedDate ? null : new DateTime(lastModifiedDate);
            	}
            
            	@Override
            	public void setLastModifiedDate(DateTime lastModifiedDate) {
            		this.lastModifiedDate = null == lastModifiedDate ? null : new DateTime(lastModifiedDate);
            	}
            	
            	@Override
            	@Transient
            	public boolean isNew() {
            		return countryId == null ? true : false;
            	}
            	
            	@Override
            	@Transient
            	public Integer getId() {
            		return getCountryId();
            	}
            }
            The dao
            Code:
            package se.testproject.useraccount;
            
            public interface UserAccountDAO extends JpaRepository<UserAccount, Integer> {
            	public UserAccount findByName(String name);
            }
            
            package se.testproject.country;
            
            public interface CountryDAO extends JpaRepository<Country, Integer> {
            }
            The auditor aware implementation
            Code:
            package se.testproject;
            
            public class AuditorAwareImpl implements AuditorAware<UserAccount> {
            	private UserAccountDAO userAccountDAO;
            	
            	public AuditorAwareImpl(UserAccountDAO userAccountDAO) {
            		this.userAccountDAO = userAccountDAO;
            	}
            	public UserAccount getCurrentAuditor() {
            		return userAccountDAO.findByName("test");
            	}
            
            }
            Stacktrace

            Code:
            	org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219)
            	org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99)
            	org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58)
            	org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:1185)
            	org.hibernate.impl.SessionImpl.list(SessionImpl.java:1261)
            	org.hibernate.impl.QueryImpl.list(QueryImpl.java:102)
            	org.hibernate.ejb.QueryImpl.getSingleResult(QueryImpl.java:274)
            	org.hibernate.ejb.criteria.CriteriaQueryCompiler$3.getSingleResult(CriteriaQueryCompiler.java:264)
            	org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:116)
            	org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:54)
            	org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:94)
            	org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:84)
            	org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:301)
            	org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
            	org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
            	org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
            	org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:155)
            	org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
            	org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
            	$Proxy33.findByName(Unknown Source)
            	se.senai.AuditorAwareImpl.getCurrentAuditor(AuditorAwareImpl.java:18)
            	se.senai.AuditorAwareImpl.getCurrentAuditor(AuditorAwareImpl.java:1)
            	sun.reflect.GeneratedMethodAccessor57.invoke(Unknown Source)
            	sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            	java.lang.reflect.Method.invoke(Method.java:601)
            	org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:309)
            	org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:196)
            	$Proxy28.getCurrentAuditor(Unknown Source)
            	org.springframework.data.jpa.domain.support.AuditingEntityListener.touchAuditor(AuditingEntityListener.java:150)
            	org.springframework.data.jpa.domain.support.AuditingEntityListener.touch(AuditingEntityListener.java:129)
            	org.springframework.data.jpa.domain.support.AuditingEntityListener.touchForUpdate(AuditingEntityListener.java:117)
            	sun.reflect.GeneratedMethodAccessor59.invoke(Unknown Source)
            	sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            	java.lang.reflect.Method.invoke(Method.java:601)
            	org.hibernate.ejb.event.ListenerCallback.invoke(ListenerCallback.java:45)
            	org.hibernate.ejb.event.EntityCallbackHandler.callback(EntityCallbackHandler.java:94)
            	org.hibernate.ejb.event.EntityCallbackHandler.preUpdate(EntityCallbackHandler.java:79)
            	org.hibernate.ejb.event.EJB3FlushEntityEventListener.invokeInterceptor(EJB3FlushEntityEventListener.java:61)
            	org.hibernate.event.def.DefaultFlushEntityEventListener.handleInterception(DefaultFlushEntityEventListener.java:349)
            	org.hibernate.event.def.DefaultFlushEntityEventListener.scheduleUpdate(DefaultFlushEntityEventListener.java:287)
            	org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:155)

            Comment


            • #7
              I'm also bitten badly by this exception. Here are my observation:

              Whenever I try to search for the current authenticated user using Spring Data JPA Repository, I get an infinite loop where the repository keeps searching for the record. It finds the record until and does this infinitely until an exception is thrown out.

              The problem occurs when you call the repository with all the following variants:
              Code:
              userRepository.findByUsername(username) <-- A custom method I added in the interface
              userRepository.findOne(id); <-- Built-in method
              userRepository.findAll(predicate) <-- Built-in method but you must pass a Predicate (i.e QueryDSL)
              userRepository.findOne(id) has an interesting anomaly. If we call it like the following:
              Code:
              userRepository.findOne(1L)
              A record is found! However, this is impractical because we have no access to the id and we can't set a static id there because we don't know the id in the first place.

              As a workaround, I tried delegating the id call from a service:
              Code:
              userRepository.findOne(userService.findId(SecurityContextHolder.getContext().getAuthentication().getName()))
              However, this triggers an infinite loop again. So we're back to square one.

              I had a feeling that it's the Spring Data JPA Repository that's causing to trigger an infinite loop. So I decided to call directly the EntityManager and it worked. Here's how I did it:

              Code:
              public class UserJpaAuditor implements AuditorAware<User> {
              
              	protected static Logger logger = Logger.getLogger("aop");
              	
              	@Autowired
              	private IUserService userService;
              	
              	@Override
              	public User getCurrentAuditor() {
              		logger.debug("getCurrentJpaAuditor: " + SecurityContextHolder.getContext().getAuthentication());
              		
              		User user = userService.getAuthenticatedUser();
              		return user;
              	}
              
              }
              Code:
              @Service
              @Transactional
              public class UserService implements IUserService {
              	@Override
              	public User getAuthenticatedUser() {
              		PathBuilder<User> entityPath = new PathBuilder<User>(User.class, "user");
              		StringPath path = entityPath.get(new StringPath("username"));
              		BooleanExpression hasUsername = path.eq(SecurityContextHolder.getContext().getAuthentication().getName());
              		BooleanBuilder builder = new BooleanBuilder();
              		builder.and(hasUsername);
              		
              		return entityService.find(builder, User.class);
              	}
              	
              }
              Code:
              @Service
              public class EntityService<T extends Serializable> implements IEntityService<T> {
              	@Autowired
              	private EntityManagerFactory emf;
              	
              	@Override
              	public T find(BooleanBuilder builder, Class<T> clazz) {
              		String variable = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
              		PathBuilder<T> entityPath = new PathBuilder<T>(clazz, variable);
              		EntityManager em = emf.createEntityManager();
              		EntityPath<T> path = entityPath;
              		
              		JPQLQuery result = new JPAQuery(em).from(path).where(builder);
              		
              		return result.uniqueResult(entityPath);
              	}
              }
              By the way I'm using QueryDSL dependency here for the BooleanBuilder which I highly recommend
              Code:
              import com.mysema.query.BooleanBuilder;
              import com.mysema.query.jpa.JPQLQuery;
              import com.mysema.query.jpa.impl.JPAQuery;
              import com.mysema.query.types.EntityPath;
              import com.mysema.query.types.path.PathBuilder;

              Comment


              • #8
                The code below was what I settled on. The magical line is setting the flushmode on the query before executing.

                Note, I was unable to use skram's code because I can't allow a dependency on QueryDSL at this moment.

                Code:
                	public User getCurrentAuditor() {
                
                		final String id = SecurityContextHolder.getContext()
                				.getAuthentication().getName();
                
                		User user = jpaTemplate.execute(new JpaCallback<User>() {
                
                			@Override
                			public User doInJpa(EntityManager em)
                					throws PersistenceException {
                				
                			
                				TypedQuery<User> query = em.createQuery(
                						"SELECT x FROM RascalUser x WHERE x.userId = '" + id+"'",
                						User.class);
                				query.setFlushMode(FlushModeType.COMMIT); 
                				User user=query.getSingleResult();
                				em.detach(user);
                				return user;
                			}
                		});
                
                		return user;

                Comment


                • #9
                  I'm also seeing this problem

                  Hi Oliver,

                  Has any further investigation been made into this issue? I came across it yesterday and have had a identical experience to the other guys here.

                  This problem is consistent but it doesn't happen every time I try to persist an Auditable entity. In my situation, the problem is triggered based on additional entity relationships - for example, my Auditable entity has a @ManyToMany relationship with another entity, but the recursive loop only arises when a join is present. If no join is present, the persist does not enter the recursive loop.

                  I'm going to try with one of the workarounds suggested but I do believe this warrants further investigation because it seems pretty convincing to me that this a bug in Spring Data JPA - most likely in the area of dynamic finders.

                  Thanks for any time you can spend on looking into this.

                  Cheers,

                  Andrew

                  Comment


                  • #10
                    Hi

                    im having this problem and have been trying to look for a solution for sometime. until now i haven't found a solution.

                    could someone advice how to fix this issue?

                    Thanks in advance

                    Comment


                    • #11
                      My solution is do not query User object from persistence layer instead of retrieve from spring-security directly:

                      Code:
                      	@Override
                      	public User getCurrentAuditor() {
                      		User auditor;
                      
                      		Authentication authentication = SecurityContextHolder.getContext()
                      				.getAuthentication();
                      		if (authentication != null) {
                      			Object principal = authentication.getPrincipal();
                      
                      			if (principal instanceof User) {
                      				auditor = (User) principal;
                      			} else {
                      				auditor = null;
                      				log.warn("The principal is not a user.");
                      			}
                      		} else {
                      			auditor = null;
                      		}
                      
                      		return auditor;
                      	}

                      Comment


                      • #12
                        I got the same issue and what I did was just change the propagation on the findByusername(username) method to Propagation.REQUIRES_NEW, I suspected that was a problem with the transactions, so I changed to use a new transaction and that worked well for me. I hope this can help.

                        Comment


                        • #13
                          Please, move your question to the StackOverflow - we are going to close this forum soon and rely on SO.
                          We need to clean this forum (old unanswered question) before close it.

                          Thanks for understanding

                          Comment

                          Working...
                          X