Histórico de datos, auditoría y diferencias entre objetos con Javers en Java

Escrito por el .
java planeta-codigo programacion
Enlace permanente Comentarios

Por seguridad, por auditoría o histórico de datos una aplicación puede requerir no solo guardar los valores actuales de los datos que maneja sino también las versiones anteriores y los cambios en cada valor de los mismos. En Java hay una librería llamada Javers que nos proporciona funcionalidades como comparación, snapshots, persistencia y un lenguaje de consulta para hacer búsquedas.

Java

Por necesidades de negocio y requerimientos funcionales puede ser necesario guardar un histórico de ciertos datos de una aplicación en vez de solo la última versión de los datos. Tener solo la última versión de algunos datos puede no ser lo que se necesita. Por ejemplo, supongamos que una aplicación maneja una entidad de dominio producto y esta tiene un campo que es el precio y queremos guardar los cambios que se hacen a esta entidad para conocer el cambio de precio que han sufrido los productos. Otras necesidades pueden ser por auditoría o seguridad para saber que usuario ha hecho que cambios en los datos, para disponer de versiones anteriores de una entidad, comparar dos versiones de la misma entidad o lanzar consultas para obtener información de los cambios que se han producido.

Una librería que en Java nos ofrece toda esta información de auditoría es Javers con la posibilidad de persistirla en diferentes sistemas, en las tradicionales base de datos relacionales o en una base de datos no SQL como MongoDB. En la documentación encontramos como podemos comparar dos objetos, persistir cambios o lanzar consultas.

Javers diferencia dos tipos de objetos Entities o ValueObjects. Los ValueObjects son objetos java bean tradicionales de Java que no tienen identificador asignado y no son persistibles pero se pueden usar para hacer comparaciones entre dos objetos según las propiedades de los java beans. Los objetos java bean Entities tienen una propiedad que representa el identificativo de la entidad y las comparaciones se pueden hacer entre diferentes versiones del mismo.

En el siguiente ejemplo muestro como hacer comparaciones, como hacer cambios y persistirlos, como lanzar una consulta para obtener los cambios que se han producido u obtener snapshots de versiones anteriores y como persistir estos cambios en una base de datos PostgreSQL en la que utilizaré Docker. En la primera sección del ejemplo se comparan dos objetos y obtienen sus diferencias, posteriormente se persisten varios cambios y finalmente se realiza una consulta para obtener los cambios que haya habido en la propiedad price.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package io.github.picodotdev.blogbitix.holamundojavers;

import org.javers.common.collections.Sets;
import org.javers.core.Javers;
import org.javers.core.JaversBuilder;
import org.javers.core.diff.Change;
import org.javers.core.diff.Diff;
import org.javers.repository.jql.QueryBuilder;
import org.javers.repository.sql.ConnectionProvider;
import org.javers.repository.sql.DialectName;
import org.javers.repository.sql.JaversSqlRepository;
import org.javers.repository.sql.SqlRepositoryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

@SpringBootApplication
public class Main implements CommandLineRunner {

    @Bean
    public ConnectionProvider connectionProvider() {
        return new ConnectionProvider() {
            @Override
            public Connection getConnection() throws SQLException {
                try {
                    Class.forName("org.postgresql.Driver");
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    return null;
                }

                Properties props = new Properties();
                props.setProperty("user", "admin");
                props.setProperty("password", "admin");
                return DriverManager.getConnection("jdbc:postgresql:javers", props);
            }
        };
    }

    @Bean
    public Javers javers(ConnectionProvider connectionProvider) {
        JaversSqlRepository sqlRepository = SqlRepositoryBuilder
                .sqlRepository()
                .withConnectionProvider(connectionProvider)
                .withDialect(DialectName.POSTGRES).build();
        return JaversBuilder.javers().registerJaversRepository(sqlRepository).build();
    }

    @Autowired
    private Javers javers;

    @Override
    public void run(String... args) {
        Category drink = new Category("Drink");
        Category sport = new Category("Sport");
        Category fruit = new Category("Fruit");

        Product aquarius1 = new Product("Aquarius", new BigDecimal("1.75"), Collections.singleton(drink));
        Product aquarius2 = new Product("Aquarius", new BigDecimal("0.90"), Collections.singleton(sport));

        // Diff
        System.out.println("Diff...");
        Diff diff1 = javers.compare(aquarius1, aquarius2);
        System.out.println(diff1);

        // Commit
        System.out.println("Commit...");
        javers.commit("author", aquarius1);

        aquarius1.setPrice(new BigDecimal("2.00"));
        javers.commit("author", aquarius1);

        aquarius1.setPrice(new BigDecimal("1.60"));
        aquarius1.setCategories(Sets.asSet(drink, sport));
        javers.commit("author", aquarius1);

        // JQL
        System.out.println("Query...");
        List<Change> changes = javers.findChanges(QueryBuilder.byInstanceId("Aquarius", Product.class).andProperty("price").build());
        changes.stream().forEach(change -> {
            System.out.println(change);
        });
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package io.github.picodotdev.blogbitix.holamundojavers;

import javax.persistence.Id;

public class Category {

    @Id
    private String name;

    public Category(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
Category.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package io.github.picodotdev.blogbitix.holamundojavers;

import javax.persistence.Id;
import java.math.BigDecimal;
import java.util.Set;

public class Product {

    @Id
    private String name;
    private BigDecimal price;
    private Set<Category> categories;

    public Product(String name, BigDecimal price, Set<Category> categories) {
        this.name = name;
        this.price = price;
        this.categories = categories;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Set<Category> getCategories() {
        return categories;
    }

    public void setCategories(Set<Category> categories) {
        this.categories = categories;
    }
}
Product.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[picodotdev@archlinux HolaMundoJavers]$ ./gradlew run
:compileJava
:processResources UP-TO-DATE
:classes
:run

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.4.1.RELEASE)

Diff...
Diff:
1. NewObject{globalId:'io.github.picodotdev.blogbitix.holamundojavers.Category/Sport'}
2. ObjectRemoved{globalId:'io.github.picodotdev.blogbitix.holamundojavers.Category/Drink'}
3. ValueChange{globalId:'io.github.picodotdev.blogbitix.holamundojavers.Product/Aquarius', property:'price', oldVal:'1.75', newVal:'0.90'}
4. SetChange{globalId:'io.github.picodotdev.blogbitix.holamundojavers.Product/Aquarius', property:'categories', containerChanges:[removed:'io.github.picodotdev.blogbitix.holamu
ndojavers.Category/Drink', added:'io.github.picodotdev.blogbitix.holamundojavers.Category/Sport']}

Commit...
Query...
ValueChange{globalId:'io.github.picodotdev.blogbitix.holamundojavers.Product/Aquarius', property:'price', oldVal:'2.00', newVal:'1.60'}
ValueChange{globalId:'io.github.picodotdev.blogbitix.holamundojavers.Product/Aquarius', property:'price', oldVal:'1.75', newVal:'2.00'}

BUILD SUCCESSFUL

Total time: 4.954 secs
System.out
1
2
3
4
5
6
7
8
postgres:
    image: postgres
    ports:
        - 5432:5432
    environment:
        - POSTGRES_USER=admin
        - POSTGRES_PASSWORD=admin
        - POSTGRES_DB=javers
docker-compose.yml

La información se persistirán en varias tablas en este caso en un base de datos relacional PostgreSQL que Javers creará al iniciarse la aplicación, ejecutada la aplicación encontraremos datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ docker exec -it 25135430da0d bash
root@25135430da0d:/# psql javers admin
javers=# \dt
              List of relations
 Schema |        Name        | Type  | Owner
--------+--------------------+-------+------
 public | jv_commit          | table | admin
 public | jv_commit_property | table | admin
 public | jv_global_id       | table | admin
 public | jv_snapshot        | table | admin
(4 rows)
tables.sql
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: