【问题标题】:Envers: wrong audit table on a Spring MVC projectEnvers:Spring MVC 项目上的错误审计表
【发布时间】:2014-10-30 15:55:15
【问题描述】:

我将Spring Data JPA 1.6.4Hibernate 4.3.6.Final + envers 一起使用到Spring MVC 4.0.7Spring Security 3.2.5 保护的Web 应用程序中。 Web 应用程序部署在 Tomcat 7.0.52 上 Web 容器,配置有 JNDI 数据源:

<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 Server 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 上工作。问题是修订表不一致然后我有一个休眠断言失败。

似乎只有在 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()。也许是这个原因?

我无法理解这种行为!任何建议将不胜感激!

【问题讨论】:

  • 您对用户的存储和存储似乎有问题。您最终可能会将公司添加到另一个用户,而旧用户仍然是旧公司的一部分。这是因为您使用的是分离的实例,也可能是附加的实例。这可能会带来问题。
  • 谢谢@M.Deinum!所以在您看来问题可能出在 CustomerSecurityService 上?在这里,我检查客户(存储在 SecurityContextHolder 中)是否是实体的所有者,以及他是否可以更新它。要检查我从数据库中获取的实体(实体来自视图中的更新,我不会将不可更新的字段映射到隐藏的字段)。然后我比较它们。如果我理解您的评论,您是说在进行保存之前查询数据库中的实体以便比较它是问题所在,我是对的吗?

标签: java spring hibernate spring-mvc spring-data


【解决方案1】:

经过一些麻烦,我找到了解决方案:

mysql 5.5 指南状态:

InnoDB 使用内存中的自动增量计数器,只要服务器 运行。当服务器停止并重新启动时,InnoDB 重新初始化 每个表的第一个 INSERT 表的计数器,如 前面已经介绍过了。

这对我来说是个大问题。我正在使用 envers 来保持实体审计。我得到的错误与我删除的“最后一行”一样多。

假设我开始将数据插入一个空表。假设插入 10 行。然后假设删除最后 8 个。在我的数据库中,我将得到 2 个实体,id 分别为 1 和 2。在审计表中,我将拥有所有 10 个实体,id 从 1 到 10,id 从 3 到 10 的实体将有 2 个操作:创建操作和删除操作。

自动增量计数器现在设置为 11。重新启动 mysql 服务自动增量计数器变为 3。因此,如果我插入一个新实体,它将以 id 3 保存。但在审计表中也有一个实体id = 3。该实体已被标记为已创建和已删除。这会导致更新/删除操作期间的断言失败,因为 envers 无法处理这种不一致的状态。

【讨论】:

  • 您说您找到了解决方案,但是... 它是什么?我现在面临着类似的问题:-/
  • 是的.. 解决方案不是正确的词。我找到了为什么会发生这种情况。一个解决方案可能是执行一个脚本,将每个表的自动增量基值 eq 设置为相关审计表的 max+1。
  • @Kazbeel 显然只有在 mysql 服务重启时才需要执行此操作。希望对您有所帮助!
  • 解决方法请看这里的答案:stackoverflow.com/a/61157228/1785788
猜你喜欢
  • 2021-08-11
  • 2018-05-23
  • 2010-09-24
  • 1970-01-01
  • 2019-04-06
  • 2020-09-22
  • 2015-03-14
  • 1970-01-01
  • 2016-05-13
相关资源
最近更新 更多