0 Replies Latest reply on Sep 6, 2014 9:30 AM by Marco Serioli

    Envers generate wrong audit table

    Marco Serioli Newbie

      I'm using Spring Data JPA 1.6.4 with Hibernate 4.3.6.Final + envers into a Spring MVC 4 web application secured with Spring Security 3.2.5. The web application is deployed on a Tomcat 7.0.52 web container, configured with a JNDI datasource:

       

       

      [code]<Resource

                    name="jdbc/appDB"

                    auth="Container"

                    factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"

                    type="javax.sql.DataSource"

                    initialSize="4"

                    maxActive="8"

                    maxWait="10000"

                    maxIdle="8"

                    minIdle="4"

                    username="user"

                    password="password"

                    driverClassName="com.mysql.jdbc.Driver"

                    url="jdbc:mysql://ip/schema?zeroDateTimeBehavior=convertToNull"

                    testOnBorrow="true"

                    testWhileIdle="true"

                    validationQuery="select 1"

                    validationInterval="300000" />[/code]

      I have a strange behaviour with audit table Customers_H: noticed that sometimes audit tables are populated in a wrong way by envers,

       

       

      I have no idea of why and when It happens, but I have as a result a revision table like the following:

       

       

      [code]ID        ACTION TYPE        REV END        USER

      23                       0                        256          U1

      23                       2                        NULL        NULL

      23                       0                        NULL         U2[/code]

      The strange thing is that U1 is the owner of an entity with id = 6 (not of the entity with id = 23!), while U2 has really worked on entity ID 23. The problem is that the revision table is inconsinstent and then I have an Hibernate ASSERTION FAILURE

       

       

      ERROR org.hibernate.AssertionFailure - HHH000099: an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): java.lang.RuntimeException: Cannot update previous revision for entity Customer_H and id 23.

      This prohibit user to update the entity.

       

       

      My problem is to investigate how this can happen!

       

       

      Here is Customer domain:

       

       

      [code]@SuppressWarnings("serial")

      @Entity

      @Audited

      public class Customer extends AbstractDomain{

       

       

          @ManyToOne(optional=false)

          @JoinColumn(updatable=false, nullable=false)

          @JsonIgnore

          private Company company;

       

       

          @OneToMany(mappedBy="customer", cascade=CascadeType.REMOVE)

          private Set<Plant> plants = new HashSet<Plant>();

       

       

          @Enumerated(EnumType.STRING)

          @Column(nullable=false)

          private CustomerType customerType;

       

       

          private String code;

       

       

          // other basic fields + getter and settes

      }[/code]

      Here is AbstractDomain class:

       

       

      [code]@SuppressWarnings("serial")

      @MappedSuperclass

      @Audited

      public abstract class AbstractDomain implements Auditable<String, Long>, Serializable {

       

       

          @Id

          @GeneratedValue(strategy=GenerationType.IDENTITY)

          private Long id;  

       

       

          @Version

          @JsonIgnore

          private int version;

       

       

          @JsonIgnore

          @Column(updatable=false)

          private String createdBy;

       

       

          @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")

          @DateTimeFormat(iso=ISO.DATE_TIME)

          @JsonIgnore

          @Column(updatable=false)

          private DateTime createdDate;

       

       

          @JsonIgnore

          private String lastModifiedBy;

       

       

          @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")

          @DateTimeFormat(iso=ISO.DATE_TIME)

          @JsonIgnore

          private DateTime lastModifiedDate;

       

       

          public Long getId() {

              return id;

          }

          public void setId(Long id) {

              this.id = id;

          }

       

       

          public int getVersion() {

              return version;

          }

          public void setVersion(int version) {

              this.version = version;

          }

       

       

          @Override

          public String getCreatedBy() {

              return createdBy;

          }

          @Override

          public void setCreatedBy(String createdBy) {

              this.createdBy = createdBy;

          }

       

       

          @Override

          public DateTime getCreatedDate() {

              return createdDate;

          }

          @Override

          public void setCreatedDate(DateTime createdDate) {

              this.createdDate = createdDate;

          }

       

       

          @Override

          public String getLastModifiedBy() {

              return lastModifiedBy;

          }

          @Override

          public void setLastModifiedBy(String lastModifiedBy) {

              this.lastModifiedBy = lastModifiedBy;

          }

       

       

          @Override

          public DateTime getLastModifiedDate() {

              return lastModifiedDate;

          }

          @Override

          public void setLastModifiedDate(DateTime lastModifiedDate) {

              this.lastModifiedDate = lastModifiedDate;

          }

       

       

          @Transient

          @Override

          public final boolean isNew() {

              if (id == null) {

                  return true;

              } else {

                  return false;

              }

          } 

      }[/code]

      Here is CustomerService:

       

       

      [code]@Service

      @Repository

      @Transactional(readOnly=true)

      public class CustomerServiceImpl implements CustomerService{

       

       

          @Autowired

          private CustomerRepository customerRepository;

       

       

          @Override

          @PostAuthorize("@customerSecurityService.checkAuth(returnObject)")

          public Customer findById(Long id) {

              return customerRepository.findOne(id);

          }

       

       

          @Override

          @PreAuthorize("isAuthenticated()")

          @Transactional(readOnly=false)

          public Customer create(Customer entry) {

              entry.setCompany(SecurityUtils.getCustomer().getCompany());

              return customerRepository.save(entry);

          }

       

       

          @Override

          @PreAuthorize("@customerSecurityService.checkAuth(#entry)")

          @Transactional(readOnly=false)

          public Customer update(Customer entry) {

              return customerRepository.save(entry);

          }

       

       

          ....

      }

      Here is my CustomerRepository

       

       

      public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long>,  QueryDslPredicateExecutor<Customer> {

       

       

      }[/code]

      Here the Service I use to do security checks in @PreAuthorize @PostAuthorize annotations in CustomerService methods:

       

       

      [code]@Component

      @Transactional(readOnly=true)

      public class CustomerSecurityService {

       

       

          Logger LOGGER = LoggerFactory.getLogger(CustomerSecurityService.class);

       

       

          @Autowired

          private CustomerRepository customerRepository;

       

       

          public boolean checkAuth(Customer customer) {

              if(customer == null) {

                  LOGGER.error("customer NULL!");

                  return false;

              }

       

       

       

       

              if (customer.getId()==null) {

                  return true;

              }

       

       

       

       

              if (customer.getId()!=null) {

                  Customer dbCustomer = customerRepository.findOne(customer.getId());

       

       

                  if (dbCustomer.getCompany().getId().equals( SecurityUtils.getCustomer().getCompany().getId())){

                      return true;

                  }else {

                      return false;

                  }

              }

              return false;

          }

       

       

          public boolean checkPage(Page<Customer> pages) {

              for(Customer customer : pages.getContent()) {

                  Customer dbCustomer = customerRepository.findOne(customer.getId());

       

       

                  if (!dbCustomer.getCompany().getId().equals(SecurityUtils.getCustomer().getCompany().getId())){

                      return false;

                  }

              }

              return true;

          }

      }[/code]

      My SecurityUtils class

       

       

      [quote]public class SecurityUtils {

       

       

          private SecurityUtils(){}

       

       

          private static Logger LOGGER = LoggerFactory.getLogger(SecurityUtils.class);

       

       

          public static Customer getCustomer() {

              Customer customer = null;

              if (SecurityContextHolder.getContext().getAuthentication()!=null) {

                  customer = ((User)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getCustomer();

                  LOGGER.debug("Customer found: "+customer.getUserName());

              }else {

                  LOGGER.debug("Customer not bound.");

              }

              return customer;      

          }

       

       

       

       

          public static boolean isUserInRole(String role) {

              for (GrantedAuthority grantedAuthority : SecurityContextHolder.getContext().getAuthentication().getAuthorities()) {

                  if (grantedAuthority.getAuthority().equals(role)) {

                      return true;

                  }

              }

              return false;

          }

      }[/quote]

      And finally xml jpa configuration:

       

       

      [code]<?xml version="1.0" encoding="UTF-8"?>

      <beans xmlns="http://www.springframework.org/schema/beans"

          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

          xmlns:context="http://www.springframework.org/schema/context"

          xmlns:jpa="http://www.springframework.org/schema/data/jpa"

          xmlns:tx="http://www.springframework.org/schema/tx"

          xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

              http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd

              http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd

              http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

       

       

          <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">

              <property name="entityManagerFactory" ref="emf"/>

          </bean>

       

       

          <bean id="hibernateJpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />

       

       

          <tx:annotation-driven transaction-manager="transactionManager" />

       

       

          <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">

              <property name="dataSource" ref="dataSource" />

              <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter" />

       

       

              <property name="packagesToScan" value="scan.domain"/>

       

       

              <property name="persistenceUnitName" value="persistenceUnit"/>

              <property name="jpaProperties">

                  <props>

                      <prop key="hibernate.dialect">${hibernate.dialect}</prop>

                      <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>

                      <!--${hibernate.format_sql} -->

                      <prop key="hibernate.format_sql">true</prop>

                      <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>

                      <!-- ${hibernate.show_sql} -->

                      <prop key="hibernate.show_sql">false</prop>

       

       

                      <prop key="hibernate.connection.charSet">UTF-8</prop>

       

       

                      <prop key="hibernate.max_fetch_depth">3</prop>

                      <prop key="hibernate.jdbc.fetch_size">50</prop>

                      <prop key="hibernate.jdbc.batch_size">20</prop>

       

       

                      <prop key="jadira.usertype.databaseZone">jvm</prop>

       

       

                      <prop key="org.hibernate.envers.audit_table_suffix">_H</prop>

                      <prop key="org.hibernate.envers.revision_field_name">AUDIT_REVISION</prop>

                      <prop key="org.hibernate.envers.revision_type_field_name">ACTION_TYPE</prop>

                      <prop key="org.hibernate.envers.audit_strategy">org.hibernate.envers.strategy.ValidityAuditStrategy</prop>

                      <prop key="org.hibernate.envers.audit_strategy_validity_end_rev_field_name">AUDIT_REVISION_END</prop>

                      <prop key="org.hibernate.envers.audit_strategy_validity_store_revend_timestamp">True</prop>

                      <prop key="org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name">AUDIT_REVISION_END_TS</prop>             

                  </props>

              </property>

          </bean>

       

       

          <jpa:repositories base-package="scan.repository"

                            entity-manager-factory-ref="emf"

                            transaction-manager-ref="transactionManager"/>

       

       

          <jpa:auditing auditor-aware-ref="auditorAwareBean" />

       

       

          <bean id="auditorAwareBean" class="auditor.AuditorAwareBean"/>

       

       

      </beans>[/code]

      In the project I have about 50 domain classes, some of them with Inheritance Table_Per_Class.

       

       

      The application is now used by few users, that are not connected at the same time. So I can say that only one user is using my application at a given time.

       

       

      I also not understand how can I make an unsafe use of the Session. I never work directly with Hibernate Session. I always use a higher level abstraction with Spring Data Repositories. Sometimes I need to extends JpaRepository interface in order to call saveAndFlush() or explicitly call flush(). Maybe that the cause?

      I can't understand this behaviour! Any suggestion would be appreciated!!