2014-09-05 27 views
2

我正在使用Spring Data JPA 1.6.4Hibernate 4.3.6.Final + enversSpring MVC 4.0.7網絡應用程序保護爲Spring Security 3.2.5。 Web應用程序部署在一個Tomcat 7.0.52 Web容器,用JNDI數據源配置:Envers:Spring MVC項目上的錯誤審計表

<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" /> 

數據庫的MySQL服務器版本5.5上運行,並有一個InnoDB架構。

我有審計表Customers_H一個奇怪的現象:發現有時審計表是在一個錯誤的方式通過envers填充。大多數情況下,工作正常。

我不爲什麼,當它發生的主意,但我作爲插入的結果的修正表如下所示:

ID  ACTION TYPE  REV END  USER 
23    0    256   U1 
23    2    NULL  NULL 
23    0    NULL   U2 

奇怪的是,U1是一個實體的擁有者id = 6(不是id = 23的實體),而U2確實在處理實體ID 23.問題是修訂表不一致,然後我有一個Hibernate ASSERTION FAILURE。

看來,它應該是確定只有envers創造了第三排。但爲什麼它也創建了第一個(帶有動作CREATE)和第二個(帶有動作DELETE)?

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. 

這禁止用戶更新實體。

我的問題是調查如何發生這種情況!

這裏是Customer域:

@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 
} 

而且Company域具有反向映射到Customer

@OneToMany(mappedBy="company", cascade=CascadeType.REMOVE) 
Set<Customer> customers = new HashSet<Customer>(); 

這裏是AbstractDomain類:

@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; 
     } 
    } 
} 

這裏是CustomerService

@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); 
    } 

    .... 
} 

這裏是我的CustomerRepository

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

} 

這裏我用做安全檢查中@PreAuthorize@PostAuthorize註釋中CustomerService方法的服務:

@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; 
    } 
} 

SecurityUtils

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; 
    } 
} 

最後XML JPA配置:

<?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> 

在項目我有大約50域類,其中一些帶有遺傳SINGLE_TABLE

該應用程序現在被少數用戶使用,但沒有同時連接。所以我可以說只有一個用戶在給定的時間使用我的應用程序。

我也不明白我怎麼能不安全地使用Session。我從不直接使用Hibernate Session。我總是對Spring Data Repositories使用更高級別的抽象。有時我需要擴展JpaRepository接口,以便撥打saveAndFlush()或明確呼叫flush()。也許是原因?

我無法理解這種行爲!任何建議將不勝感激!

+1

你的比較和存儲的用戶似乎魚腥味。您可能最終將公司添加到另一個用戶,而舊用戶仍然是舊公司的一部分。這是由於您正在使用分離的實例和可能的附加實例。這可能會帶來問題。 – 2014-09-12 14:04:25

+0

謝謝@ M.Deinum!所以你認爲這個問題可能在CustomerSecurityService中?在這裏我檢查客戶(存儲在SecurityContextHolder中)是否爲實體的所有者,以及他是否可以更新它。要檢查實體,我從數據庫中獲取它(實體來自視圖中的更新,並且我沒有將不可更新的字段映射到隱藏)。然後我比較它們。如果我理解你的評論,你說在查詢數據庫中的實體以比較它,然後繼續保存是問題,我是對的? – gipinani 2014-09-12 16:14:56

+0

@ M.Deinum你是指這個問題https://hibernate.atlassian.net/plugins/servlet/mobile#issue/HHH-6361? – gipinani 2014-09-15 19:23:29

回答

0

一些麻煩後,我已經找到了解決辦法:

的MySQL 5.5指南指出:只要

InnoDB使用在內存中的自動增長計數器作爲服務器 運行。當服務器停止並重新啓動時,InnoDB將 計數器重新初始化爲表中第一個INSERT的每個表的計數器,如前面描述的 。

這對我來說是個大問題。我正在使用envers來保持實體審計。我得到的錯誤與我刪除的許多「最後一行」一樣多。

假設我開始將數據插入空表中。假設要插入10行。然後假設刪除最後的8.在我的分貝,我將有結果2實體,分別爲1和2。在審計表中,我將擁有所有10個實體,ID爲1到10,ID爲3到​​10的實體將有2個操作:創建操作和刪除操作。

自動遞增計數器現在設置爲11.重新啓動mysql服務自動遞增計數器將變爲3.所以,如果我插入一個新的實體,它將以id 3保存。但是在審計表中,還有一個實體與id = 3。該實體已被標記爲創建和刪除。它在更新/刪除操作期間導致斷言失敗,因爲envers無法處理這種不一致的狀態。

+1

你說你找到了解決方案,但是......它是什麼?我現在正面臨類似的問題: -/ – kazbeel 2015-01-19 09:18:04

+1

是的..解決方案不是適當的詞彙。我找到了爲什麼發生這種情況。解決方案可以是執行一個腳本,該腳本爲每個表自動增量基值eq設置爲相對審計表的max + 1。 – gipinani 2015-01-19 09:22:25

+0

@Kazbeel顯然你只有在重啓mysql服務時才需要這個操作..希望這有助於! – gipinani 2015-01-19 12:55:44