2014-09-05 3 views
2

Я использую Spring Data JPA 1.6.4 с Hibernate 4.3.6.Final + envers в веб-приложении Spring MVC 4.0.7, защищенном Spring Security 3.2.5. Веб-приложение развернуто на Tomcat 7.0.52 веб-контейнер, выполненный с 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 действительно работал над идентификатором объекта 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. Я всегда использую абстракцию более высокого уровня с репозиториями Spring Data. Иногда мне нужно расширить интерфейс JpaRepository, чтобы позвонить saveAndFlush() или явно позвонить flush(). Может быть, причина?

Я не могу понять это поведение! Любое предложение будет оценено!

+1

Ваш camparison и хранение пользователя кажется подозрительным. Вы можете в конечном итоге добавить компанию к другому пользователю, в то время как старый пользователь все еще является частью старой компании. Это связано с тем, что вы работаете с отдельными экземплярами и, возможно, с прикрепленными экземплярами. Это может вызвать проблемы. –

+0

Спасибо @ M.Deinum! Итак, по вашему мнению, проблема может быть в CustomerSecurityService? Здесь я проверяю, является ли Клиент (хранится в SecurityContextHolder) владельцем объекта, и если он может его обновить. Чтобы проверить объект, я получаю его из db (сущность исходит из обновления в представлении, и я не привязываю обновляемые поля к hiddden). Затем я сравниваю их. Если я понимаю ваш комментарий, вы говорите, что запрос для объекта в db, чтобы сравнить его, прежде чем приступить к спасению, является проблемой, я прав? – gipinani

+0

@ M.Deinum вы ссылаетесь на эту проблему https://hibernate.atlassian.net/plugins/servlet/mobile#issue/HHH-6361? – gipinani

ответ

0

После некоторых проблем я нашел решение:

MySQL 5.5 руководство заявляет:

InnoDB использует в памяти автоинкрементируемого счетчик до тех пор, как сервер пробегов. Когда сервер останавливается и перезапускается, InnoDB повторно инициализирует счетчик для каждой таблицы для первого INSERT для таблицы, как , описанный ранее.

Это большая проблема для меня. Я использую envers для аудита сущностей. Я получаю столько ошибок, сколько «последних строк», которые я удаляю.

Предположим, что я начинаю вставлять данные в пустой стол. Предположим, чтобы вставить 10 строк. Тогда предположим, что мы удалим последний 8. В моем db у меня будет как результат 2 объекта, с идентификаторами 1 и 2 соответственно.В таблице аудита у меня будут все 10 сущностей с идентификатором от 1 до 10, а объекты с идентификатором от 3 до 10 будут иметь 2 действия: создать действие и удалить действие.

счетчик автоинкремента теперь настроен на 11. Перезапуск счетчика автоинкремента службы mysql равен 3. Поэтому, если я вставляю новый объект, он будет сохранен с идентификатором 3. Но в таблице аудита есть также объект с id = 3. Этот объект уже отмечен как созданный и удаленный. Это приводит к ошибке утверждения во время действия update/delete, потому что envers не могут справиться с этим несогласованным состоянием.

+1

Вы сказали, что нашли решение, но ... Что это? Я столкнулся с аналогичной проблемой на данный момент: -/ – kazbeel

+1

Да .. решение - это не правильное слово. Я нашел, почему это происходит. Решением может быть выполнение сценариев, которые устанавливают для каждого базового значения байта таблицы eq равным max + 1 относительной таблицы аудита. – gipinani

+0

@ Kazbeel, очевидно, вам нужна эта операция только при перезапуске службы mysql .. надеюсь, что это поможет! – gipinani

Смежные вопросы