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" />
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
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
}
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;
        }
    }   
}
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> {
}
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;
    }
}
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;
    }
}
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>
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!!