Capítulo Regras de Negócio & Batch 6 A 17 Implementação do Caso de Uso “UC005 Calcular Folha de Pagamento!” - Analisando a Especificação – Caso de Uso Principal O nosso próximo Caso de Uso não irá se encaixar em nenhum padrão do jCompany Patterns & Methods e esta é uma situação de se esperar. Toda aplicação possuirá algum percentual de demandas deste tipo - não generalizáveis por frameworks “horizontais”, ou porque são Casos de Uso que não possuem cenários previsíveis, repetitivos, passíveis de padronização; ou porque são, fundamentalmente, agrupamentos de regras de negócio (como é o nosso caso). Ainda assim reutilizaremos diversos serviços úteis do jCompany FS Framework. Afinal, como vimos no capítulo 1, o jCompany Developer Suite é muito mais que os geradores do jCompany IDE, utilizados para Casos de Uso Padrões. Figura E17.1. Diagrama de Caso de Uso para “UC005 Calcular Folha de Pagamento!”. Como não é um Caso de Uso Padrão, esta especificação requer maiores explicações. Note que este é um Caso de Uso disparado por algum mecanismo de “temporização”, ou de “escalonamento”, simbolizado pelo Ator com a palavra reservada “Tempo”. Nestes casos, a interatividade em si do Ator com o Caso de Uso é trivial, sendo o cenário basicamente definido por um passo de “disparo” e um segundo passo de reação da aplicação. A descrição de cenário para este Caso de Uso “UC005” está, portanto, definida da seguinte forma no repositório da ferramenta CASE: 1. O escalonador automático dispara o Cálculo da Folha, todo dia 3 de cada mês, a partir de 01h00min da madrugada. 2. A aplicação recupera todos os funcionários e executa cálculo para cada um, segundo regras definidas em “REQ001 - Cálculo de Folha”. Capítulo E17 Veja que o cálculo da folha de pagamento em si é definido em um requisito à parte. O cálculo que iremos exemplificar é bem simplificado e altamente fictício, mas suficiente para nossos propósitos. Ele é reproduzido abaixo: “Para cada funcionário com horas trabalhadas informadas para o período de referência, imediatamente anterior ao corrente (último mês), o cálculo deve ser feito da seguinte forma: 1. Pegar o último salário base de seu histórico funcional. 2. Calcular o ‘salário inicial’ tendo como base 22 dias úteis, e realizando cálculo proporcional (regra de três) com o número de dias efetivamente trabalhados informados em provento do tipo DT. 3. Em seguida calcular o ‘salário bruto’ somando os proventos gerais e subtraindo os descontos gerais, dos lançamentos de Proventos e Descontos para o mês. Exceções a tratar: 3.1. Se o número de dias trabalhados informado for mais que 22, gerar erro 'Numero de dias ultrapassa o possível para o período'. 3.2. Se o salário bruto for negativo, gerar erro 'Cálculo de salário negativo para funcionário [nome - cpf]'. 3.3 Se o cálculo estiver correto, gravar ocorrência em ProventoDesconto com natureza SL, com o valor calculado. 4. Calcular o IR com base no ‘salário bruto’, descontando 15% após deduzir 200,00 para cada dependente. Ex: Para um funcionário com salário de 1.400,00 com dois dependentes, o IR deverá ser 15% de 1.000,00, ou seja, 150,00. 4.1. Se a dedução for maior que o salário, gravar IR com zero. 4.2. Gravar ocorrência em ProventoDesconto com natureza IR. 5. Calcular ‘salário liquido’ final, subtraindo o IR do ‘salário bruto’ e gravando-o em Provento com tipo SL. 6. Se alguma exceção ocorrer durante um cálculo especifico, ele deve ser interrompido e o erro enviado por e-mail para “[email protected]”, além de gravado em log de erro. O processamento deve continuar para os demais. 7. Ao final, se todos os funcionários possuírem o salário calculado, a aplicação gera um fechamento de período, atualizando “anoMesFechamento”. Uma mensagem de sucesso também deve ser enviada para o log”. Foi definida uma Colaboração com o estereótipo “plcBatch”, que não nos traz muita informação, diferentemente das Colaborações Padrões anteriores. A marcação indica apenas que deveremos utilizar um disparo escalonado para esta rotina e implementar sua orquestração a partir de uma classe de negócio/domínio dedicada a processamento de cálculos, chamada “CalculoFolha”. O grafo de entidades envolvido costuma ser grande em programações batch. Em nosso exemplo, ele está representado pela Figura E17.2. Figura E17.2. Grafo de Entidades, envolvido no Caso de Uso “UC005 Calcular Folha de Pagamento!”. Regras de Negócio e Batch Uma nova classe “FolhaPagamento” foi definida para abrigar o valor do último período de fechamento. Esta classe foi estereotipada como “plcPrefAplicacao”, pois contém somente um objeto e é um padrão definido pelo jCompany Patterns & Methods. O jCompany IDE, inclusive, pode gerar um Caso de Uso Padrão para sua manutenção interativa, se preciso for – não é o nosso caso, já que iremos gravar o “anoMesUltimoFechamento” programaticamente. Note que modelamos a referência à classe “FolhaPagamento” em “ProventoDesconto” como uma “variável de classe” dinâmica. Esta é uma conceituação opcional, mas, fazendo deste modo, conseguimos rastrear a classe “ProventoDesconto” com “FolhaPagamento”. Finalmente uma parte do grafo de Funcionário foi incluída pois o número de seus dependentes e seu último salário serão ambos utilizados no cálculo. É uma boa prática de modelagem explicitar claramente o grafo de Entidades envolvidas no Caso de Uso. - Analisando a Especificação – Extensão Existe uma Extensão definida para nosso Caso de Uso, utilizada por usuários com papel “FolhaPagamento”, que permite a estes usuários informarem um período não encerrado (somente o mês corrente ou o anterior, por exemplo) e dispararem o mesmo cálculo que roda em batch, de forma interativa. Os requisitos são os mesmos, mas há uma restrição nova, já que o período agora será informado: “RES01. Verifica Período. O sistema verifica se o período é válido, ou seja, se é maior que o 'anoMesFechamento'. Se não for, impede o cálculo e exibe mensagem 'Não é permitido fazer cálculo para este período, pois ele já foi encerrado'.” O formulário a ser utilizado é, também, fora do padrão, apresentando botão de disparo ao lado do campo de período e as mensagens abaixo do botão de disparo. As mensagens estão em cores distintas, utilizando verde para o número de calculados com sucesso e vermelho para o número com problemas e mensagens de erro. As mensagens de erro, além de exibidas para o usuário, também devem ser enviadas por e-mail e log. A Colaboração que irá implementar a extensão possui estereótipo “plControle”, conhecido também como “Controle Simples”, que é na verdade um padrão “livre” do jCompany, se é que se pode dizer assim. É uma Colaboração utilizada quando se deseja realizar implementações livremente, a partir da camada Controle, mas ainda utilizando leiautes Facelets e possivelmente anotações de metadados para acionar reúso de recursos do framework. Veremos a implementação desta extensão no próximo capítulo. - Obtendo a Entidade em Java Vamos criar a única Entidade nova “FolhaPagamento” manualmente através do Eclipse, conforme a Figura E17.3. 1. Crie um novo pacote “folhaPagamento”, abaixo de “entidade” em “rhtutorial_commons”, e digite o código abaixo. Figura E17.3. Novo pacote “folhaPagamento”, abaixo de “entidade” e classe “FolhaPagamento”. - Gerando Mapeamento para Classe - Objeto-Relacional VI 1. Edite a classe “FolhaPagamento” e aperte “Control+N” para acionar as opções de plugins. Selecione “01 – Mapeamento Objeto-Relacional”. As informações importantes para o caso do mapeamento O-R de “FolhaPagamento” se encontram na Figura E17.4. Capítulo E17 Figura E17.4. Mapeamento Objeto-Relacional para "FolhaPagamento". Modifique apenas a indicação de “lookup” (para usar anoMesUltimoFechamento no “toString” da Classe) e tamanho “8” (desnecessário para o JPA, mas pode ser útil se formos gerar formulários que contenham esta data). 2. Após a geração, edite a classe “FolhaPagamentoEntity” e ajuste o método "toString" para exibir a data com máscara “MM/yyyy”, utilizando o SimpleDateFormat, por exemplo. Figura E17.5. Ajuste na exibição da Data em FolhaPagamentoEntity. Note que o jCompany sempre gera uma NamedQuery buscando o "Object-Id + Propriedades de Lookup", com sufixo padrão “querySelLookup”. A query nomeada será útil em tempo de programação, para recuperarmos esta entidade e formatarmos mensagens de erro com a data. - Implementando Classes de Negócio – Programação de Camada Modelo I Vamos realizar então nossa primeira programação em camada Modelo, utilizando classes típicas para Regras de Domínio/Negócio. Crie a classe CalculaFolha no projeto “rhtutorial_model” definindo um novo sub-pacote “folhaPagamento” abaixo de “modelo”. Figura E17.6. Criação de classe de CalculaFolha na camada Modelo. #1. Serviços de negócio ficam no projeto “rhtutorial_model”. Regras de Negócio e Batch #2. Ao criar a classe, após posicionar no pacote “com.empresa.rhtutorial.model” e acionar “New Java Class” com o clique direito do mouse, pode-se complementar o pacote origem com extensões como “.folhaPagamento”, para que esta extensão seja criada em conjunto com a classe. #3. Digite o nome da classe, conforme especificado. 2. Implemente o algoritmo principal de orquestração da Regra, conforme a Figura E17.7. Figura E17.7. Codificação do método principal de orquestração. #1. Caso não se esteja utilizando um ambiente que proveja “rastreabilidade” entre requisitos e código (como uma suíte de ALM), a inclusão do identificador no Javadoc é recomendada. Na camada Modelo, em regras do negócio, o Javadoc é especialmente importante. #2. Definimos um objeto de exceção especializado para encapsular exceções internas geradas em nosso cálculo. Vamos discuti-los no próximo tópico. #3. Perceba que definimos o método como “protected”! Como somente iremos acioná-lo via escalonador, esta é uma restrição útil. Veremos a técnica de disparo mais a frente. Poderíamos também não receber argumentos, inferindo o mês de referência dentro de nosso método que seria o mês anterior ao que estamos. Porém, deste modo o tornamos menos reutilizável. Deixaremos pré-condições relacionadas à temporalidade em si para a tarefa de escalonamento, que descobrirá o período e nos informará. Além do mais, já sabemos que haverá um disparo interativo informando “mês de referência” para cálculo, o que reforça esta nossa estratégia. #4. Vamos adiar a recuperação da lista de funcionários, por enquanto, para nos concentrarmos no algoritmo de orquestração em si. Mas uma coisa é certa: iremos precisar de um serviço de persistência que traga uma coleção de subgrafos de “Funcionario” contendo somente aqueles que possuem “DT” (Dias Trabalhados) informados para o período. Benefício do JPA: em nossas regras de negócio não iremos usar “Result Set” JDBC ou qualquer outro “conjunto relacional” como fonte de dados, mas nosso modelo de Entidades de Domínio, expresso pela declaração do tipo de retorno como “List<Funcionario>”. Isso tornará nossas regras mais legíveis e manuteníveis, um dos grandes benefícios do JPA . #5. Iremos totalizar os funcionários calculados corretamente, conforme requisitado. Capítulo E17 #6. Manteremos uma lista de exceções geradas em cálculos individuais de funcionários para encapsular as causas de erro associadas a alguns dados do funcionário. #7. Os elementos de programação tipicamente encontrados em orquestradores de regras de negócio serão laços e condicionais. #8. É uma boa prática subdividir métodos de orquestração para mantê-los compreensíveis e reutilizáveis. Na sequência, continuaremos a desenvolver nosso raciocínio procedimental, para cada um dos métodos faltantes. Neste caso, teremos um método em separado para o cálculo do pagamento de cada funcionário. #9. O método que calcula salário para cada funcionário, caso encontre algum dos erros apontados na especificação, irá disparar a exceção “CalculoFolhaFuncionarioException”, que neste tratamento será acumulada, conforme solicitado. #10. Em nosso caso, como recomendado para lógicas de atualização em lote, não devemos encerrar todas as gravações em uma única ULT (Unidade Lógica de Transação ou “commit”). Correríamos o risco, neste caso, de comprometer recursos do SGBD excessivamente, já que transações muito longas (conhecidas como “long running transactions”) aumentam perigosamente o tamanho e esforço do SGBD para manutenção de áreas de “log/redo”, dentre outros problemas. Além disso, um erro ao final do processo nos faria perder todos os cálculos corretos que fizemos até ali. Por estes motivos, é recomendável gerenciarmos transações manualmente em programações batch de alto volume – ainda assim reutilizando alguns serviços do framework. No nosso caso, vamos efetuar um “commit” a cada 100 funcionários calculados corretamente, chamando o serviço “commit()” disponível na implementação de DAO do jCompany FS Framework. Ao final, também faremos uma chamada adicional, para garantir a confirmação dos restantes, abaixo de 100. #11. Se, ao final do processamento, houver algum erro de cálculo individual, nosso método irá encerrar disparando uma exceção principal que irá encapsular as várias exceções individuais e também o total calculado “ok”. Estas são informações que projetamos para que o método “chamador” (que chamou o atual) possa tratar o erro e apresentá-lo conforme requisitado. #12. Precisaremos também de uma função que verifique se o total de cálculos efetuados até aqui é igual ao universo de cálculo potencial. Isso porque, segundo a especificação, neste caso devemos atualizar a data do último fechamento. #13. Se a função não disparar uma exceção, ela deve devolver o total de funcionários cujo salário foi calculado para exibição da mensagem de confirmação, no padrão solicitado. - Implementando Objetos de Exceção (Exception) – Programação de Camada Modelo II Como teremos que devolver muitas informações em caso de exceções (não somente uma mensagem), nós decidimos utilizar objetos de Exceção especiais para encapsulá-las. Ao decidirmos por “disparar uma exceção” em nosso método, delegamos a responsabilidade de finalizar o tratamento de erros para um método “cliente”, mas continuamos responsáveis por prover todas as informações necessárias para tanto, encapsuladas nos objetos de Exceção. As classes de Exceção devem ser criadas na camada ortogonal “comuns” em “rhtutorial_commons”, pois objetos deste tipo podem ser disparados entre camadas. Além disso devem especializar a classe “java.lang.Exception”. 1. Siga as orientações das Figura E17.8 e Figura E17.9 para criar as classes de Exceção que utilizaremos. Regras de Negócio e Batch Figura E17.8. Exceção que encapsula problemas em cálculos individuais de funcionários. #1. Crie a classe com nome “CalculoFolhaFuncionarioException” abaixo de “com.empresa.rhtutorial.commons", em “rhtutorial_comons”. #2. Esta classe deve herdar de “java.lang.Exception”, ancestral para exceções controladas. #3. Declare as propriedades "messageKey", “nomeFuncionario” e “cpfFuncionario”. #4. Note que não foram criados “setters” para as novas propriedades. Deste modo, elas somente podem ser incluídas através de um novo construtor que também recebe a mensagem do problema em si. Figura E17.9. Exceção que encapsula todos os problemas de um cálculo. #1. A segunda classe de Exceção deve ser criada com nome “CalculoFolhaException” no mesmo diretório da anterior e também herdando de “java.lang.Exception”. Capítulo E17 #2. Ela irá encapsular uma coleção de exceções individuais e também o total de calculados ok. #3. Da mesma forma (é um padrão para classes Exception), as novas variáveis somente são recebidas em construtores, imutáveis. - Implementando Data Access Objects (DAO) – Programação de Persistência I Vamos agora escrever o nosso primeiro serviço de persistência específico, na Classe “FuncionarioDAO”, que provê serviços de acesso a funcionários. 1. Codifique o método "recuperaComDTInformado" em "FuncionarioDAO". 2. A primeira coisa que métodos de DAO tipicamente conterão é uma cláusula “try-catch” para garantir tratamento contra problemas externos inesperados. No caso específico destas classes, presentes em uma camada considerada de “integração”, existem diversos externos possíveis, como problemas de JDBC, Pool de Conexões, SGBD-R, Rede, etc. Utilize o tratamento padrão “pauta mínima” do jCompany simplesmente digitando “try” + Control + Space. Um “snippet” (trecho padrão de código) pré-configurado no Eclipse pelo jCompany gera todo o código necessário. Figura E17.10. Digitação do “try-catch” padrão do jCompany. 3. Utilize "control+shift+O" para importar dependências, especialmente a exceção padrão para problemas inesperados, "PlcException". Note também que uma ”classe de log” (do framework Log4j) é utilizada no disparo da exceção, mas não é preciso declará-la, pois já está injetada pelo CDI no ancestral "PlcBaseJPADAO". Figura E17.11. Tratamento de try-catch padrão gerado via “snippet” e classe de log declarada. 4. Vamos agora obter uma sessão de persistência. Se estivéssemos utilizando EJB 3.1 (que está fora do escopo deste livro) poderíamos usar CDI para injetar o EntityManager como variável de instância do FuncionarioDAO. Mas como estamos com POJOs, podemos obter a sessão de persistência com o método "getEntityManager" como abaixo: ... @Inject PlcBaseContextVO context; … EntityManager entityManager = getEntityManager(context); … Código E17.1. Obtenção de sessão de persistência para fábrica default. Regras de Negócio e Batch O context é um objeto que segue o Design Pattern "Context Param". Trata-se de um POJO que encapsula informações de contexto na camada de Modelo, tais como perfil do usuário corrente, fábrica JPA, definição do grafo de entidades (metadados), dentre alguns outros... Este objeto pode ser modificado para a obtenção de acesso a outras fábricas, mas em nosso caso basta recebê-lo por injeção e repassá-lo. 5. O método de persistência utilizando NamedQuery fica conforme abaixo: Figura E17.12. Método típico de DAO. #1. Podemos, ao final da cláusula “createQuery” ou "createNamedQuery", incluir quantos “setParameter” aninhados desejarmos para passar os valores dos argumentos. No nosso caso, somente temos um. #2. Um último comando “getResultList”, ao final de todo o comando, retorna uma coleção do tipo “List<Object[]>”, com cada posição do array interno de objetos trazendo o valor de retorno de uma propriedade na ordem em que aparecem na cláusula “select”. 6. A query em si deve ser declarada na Entidade, tendo assim sua sintaxe verificada em tempo de inicialização da fábrica de persistência. Figura E17.13. NamedQuery em sintaxe JPA-QL #1. Um primeiro objetivo foi trazer, em uma única cláusula, todas as informações do “grafo de consulta” que precisamos incluindo o Object-Id (identificador interno do Funcionário), o “nome” e “cpf”, além do valor de “Dias Trabalhados” em “ProventoDesconto”, o total de dependentes e seu salário atual (imaginando, de forma simplificada, que podemos inferir o salário atual como o “maior”). Note que funções de agregação, expressões, subqueries e outras técnicas consagradas pelo SQL estão presentes em JPAQL. Capítulo E17 #2. Nossa classe “pivô” escolhida foi “ProventoDesconto”, pois dela consegue-se partir para todas as demais! Este é um ponto importante em JPAQL: Se partíssemos de “Funcionario” não chegaríamos em “ProventoDesconto” via navegação OO, já que não temos um “OneToMany” de “Funcionario” para “ProventoDesconto”*. Veja o grafo percorrido no diagrama da Figura E17.14, possibilitado pelas setas de navegação. O símbolo de composição sempre indica uma navegação possível, no caso de ”ProventoDesconto -> Funcionario”. Mas não temos seta de “Funcionario -> ProventoDesconto”†. Figura E17.14. Grafo percorrido no modelo de classes, por nossa cláusula JPAQL. #3. A navegação (junção com “join”) de “ProventoDesconto” para “Funcionario” é obrigatória‡. De fato, pela associação, um “ProventoDesconto” deve ter no mínimo 1 (um) “Funcionario”. #4. Já a navegação de funcionário para seus dependentes é explicitamente codificada como “left join”§. Em nosso caso, sabemos que existem funcionários que não possuem dependentes (veja cardinalidade mínima 0!). Portanto, esta é a estratégia correta - deste modo, funcionários que não tenham dependentes também serão recuperados. #5. A navegação de funcionário com seu histórico, por sua vez, é obrigatória novamente já que a multiplicidade mínima é também 1 (um). Importante: Perceba que todas as associações de “join” são definidas utilizando o alias “pd”, ou seja, partindo da classe “pivô” e navegando com notação de pontos pelo grafo de objetos. #6. Na cláusula “where” utilizamos dois tipos de filtros: - Para a enumeração “naturezaProventoDesconto”, recuperada através da constante “DT”, podemos simplesmente utilizar valores alfanuméricos dentro de aspas simples, como no SQL (e como se seu tipo fosse um String); - Já para o “anoMesReferencia” será enviado um argumento, nomeado de forma idêntica à propriedade (poderia ser qualquer nome desejado). #7. Como estamos utilizando funções de agregação “count” e “max” juntamente com valores não agregados, precisamos colocar as demais propriedades ausentes nas agregações, na cláusula “group by”, tal como faríamos em SQL. - Aprimorando cláusulas de JPAQL – Programação de Persistência II Poderíamos nos dar por satisfeitos com a versão atual de nosso método de DAO: o Ele trata exceção apropriadamente, utilizando logging para arquivo, envio de e-mail e exibição de problemas inesperados para usuários. * Também seria possível, a despeito do mapeamento, realizar “joins” relacionais entre estas classes mas, deste modo, a solução fica mais extensa e menos elegante. † Note que um modelo de classes que também traga as “roles” das associações (propriedades de navegação) explicitadas pode facilitar bastante na confecção das queries. ‡ Se informado somente “join”, o “inner join” é assumido como default. § Se informado “left” ou “right”, um “outer join” é utilizado mudando apenas a direção do “outer”. Regras de Negócio e Batch o Ele reutiliza o gerenciamento de sessões de persistência, transações e pool de conexões de camadas de arquitetura, evitando problemas sérios que podem advir do uso programático, em quaisquer destas áreas. o Ele utiliza bem o recurso de JPAQL, procurando trazer em um único comando o maior conjunto de dados úteis possível. Ao evitar o envio de comandos a cada iteração de cálculo, ou minimizá-lo*, otimizamos significantemente a performance. o Ele utiliza “prepared stament”, passando os argumentos via “setParameter”, ao invés de concatená-los à cláusula JPAQL principal. Isto não somente é mais seguro, como mais performático. Porém, no estágio em que nosso método se encontra, utilizando “select f.id, f.cpf, f.nome, pd.valor, count(d.id), max(hp.salario) ...”, ele ainda é um péssimo exemplo. O seu uso forçaria cada método “cliente” (e esperamos que sejam vários, ao longo do tempo) a receber tipos pobres no estilo “COBOL” ou “Cliente/Servidor Relacional”, como exemplificado no Código E17.2. // 1 List<Object[]> listaComDTInformadoNoPeriodo = funcDAO.recuperaComDTInformado(anoMesReferencia); for (Iterator i = listaComDTInformadoNoPeriodo.iterator(); i.hasNext();) { // 2. Recebimento do tipo "result set" Object[] resultado = (Object[]) i.next(); ... Long idFuncionario = (Long) resultado[0]; String cpfFuncionario = (String) resultado[1]; String nomeFuncionario = (String) resultado[2]; BigDecimal proventoDescontoDiasTrabalhados = (BigDecimal) resultado[3]; Long dependenteTotal = Long resultado[4]; BigDecimal salarioAtual = (BigDecimal) resultado[5]; Código E17.2. Recebimento de retorno do tipo “result set” com List<Object[]>. #1. Cláusulas JPAQL que usam “select <propriedades>” retornam coleções de vetores de objetos, “List<Object[]>”. #2. O laço que irá percorrer a coleção deve, portanto, fazer o casting apropriado de cada atributo para cada tipo correto (veja que neste pequeno exemplo de JPAQL já temos uma mistura de três tipos distintos). Este padrão não somente é pouco produtivo para quem reutiliza o serviço de dados, mas também sujeito a diversos erros e extremamente inflexível. Uma eventual introdução de novas propriedades na cláusula “select” pode introduzir erros graves no método “cliente”, não detectáveis em tempo de compilação e, o que é pior, muitas vezes nem em tempo de execução! Faça uma reflexão: Qual o nível de caos aconteceria se, em uma manutenção futura do método de DAO, introduzíssemos o retorno de um valor adicional com o total de descontos, antes do salário, como “select f.id, f.cpf, f.nome, pd.valor, count(d.id), sum(hp.valor), max(hp.salario) ...”? O cenário do Código E17.3 chega a ser ainda pior - e acontecerá na prática se o serviço de dados incentivar, especialmente se forem dezenas de atributos de retorno a serem manipulados. É o que chamamos de manipulação do tipo “result set”. É lamentável ver um Desenvolvedor Java EE (em tese, OO também) programando regras de negócio com dados estruturados como “tabelas relacionais”, como no exemplo. ... List<Object[]> listaComDTInformadoNoPeriodo = funcDAO.recuperaComDTInformado(anoMesReferencia); for (Iterator i = listaComDTInformadoNoPeriodo.iterator(); i.hasNext();) { Object[] resultado = (Object[]) i.next(); * Aliás, como dica geral, deve-se evitar o uso de recuperações de JPAQL dentro de laços, sempre que possível. Em casos de programações batch, como é o nosso caso, vamos ainda utilizar mais uma cláusula durante o cálculo, mas isto é razoável considerando-se que pode não haver memória disponível para se manter tudo em caching, no início dos cálculos. Capítulo E17 BigDecimal salarioLiquido = ((BigDecimal)resultado[5]).subtract( ((BigDecimal)resultado[4]).multiply(new BigDecimal(200))); ... Código E17.3. O pesadelo do “COBOL”, aplicado ao mundo OO. Faça uma segunda reflexão: Qual o esforço você tem que fazer para entender que a fórmula acima em destaque está “subtraindo R$ 200,00 de seu salário base, para cada dependente”? Introduzimos este anti-padrão “JPAQL retornando Result Set” em nosso tutorial apenas para enfatizarmos a importância da utilização de coleções de Grafos de Entidades de Domínio como retorno de JPAQLs, sempre que possível. Vamos refatorar nosso método, então, para retornar valores deste tipo com maior semântica para o negócio. 1. Altere a cláusula “select” introduzindo um “new FuncionarioEntity(<propriedades>)” que passa todos os valores retornados em seu construtor. "select new FuncionarioEntity(f.id,f.cpf,f.nome,pd.valor,count(d.id),max(hp.salario))" Código E17.4. Uso de JPAQL retornando objetos do domínio. 2. Neste padrão assumimos o trabalho que cada método cliente teria para fazer “casting” de Object[] para variáveis com nomes significativos (se fizesse). Assim, como projetistas de serviços para reuso, cuidaremos de acomodar os dados em um modelo de Domínio com maior semântica, livrando os nossos “clientes Desenvolvedores” deste trabalho e, principalmente, da tentação de escreverem código “relacional” em Java. Note, portanto, que não haverá “aumento de trabalho”, apenas “fatoração de trabalho” que haveria de ser feito de qualquer modo em cada método cliente. ... public class Funcionario extends AppBaseEntity { // 1. Auxiliares transientes @Transient private transient BigDecimal diasTrabalhados; @Transient private transient Long totalDependentes; @Transient private transient BigDecimal salarioAtual; // 2. Somente getters gerados. public BigDecimal getDiasTrabalhados() {return diasTrabalhados; public Long getTotalDependentes() {return totalDependentes; public BigDecimal getSalarioAtual() {return salarioAtual; } … } } } public class FuncionarioEntity extends Funcionario { // 3. Construtor apropriado public FuncionarioEntity(Long id, String cpf, String nome, BigDecimal proventoDescontoDiasTrabalhados,Long dependenteTotalDependentes, BigDecimal historicoProfissionalSalarioAtual) { setId(id); setCpf(cpf); setNome(nome); this.diasTrabalhados=proventoDescontoDiasTrabalhados; this.totalDependentes=dependenteTotalDependentes; this.salarioAtual=historicoProfissionalSalarioAtual; // 4. Reconstituição do grafo persistente - apenas exemplo. //setHistoricoProfissional(historicoProfissional); } ... } Código E17.5. Uso JPAQL retornando propriedades de Entidades de Domínio. #1. Propriedades “transientes” são criadas na classe abstrata “Funcionario” para acomodarem resultados temporários obtidos de seu grafo e serem utilizadas em cálculos do negócio. O padrão é nomear as propriedade transientes de forma a manter a classe de origem de sua informação bem clara utilizando “[propriedadeAgregacao][Propriedade]”. #2. Além da visibilidade “private” provemos somente métodos “getters” para estas propriedades para garantir o uso seguro, apenas via construtor, das propriedades transientes. #3. Na classe concreta, criamos o construtor que acomoda o “result set” em propriedades de objetos. Mantendo o construtor na classe concreta, visamos “despoluir” ao máximo da classe abstrata para que contenha métodos de negócio somente (além dos inevitáveis “getters” e “setters”, é claro). #4. Note que, se um “select” retornasse um objeto ou uma coleção de objetos “OneToMany” (Ex.: “select new FuncionarioEntity(f.id, f.cpf, f.nome, f.historicoProfissional)...”) o correto seria Regras de Negócio e Batch recompor a agregação e não utilizar propriedades novas. O mesmo vale para objetos “ManyToOne”. 3. Agora assim, podemos indicar que nosso método retorna “List<Funcionario>”, alterando sua assinatura para “public List<Funcionario> recuperaComDTInformado(Date anoMesReferencia) “. 4. Nosso método “cliente” agora pode trabalhar de uma forma bem mais elegante, compreensível e estável ao longo do tempo. Compare o Código E17.2 e o Código E17.3 com o Código E17.6. ... List<FuncionarioEntity> listaComDTInformadoNoPeriodo = fDAO.recuperaComDTInformado(anoMesReferencia); for (Iterator i = listaComDTInformadoNoPeriodo.iterator(); i.hasNext();) { FuncionarioEntity funcionario = (FuncionarioEntity) i.next(); BigDecimal salarioLiquido = funcionario.getSalarioAtual().subtract( funcionario.getTotalDependentes().multiply(new BigDecimal(200)); ... Código E17.6. Exemplo de chamada OO. - Externando cláusulas JPAQL em NamedQueries – Programação de Persistência III As vantagens das “NamedQueries” como a que criamos são muitas.Vamos rever seu mecanismo: 1. Cláusula do método de DAO para a parte “query” fica declarada em alguma das entidades envolvidas (poderíamos também optar por FuncionarioEntity). ... @NamedQueries({ ... @NamedQuery(name="ProventoDescontoEntity.recuperaComDTInformado", query="select new FuncionarioEntity(f.id,f.cpf,f.nome,pd.valor,count(d.id),max(hp.salario))" + " from ProventoDescontoEntity pd" + " join pd.funcionario f" + " left join pd.funcionario.dependente d" + " join pd.funcionario.historicoProfissional hp" + " where pd.naturezaProventoDesconto='DIASTRAB' and pd.anoMesReferencia=:anoMesReferencia" + " group by f.id,f.cpf,f.nome,pd.valor") }) public class ProventoDescontoEntity extends ProventoDesconto { ... Código E17.7. NamedQuery “ProventoDescontoEntity.recuperaComDTInformado”. 2. O método de DAO recupera a cláusula nomeada usando “createNamedQuery” e informando o nome da NamedQuery em lugar da cláusula JPAQL em si. ... return em.createNamedQuery("ProventoDescontoEntity.recuperaComDTInformado") .setParameter("anoMesReferencia", anoMesReferencia) .getResultlist(); ... Código E17.8. Comando de classe DAO, agora utilizando JPAQL externo em NamedQuery. As vantagens de externar cláusulas JPAQL são muitas: * o Sintaxes de NamedQueries são analisadas em tempo de entrada da aplicação - e erros impedem a configuração da fábrica de persistência. Deste modo, não se corre o risco de entrar em produção com cláusulas cancelando na mão de usuários finais! o Metadados (NamedQueries)* que dizem respeito a uma Agregação de Entidades ficam “encapsulados” na sua classe Raiz. Note que todos os dados recuperados em nossa cláusula NamedQueries anotadas em Entidades, ao contrário do que uma análise superficial pode sugerir, não “acoplam” Entidades do negócio a mecanismos de persistência! Na verdade, são como as próprias anotações de mapeamento nas propriedades: informações de “metadados” (configuração). Nesta categoria, elas nem sequer precisam ser utilizadas pelas classes que as contêm - como, de fato, não o serão. Capítulo E17 de exemplo participam da agregação de “Funcionario” - deste modo, mantendo um ponto único e intuitivo para análise de impacto e reuso. o A facilidade para se editar e conferir construtores associados a cláusulas JPAQL é maior por estarem ambos no mesmo artefato. o As cláusulas JPAQL que percorrem diversos grafos de objetos são as únicas que não devem ficar externadas em nenhuma agregação. Deste modo, segmenta-se claramente esta diferente categoria de cláusulas “de varredura” (ou de “relatórios”) das “encapsuláveis” no escopo de uma Agregação de Entidades. o Ao passarmos a nomear cláusulas e desacoplá-las de métodos tornamos possível categorizálas em padrões de nível de arquitetura, reutilizando-as em programações genéricas, como faz o jCompany, gerando e utilizando NamedQueries com as seguintes convenções padronizadas: “[Entidade].querySel[Complemento]”: Utilizada como padrão para Colaborações “plcSelecao”. É possível definir mais de uma opção com sufixos diferenciados após o “querySel” (Ex.: “querySel2” ou “querySelTodos”) e indicar, para uma Colaboração específica, qual delas utilizar. “[Entidade].queryEdita”: Utilizada como padrão para Colaborações de manutenção de “uma Agregação por vez”, no momento da edição. Se não encontrada, o jCompany usa “from “[Entidade] obj” como default. “[Entidade].querySelLookup”: Utilizada como padrão para recuperação da Entidade quando vinculada ao grafo de manutenção de outra Agregação (ou seja, referenciada, mas não mantidade). Neste caso, a cláusula JPAQL evita trazer muitas propriedades, mas somente as utilizadas no método “toString” definidas como “lookup” durante o Assistente de Criação de mapeamento O-R. Além disso, esta cláusula espera um argumento de Object-Id na parte “where”, por recuperar um objeto por vez - ao contrário da “querySel”, utilizada em lógicas de QBE (Query By Example), onde a “where” é montada dinamicamente e pode permitir retorno de vários objetos. “[Entidade]. naoDeveExistir[Complemento]“: Cláusulas JPAQL utilizadas para verificação de restrições de integridade referenciais e de existência para melhorar o tratamento de mensagens de erro do SGBD ou em situações onde não se possa confiar somente nas restrições declarativas em seu nível. - Entendendo o mecanimos OO para classes DAO – Programação de Persistência IV A herança de “PlcBaseJpaDAO” já trouxe simplicidade para o nosso DAO, nos permitindo herdar uma classe de log e obter o EntityManager em POJOs facilmente. Podemos ser ainda mais sucintos com a sintaxe que codificamos, trazendo o "getEntityManager" para a mesma linha da query e também retirando a cláusula que redispara "PlcException" no tratamento de exceção gerado, pois sabemos que não haverá esta possibilidade em nossa chamada. Vamos verificar o quão simples então ficou o método ao final: ... @PlcAggregationDAOIoC(FuncionarioEntity.class) @SPlcDataAccessObject @PlcQueryService public class FuncionarioDAO extends PlcBaseJpaDAO { (...) public List<Funcionario> recuperaComDTInformado(Date anoMesReferencia) { try { return getEntityManager(context).createNamedQuery("ProventoDescontoEntity.recuperaComDTInformado") Como exemplo de que estes metadados não trazem sequelas para estas classes, basta removê-los e perceber que nenhum comportamento interno destas Entidades é afetado. Por outro lado, assim utilizados, aprimoram a organização. Regras de Negócio e Batch .setParameter("anoMesReferencia", anoMesReferencia) .getResultList(); } catch (Exception e) { throw new PlcException("FuncionarioDAO", "recuperaComDTInformado",e, log, ""); } } } ... Código E17.9. Classe de DAO completa com método típico, utilizando herança direta do jCompany para exemplo. - Utilizando CDI em classes de cálculos de negócio Agora que finalizamos a nossa implementação da classe de persistência, deixando-a tão simples quanto possível, vamos ajustar nossa classe de cálculo para reconhecê-la. Altere a classe CalculoFolha para receber o serviço de persistência via Injeção de Dependência do CDI, como abaixo. ... public class CalculoFolha { @Inject private FuncionarioDAO funcionarioDAO; protected Long calculaFolha(Date anoMesReferencia) throws CalculoFolhaException { ... } ... } Código E17.10. Chamada de AS para DAO via CDI com “amarração automática via padrão de nomenclatura”. - Criando serviços adicionais em “FuncionarioDAO” – Programação de Persistência V Analisando nossa especificação, vemos que ainda restam informações a serem recuperadas via serviços de persistência, como os ”totais gerais de descontos e proventos para um Funcionário, em um período”. Decidimos, hipoteticamente, por não recuperar todos estes totais inicialmente, como fizemos para a maior parte dos dados de funcionário para que nosso exemplo fique mais próximo das restrições que encontramos no mundo real. Em geral, sempre que possível, será muito mais performático recuperar todos os dados envolvidos em uma transação “batch” em um único comando JPAQL inicial - e depois varrermos o resultado em memória. Mas muitas vezes isso não é possível por restrições de memória disponível. No mundo prático dos projetos corporativos, uma das grandes complexidades de programação é ter de lidar com imperfeições advindas de limitações tecnológicas. Restrições típicas são memória disponível, capacidade de comunicação via rede (throughput) e de processamento. Precisamos, portanto, de recuperar o saldo de Proventos e Descontos com naturezas “PA” e “DG” para cada funcionário/período. Este é um serviço que, basicamente, irá acessar a entidade “ProventoDesconto” mas, em nosso caso, ficará acomodado na mesma classe de implementação “FuncionarioDAO”... Mas será que o correto não seria definir um “ProventoDescontoDAO”? Poderia ser, se não tivéssemos modelado “ProventoDesconto” como parte da Agregação de “Funcionario” (uma composição, na verdade), como pode ser conferido na Figura E17.14, pelo símbolo UML de losango preto entre “ProventoDesconto” e “Funcionario”*. Existem duas outras hipóteses de modelagem que nos induziria a utilizar um “ProventoDescontoDAO”: o * Se houvéssemos optado por utilizar uma “agregação compartilhada” (losango claro) estaríamos definindo que não somente “Funcionario”, mas outras Entidades Raízes poderão “se relacionar” com “ProventoDesconto”. Note que, apesar de termos realizado esta especificação para o modelo de Domínio, é uma boa prática utilizar estes mesmos critérios para o modelo de classes de Persistência quando possuir correlação. Capítulo E17 Se houvéssemos modelado “ProventoDesconto” como “plcRaiz” também exporíamos esta Entidade de uma forma direta para fora da agregação de “Funcionario”. o Agora que já justificamos o encapsulamento que iremos seguir e que já conhecemos a arquitetura básica de classes DAO sugerida pelo jCompany vamos acelerar nosso tutorial: 1. Defina o método “recuperaSaldoGeralPorFuncionario” em nossa classe “FuncionarioDAO”. ... /** * @param anoMesReferencia Período de referência (MM/yyyy) * @return BigDecimal, contendo o saldo de proventos e descontos gerais, para o funcionário, no * período. */ public BigDecimal recuperaSaldoGeralPorFuncionario(Date anoMesReferencia,Funcionario funcionario) { ... } Código E17.11. Novos métodos para recuperar saldo de “ProventoDesconto”. 2. Implemente o método em “FuncionarioDAO” utilizando a mesma organização que já discutimos anteriormente, com a NamedQuery em “ProventoDescontoEntity”. ... public BigDecimal recuperaSaldoGeralPorFuncionario(Date anoMesReferencia,Funcionario funcionario) throws PlcException { return (BigDecimal) getEntityManager(context).createNamedQuery( "ProventoDescontoEntity.recuperaSaldoGeralPorFuncionario") .setParameter("anoMesReferencia", anoMesReferencia) .setParameter("funcionario", funcionario) .getSingleResult(); ... Código E17.12. Novos métodos para recuperar saldo de “ProventoDesconto”. 3. Declare a cláusula JPAQL em “ProventoDescontoEntity”. Note que agora utilizamos dois argumentos, inclusive com um deles sendo uma “Entidade” e a agregação “sum” para somar “débitos e créditos” que estão representados pelos tipos ‘PA’ e ’DG’ em nossos proventos e descontos. Esta soma simples funciona porque, no momento dos lançamentos, transformamos os valores de débito em valores negativos. ... @NamedQueries({ ... @NamedQuery(name="ProventoDescontoEntity.recuperaSaldoGeralPorFuncionario", query="select sum(pd.valor) as saldo" + " from ProventoDescontoEntity pd" + " where pd.anoMesReferencia = :anoMesReferencia and pd.funcionario=:funcionario" + " and (pd.naturezaProventoDesconto='PROVENTO' or pd.naturezaProventoDesconto='DESCONTO')") }) public class ProventoDescontoEntity extends ProventoDesconto { ... Código E17.13. NamedQuery em “ProventoDescontoEntity”. - Delegando cálculos do negócio para Entidades – Programação de Domínio II 1. Retornando ao nosso algoritmo principal em “CalculoFolha” vamos implementar agora o método de cálculo individual “calculaFolhaUmFuncionario”. Implemente-o como orientado pelo Código E17.14. ... // 3. Constantes externadas public static final int TAXA_IR = 15; public static final BigDecimal DESCONTO_DEPENDENTE = BigDecimal.valueOf(200); ... /** * Calcula salário e impostos, para um Funcionário * @param anoMesReferencia Período de referência * @param funcionario Funcionário a ser utilizado */ private void calculaFolhaFuncionario(Date anoMesReferencia,Funcionario funcionario) throws PlcException,CalculoFolhaFuncionarioException { // 1. Saldo de proventos e descontos gerais para funcionario BigDecimal saldoProventosDescontosGerais = funcionarioDAO.recuperaSaldoGeralPorFuncionario(anoMesReferencia,funcionario); // 2. Calcula salário Bruto BigDecimal salarioBruto = funcionario.calculaSalarioBruto(saldoProventosDescontosGerais); Regras de Negócio e Batch // 3. Calcula IR, passando taxa para manter flexibilidade BigDecimal ir = funcionario.calculaIR(salarioBruto,TAXA_IR,DESCONTO_DEPENDENTE); // 4. Calcula Salário Líquido BigDecimal salarioLiquidoFinal = funcionario.calculaSalarioLiquido(salarioBruto,ir); // Grava ... } ... Código E17.14. Método “calculaFolhaUmFuncionario” em “CalculoFolhaAS”. #1. Perceba que, para cada funcionário, estamos recuperando o saldo de Proventos e Descontos gerais - para depois delegar para a Entidade “Funcionario” a realização dos cálculos em si. #2. 3, e 4. Como dissemos, classes de Cálculo não devem realizar cálculos ou operações de negócio atômicos, mas se concentrar no “controle” de todo o algoritmo. Note que fizemos três chamadas distintas a “Funcionario” a partir de nosso CalculoFolha. Se não precisássemos dos valores de IR e Salário Líquido ao final para gravarmos entradas em “ProventoDesconto”, uma única delegação ao “Funcionario” seria suficiente. Obs.: Note também o uso de constantes em escopo do cálculo, em lugar de valores literais, como boa prática. 2. Vamos implementar agora os três métodos na classe abstrata de Domínio “Funcionario”. Implemente o método “calculaSalarioBruto” testando as pré-condições e pós-condições, conforme estabelecidas na especificação. ... public abstract class Funcionario extends AppBaseEntity { // 1 public static final int TOTAL_DIAS_MES_REFERENCIA = 22; … // 2 public BigDecimal calculaSalarioBruto(BigDecimal saldoProventosDescontosGerais) throws CalculoFolhaFuncionarioException { // 3. Pré-condição para o cálculo. Exemplo I18n if (getDiasTrabalhados().compareTo(BigDecimal.valueOf(TOTAL_DIAS_MES_REFERENCIA))<0) throw new CalculoFolhaFuncionarioException("rhtutorial.erro.calculo.dias.trabalhados",getNome(),getCpf()); // 4. Cálculo BigDecimal salarioBruto = getSalarioAtual().divide(BigDecimal.valueOf(TOTAL_DIAS_MES_REFERENCIA)) .multiply(getDiasTrabalhados()); // 5. Pós-condição para o cálculo. Exemplo português - iniciando com token '#' if (salarioBruto.compareTo(BigDecimal.valueOf(0))<0) throw new CalculoFolhaFuncionarioException( "#Cálculo de salário negativo para funcionário ["+getNome()+" - "+getCpf()+"]", getNome(),getCpf()); return salarioBruto; } ... Código E17.15. Método “calculaSalarioBruto” em “Funcionario”. #1. Constante com valor 22 para “dias totais do mês” criada para evitar proliferação de uso literal. #2. Nunca é pouco lembrar: os métodos de negócio, especialmente, devem possuir comentários Javadoc bem elaborados. #3. Pré-condição especificada, com exemplo de tratamento utilizando uma exceção customizada e enviando mensagem internacionalizada com argumentos em separado. #4. O cálculo em si reside nesta linha. Utilizamos recursos da classe “java.math.BigDecimal” para fazer uma “regra de três” entre os dias totais e os efetivamente trabalhados. #5. Pós-condição especificada, com exemplo de exceção disparada agora sem internacionalizar, para variar o exemplo somente. O uso do token “#”, no início da mensagem, indica para o jCompany não tentar traduzir este texto. 3. Implemente agora os métodos “calculaIR” e "calculaSalarioLiquido", que dispensam maiores explicações: Capítulo E17 ... public BigDecimal calculaIR(BigDecimal salarioBruto,int taxaIR, BigDecimal valorDescontoPorDependente) throws PlcException { // Calcula descontos, multiplicando valor de desconto por número de dependentes BigDecimal descontos = valorDescontoPorDependente.multiply(BigDecimal.valueOf(getTotalDependentes())); // Calcula taxa, retirando percentual de IR informado (em escala taxaIR/100) return salarioBruto.subtract(descontos).multiply(BigDecimal.valueOf(taxaIR,2)); } public BigDecimal calculaSalarioLiquido(BigDecimal salarioBruto, BigDecimal ir) { return salarioBruto.subtract(ir); } ... Código E17.16. Métodos “calculaIR” e "calculaSalarioLiquido" em “Funcionario”. 4. E finalmente os métodos auxiliares que obtêem informações da agregação de Funcionario: ... /** * @return pressupõe que registros estão ordenados ascendente pela data de início do cargo */ private BigDecimal getSalarioAtual() { return getHistoricoProfissional().get(getHistoricoProfissional().size()-1).getSalario(); } /** * @return total de dias de referencia. Simplificado para exemplo. */ private BigDecimal getDiasTrabalhados() { return BigDecimal.valueOf(TOTAL_DIAS_MES_REFERENCIA); } ... /** * @return total de dependentes */ private long getTotalDependentes() { if (getDependente()==null) return 0; else return getDependente().size(); } Código E17.17. Métodos auxiliares que pegam informações da agregação de “Funcionario”. - Realizando manutenções programaticamente – Programação de Persistência VI Até agora implementamos classes de DAO apenas para recuperar dados. A partir deste tópico, aprenderemos como persistir objetos para gravação dos cálculos. 1. Defina um terceiro método em “FuncionarioDAO” conforme o Código E17.18. ... /** * Recebe valores discretos e inclui um novo ProventoDesconto, com "batch" como usuário da última * alteração. */ public void incluiProventoDesconto(Funcionario funcionario, Date anoMesReferencia, NaturezaProventoDesconto naturezaProventoDesconto, BigDecimal valor) { ... } Código E17.18. Nova cláusula de contrato com a persistência, para incluir objetos “ProventoDesconto”. 2. Implemente agora o método explicado em Código E17.19. ... public void incluiProventoDesconto(Funcionario funcionario, Date anoMesReferencia, NaturezaProventoDesconto naturezaProventoDesconto, BigDecimal valor) { // 1 ProventoDescontoEntity proventoDescontoEntity = new ProventoDescontoEntity(); // 2 proventoDescontoEntity.setFuncionario(funcionario); proventoDescontoEntity.setAnoMesReferencia(anoMesReferencia); proventoDescontoEntity.setNaturezaProventoDesconto(naturezaProventoDesconto); proventoDescontoEntity.setValor(valor); // 3 proventoDescontoEntity.setUsuarioUltAlteracao("batch"); proventoDescontoEntity.setDataUltAlteracao(new Date()); proventoDescontoEntity.setDescricao("Executando cálculo folha"); Regras de Negócio e Batch // 4 insert(context,proventoDescontoEntity); } ... Código E17.19. Método que realiza inclusões programaticamente. #1. O método inicia criando uma nova instância da classe a ser persistida. #2. Em seguida, os valores recebidos como parâmetros são incluídos na classe (um outro modo mais sucinto poderia ser criar um construtor especial na classe, para este fim). #3. Valores obrigatórios e não informados são preenchidos. O usuário da auditoria “pauta mínima” deve ser incluído manualmente, em programações “batch”. #4. É possível se chamar o comando de persistência do JPA EntityManager diretamente (em.persist), mas o “insert” do jCompany já o encapsula e deve ser chamado preferencialmente. Importante: o método “insert” chama o "em.persist" internamente, que apenas “registra” o objeto em memória para ser persistido - mas não emite nenhum SQL para o SGBD! Os SQLs em si ficam em caching no escopo da sessão de persistência, até que explicitamente se envie um comando para “descarregar” todos eles (Ex.: “flush”) ou um “commit”, que também descarrega todo o cache antes de confirmar a transação. Em nosso caso, é uma boa idéia deixar que todos os “INSERTs” sejam gerados em conjunto, de 100 em 100 registros, no mesmo momento do “commit”, pois deste modo nossa Unidade Lógica de Transação dura menos tempo. Por isso, em nosso exemplo, bastará implementarmos o método pendente “encerraTransacao()”. Vamos no entanto, para efeitos didáticos, implementar os dois serviços no DAO: o Um que simplesmente descarrega o buffer de persistência enviando para o SGBD todos os SQLs em caching desde o último envio (somente para exemplo); o E outro que realiza um fechamento de transação com “commit” que implicitamente também envia os comandos pendentes (o que utilizaremos efetivamente). 3. Defina mais dois métodos em FuncionarioDAO, conforme o Código E17.20. ... public class FuncionarioDAO extends PlcBaseJpaDAO { ... /** * Sinaliza para o despacho imediato de comandos de persistencia. Somente necessário para * controle manual de transações (Ex.: batch) */ public void enviaComandos() {} /** * Sinaliza para finalização definitiva da transação, com confirmação de dados. * Somente necessário para controle manual de transações (Ex.: batch). * Obs.: Se a operação "enviaComandos" não foi chamada, dispara todos os comandos em *caching, neste momento. */ public void encerraTransacao() {} ... } ... Código E17.20. Cláusulas típicas para gerenciamento manual de transações. 4. Implemente os métodos acima conforme o Código E17.21. ... public class FuncionarioDAO extends PlcBaseJpaDAO { … public void encerraTransacao(){ super.commit(); } public void enviaComandos(){ super.sendFlush(context); } } ... Código E17.21. Implementação com reúso em “FuncionarioDAO”. Capítulo E17 5. Troque agora o nosso esboço inicial de fechamento de transação, em “CalculoFolha”, para usar o serviço DAO. ... for (Funcionario funcionario : listaComDTInformadoNoPeriodo) { try { calculaFolhaFuncionario(anoMesReferencia, (FuncionarioEntity) funcionario); calculadoOk++; } catch (CalculoFolhaFuncionarioException fe) { feLista.add(fe); } if (calculadoOk==100) funcionarioDAO.encerraTransacao(); } funcionarioDAO.encerraTransacao(); if (feLista.size()>0) ... Código E17.22. Uso de fechamento de transação “manual”. 6. E, finalmente, inclua no cálculo individual as linhas finais para inclusão de salário e IR, em “CalculoFolha”. ... private void calculaFolhaFuncionario(Date anoMesReferencia,Funcionario funcionario) throws PlcException,CalculoFolhaFuncionarioException { ... // Calcula Salário Líquido BigDecimal salarioLiquidoFinal = funcionario.calculaSalarioLiquido(salarioBruto,ir); // Grava Salário Liquido e IR calculados, como Proventos e Descontos funcionarioDAO.incluiProventoDesconto(funcionario,anoMesReferencia, NaturezaProventoDesconto.IR,ir); funcionarioDAO.incluiProventoDesconto(funcionario,anoMesReferencia, NaturezaProventoDesconto.SALARIO,salarioLiquidoFinal); } ... Código E17.23. Chamada para geração de novos objetos “ProventoDesconto”. - Utilizando subqueries – Programação de Persistência VII Para finalizar as partes pendentes do nosso método “calculaFolha” de “CalculoFolhas”, precisamos somente finalizar o método que chamamos “verificaFechamento”. Este método deverá verificar se todos os funcionários ativos já tiveram seu salário calculado para um período e, se for o caso, encerrar o período, gravando a data de fechamento em “FolhaPagamento”. 1. Crie o último método que precisaremos em “FuncionarioDAO”, conforme o Código E17.24. ... public class FuncionarioDAO extends PlcBaseJpaDAO { ... /** * Conta o total de funcionários sem salário calculado, no período */ public Long contaFuncionarioSemSalario(Date anoMesReferencia) {} } ... Código E17.24. Última cláusula de persistência de nosso exemplo atual. 2. Implemente em “FuncionarioDAO”. ... public Long contaFuncionarioSemSalario(Date anoMesReferencia) { ... return (Long) getEntityManager(context).createNamedQuery( "FuncionarioEntity.contaFuncionarioSemSalario") .setParameter("anoMesReferencia", anoMesReferencia) .getSingleResult(); ... } ... Código E17.25. Implementação da última cláusula de persistência. Regras de Negócio e Batch 3. Declare a NamedQuery em “FuncionarioEntity”. Perceba que agora estamos utilizando um recurso de “subquery” bastante poderoso, também disponível em JPAQL. Outro detalhe importante é o teste “f.sitHistoricoPlc=’A’”... Lembra-se que funcionários são excluídos apenas logicamente? ... @NamedQuery(name="FuncionarioEntity.contaFuncionarioSemSalario", query="select count(*) from FuncionarioEntity f where f.sitHistoricoPlc='A' and f.id not in" + " (select pd.funcionario.id from ProventoDescontoEntity pd" + " where pd.anoMesReferencia = :anoMesReferencia and pd.naturezaProventoDesconto='SALARIO')") }) public class FuncionarioEntity extends Funcionario { ... Código E17.26. Cláusula JPAQL que utiliza função de agregação “count” e “subquery”. 4. Agora podemos implementar o método que falta em “CalculoFolha”, conforme o Código E17.27. ... private void verificaFechamento(Date anoMesReferencia) throws PlcException { Long totalCalculado = funcionarioDAO.contaFuncionarioSemSalario(anoMesReferencia); if (totalCalculado==0) funcionarioDAO.incluiFechamento(anoMesReferencia); funcionarioDAO.encerraTransacao(); } ... Código E17.27. Método que testa se é para gerar fechamento de folha. Colocaremso o método "incluiFechamento" no mesmo FuncionarioDAO”. Como sugerido no modelo, estamos compreendendo “FolhaPagamento” como parte da agregação de FuncionarioDAO. Figura E17.15. FolhaPagamento participante da agregação de “Funcionario”. O uso de mais uma transação, como sugerido, é também necessário já que não estamos mais no laço principal e realizamos mais uma gravação! A implementação deste método pode ser vista no Código E17.28. ... public void incluiFechamento(Date anoMesReferencia) { FolhaPagamento folhaPagamento = new FolhaPagamentoEntity(); folhaPagamento.setAnoMesUltimoFechamento(anoMesReferencia); folhaPagamento.setDataUltAlteracao(new Date()); folhaPagamento.setUsuarioUltAlteracao("batch"); insert(context,folhaPagamento); } ... Código E17.28. Método de inclusão de registro de Folha de Pagamento em FuncionarioDAO. - Realizando escalonamentos temporais básicos – Programação Batch I Vamos agora tratar da temporalidade de nossa especificação, definindo uma rotina que irá disparar nosso cálculo, para o mês passado, todo dia 3. É uma especificação muito simplista, mas que servirá para nos introduzir nas possibilidades embutidas na linguagem Java para este fim. Capítulo E17 Podemos realizar o escalonamento batch, tanto a partir da camada Controle quanto diretamente da camada Modelo. Esta decisão dependerá de como pretendemos distribuir a aplicação. Escalonamentos na camada Modelo, em uma liberação distribuída, irão iniciar e produzir logs no container EJB3. Já escalonamentos na camada Controle produzirão logs naturalmente no container Web. Para criar rotinas temporais a partir da camada Controle deve-se especializar da classe “PlcBaseTimerTask” que se encontra em “com.powerlogic.jcompany.controle.batch” no projeto “jcompany_controle”. Em nosso caso, faremos o escalonamento da camada Modelo seguindo os passos abaixo. 1. Crie a classe “CalculoFolhaTimerTask” no mesmo diretório do “CalculoFolha”, herdando de “java.util.TimerTask”. 2. Codifique o método padrão “run” conforme a Código E17.17. Figura E17.16. Exemplo de classe de escalonamento, baseada no “java.util.TimerTask”. #1. O “TimerTask” do Java provê escalonamentos simples. Pode-se utilizar EJB 3.1 para outros mais sofisticados #2. Injeção de todas as dependência via CDI. #3. O método “run” é o que será executado periodicamente seguindo escalonamento que iremos informar. #4. Utilizamos o utilitário de datas do jCompany para que, dado o dia atual, subtraia 28 dias para obter o mês anterior. Desta forma, mesmo com uma boa margem de erro no escalonador, a rotina funcionará. #5. Ao iniciar e encerrar rotinas de processamento batch, é recomendável que se envie informações para o log, em nível WARN ou mesmo INFO. #6. A chamada da rotina do CalculoFolha é realizada aqui. #7. O tratamento de exceções controladas será feito a seguir. Já o tratamento de exceções inesperadas pode ser realizado somente com envio de log.error. 3. O método de tratamento de exceções controladas está em Código E17.29. ... private void trataErro(CalculoFolhaException cf) { log.error("Erros em cálculo de folha"); for (CalculoFolhaFuncionarioException e : cf.getFeLista()) { Regras de Negócio e Batch CalculoFolhaFuncionarioException fe = (CalculoFolhaFuncionarioException) e; log.error(fe.toString()); } } ... Código E17.29. Tratamento de exceção controlada. Neste caso somente exibindo logs de erro. 4. Obs.: para que as linhas de erros individuais apareçam formatadas corretamente, precisamos redefinir o método “toString” para as exceções de funcionário, como em Código E17.30. ... public class CalculoFolhaFuncionarioException extends AppException { … public String toString() { return getMessageKey()+" ["+getNomeFuncionario()+" - "+ getCpfFuncionario()+"]"; } } ... Código E17.30. Método que formata exibição de exceções. 5. Somente falta agora implementar a rotina de disparo em si. A subcamada candidata para iniciar rotinas na camada Modelo é a de fachada. Portanto, vamos implementar nossas rotinas de iniciação de escalonamento na classe “AppFacadeImpl”. ... @QPlcDefault @SPlcFacade public class AppFacadeImpl extends PlcFacadeImpl implements IAppFacade{ private static long MILISEG_1_DIA = 3600000L * 24; // 1 private static Timer timerMensal = new Timer(true); // 2 static { // 3 CalculoFolhaTimerTask calculoFolhaTimerTask = PlcCDIUtil.getInstance() .getInstanceByType(CalculoFolhaTimerTask.class); // 4 timerMensal.schedule(calculoFolhaTimerTask, primeiroDiaUmaHora(),MILISEG_1_DIA); //5 } // 6 private static Date primeiroDiaUmaHora() { } Calendar diaHoje = Calendar.getInstance(); if (diaHoje.get(Calendar.DAY_OF_MONTH) < 3) { return PlcDateUtil.newDate(diaHoje.get(Calendar.YEAR) + 1900, diaHoje.get(Calendar.MONTH) + 1, 3, 1, 0, 0); } else if ((diaHoje.get(Calendar.MONTH) + 1) == 12) { return PlcDateUtil.newDate(diaHoje.get(Calendar.YEAR) + 1900 + 1, 1, 3, 1, 0, 0); } else { return PlcDateUtil.newDate(diaHoje.get(Calendar.YEAR) + 1900, diaHoje.get(Calendar.MONTH) + 1 + 1, 3, 1, 0, 0); } }... Código E17.31. Classe AppFacadeImpl, em método estático, iniciando disparo de rotinas batch. #1. Constante que simboliza o período de 1 (um) dia, em milisegundos. #2. Declaração de um Timer (temporizador), que é uma Thread ultraleve, que rodará para fazer ativações temporais escalonadas. #3. Esta implementação na cláusula “static” garantirá que, ao primeiro acesso à classe “AppFacadeImpl” (qualquer transação padrão, portanto), a rotina de escalonamento iniciará. #4. Obtenção da instancia do serviço batch de cálculo de folha. Como está em um método estático, é feita manualmente. #5. Escalonamento propriamente dito. Os parâmetros, respectivamente, são: objetos de classe que herdam de “TimerTask”; data/hora de início; período de defasagem para reescalonamento, em milisegundos. Capítulo E17 #6. Cálculo do primeiro dia para execução (que neste caso será o dia 3 do mês corrente) se o dia atual for anterior a este dia; ou do próximo mês, se o dia atual for depois. Note que o Timer não nos dá opções ricas, como é de esperar de um escalonador corporativo. Conseguimos programar a primeira execução para este dia, mas não temos uma opção de reescalonamento do tipo “execute dia 3 de cada mês”. Para isso, poderíamos utilizar algum produto Open Source como o Quartz ou EJB 3.1. Em nosso caso, de forma simplificada, vamos contornar esta limitação fazendo nosso escalonador rodar todo dia, 1:00 hora da madrugada; e fazendo com que a própria rotina verifique se é o dia correto para execução. 6. Edite o “CalculoFolhaTimerTask”, portanto, e acrescente o teste do Código E17.32. ... public void run() { } // Impede execução se não for dia 3, uma vez que o escalonador programa verificação diariamente. Calendar diaHoje = Calendar.getInstance(); if (diaHoje.get(Calendar.DAY_OF_MONTH)==3) { Date periodoCalculo = plcDateUtil.dataNumDias(new Date(), -28); … } ... Código E17.32. Testando o dia correto na rotina “TimerTask”. É importante entender as limitações desta abordagem: #1. Não temos flexibilidade de alterar o período de escalonamento dinamicamente (o que é possível com aplicativos escalonadores corporativos). #2. Nosso algoritmo não está pronto para trabalhar em “cluster” – precisaríamos criar algum tipo de semáforo, para isso, marcando em banco de dados o início de um processamento para que outro nó não o faça paralelamente. #3. Precisamos fazer contas e subterfúgios em código para suprir as limitações da rotina de escalonamento da classe “java.util.Timer”. Ainda assim, é uma abordagem imediatamente possível que resolve situações mais simples. Ela pode ser utilizada transitoriamente até que se disponha de um ambiente de escalonamento batch mais apropriado. - Testando processamento batch – Programação Batch II Ficará como exercício para o desenvolvedor acompanhar em modo de teste o mecanismo de execução batch que desenvolvemos. Experimente trocar o período de escalonamento e também o dia do agendamento para testar a rotina com mais eficácia, em um período menor. Regras de Negócio e Batch Sumário Neste capítulo implementamos regras de negócio disparadas a partir de rotinas de escalonamento batch, utilizando classes de cálculo de negócio/domínio implementadas na camada Modelo, além de Data Access Objects (DAO), tarefas temporais (TimerTask) e temporizadores para escalonamento (Timers). Colocamos em prática um pouco das amplas possibilidades que o jCompany FS Framework oferece para simplificar implementações de negócio e organizá-las de uma maneira Orientada a Objetos, com gerenciamento de transações flexível. No próximo capítulo iremos ver mais implementações de negócio, enfatizando o uso de Template Methods da camada de Modelo, através das classes de Repository, além de implementar fachadas e disparos da camada Controle.