Skip to main content

Simplifying DB Auditing with Spring Boot and Javers

· 8 min read
Huseyin BABAL
Software Developer

Introduction

Database auditing is essential for tracking changes to data over time, providing a clear history of who changed what and when. It helps ensure compliance with security and legal standards, allowing developers to detect anomalies, recover data, or conduct audits efficiently. In this article, we will explore how to implement automatic database auditing in a Spring Boot application using Javers, a powerful Java library that simplifies object auditing and diffing.

What is Javers?

Javers is a Java library designed for auditing object changes, often in databases. By comparing versions of objects, Javers creates an audit trail of differences between them. Unlike traditional database auditing methods that rely on triggers or manually logging changes, Javers operates at the application layer, making it easier to track object changes while maintaining flexibility.

Use Cases for Javers

  • Tracking Entity Changes: Javers can record changes to your domain objects and persist those changes, enabling easy retrieval of past object versions.

  • Data Synchronization: It can be used in distributed systems to ensure data consistency across multiple systems by comparing snapshots of the same object.

  • Data Governance and Compliance: For businesses that need to comply with regulations like GDPR, Javers provides an efficient way to track and monitor changes.

Understanding Javers' Tables

Javers uses a set of database tables to store the change history. These tables include:

  • jv_commit: Stores information about who made changes, the timestamp, and the commit ID.
  • jv_snapshot: Records object state at the time of a commit.
  • jv_global_id: Manages global identifiers for entities.
  • jv_commit_property: Stores metadata related to the commit, like environment details. These tables are automatically created when Javers is integrated with your Spring Boot project, allowing you to persist audit information without any manual setup.

In Javers, the type column is used to indicate the type of change captured in a snapshot. This column helps distinguish between various operations on an entity. Here's an explanation of the most common type values:

  • INITIAL: Indicates the first time an object is added to the database or tracked by Javers. This value represents the creation of the entity, meaning that there was no prior state to compare against.

  • UPDATE: Represents changes made to an existing object. When any field within the entity is modified, Javers captures the updated state and marks the change with this type. It indicates that this is not a new entity but an updated one.

  • TERMINAL: This value signifies that the entity has been deleted. When an object is removed from the system, Javers marks this event with a TERMINAL entry to signify the final state of the object before deletion.

  • INITIALIZED: This type appears in some cases to signify that a value was initialized or restored from a default state.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

We will implement a REST endpoint to create and update products to see db audit history by using Javers. It contains implementing entity, repository interface and a controller to create/update product.

Dependencies

Be sure you have the following dependencies installed by using your favourite dependency management tool e.g. maven, gradle.

pom.xml
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<!-- This is for automatic db auiditing with the integration to spring security -->
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-boot-starter-sql</artifactId>
<version>7.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Spring Web Security Configuration

In order to enable Spring Web Security and to be able to send request to REST endpoints, you need to configure it in your SecurityConfig.java file as shown below. By default, Spring expects to see csrf tokens in every request. With the following configuration, you can disable csrf protection.

SecurityConfig.java
@Configuration
@EnableWebSecurity
class WebSecurityConfiguration {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}

Line 2: Add @EnableWebSecurity to the SecurityConfig class to protect the API endpoints.

Line 7: Disable CSRF protection.

Line 12: Set the session creation policy to STATELESS to ensure sessions are not maintained.

Add the Product Entity

Create a simple Product entity that will be audited by Javers.

Product.java
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
class Product {

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

private String name;
}

Implement the Product Repository

Create a simple ProductRepository interface that will be audited by Javers.

ProductRepository.java
@JaversSpringDataAuditable
interface ProductRepository extends CrudRepository<Product, Long> {}

Line 1: Add @JaversSpringDataAuditable to the ProductRepository interface to enable Javers auditing for the product entity automatically.

Implement the Product Controller

Create a simple ProductController class for creating and updating products. We use POST method for creating, and PATCH method for updating single field of Product.

ProductController.java
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
class ProductController {

private final ProductRepository productRepository;

@PostMapping
public Product createProduct() {
Product product = new Product();
product.setName("Product " + System.currentTimeMillis());
return productRepository.save(product);
}

@PatchMapping
public void updateProduct() {
productRepository.findById(1L).ifPresent(product -> {
product.setName("Product " + System.currentTimeMillis());
productRepository.save(product);
});
}
}

Application Configuration

Next, configure database access in your application.yaml file. At a minimum, you need to specify the database connection details:

application.yaml
spring:
application:
name: spring-db-audit
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://<host>:<port>/<user>?sslmode=require&application_name=spring-db-audit&user=<user>&password=<password>
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
security:
user:
name: developer
password: s3cr3t

Line 13: Set the user property to developer and password to s3cr3t. We will use those credentials in curl requests to test the application.

Demo

Create Product

curl -XPOST \
-u developer:s3cr3t -H \
"Content-Type: application/json" \
http://localhost:8080/products -v

Javers will detect the following changes in the Product entity and save them in the database as follows.

> select *from jv_commit;
commit_pk | author | commit_date | commit_date_instant | commit_id
-----------+--------+-------------------------+-----------------------------+-----------
100 | user | 2024-09-08 01:58:49.728 | 2024-09-07T22:58:49.728964Z | 1.00
> select *from jv_snapshot;
snapshot_pk | type | version | state | changed_properties | managed_type | global_id_fk | commit_fk
-------------+---------+---------+------------------------------------+--------------------+------------------------------------+--------------+-----------
100 | INITIAL | 1 | { +| [ +| com.rapidapp.springdbaudit.Product | 100 | 100
| | | "name": "Product 1725749929156",+| "name", +| | |
| | | "id": 1 +| "id" +| | |
| | | } | ] | | |

Since this is a create operation, it is saved as INITIAL. Let's update product entity to see how Javers maintains its changes.

Update Product

curl -XPATCH \
-u developer:s3cr3t -H \
"Content-Type: application/json" \
http://localhost:8080/products -v

Javers will detect the following changes in the Product entity and save them in the database as follows.

> select *from jv_snapshot WHERE type='UPDATE';
snapshot_pk | type | version | state | changed_properties | managed_type | global_id_fk | commit_fk
-------------+--------+---------+------------------------------------+--------------------+------------------------------------+--------------+-----------
500 | UPDATE | 2 | { +| [ +| com.rapidapp.springdbaudit.Product | 100 | 400
| | | "name": "Product 1725777539405",+| "name" +| | |
| | | "id": 1 +| ] | | |
| | | } | | | |

As you can see;

  • type field is UPDATE
  • changed_properties is name to understand which field(s) are changed.
  • We can follow the history by checking version field. So, who did this change? Let's take a look at jv_commit table.
> select *from jv_commit order by commit_id desc limit 1;
commit_pk | author | commit_date | commit_date_instant | commit_id
-----------+--------+-------------------------+-----------------------------+-----------
400 | john | 2024-09-08 09:38:59.639 | 2024-09-08T06:38:59.639471Z | 5.00

You can see the author field to see the owner of this change. As you can see, john is the owner of this change.

Conclusion

By using Javers with Spring Boot, database auditing becomes simple and flexible. Javers' ability to track object changes over time and store audit data in a structured way makes it a great choice for any application that requires a detailed history of data changes. With this implementation, you can now easily integrate automatic auditing in your Spring Boot applications.

tip

You can find the complete source code for this project on GitHub.