Trilha de auditoria em java

Provavelmente isso não é possível com o Envers porque nem é o escopo dele. O Envers serve para auditar entidades e não operações no banco de dados. Inclusive, você pode fazer queries com o entity manager dele para buscar uma entidade em um determinado instante de tempo, montar a entidade dessa forma seria muito complexo se o Envers auditasse operações no banco em vez da entidade como um todo.

Se você realmente precisa fazer a auditoria no banco de dados, o Envers não será de muita utilidade.[/quote]

Ataxexe,

Bem, o que você me sugere? Fazer auditoria criando mais uma tabela para cada uma existente, fica exagerada, em questão de tamanho, minha base de dados, pois, é isso que faz o Hibernate Envers. No meu caso são, por alto, 90 entidades, imagina.

Obrigado.

[quote=Ironlynx]eude.lacerda,
é interessante também conversar com o cliente para ver o que ele quer de verdade.Já tive que fazer um sistema com documentos em que NADA era realmente apagado.A diferença estava do que cada user/role podia ver no sistema. A marcação de deletar apenas deixava NÃO visível aquele documento. [/quote]

Melhor resposta que ficou obfuscada só porque não citou nenhum framework da moda.

Provavelmente isso não é possível com o Envers porque nem é o escopo dele. O Envers serve para auditar entidades e não operações no banco de dados. Inclusive, você pode fazer queries com o entity manager dele para buscar uma entidade em um determinado instante de tempo, montar a entidade dessa forma seria muito complexo se o Envers auditasse operações no banco em vez da entidade como um todo.

Se você realmente precisa fazer a auditoria no banco de dados, o Envers não será de muita utilidade.[/quote]

Ataxexe,

Bem, o que você me sugere? Fazer auditoria criando mais uma tabela para cada uma existente, fica exagerada, em questão de tamanho, minha base de dados, pois, é isso que faz o Hibernate Envers. No meu caso são, por alto, 90 entidades, imagina.

Obrigado.[/quote]

Mas a auditoria em uma única tabela também não é nada leve e, em alguns casos, produz até mais registros (quando são alterados muitos dados de uma entidade, por exemplo). Você também pode colocar as tabelas de auditoria em outro schema, limitar as colunas auditáveis. Mas isso só será uma solução para você se o alvo da auditoria for a entidade, se o alvo da auditoria for a coluna de uma tabela, nem tente usar o Envers.

Em termos de espaço, você decide se quer ter uma tabela com milhões de registro ou algumas tabelas com milhares. É uma conta que deve ser feita com cuidado porque pode favorecer qualquer um dos mecanismos de auditoria.

Em termos de praticidade, flexibilidade e, o mais importante, visualização, auditar a entidade é melhor. Por exemplo, como você faria para consultar os dados de uma entidade há 3 meses? Auditando a entidade é muito simples, auditando as colunas não seria tão simples assim.

O problema é você querer forçar a barra usando auditoria de entidades se não é isso que vai resolver o problema do cliente. Quando tem DBAs no meio, geralmente a auditoria de entidades vai pelo ralo (e quase sempre por motivos fúteis).

Bom dia, Pessoal,

Bom, encontrei um um código do Carlos Sotolani, que por sinal muito bem produzido, resolveu minha necessidade última de uma única entidade receber todas as alterações. Apresentei ao líder do projeto, e ele concordou. Então o que fiz, estudei o código e tentei adaptá-lo ao que precisava, funcionou perfeitamente, vejam o que fiz.

Classe: AuditListener

public class AuditListener{
    
    public static enum TipoOperacao{
        INCLUSAO,
        ATUALIZACAO,
        EXCLUSAO,
    }
    
    @PostPersist
     public void postPersist(Object object) {
        String id = null;

        // Busca pelo id
        Field[] fields = object.getClass().getDeclaredFields();

        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
                if (field.isAnnotationPresent(Id.class)) {
                    field.setAccessible(true);

                    try {
                        id = field.get(object).toString();
                    } catch (Exception e) {
                        return;
                    }

                    break;
                }
            }
        }
        // Fim

        if (id != null) {
            Auditoria audit = new Auditoria();
            audit.setNomeEntidade(object.getClass().getSimpleName());
            audit.setIdEntidade(Integer.parseInt(id));
            audit.setOperacao(String.valueOf(TipoOperacao.INCLUSAO));
            audit.setUsuario("CARLOS");
            audit.setData(new Date());
            audit.setAlteracao("");

            try {
                salvarAuditoria(audit);
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
        }
    }

     
     
    @PreUpdate
    public void preUpdate(Object object) {
        Object id = null;

        // Busca pelo id
        Field[] fields = object.getClass().getDeclaredFields();

        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
                if (field.isAnnotationPresent(Id.class)) {
                    field.setAccessible(true);

                    try {
                        id = field.get(object);
                    } catch (Exception e) {
                        return;
                    }
                }
            }
        }
        // Fim

        Object objectOld = null;

        try {
            objectOld = buscarObjetoPorIdNovaTransacao(object.getClass(), id);
        } catch (Exception e1) {
            return;
        }

        String alteracao = "";

        if (objectOld != null) {
            Field[] fieldsOld = objectOld.getClass().getDeclaredFields();

            if (fields != null && fields.length > 0 && fieldsOld != null && fieldsOld.length > 0) {

                for (Field field : fields) {
                    field.setAccessible(true);

                    Object fieldValue = null;

                    try {
                        fieldValue = field.get(object);
                    } catch (Exception e) {
                        return;
                    }

                    for (Field fieldOld : fieldsOld) {
                        if (field.getName().equals(fieldOld.getName())) {
                            fieldOld.setAccessible(true);

                            Object fieldValueOld = null;

                            try {
                                fieldValueOld = fieldOld.get(objectOld);
                            } catch (Exception e) {
                                return;
                            }

                            if (fieldValue instanceof String || fieldValue instanceof Integer || fieldValue instanceof Date) {
                                if (fieldValue == null && fieldValueOld == null) {
                                } else if (fieldValue == null && fieldValueOld != null) {
                                    alteracao += field.getName() + ": " + fieldValueOld + " para null";
                                } else if (fieldValue != null && fieldValueOld == null) {
                                    alteracao += field.getName() + ": null para " + fieldValue;
                                } else if (!fieldValue.equals(fieldValueOld)) {
                                    alteracao += field.getName() + ": " + fieldValueOld + " para " + fieldValue;
                                }
                            } else if (fieldValue instanceof List) {
                                if (fieldValue == null && fieldValueOld == null) {
                                } else if (fieldValue == null && fieldValueOld != null) {
                                    alteracao += field.getName() + ": " + ((List<?>) fieldValueOld).size() + " para null";
                                } else if (fieldValue != null && fieldValueOld == null) {
                                    alteracao += field.getName() + ": null para " + ((List<?>) fieldValue).size();
                                } else if (!fieldValue.equals(fieldValueOld)) {
                                    alteracao += field.getName() + ": " + ((List<?>) fieldValueOld).size() + " para " + ((List<?>) fieldValue).size();
                                }
                            }
                        }
                    }
                }
            }
        }

        if (!alteracao.equals("")) {

            Auditoria audit = new Auditoria();
            audit.setNomeEntidade(object.getClass().getSimpleName());
            audit.setIdEntidade(Integer.parseInt(id.toString()));
            audit.setOperacao(String.valueOf(TipoOperacao.ATUALIZACAO));
            audit.setUsuario("CARLOS");
            audit.setData(new Date());
            audit.setAlteracao(alteracao);
            
            try {
                salvarAuditoria(audit);
            } catch (Exception e) {
                return;

            }
        }
    }

    @PostRemove
    public void postRemove(Object object) {
        if (object != null) {

            String id = null;

            // Busca pelo id
            Field[] fields = object.getClass().getDeclaredFields();

            if (fields != null && fields.length > 0) {
                for (Field field : fields) {
                    if (field.isAnnotationPresent(Id.class)) {
                        field.setAccessible(true);

                        try {
                            id = field.get(object).toString();
                        } catch (Exception e) {
                            return;
                        }

                        break;
                    }
                }
            }
            // Fim

            if (id != null) {
                Auditoria audit = new Auditoria();
                audit.setNomeEntidade(object.getClass().getSimpleName());
                audit.setIdEntidade(Integer.parseInt(id));
                audit.setOperacao(String.valueOf(TipoOperacao.EXCLUSAO));
                audit.setUsuario("CARLOS");
                audit.setData(new Date());
                audit.setAlteracao("");
                try {
                    salvarAuditoria(audit);
                } catch (Exception e) {
                    e.getMessage();
                    return;
                }
            }
        }
    }

    public void salvarAuditoria(Auditoria audit){
       EntityManager em = JPAUtil.getEntityManager();
        em.getTransaction().begin();
        em.persist(audit);
        em.getTransaction().commit();
    }
    
    public Object buscarObjetoPorIdNovaTransacao(Class<?> classes, Object object) {
       return JPAUtil.getEntityManager().find(classes, object);
    }
}

Entidade: Auditoria

@Entity
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "Auditoria.buscaTodos", query = "SELECT f FROM Auditoria f")
})
public class Auditoria implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @SequenceGenerator(name="SEQ_AUDITORIA", sequenceName="SEQ_ID_AUDITORIA",initialValue = 1,allocationSize = 1)
    @GeneratedValue(generator="SEQ_AUDITORIA", strategy=GenerationType.SEQUENCE)
    private Long id;
    private String nomeEntidade;
    private int idEntidade;
    private String operacao;
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date data;
    @Column()
    private String alteracao;
    private String usuario;
    
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Auditoria)) {
            return false;
        }
        Auditoria other = (Auditoria) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "entidades.Auditoria[ id=" + id + " ]";
    }

    public String getNomeEntidade() {
        return nomeEntidade;
    }

    public void setNomeEntidade(String nomeEntidade) {
        this.nomeEntidade = nomeEntidade;
    }

    public int getIdEntidade() {
        return idEntidade;
    }

    public void setIdEntidade(int idEntidade) {
        this.idEntidade = idEntidade;
    }

    public String getOperacao() {
        return operacao;
    }

    public void setOperacao(String operacao) {
        this.operacao = operacao;
    }

    public Date getData() {
        return data;
    }

    public void setData(Date data) {
        this.data = data;
    }

    public String getAlteracao() {
        return alteracao;
    }

    public void setAlteracao(String alteracao) {
        this.alteracao = alteracao;
    }

    public String getUsuario() {
        return usuario;
    }

    public void setUsuario(String usuario) {
        this.usuario = usuario;
    }
    
}

E mágica acontece aqui, somente é inserida a anotação @EntityListener chamando AuditListener.class na entidade Pessoa.

@Entity
@Table(name = "Pessoa")
@EntityListeners({AuditListener.class})
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "Pessoa.buscaTodos", query = "SELECT f FROM Pessoa f")
})
public class Pessoa implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @SequenceGenerator(name="SEQ_PESSOA", sequenceName="SEQ_ID_PESSOA",initialValue = 1,allocationSize = 1)
    @GeneratedValue(generator="SEQ_PESSOA", strategy=GenerationType.SEQUENCE)
    private Long id;
    

    @Column(name = "nome",length = 40)
    private String nome;
    
    @Column(length = 11)
    private String cpf;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Pessoa)) {
            return false;
        }
        Pessoa other = (Pessoa) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "br.com.teste.entidades.Pessoa[ id=" + id + " ]";
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getCpf() {
        return cpf;
    }

    public void setCpf(String cpf) {
        this.cpf = cpf;
    }
    
}

Algumas vezes nem tudo que já vem pronto atende nossas necessidades.

Pessoal, obrigado pelas sugestões. Considero este post com fechado.

Se atualmente não existe previsão real para mudança de banco, não invente, use trigger. Usar bola de cristal só traz complexidade ou obscuridade.

Geralmente ADs que devem cuidar disso, usando ferramentas para gerar e manter esses scripts.

Se é para considerar previsões por somente “se acontecer”, se quiser prever também que outro sistema poderá acessar esse banco, a solução via hibernate estará furada, onde qualquer mexida por fora do sistema, até mesmo por invasão, não será logada, o que até é mais preocupante do que mudança de banco não prevista, que terá investimento quando necessário.

[quote=javaflex]Se atualmente não existe previsão real para mudança de banco, não invente, use trigger. Usar bola de cristal só traz complexidade ou obscuridade.

Geralmente ADs que devem cuidar disso, usando ferramentas para gerar e manter esses scripts.

Se é para considerar previsões por somente “se acontecer”, se quiser prever também que outro sistema poderá acessar esse banco, a solução via hibernate estará furada, onde qualquer mexida por fora do sistema, até mesmo por invasão, não será logada, o que até é mais preocupante do que mudança de banco não prevista, que terá investimento quando necessário.[/quote]

Concordo. Existem tantos pontos de falha e de baixo desempenho no código acima que é mais simples dar manutenção em uma trigger do que nele.

Se quiser usar realmente os interceptors, é bom dar um banho de loja no código pra ele não se tornar um vilão no desempenho do sistema a na manutenção.

Alguns exemplos em uma rápida analisada no código:

  • Reflection usada a torto e a direito em uma entidade sendo que poderia ser feito um cache na primeira vez
  • Somente campos declarados como atributos podem ser auditados
  • Apenas o uso de anotações em atributos é suportado (eu prefiro, mas vai que alguém coloca as anotações em getters, já que a especificação permite…)
  • Terrível uso de instanceof e inúmeros ifs tornam o código muito propenso a ser deixado de lado quando houver problemas
  • Péssimo tratamento de erros (o famoso PokemonExceptionHandler, pega todas as exceções e guarda na sua “pokebola” pra ninguém mais ver o que aconteceu)

Talvez o esforço necessário para criar um código enxuto e flexível para o seu caso seja mais caro do que usar uma solução direto no banco de dados, é importante colocar isso na ponta do lápis.

[quote=Ataxexe][quote=javaflex]Se atualmente não existe previsão real para mudança de banco, não invente, use trigger. Usar bola de cristal só traz complexidade ou obscuridade.

Geralmente ADs que devem cuidar disso, usando ferramentas para gerar e manter esses scripts.

Se é para considerar previsões por somente “se acontecer”, se quiser prever também que outro sistema poderá acessar esse banco, a solução via hibernate estará furada, onde qualquer mexida por fora do sistema, até mesmo por invasão, não será logada, o que até é mais preocupante do que mudança de banco não prevista, que terá investimento quando necessário.[/quote]

Concordo. Existem tantos pontos de falha e de baixo desempenho no código acima que é mais simples dar manutenção em uma trigger do que nele.

Se quiser usar realmente os interceptors, é bom dar um banho de loja no código pra ele não se tornar um vilão no desempenho do sistema a na manutenção.

Alguns exemplos em uma rápida analisada no código:

  • Reflection usada a torto e a direito em uma entidade sendo que poderia ser feito um cache na primeira vez
  • Somente campos declarados como atributos podem ser auditados
  • Apenas o uso de anotações em atributos é suportado (eu prefiro, mas vai que alguém coloca as anotações em getters, já que a especificação permite…)
  • Terrível uso de instanceof e inúmeros ifs tornam o código muito propenso a ser deixado de lado quando houver problemas
  • Péssimo tratamento de erros (o famoso PokemonExceptionHandler, pega todas as exceções e guarda na sua “pokebola” pra ninguém mais ver o que aconteceu)

Talvez o esforço necessário para criar um código enxuto e flexível para o seu caso seja mais caro do que usar uma solução direto no banco de dados, é importante colocar isso na ponta do lápis.[/quote]
Ótimas observações. Seguir o que é mais usado pelos responsáveis da área específica é sempre menos arriscado.

[quote=Ataxexe]Concordo. Existem tantos pontos de falha e de baixo desempenho no código acima que é mais simples dar manutenção em uma trigger do que nele.

Se quiser usar realmente os interceptors, é bom dar um banho de loja no código pra ele não se tornar um vilão no desempenho do sistema a na manutenção.

Alguns exemplos em uma rápida analisada no código:

  • Reflection usada a torto e a direito em uma entidade sendo que poderia ser feito um cache na primeira vez
  • Somente campos declarados como atributos podem ser auditados
  • Apenas o uso de anotações em atributos é suportado (eu prefiro, mas vai que alguém coloca as anotações em getters, já que a especificação permite…)
  • Terrível uso de instanceof e inúmeros ifs tornam o código muito propenso a ser deixado de lado quando houver problemas
  • Péssimo tratamento de erros (o famoso PokemonExceptionHandler, pega todas as exceções e guarda na sua “pokebola” pra ninguém mais ver o que aconteceu)

Talvez o esforço necessário para criar um código enxuto e flexível para o seu caso seja mais caro do que usar uma solução direto no banco de dados, é importante colocar isso na ponta do lápis.[/quote]
A única coisa que eu pensei ao ver o código foi: “coitado de quem der manutenção”.

Isso para mim é o famoso código bomba…

Boa tarde, pessoal,

Vejo que estão criticando código que foi postado, como solução para meu problema, peço a vocês que apresentem um solução para tal. Vim aqui com o intuito de pedir ajuda, quando apresentei uma solução para compartilhar com todos, criticam aquilo que postei. Acho que me enganei com este fórum.

Obrigado.

[quote=eude.lacerda]Boa tarde, pessoal,

Vejo que estão criticando código que foi postado, como solução para meu problema, peço a vocês que apresentem um solução para tal. Vim aqui com o intuito de pedir ajuda, quando apresentei uma solução para compartilhar com todos, criticam aquilo que postei. Acho que me enganei com este fórum.

Obrigado. [/quote]

Você não se enganou não. Fórum não é local de passar a mão na cabeça.

Eu listei vários problemas que podem ser usados pra solucionar seu código, se eu gastar um tempo corrigindo ele estarei trabalhando de graça pra você, como eu não posso fazer isso ainda (preciso trabalhar para a empresa que me contratou) achei melhor colocar os pontos falhos do código na esperança de que você pudesse corrigí-los e pedir ajudas pontuais em caso de dúvidas.

Mas parece que você quer que a gente ajude concordando com você, não é? Quando as pessoas dizem que está lindo não precisam dizer o motivo e tudo está bem, agora, se alguém diz que está feio e mostra onde está feio é tratado como se não quisesse ajudar…vai entender.

Nós demos uma solução viável (as triggers) e eu apontei problemas no código que devem ser cuidadosamente revistos caso você queira usar os interceptors (está lá na minha mensagem, leia novamente e verá que eu em momento algum disse pra você não usar).

Tentando novamente ajudar um pouco, responda as perguntas a si mesmo:

1- Você sabe usar reflection?
2- Você consegue refatorar um código valendo-se dos conceitos de orientação a objetos e padrões de projeto?

Se não puder responder “sim” para as duas perguntas acima, desconsidere usar o código citado ou estude bastante antes de colocá-lo em produção. Eu, como cliente, iria detestar usar um produto cujo próprio fabricante desconhece o que colocou lá dentro (sei que existem vários por aí e que eu, sim, provavelmente uso, mas digo isso porque eu me recuso a colocar coisas que não conheço em algo que estou fazendo).

Agora, se for ficar de mimimi é melhor mudar de fórum mesmo, pois apontar erros e sugerir outras soluções (como estamos fazendo aqui) é, sim, uma forma de ajudar e é, sim, como os participantes sérios do GUJ costumam fazer.

1 curtida

[quote]Boa tarde, pessoal,

Vejo que estão criticando código que foi postado, como solução para meu problema, peço a vocês que apresentem um solução para tal. Vim aqui com o intuito de pedir ajuda, quando apresentei uma solução para compartilhar com todos, criticam aquilo que postei. Acho que me enganei com este fórum.

Obrigado.[/quote]

Até entendo seu desespero, mas é claro que aqui dificilmente vão fazer tudo pra você. O máximo que as pessoas podem fazer é apontar caminhos. Já foi dado milhares de caminhos aqui que você pode seguir. Como já foi dito acima, escolha um dos caminhos e depois pergunte pontualmente sobre cada problema que poder vir a ter.

[quote=rodrigo.uchoa][quote]Boa tarde, pessoal,

Vejo que estão criticando código que foi postado, como solução para meu problema, peço a vocês que apresentem um solução para tal. Vim aqui com o intuito de pedir ajuda, quando apresentei uma solução para compartilhar com todos, criticam aquilo que postei. Acho que me enganei com este fórum.

Obrigado.[/quote]

Até entendo seu desespero, mas é claro que aqui dificilmente vão fazer tudo pra você. O máximo que as pessoas podem fazer é apontar caminhos. Já foi dado milhares de caminhos aqui que você pode seguir. Como já foi dito acima, escolha um dos caminhos e depois pergunte pontualmente sobre cada problema que poder vir a ter.

[/quote]

Não quero que façam por mim. Estou irritado devido… puxa! Foi a única solução que encontrei, que atendeu a minha necessidade, cuja a que tentei não deu, Hibernate Envers. E que o projeto pede, então, não vejo que estou querendo que façam por mim.

Relaxa. Se a única que deu certo foi aquela que você postou o código, vai com ela por enquanto e na medida do possível tenta melhorar os pontos que foram ditos aqui.

Só quem conhece o seu problema de verdade é você, pra saber a melhor solução.

[quote=eude.lacerda]Boa tarde, pessoal,

Vejo que estão criticando código que foi postado, como solução para meu problema, peço a vocês que apresentem um solução para tal. [/quote]
Trigger.

[quote=eude.lacerda]Boa tarde, pessoal,

Vejo que estão criticando código que foi postado, como solução para meu problema, peço a vocês que apresentem um solução para tal. Vim aqui com o intuito de pedir ajuda, quando apresentei uma solução para compartilhar com todos, criticam aquilo que postei. Acho que me enganei com este fórum.

Obrigado. [/quote]
Entenda que não é só por que foi a “única”, não quer dizer que é viável. As vezes vale a pena rever os requisitos, ou até mesmo, as tecnologias.

Como foi dito, várias soluções foram apresentadas. Qualquer opção que você escolhesse estaria sujeita à críticas, umas mais que outras.

OBS.: Se você for reagir desse modo toda vez que alguém bater de frente com algo que você fez, a vida vai ser ainda mais difícil para você. O jeito é sempre parar e analisar: “o que tem de errado nessa situação? o que pode ser mudado?” [=

Se o líder aprovou então vá em frente…

Sensacional!

Previsão para o futuro:
“Daqui 4 meses o requisito muda, você vai olhar pra esse código, não vai entender bulhufas, vai buscar outra solução pra auditoria e de quebra, muito provavelmente, irá precisar migrar oq foi auditado durante os 4 meses. Deixar numa tabela só, hmmmmm, sei ñao.”

SGBD são sistemas que existem há trocentos anos, são mantidos de forma séria e resolvem diversos problemas comuns de infraestrutura. Como a galera, já destacou, “Trigger for the rescue!”.

Galera,

O requisito é esse: “Toda e qualquer INSERÇÃO / ALTERAÇÃO / EXCLUSÃO, deve ser registrada pelo sistema”. Foi exigência do cliente. Então, pronto.

Como disse anteriormente, se o líder aprovou, ignore os haters…

[quote=eude.lacerda]Galera,

O requisito é esse: “Toda e qualquer INSERÇÃO / ALTERAÇÃO / EXCLUSÃO, deve ser registrada pelo sistema”. Foi exigência do cliente. Então, pronto. [/quote]

Apenas duas colocações:

  1. As pessoas estão criticando a solução (como foi feito) e não a especificação (o que é pra ser feito).

  2. A especificação é feita pelo analista e a solução aprovada pelo líder do projeto, em ambos os casos o cliente não tem na a ver com a historia.

[quote=eude.lacerda]Galera,

O requisito é esse: “Toda e qualquer INSERÇÃO / ALTERAÇÃO / EXCLUSÃO, deve ser registrada pelo sistema”. Foi exigência do cliente. Então, pronto. [/quote]Então de onde saiu o requisito de que coluna A deve ser salva e a B não?