Eu sei o que vocês fizeram no verão passado
Não se assustem! Não se trata de mais um filme de quinta categoria com adolescentes gostosas e sem nenhum sentido do cinema americano.
Vim aqui hoje falar do Envers. O Envers é um dos mais novos frameworks da JBoss a alcançar a maturidade GA (General Availability), a grande sacada do envers é gerar um histórico de todas modificações que ocorrem para uma determinada entidade. Quantas vezes você já não esbarrou em um requisito de projeto onde o cliente solicitava a capacidade de restaurar uma versão do objeto modificado há 3 meses atrás com os valores que este tinha na época? Ou então por motivo de alguma auditoria interna, você precisou guardar esta modificações em uma tabela “histórico”. Bem, eu já passei pelas duas situações, e tenho certeza que muitos de vocês já tiveram o mesmo problema.
Para resolver este problema chegou o Envers. O Envers funciona como um interceptor do Hibernate que atua em toda modificação que é realizada para uma classe anotada para ser versionada. Quando sua Session modifica a classe o envers gera uma entrada em uma tabela de histórico. A configuração é extremamente simples. E o seu uso mais ainda, mas achei que valia a pena fazer um pequeno post sobre ele.
Como todos exemplos do meu blog, vamos iniciar criando um novo projeto seam (consulte posts anteriores e a documentação do Seam para isso)
Uma vez que seu projeto esteja criado. Defina uma nova entidade:
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 | package com.furiousbob.com; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import org.jboss.envers.Versioned; @Entity @Table(name="NEWS") @Versioned public class News implements Serializable { @Id @Column(name="ID") @GeneratedValue(strategy=GenerationType.IDENTITY) private long id; @Column(name="TITLE") private String title; @Column(name="CONTENTS") private String contents; public long getId() { return id; } public String getTitle() { return title; } public String getContents() { return contents; } public void setId(long id) { this.id = id; } public void setTitle(String title) { this.title = title; } public void setContents(String contents) { this.contents = contents; } } |
Note na linha 16 a presença de uma anotação que define que nossa classe é agora versionada. O Envers suporta versionamento de classes completas ou de atributos.
Vá no console do seam, e execute ./seam generate-ui. Isso vai gerar todo o código e interfaces html para nosso CRUD.
Vamos então criar uma nova entidade. Ao clicar em create news, o sistema vai solicitar que você se autentique (admin:admin). Vamos então entrar com algum texto para nossa entidade, conforme a figura abaixo:
Clique em “Done” e você será redirecionado para a tela que apresenta a entidade. Uma das colunas possui uma ação para selecionar a entidade para edição “Select”, clique nesta ação e mude por exemplo o valor da propriedade contents conforme a figura abaixo:
Oops, alguem modificou o conteúdo! Como podemos restaurar a ordem no sistema? Se você estiver usando o Envers a tarefa é simples :). Baixe o envers, adicione o jar ao seu lib. Edite o arquivo deployed-jars-ear.list e adicione a lib do envers. Edite seu persistence.xml de forma que fique parecido com o abaixo:
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 | <?xml version="1.0" encoding="UTF-8"?> <!-- Persistence deployment descriptor for dev profile --> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="envers"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jta-data-source>java:/enversDatasource</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="jboss.entity.manager.factory.jndi.name" value="java:/enversEntityManagerFactory"/> <property name="hibernate.ejb.event.post-insert" value="org.jboss.envers.event.VersionsEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.jboss.envers.event.VersionsEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.jboss.envers.event.VersionsEventListener" /> </properties> </persistence-unit> </persistence> |
As linhas 17-22 são as responsáveis por ativar o envers. Se você checar seu SGDB agora, pode notar 2 tabelas novas: NEWS_versions e _revisions_info. Estas são as tabelas criadas automaticamente pelo Envers para você. A tabela
Bem, vamos modificar nosso arquivo NewsList.xhtml (gerado pelo Seam) de forma que agora possamos verificar as versões existentes para cada uma de nossas entidades. A lista abaixo apresenta a nova coluna que inserimos:
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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | <!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:rich="http://richfaces.org/rich"
template="layout/template.xhtml">
<ui:define name="body">
<h:messages globalOnly="true" styleClass="message" id="globalMessages"/>
<h:form id="newsSearch" styleClass="edit">
<rich:simpleTogglePanel label="News search parameters" switchType="ajax">
<s:decorate template="layout/display.xhtml">
<ui:define name="label">contents</ui:define>
<h:inputText id="contents" value="#{newsList.news.contents}"/>
</s:decorate>
<s:decorate template="layout/display.xhtml">
<ui:define name="label">title</ui:define>
<h:inputText id="title" value="#{newsList.news.title}"/>
</s:decorate>
</rich:simpleTogglePanel>
<div class="actionButtons">
<h:commandButton id="search" value="Search" action="/NewsList.xhtml"/>
</div>
</h:form>
<rich:panel>
<f:facet name="header">News search results</f:facet>
<div class="results" id="newsList">
<h:outputText value="The news search returned no results."
rendered="#{empty newsList.resultList}"/>
<rich:dataTable id="newsList"
var="news"
value="#{newsList.resultList}"
rendered="#{not empty newsList.resultList}">
<h:column>
<f:facet name="header">
<s:link styleClass="columnHeader"
value="id #{newsList.order=='id asc' ? messages.down : ( newsList.order=='id desc' ? messages.up : '' )}">
<f:param name="order" value="#{newsList.order=='id asc' ? 'id desc' : 'id asc'}"/>
</s:link>
</f:facet>
#{news.id}
</h:column>
<h:column>
<f:facet name="header">
<s:link styleClass="columnHeader"
value="contents #{newsList.order=='contents asc' ? messages.down : ( newsList.order=='contents desc' ? messages.up : '' )}">
<f:param name="order" value="#{newsList.order=='contents asc' ? 'contents desc' : 'contents asc'}"/>
</s:link>
</f:facet>
#{news.contents}
</h:column>
<h:column>
<f:facet name="header">
<s:link styleClass="columnHeader"
value="title #{newsList.order=='title asc' ? messages.down : ( newsList.order=='title desc' ? messages.up : '' )}">
<f:param name="order" value="#{newsList.order=='title asc' ? 'title desc' : 'title asc'}"/>
</s:link>
</f:facet>
#{news.title}
</h:column>
<h:column>
<f:facet name="header">action</f:facet>
<s:link view="/#{empty from ? 'News' : from}.xhtml"
value="Select"
id="news">
<f:param name="newsId"
value="#{news.id}"/>
</s:link>
</h:column>
<h:column>
<f:facet name="header">Revisions</f:facet>
<s:link view="/newsRevision.xhtml" value="check revisions">
<f:param name="id" value="#{news.id}"/>
</s:link>
</h:column>
</rich:dataTable>
</div>
</rich:panel>
<div class="tableControl">
<s:link view="/NewsList.xhtml"
rendered="#{newsList.previousExists}"
value="#{messages.left}#{messages.left} First Page"
id="firstPage">
<f:param name="firstResult" value="0"/>
</s:link>
<s:link view="/NewsList.xhtml"
rendered="#{newsList.previousExists}"
value="#{messages.left} Previous Page"
id="previousPage">
<f:param name="firstResult"
value="#{newsList.previousFirstResult}"/>
</s:link>
<s:link view="/NewsList.xhtml"
rendered="#{newsList.nextExists}"
value="Next Page #{messages.right}"
id="nextPage">
<f:param name="firstResult"
value="#{newsList.nextFirstResult}"/>
</s:link>
<s:link view="/NewsList.xhtml"
rendered="#{newsList.nextExists}"
value="Last Page #{messages.right}#{messages.right}"
id="lastPage">
<f:param name="firstResult"
value="#{newsList.lastFirstResult}"/>
</s:link>
</div>
<s:div styleClass="actionButtons" rendered="#{empty from}">
<s:button view="/NewsEdit.xhtml"
id="create"
value="Create news">
<f:param name="newsId"/>
</s:button>
</s:div>
</ui:define>
</ui:composition> |
O que nos interessa aqui são as linhas 87-92, que adicionam uma nova coluna a tabela de listagem de entidades. Após ter feito este passo, sua tela deverá parecer com a tela abaixo:
Bem, precisamos agora criar a página que irá apresentar as versões de nosso objeto, a página newsRevision.xhtml é apresentada abaixo:
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 | <!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:rich="http://richfaces.org/rich"
template="layout/template.xhtml">
<ui:define name="body">
<h:messages globalOnly="true" styleClass="message" id="globalMessages"/>
<rich:panel>
<f:facet name="header">Revision history for news id #{newsRevision.newsId}</f:facet>
<rich:dataTable value="#{revisions}" var="_revision">
<rich:column>
<f:facet name="header">Title</f:facet>
#{_revision[0].title}
</rich:column>
<rich:column>
<f:facet name="header">Contents</f:facet>
#{_revision[0].contents}
</rich:column>
<rich:column>
<f:facet name="header">Version</f:facet>
#{_revision[1]}
</rich:column>
<rich:column>
<f:facet name="header">Modified at</f:facet>
#{newsRevision.fetchDate(_revision[1])}
</rich:column>
<rich:column>
<f:facet name="header">Action</f:facet>
<s:link action="#{newsRevision.replaceRevision(_revision[0])}" value="Replace current instances"></s:link>
</rich:column>
</rich:dataTable>
<div style="clear:both"/>
</rich:panel>
</ui:define>
</ui:composition> |
Esta página precisa de receber um parâmetro chamado newsId, por isso, adicione as seguintes linhas no seu pages.xml
19 20 21 | <page view-id="/newsRevision.xhtml" action="#{newsRevision.fetchRevisions}"> <param name="id" value="#{newsRevision.newsId}"/> </page> |
A tela produzida pelo código acima é mostrada na figura abaixo:
Finalmente a nosso backing bean associado à página (NewsRevisionService.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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | package com.furiousbob.beans; import java.io.Serializable; import java.util.Date; import java.util.List; import javax.persistence.EntityManager; import org.jboss.envers.VersionsReader; import org.jboss.envers.VersionsReaderFactory; import org.jboss.envers.query.VersionsRestrictions; import org.jboss.seam.annotations.Factory; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.datamodel.DataModel; import com.furiousbob.com.News; @Name("newsRevision") public class NewsRevisionService implements Serializable { @In private EntityManager entityManager; private Long newsId; @DataModel(value="revisions") private List revisions; @Factory(value="revisions") public void fetchRevisions(){ VersionsReader reader = VersionsReaderFactory.get(entityManager); this.revisions = reader.createQuery().forRevisionsOfEntity(News.class, false).add(VersionsRestrictions.idEq(newsId)).getResultList(); } public Date fetchDate(long n){ VersionsReader reader = VersionsReaderFactory.get(entityManager); Date d = reader.getRevisionDate((int)n); return d; } public void replaceRevision(News n){ entityManager.merge(n); } public Long getNewsId() { return newsId; } public void setNewsId(Long newsId) { this.newsId = newsId; } } |
Para buscar as revisões de uma entidade, usamos o método fetchRevisions, para acessar o SessionFactory do envers, basta criarmos seu objeto e passarmos como referência nosso entityManager. O método createQuery().forRevisionsOfEntity(News.class, false) retorna uma lista de arrays de tamanho 2, onde a primeira posição é a entidade procurada e a segunda posição o número da revisão. Além disto precisamos buscar a data em que a revisão foi feita, note que em nosso template usamos a EL extendida do Seam para invocar um método no nosso backing bean passando um parâmetro para o mesmo #{newsRevision.fetchDate(_revision[1])}.
Nosso backing bean possui ainda um método para voltar com a versão da entidade, de acordo com a versão selecionada.
Este exemplo demonstra como é fácil gerar versionamento de entidades em seu sistema com o envers. Usando o envers e a API hibernate é possível realizar operações de restauração de objetos ou então apenas auditoria no seu sistema.
Isso é apenas um exemplo simples, uma funcionalidade desejável seria a adição do usuário corrente dentro de sua entidade, para isso basta usar as interfaces de callback do Hibernate (@PrePersist,@PreUpdate e @PreRemove) de forma que adicionem o usuário corrente em uma propriedade base de sua entidade.
Uma coisa que na minha opinião, faltou ao envers é expor de forma simples, além da versão e timestamp, o tipo de operação (INSERT,UPDATE,REMOVE) que foi realizada na entidade. Mas para isso, já abri um JIRA junto ao projeto, se você acha que esta funcionalidade vai ajudá-lo por favor vote nela para aumentar a chance de a mesma ser implementada
Bem, acho que ficamos por aqui, próximos posts vou sair um pouco de Seam e vou falar de WebServices, fiquem atentos.
Inté



