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.
Download

Regras de Negócio & Batch