如何使用 Spring Data JPA 的审计功能指示历史 table 的生效日期?

How to indicate an effective date on historical table using audit capabilities of Spring Data JPA?

我有一个带有名称列的 Product table,我想在 ProductHistory table 中维护名称更改的历史记录。 (Product table 还有一些其他列。我不需要跟踪这些列的数据更改。)

当用户请求 运行 给定日期的报告时,我的应用程序将使用 ProductHistory table。报告需要显示截至报告日期的产品名称。

我正在使用 Spring Boot 1.5.2 并包括 Spring Boot 的 Starter Data JPA 依赖项。我计划使用 Hibernate Envers within Spring Data JPA to help accomplish my goal. I have read through this doc,但不确定如何在 ProductHistory 上指定生效日期。

我已经启用了 JPA 审计:

@Configuration
@EnableJpaRepositories("com.example.repository")
@EnableJpaAuditing
public class DatabaseConfig {
    //config items
}

使用 the doc 作为参考,我应该将实体设置为:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @LastModifiedDate
    private Date effectiveDateUtc;

    private String name;

    //other fields corresponding to othercols
 }

但是 effectiveDateUtc 不是 Product 的字段/列。如何表明 effectiveDateUtcProductHistory 的 属性?

更新

我一直无法找到一种方法来使用 Spring Data JPA 的审计功能来完成上述任务。不过,使用 Naros / Hibernate envers 发布的答案,我能够实现我的目标。

除了 Naros 发布的内容之外,我发现我还需要做些什么才能让它与我的 Spring 应用程序一起工作:

将所需的 jar 添加到我的 gradle 构建中:

compile('org.hibernate:hibernate-envers:5.2.10.Final')
compile('org.hibernate:hibernate-core:5.2.10.Final')
compile('joda-time:joda-time:2.1')

在数据库中创建了必要的架构。

我还设置了一个集成测试用例来验证我是否可以对修订数据进行报告类型查询。为此,我需要获取实体管理器并将其传递给审计 reader 工厂:

@RunWith(SpringRunner.class)
@SpringBootTest(classes=ProductDataApplication.class)
public class ProductRepositoryTest {

    @PersistenceContext
    private EntityManager entityManager;

    @Test
    @Transactional
    public void testEnversQuerying() {
        AuditReader ar = AuditReaderFactory.get(entityManager);
        Product revisions = ar.find(Product.class, 1L, new Date());
        // assert code
    }
}

正如 Naros 推测的那样,我还需要存储与修订相关联的用户 ID。我是这样做的:

public class UserRevisionListener implements RevisionListener {

    @Override
    public void newRevision(Object revisionEntity) {
        UserRevision revision = (UserRevision) revisionEntity;

        if (SecurityContextHolder.getContext().getAuthentication() == null ||
                SecurityContextHolder.getContext().getAuthentication().getName() == null) {
            revision.setModifiedBy("system");
        } else {
            revision.setModifiedBy(SecurityContextHolder.getContext().getAuthentication().getName());
        }

    }
} 

我知道您可能正在寻找与 Spring 相关的回复;但是,如果您直接使用 Hibernate Envers,我至少可以向您提供有关其工作原理的详细信息。

  1. 让我们进行实体设置:

    @Entity
    public class Product {
      @Id
      private Integer id;
      @Audited
      private String name;
      // other attributes
    }
    

    如您所见,我们基本上只是想审核产品名称。您在实体中映射的任何其他属性都将被忽略,因为 class 未使用 @Audited 注释,我们专门针对 属性 name

  2. 与您说明的 ProductHistory 模型相反,Envers 实际上会为您创建两个 table。第一个是 Product_AUD table,我们将在其中存储有关对您的 Product 实体所做更改的审计日志。

    Product_AUD table 将包含与此非常相似的列:

    REV INT NOT NULL         <- Foreign Key to the Revision Entity Table (REVINFO)
    PRODUCT_ID INT NOT NULL  <- Your PK from your Product Table
    NAME VARCHAR(255)        <- The name column annotated with @Audited
    REVTYPE INT              <- Revision type (0=INSERT,1=UPDATE,2=DELETE)
    

    下一个要建造的table将是REVINFO。默认情况下,此 table 仅由两列构成,如下所示:

    REV INT NOT NULL         <- The revision number (PK)
    REVTSTMP BIGINT          <- The time in milliseconds
    

    Envers 以这种方式构建模型的原因是因为在事务中修改多个实体并不罕见,而不是在所有实体审计中复制大量特定于修订的属性tables,我们以规范化的方式整合它,并将数据存储在 REVINFO table.

你可能想要的一件事似乎是你倾向于直接获得 Spring 数据是为了能够捕获用户以及可能在修订发生时与环境相关的其他属性.您很幸运,因为 Envers 也提供了一种快速且无痛的方法。

  1. 定义自定义修订实体。这可以通过简单地扩展我们为您提供的默认实现之一来完成,或者您可以简单地创建一个功能齐全的实体。我个人的偏好和建议是创建您自己的完整实现。该实体基本上使用自定义列扩展了默认值 REVINFO table。这是一个镜像默认 table 设置的示例,其中包含一个额外的列:

    @Entity
    @Table(name = "REVINFO")
    @RevisionEntity(MyRevisionEntityListener.class)
    public class MyRevisionEntity {
      @Id
      @RevisionNumber
      @Column(name = "REV", nullable = false, updatable = false)
      private Integer id;
      @RevisionTimestamp
      @Column(name = "REVTSTMP", nullable = false, updatable = false)
      private Long timestamp;
      @Column(name = "MODIFIED_BY", length = 100)
      private String modifiedBy;
      // getter/setters and perhaps other attributes
    }
    
  2. 定义用于在修订实体实例上填充自定义属性的修订实体侦听器:

    class MyRevisionEntityListener implements RevisionListener {
      @Override
      public void newRevision(Object revisionEntity) {
      }
    }
    

    在此侦听器中,您要做的是获取要注入到 Envers 修订实体中的任何上下文信息,并将其设置在您的修订实体上 class。

    将信息获取到此侦听器中的一种方法是将其存储在 ThreadLocal 变量中并在侦听器回调中访问该变量。如果您使用 Spring 安全性作为示例,它已经在其 SecurityContextHolder 线程本地容器中为您完成了此操作。如果您使用其他方式,您可以简单地以它们的实现为例。

正如我最初提到的,这确实没有直接解决您的 Spring 数据需求,但它确实说明了如果您决定直接使用 Envers 会发生什么。