Hibernate Envers Tincture

Somehow they set me the task of doing auditing in our service. After reading a little, I decided to use Hibernate Envers, everything seems to work out of the box and without problems.

I want to tell you how this “VZHUH” works.

Here is a small test project, a couple of entities, controllers and standard CRUD. We are interested in our entities, it is over them that we need to hang annotations.

Preparation

@Data
@Entity
@Table(name = "message", schema = "forum")
public class Message {

    @Id
    @SequenceGenerator(name = "message_generator", sequenceName = "message_seq", schema = "forum", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "message_generator")
    private Long id;

    private String author;

    private String msg;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "forum_id")
    private Forum forum;

}
@Data
@Entity
@Table(name = "forum", schema = "forum")
public class Forum {

    @Id
    @SequenceGenerator(name = "forum_generator", sequenceName = "forum_seq", schema = "forum", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "forum_generator")
    private Long id;
    private String name;
    private String description;

    @OneToMany(mappedBy = "forum", fetch = FetchType.LAZY)
    private List<Message> messages;
}

Connection

We now decide to add auditing for any changes to these tables that have been made from the code. To do this, we need to add a dependency.

Gradle :

  compile 'org.hibernate:hibernate-envers'

Maven:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>${hibernate.version}</version>
</dependency>

Next, we add an annotation over our entities:

@Audited

This is what our entities look like now:

@Data
@Entity
@Table(name = "message", schema = "forum")
@Audited
public class Message {

    @Id
    @SequenceGenerator(name = "message_generator", sequenceName = "message_seq", schema = "forum", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "message_generator")
    private Long id;

    private String author;

    private String msg;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "forum_id")
    private Forum forum;

}

Here are the tables we have created. forum_aud, message_aud And revinfo. The revinfo table stores the sequence number and time of the change, while the forum_aud and message_aud tables store the changes themselves and a link to the entry in the original table. Let’s start with the table structure: id– forum post ID rev – record identifier in revinfo, revtype– event type 0(inser)-1(update)-2(delete) Other fields repeat the fields in the main table.

Problems

1. The first trouble that occurs, I don’t really like that all the tables are in one schema, if there are 10 tables and another 10 for auditing, there will be chaos visually.

2. Our goal is to understand not only when there were changes, but also to understand who made them, in order to know who to praise or knock on hands. But there are no such fields here.

3. If now we try to insert a record, then we will fall with the following error ERROR: relation “hibernate_sequence” does not exist This is due to the fact that, by default, identifiers are in the table revinfo will be taken from hibernate_sequence, but it doesn’t exist.

Searching of decisions

  1. To solve the first problem, there is an annotation, hung over the class

@AuditTable(value = "user_AUD", schema = "history")

Here we can specify schema and table name, don’t forget to create schema beforehand.

  1. With this, a little more complicated, here you already have to conjure a little. An easy way is to expand our main tables, and if we have 10 tables, and if this can break something, too much if, this does not suit us. Then such functionality appears, we can manually redefine the table revinfo. We can do this in two ways

    1) Create a new entity and inherit it from DefaultRevisionEntity. After that, we can add any fields.
    And also you need to create a listener and implement in it RevisionListener and override the method newRevision.

@Data
@Entity
@RevisionEntity(ExampleListener.class)
@Table(name = "REVINFO", schema = "history")
public class ExampleRevEntity extends DefaultRevisionEntity {

    private String username;

}
public class ExampleListener implements RevisionListener {

    @Override
    public void newRevision(Object revisionEntity) {
        ExampleRevEntity exampleRevEntity = (ExampleRevEntity) revisionEntity;

        exampleRevEntity.setUsername("UserName");
    }
}

Now we can add any new fields to ExampleRevEntity and describe the logic in ExampleListener in method newRevision .

2) In fact, the same as the first method, only we are not inherited from DefaultRevisionEntity , but we create it ourselves and define all the fields. In this case, we can more flexibly specify everything we need, for example, how to fill in the identifier, not from hibernate_sequence, but from our own sequence. Thanks to this, we solve the problem in the third paragraph.

@Data
@Entity
@RevisionEntity(ExampleListener.class)
@Table(name = "REVINFO", schema = "history")
public class ExampleRevEntity {

    @Id
    @RevisionNumber
    @GeneratedValue(generator = "CustomerAuditRevisionSeq")
    @SequenceGenerator(name = "CustomerAuditRevisionSeq", sequenceName = "customer_audit_revision_seq", schema = "history", allocationSize = 1)
    private int id;

    @RevisionTimestamp
    private long timestamp;

    private String username;

}

And now “VZHUH” and everything works. We see the audition records in our table.

More problems

A few more problems that I encountered, but not described above.

  • OneToMany and ManyToOne relationships can cause an error if the update occurs on several entities at once

  • If your entity inherits from another, you need to audit its fields as well

  • The problem of non-existing records if you have a strategy selected org.hibernate.envers.strategy.internal.ValidityAuditStrategy

Solutions

  • So that the connections do not break your auditing process, firstly you need to audit and these tables do the points above, the second these fields need to be marked with an annotation @AuditJoinTable Example:

    @OneToMany(mappedBy = "forum", fetch = FetchType.LAZY)
    @AuditJoinTable private List<Message> messages;

  • If you inherited an entity from another, you need to hang over the class for auditing @AuditOverride Example:
    @AuditOverride(forClass = ParentEntity.class)
    public class Forum extends ParentEntity

  • We can change the listening strategy to ValidityAuditStrategy, with this strategy in the table …_aud you will create another field return, this is the id of the post that overwritten those changes, so you can keep track of the actual posts.

    But if you already have data in the tables, then when you change them, a new record of the change will appear and the old record will be searched for in order to put it down return, but since there is no such record, everything will fall with an error. Unfortunately, I did not find a solution for this problem, only to roll the data after turning on the audition, or not to change the old data.

Conclusion

The technology is really not complicated, it does not take much to connect it to your project, but you need to spend a little time for customization. I have not tested it with large loads yet, but it is perfect for a small project.

Sources

Official documentation

https://vladmihalcea.com/the-best-way-to-implement-an-audit-log-using-hibernate-envers/

Link to GitHub

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *