Usando o JaVers para auditoria de modelo de dados no Spring Data
1. Visão geral
Neste tutorial, veremos como configurar e usar JaVers em um aplicativoSpring Boot simples para rastrear mudanças de entidades.
2. JaVers
Ao lidar com dados mutáveis, geralmente temos apenas o último estado de uma entidade armazenada em um banco de dados. Como desenvolvedores, gastamos muito tempo depurando um aplicativo, pesquisando nos arquivos de log um evento que mudou de estado. Isso fica ainda mais complicado no ambiente de produção quando muitos usuários diferentes estão usando o sistema.
Felizmente, temos ótimas ferramentas comoJaVers. JaVers é uma estrutura de log de auditoria que ajuda a rastrear alterações de entidades no aplicativo.
O uso desta ferramenta não se limita apenas à depuração e auditoria. Pode ser aplicado com sucesso para realizar análises, forçar políticas de segurança e manter o log de eventos também.
3. Project Set-up
Primeiro de tudo, para começar a usar o JaVers, precisamos configurar o repositório de auditoria para capturas instantâneas persistentes de entidades. Em segundo lugar, precisamos ajustar algumas propriedades configuráveis do JaVers. Por fim, também abordaremos como configurar nossos modelos de domínio adequadamente.
Mas, vale ressaltar que o JaVers fornece opções de configuração padrão, para que possamos começar a usá-lo com quase nenhuma configuração.
3.1. Dependências
Primeiro, precisamos adicionar a dependência inicial do JaVers Spring Boot ao nosso projeto. Dependendo do tipo de armazenamento de persistência, temos duas opções:org.javers:javers-spring-boot-starter-sqleorg.javers:javers-spring-boot-starter-mongo. Neste tutorial, usaremos o iniciador Spring Boot SQL.
org.javers
javers-spring-boot-starter-sql
5.6.3
Como vamos usar o banco de dados H2, também vamos incluir esta dependência:
com.h2database
h2
3.2. JaVers Repository Setup
O JaVers usa uma abstração de repositório para armazenar confirmações e entidades serializadas. Todos os dados são armazenados emJSON format. Portanto, pode ser uma boa opção usar um armazenamento NoSQL. No entanto, por uma questão de simplicidade, usaremos umH2 in-memory instance.
Por padrão, JaVers aproveita uma implementação de repositório na memória, e se estivermos usando Spring Boot, não há necessidade de configuração extra. Além disso,while using Spring Data starters, JaVers reuses the database configuration for the application.
O JaVers fornece duas entradas para pilhas de persistência SQL e Mongo. Eles são compatíveis com Spring Data e não requerem configuração extra por padrão. No entanto, sempre podemos substituir os beans de configuração padrão:JaversSqlAutoConfiguration.javaeJaversMongoAutoConfiguration.java respectivamente.
3.3. Propriedades do JaVers
JaVers permite configurar várias opções, emborathe Spring Boot defaults sejam suficientes na maioria dos casos de uso.
Vamos substituir apenas um,newObjectSnapshot, para que possamos obter instantâneos de objetos recém-criados:
javers.newObjectSnapshot=true
3.4. Configuração de Domínio JaVers
O JaVers define internamente os seguintes tipos: Entidades, Objetos de valor, Valores, Contêineres e Primitivos. Alguns desses termos vêm da terminologia deDDD (Domain Driven Design).
The main purpose of having several types is to provide different diff algorithms depending on the type. Cada tipo tem uma estratégia diff correspondente. Como consequência, se as classes do aplicativo forem configuradas incorretamente, obteremos resultados imprevisíveis.
Para dizer ao JaVers que tipo usar para uma classe, temos várias opções:
-
Explicitly - a primeira opção é usar explicitamente os métodosregister* da classeJaversBuilder - a segunda maneira é usar anotações
-
Implicitly - JaVers fornece algoritmos para detectar tipos automaticamente com base nas relações de classe
-
Defaults - por padrão, JaVers tratará todas as classes comoValueObjects
Neste tutorial, vamos configurar JaVers explicitamente, usando o método de anotação.
O ótimo é queJaVers is compatible with javax.persistence annotations. Como resultado, não precisamos usar anotações específicas do JaVers em nossas entidades.
4. Projeto de exemplo
Agora vamos criar um aplicativo simples que incluirá várias entidades de domínio que iremos auditar.
4.1. Modelos de domínio
Nosso domínio incluirá lojas com produtos.
Vamos definir a entidadeStore:
@Entity
public class Store {
@Id
@GeneratedValue
private int id;
private String name;
@Embedded
private Address address;
@OneToMany(
mappedBy = "store",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List products = new ArrayList<>();
// constructors, getters, setters
}
Observe que estamos usando anotações JPA padrão. O JaVers os mapeia da seguinte maneira:
-
@javax.persistence.Entity é mapeado para@org.javers.core.metamodel.annotation.Entity
-
@javax.persistence.Embeddable é mapeado para@org.javers.core.metamodel.annotation.ValueObject.
Classes incorporáveis são definidas da maneira usual:
@Embeddable
public class Address {
private String address;
private Integer zipCode;
}
4.2. Repositórios de Dados
Para auditar os repositórios JPA, o JaVers fornece a anotação@JaversSpringDataAuditable.
Vamos definir oStoreRepository com essa anotação:
@JaversSpringDataAuditable
public interface StoreRepository extends CrudRepository {
}
Além disso, teremos oProductRepository, mas não anotado:
public interface ProductRepository extends CrudRepository {
}
Agora considere um caso em que não estamos usando repositórios Spring Data. JaVers tem outra anotação de nível de método para esse propósito:@JaversAuditable.
Por exemplo, podemos definir um método para persistir um produto da seguinte maneira:
@JaversAuditable
public void saveProduct(Product product) {
// save object
}
Como alternativa, podemos até adicionar esta anotação diretamente acima de um método na interface do repositório:
public interface ProductRepository extends CrudRepository {
@Override
@JaversAuditable
S save(S s);
}
4.3. Fornecedor do Autor
Cada mudança confirmada no JaVers deve ter seu autor. Além disso, JaVers suportaSpring Security fora da caixa.
Como resultado, cada confirmação é feita por um usuário autenticado específico. No entanto, para este tutorial, criaremos uma implementação personalizada realmente simples da interfaceAuthorProvider:
private static class SimpleAuthorProvider implements AuthorProvider {
@Override
public String provide() {
return "example Author";
}
}
E como a última etapa, para que o JaVers use nossa implementação customizada, precisamos substituir o bean de configuração padrão:
@Bean
public AuthorProvider provideJaversAuthor() {
return new SimpleAuthorProvider();
}
5. JaVers Audit
Finalmente, estamos prontos para auditar nosso aplicativo. Usaremos um controlador simples para despachar alterações em nosso aplicativo e recuperar o log de confirmação do JaVers. Como alternativa, também podemos acessar o console H2 para ver a estrutura interna do nosso banco de dados:
Para ter alguns dados de amostra iniciais, vamos usar umEventListener para preencher nosso banco de dados com alguns produtos:
@EventListener
public void appReady(ApplicationReadyEvent event) {
Store store = new Store("example store", new Address("Some street", 22222));
for (int i = 1; i < 3; i++) {
Product product = new Product("Product #" + i, 100 * i);
store.addProduct(product);
}
storeRepository.save(store);
}
5.1. Confirmação inicial
Quando um objeto é criado, JaVersfirst makes a commit of the INITIAL type.
Vamos verificar os instantâneos após a inicialização do aplicativo:
@GetMapping("/stores/snapshots")
public String getStoresSnapshots() {
QueryBuilder jqlQuery = QueryBuilder.byClass(Store.class);
List snapshots = javers.findSnapshots(jqlQuery.build());
return javers.getJsonConverter().toJson(snapshots);
}
No código acima, estamos consultando JaVers para obter instantâneos para a classeStore. Se fizermos uma solicitação para este endpoint, obteremos um resultado como o abaixo:
[
{
"commitMetadata": {
"author": "example Author",
"properties": [],
"commitDate": "2019-08-26T07:04:06.776",
"commitDateInstant": "2019-08-26T04:04:06.776Z",
"id": 1.00
},
"globalId": {
"entity": "com.example.springjavers.domain.Store",
"cdoId": 1
},
"state": {
"address": {
"valueObject": "com.example.springjavers.domain.Address",
"ownerId": {
"entity": "com.example.springjavers.domain.Store",
"cdoId": 1
},
"fragment": "address"
},
"name": "example store",
"id": 1,
"products": [
{
"entity": "com.example.springjavers.domain.Product",
"cdoId": 2
},
{
"entity": "com.example.springjavers.domain.Product",
"cdoId": 3
}
]
},
"changedProperties": [
"address",
"name",
"id",
"products"
],
"type": "INITIAL",
"version": 1
}
]
Observe que o instantâneo acima deincludes all products added to the store despite the missing annotation for the ProductRepository interface.
Por padrão, o JaVers auditará todos os modelos relacionados de uma raiz agregada se eles persistirem junto com o pai.
Podemos dizer ao JaVers para ignorar classes específicas usando a anotaçãoDiffIgnore.
Por exemplo, podemos anotar o campoproducts com a anotação na entidadeStore:
@DiffIgnore
private List products = new ArrayList<>();
Consequentemente, o JaVers não rastreará alterações de produtos originados da entidadeStore.
5.2. Confirmação de atualização
O próximo tipo de confirmação é a confirmaçãoUPDATE. Este é o tipo de confirmação mais valioso, pois representa as alterações do estado de um objeto.
Vamos definir um método que atualize a entidade da loja e todos os produtos da loja:
public void rebrandStore(int storeId, String updatedName) {
Optional storeOpt = storeRepository.findById(storeId);
storeOpt.ifPresent(store -> {
store.setName(updatedName);
store.getProducts().forEach(product -> {
product.setNamePrefix(updatedName);
});
storeRepository.save(store);
});
}
Se executarmos este método, obteremos a seguinte linha na saída de depuração (no caso da mesma contagem de produtos e lojas):
11:29:35.439 [http-nio-8080-exec-2] INFO org.javers.core.Javers - Commit(id:2.0, snapshots:3, author:example Author, changes - ValueChange:3), done in 48 millis (diff:43, persist:5)
Como o JaVers manteve as alterações com êxito, vamos consultar os instantâneos para produtos:
@GetMapping("/products/snapshots")
public String getProductSnapshots() {
QueryBuilder jqlQuery = QueryBuilder.byClass(Product.class);
List snapshots = javers.findSnapshots(jqlQuery.build());
return javers.getJsonConverter().toJson(snapshots);
}
Obteremos commitsINITIAL anteriores e novos commitsUPDATE:
{
"commitMetadata": {
"author": "example Author",
"properties": [],
"commitDate": "2019-08-26T12:55:20.197",
"commitDateInstant": "2019-08-26T09:55:20.197Z",
"id": 2.00
},
"globalId": {
"entity": "com.example.springjavers.domain.Product",
"cdoId": 3
},
"state": {
"price": 200.0,
"name": "NewProduct #2",
"id": 3,
"store": {
"entity": "com.example.springjavers.domain.Store",
"cdoId": 1
}
}
}
Aqui, podemos ver todas as informações sobre a alteração que fizemos.
É importante notar queJaVers doesn’t create new connections to the database. Instead, it reuses existing connections. Os dados do JaVers são confirmados ou revertidos, juntamente com os dados do aplicativo na mesma transação.
5.3. Alterar
JaVers records changes as atomic differences between versions of an object. Como podemos ver no esquema JaVers, não há uma tabela separada para armazenar as mudanças, entãoJaVers calculates changes dynamically as the difference between snapshots.
Vamos atualizar o preço de um produto:
public void updateProductPrice(Integer productId, Double price) {
Optional productOpt = productRepository.findById(productId);
productOpt.ifPresent(product -> {
product.setPrice(price);
productRepository.save(product);
});
}
Então, vamos consultar JaVers para mudanças:
@GetMapping("/products/{productId}/changes")
public String getProductChanges(@PathVariable int productId) {
Product product = storeService.findProductById(productId);
QueryBuilder jqlQuery = QueryBuilder.byInstance(product);
Changes changes = javers.findChanges(jqlQuery.build());
return javers.getJsonConverter().toJson(changes);
}
A saída contém a propriedade alterada e seus valores antes e depois:
[
{
"changeType": "ValueChange",
"globalId": {
"entity": "com.example.springjavers.domain.Product",
"cdoId": 2
},
"commitMetadata": {
"author": "example Author",
"properties": [],
"commitDate": "2019-08-26T16:22:33.339",
"commitDateInstant": "2019-08-26T13:22:33.339Z",
"id": 2.00
},
"property": "price",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": 100.0,
"right": 3333.0
}
]
Para detectar um tipo de mudança, JaVers compara instantâneos subsequentes de atualizações de um objeto. No caso acima, como alteramos a propriedade da entidade, temos o tipo de alteraçãoPROPERTY_VALUE_CHANGED.
5.4. Sombras
Além disso, o JaVers fornece outra visão das entidades auditadas, chamadaShadow. Uma sombra representa um estado do objeto restaurado a partir de instantâneos. Este conceito está intimamente relacionado aEvent Sourcing.
Existem quatro escopos diferentes para Shadows:
-
Shallow - sombras são criadas a partir de um instantâneo selecionado em uma consulta JQL
-
Child-value-object - sombras contêm todos os objetos de valor filho pertencentes a entidades selecionadas
-
Commit-deep - sombras são criadas a partir de todos os instantâneos relacionados às entidades selecionadas
-
Deep+ - JaVers tenta restaurar gráficos completos de objetos com (possivelmente) todos os objetos carregados.
Vamos usar o escopo filho-valor-objeto e obter uma sombra para uma única loja:
@GetMapping("/stores/{storeId}/shadows")
public String getStoreShadows(@PathVariable int storeId) {
Store store = storeService.findStoreById(storeId);
JqlQuery jqlQuery = QueryBuilder.byInstance(store)
.withChildValueObjects().build();
List> shadows = javers.findShadows(jqlQuery);
return javers.getJsonConverter().toJson(shadows.get(0));
}
Como resultado, obteremos a entidade da loja com o objeto de valorAddress:
{
"commitMetadata": {
"author": "example Author",
"properties": [],
"commitDate": "2019-08-26T16:09:20.674",
"commitDateInstant": "2019-08-26T13:09:20.674Z",
"id": 1.00
},
"it": {
"id": 1,
"name": "example store",
"address": {
"address": "Some street",
"zipCode": 22222
},
"products": []
}
}
Para obter produtos no resultado, podemos aplicar o escopo Commit-deep.
6. Conclusão
Neste tutorial, vimos como o JaVers se integra facilmente com Spring Boot e Spring Data em particular. Em suma, o JaVers requer quase zero de configuração para ser configurado.
Para concluir, o JaVers pode ter aplicativos diferentes, da depuração à análise complexa.
O projeto completo para este artigo está disponívelover on GitHub.