Skip to content

5. Versão 1: Arquitetura Spring / JPA

Propomos escrever uma aplicação de consola, bem como uma aplicação gráfica, para gerar recibos de vencimento para prestadores de cuidados infantis empregados pela «Maison de la petite enfance» num município. Esta aplicação terá a seguinte arquitetura:

5.1. O banco de dados

Os dados estáticos necessários para gerar o recibo de vencimento serão armazenados numa base de dados a que nos referiremos como dbpam. Esta base de dados poderá conter as seguintes tabelas:

Tabela EMPLOYEES: contém informações sobre os vários prestadores de cuidados infantis

Estrutura:

ID
chave primária
VERSÃO
número de versão – aumenta a cada modificação da linha
SS
Número de Segurança Social do funcionário – único
NOME
Apelido do funcionário
nome
nome
ENDEREÇO
a morada deles
CIDADE
a cidade dele/dela
CÓDIGO POSTAL
o seu código postal
INDEMNITE_ID
Chave estrangeira no campo [ID] da tabela [INDEMNITES]

O seu conteúdo pode ser o seguinte:

Image

Tabela COTISATIONS: contém as percentagens necessárias para calcular as contribuições para a segurança social

Estrutura:

ID
chave primária
VERSÃO
número de versão – aumenta a cada modificação da linha
CSGRDS
Percentagem: Contribuição Social Geral + Contribuição para o Reembolso da Dívida Social
CSGD
percentagem: contribuição social geral dedutível
SECU
percentagem: segurança social, viuvez, velhice
PENSÃO
percentagem: pensão complementar + seguro de desemprego

O seu conteúdo poderia ser o seguinte:

Image

As taxas de contribuição para a segurança social são independentes do trabalhador. A tabela anterior tem apenas uma linha.

Tabela ALLOWANCES: contém os elementos utilizados para calcular o salário a pagar.
ID
chave primária
  
VERSÃO
número de versão – aumenta a cada modificação da linha
  
ÍNDICE
Índice de processamento – único
  
TARIFA HORÁRIA
Preço líquido em euros por uma hora de serviço de plantão
  
MANUTENÇÃO DIÁRIA
Subsídio diário em euros por dia de cuidados
  
REFEIÇÃO POR DIA
Subsídio de refeição em euros por dia de assistência
  
INDEMNIZAÇÃO POR FÉRIAS
Subsídio de férias pagas. Trata-se de uma percentagem aplicada ao salário base.
  
   

O seu conteúdo poderia ser o seguinte:

Image

Note-se que os subsídios podem variar de um prestador de cuidados infantis para outro. Estes estão associados a um prestador de cuidados infantis específico através da sua categoria salarial. Assim, a Sra. Marie Jouveinal, que tem a categoria salarial 2 (tabela EMPLOYEES), tem um salário por hora de 2,1 euros (tabela INDEMNITES).

5.2. Método de cálculo do salário de uma ama

Apresentaremos agora o método de cálculo do salário mensal de uma ama. Este não pretende ser o método efetivamente utilizado na prática. A título de exemplo, utilizaremos o salário da Sra. Marie Jouveinal, que trabalhou 150 horas ao longo de 20 dias durante o período de pagamento.

São tidos em conta os seguintes fatores:

[TOTALHOURS]: total de horas
trabalhadas durante o mês

[TOTALDAYS]: total de dias trabalhados
no mês
[TOTALHOURS]=150
[TOTALDAYS]= 20
O salário base do prestador de cuidados infantis
é calculado pela seguinte fórmula:
[SALÁRIO BASE]=([TOTAL DE HORAS]
*[TARIFA POR HORA])*(1+
[SUBSÍDIO CP]/100)
[SALÁRIO BÁSICO]=
(150*[2,1])*(1+0,15)= 362,25
Um determinado montante de contribuições para a segurança social
deve ser deduzida deste salário base
:

Contribuição social geral e
contribuição para o reembolso da
dívida social:
 [SALÁRIO BASE]*[CSGRDS/100]

Contribuição social geral dedutível:
 [SALÁRIO BASE]*[CSGD/100]

Segurança social, pensões de viuvez e de velhice:
 [SALÁRIO BASE]*[SECU/100]

Pensão complementar + AGPF +
Seguro de desemprego:
 [SALÁRIO BASE]*[PENSÃO/100]
CSGRDS: 12,64
CSGD: 22,28
Segurança Social: 34,02
Pensão: 28,55
Total das contribuições para a segurança social:
[CONTRIBUIÇÕES SOCIAIS]=
[SALÁRIO BASE]*(CSGRDS+CSGD
+SECU+PENSÃO)/100
[CONTRIBUIÇÕESSOCIAIS]=97,48
Além disso, a prestadora de cuidados infantis tem direito a um subsídio de subsistência e a um subsídio de refeição por cada dia trabalhado. Como tal, recebe os seguintes subsídios:
[Subsídios] = [TOTAL DE DIAS]
*(SUBSÍDIO DIÁRIO + SUBSÍDIO DIÁRIO DE REFEIÇÕES)
[SUBSÍDIOS]=104
No final, o salário líquido a pagar ao prestador de cuidados infantis é o seguinte:
[SALÁRIO BASE] - [CONTRIBUIÇÕES PARA A SEGURANÇA SOCIAL] + [SUBSÍDIOS]
[SALÁRIO LÍQUIDO]=368,77

5.3. Como funciona a aplicação de consola

Aqui está um exemplo de execução da aplicação de consola numa janela do DOS:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des Oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations :
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités :
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

Vamos escrever um programa que irá receber as seguintes informações:

  1. o número de segurança social da ama (254104940426058 no exemplo - linha 1)
  2. Número total de horas trabalhadas (150 no exemplo - linha 1)
  3. Número total de dias trabalhados (20 no exemplo - linha 1)

Vemos que:

  • linhas 9–14: apresentam informações sobre o trabalhador cujo número de segurança social foi fornecido
  • linhas 17–20: apresentam as taxas das várias contribuições
  • linhas 23–26: apresentam os subsídios associados ao nível salarial do funcionário (aqui, nível 2)
  • linhas 29–33: apresentam os componentes do salário a pagar

A aplicação assinala quaisquer erros:

Chamada sem parâmetros:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar
Syntaxe : pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés

Chamada com dados incorretos:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar  254104940426058 150x 20x
Le nombre d'heures travaillées [150x] est erroné
Le nombre de jours travaillés [20x] est erroné

Chamada com um número de segurança social incorreto:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar  xx 150 20
L'erreur suivante s'est produite : L'employé de n°[xx] est introuvable

5.4. Como funciona a aplicação gráfica

A aplicação gráfica calcula os salários dos prestadores de cuidados infantis utilizando um formulário Swing:

  • As informações passadas como parâmetros para o programa de consola são agora introduzidas através dos campos de entrada [1, 2, 3].
  • O botão [4] inicia o cálculo do salário
  • O formulário apresenta os vários componentes salariais até ao salário líquido a pagar [5]

A lista suspensa [1, 6] não exibe os números de segurança social dos funcionários, mas sim os seus nomes próprios e apelidos. Partimos do princípio de que não existem dois funcionários com os mesmos nomes próprios e apelidos.

5.5. Criação da base de dados

Iniciamos o WampServer e utilizamos a ferramenta PhpMyAdmin [1]:

  • em [2], selecione a opção [Bases de dados],
  • em [3], crie uma base de dados [dbpam_hibernate],
  • em [4], a base de dados é criada. Selecione-a,
  • em [5], queremos importar um script SQL,
  • em [6], utilize o botão [Procurar] para selecionar o ficheiro,
  • Em [7,8], selecione o script SQL,
  • em [9], executamo-lo,
  • Em [10], as tabelas foram criadas. O seu conteúdo é o seguinte:

Tabela EMPLOYEES

Image

tabela INDEMNIZAÇÕES

Image

tabela CONTRIBUIÇÕES

Image

5.6. Implementação JPA

5.6.1. Camada JPA / Hibernate

Iremos configurar a camada JPA no seguinte ambiente:

Um programa de consola irá interagir com a base de dados. Para tal, é necessário:

  • ter uma base de dados,
  • ter o controlador JDBC para o SGBD, neste caso o MySQL,
  • implementar a camada JPA utilizando o Hibernate,
  • escrever o programa de consola.

Criamos o projeto Maven [mv-pam-jpa-hibernate] [1]:

Na arquitetura da nossa aplicação, precisamos dos seguintes elementos:

  • a base de dados,
  • o controlador JDBC para o SGBD MySQL,
  • a camada JPA/Hibernate (entidades e configuração),
  • o programa da consola de testes.

5.6.1.1. A base de dados

Primeiro, vamos criar uma base de dados vazia. Iniciamos o WampServer e utilizamos a ferramenta PhpMyAdmin [1]:

  • em [2], selecione a opção [Bases de dados],
  • em [3], crie uma base de dados com o nome [dbpam_hibernate],
  • em [4], a base de dados é criada.

5.6.1.2. Configurar a camada JPA

A ligação entre a camada JDBC e a base de dados é configurada no ficheiro [persistence.xml], que configura a camada JPA. Este ficheiro pode ser criado utilizando o NetBeans:

  • no separador [Serviços] [1], ligue-se à base de dados utilizando o controlador JDBC do MySQL [2],
  • em [3], o nome da base de dados à qual pretende ligar-se.
  • em [4], a URL JDBC da base de dados,
  • em [5], inicie sessão como root sem palavra-passe,
  • em [6], pode testar a ligação,
  • em [7], a ligação foi bem-sucedida.
  • A ligação aparece em [8] e [9],
  • em [10], adicione um novo elemento ao projeto,
  • em [11] selecione a categoria [Persistência] e em [12] o elemento [Unidade de Persistência],
  • em [13], nomeie esta unidade de persistência,
  • em [14], selecionamos uma implementação do Hibernate,
  • em [15], especifique a ligação que acabámos de criar à base de dados MySQL,
  • Em [16], especifique que, quando a camada JPA for instanciada, ela deve criar as tabelas correspondentes às entidades JPA do projeto.

No final do assistente, é gerado o ficheiro [persistence.xml]:

  • o ficheiro aparece num novo ramo do projeto, na pasta [META-INF] [1],
  • que corresponde à pasta [src/main/resources] do projeto [2,3].

O seu conteúdo é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
    </properties>
  </persistence-unit>
</persistence>
  • Linha 3: o nome da unidade de persistência e o tipo de transação. RESOURCE_LOCAL indica que o projeto gere as transações por conta própria. Neste caso, o programa de consola será responsável por fazê-lo,
  • linha 4: a implementação JPA utilizada é o Hibernate,
  • linhas 6–9: as propriedades JDBC para a ligação à base de dados,
  • linha 11: solicita a criação de tabelas correspondentes às entidades JPA. Na verdade, o NetBeans gera uma configuração incorreta aqui. A configuração deve ser a seguinte:

      <property name="hibernate.hbm2ddl.auto" value="create"/>

Com a opção create, o Hibernate, aquando da instanciação da camada JPA, elimina e, em seguida, cria as tabelas correspondentes às entidades JPA. A opção create-drop faz o mesmo, mas, no final do ciclo de vida da camada JPA, elimina todas as tabelas. Existe outra opção:


      <property name="hibernate.hbm2ddl.auto" value="update"/>

Esta opção cria as tabelas se estas não existirem, mas não as elimina se já existirem.

Iremos adicionar mais três propriedades à configuração do Hibernate:


      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>

Estas configurações instruem o Hibernate a apresentar as instruções SQL que envia para a base de dados. O ficheiro completo é, portanto, o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.hbm2ddl.auto" value="create"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="use_sql_comments" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

5.6.1.3. Dependências

Voltemos à arquitetura do projeto:

Configurámos a camada JPA através do ficheiro [persistence.xml]. A implementação escolhida foi o Hibernate. Isto introduziu dependências no projeto:

  

Estas dependências devem-se à inclusão do Hibernate no projeto. Precisamos de adicionar outra dependência: o controlador JDBC do MySQL, que implementa a camada JDBC da arquitetura. Atualizamos o ficheiro [pom.xml] da seguinte forma:


<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>    
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
    </dependency>
    ...
    <dependency>
      <groupId>org.hibernate.common</groupId>
      <artifactId>hibernate-commons-annotations</artifactId>
      <version>4.0.1.Final</version>
    </dependency>
  </dependencies>

As linhas 8–12 adicionam a dependência do controlador JDBC do MySQL.

5.6.1.4. Entidades JPA


Pergunta: Seguindo a abordagem do exemplo da Secção 4.4, gere as entidades [Cotisation, Indemnite, Employe].


Notas:

  • As entidades farão parte de um pacote denominado [jpa],
  • cada entidade terá um número de versão,
  • se duas entidades estiverem ligadas por uma relação, apenas a relação primária @ManyToOne será criada. A relação inversa @OneToMany não será criada.

5.6.1.5. O código para a classe principal

Incluímos as entidades JPA desenvolvidas anteriormente [1] no projeto:

depois adicionamos [2] a seguinte classe [main.Main]:


package main;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
 
public class Main {
 
  public static void main(String[] args) {
    // creating the Entity Manager is enough to build the JPA layer
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
    EntityManager em=emf.createEntityManager();
    // resource release
    em.close();
    emf.close();
  }
}
  • Linha 10: Criamos o EntityManagerFactory para a unidade de persistência denominada [mv-pam-jpa-hibernatePU]. Este nome provém do ficheiro [persistence.xml]:

  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
  </persistence-unit>
  • Linha 12: O EntityManager é criado. Isto cria a camada JPA. O ficheiro [persistence.xml] será utilizado e, assim, as tabelas da base de dados serão criadas.
  • linhas 14–15: os recursos são libertados.

5.6.1.6. Testes

Voltemos à arquitetura do nosso projeto:

Todas as camadas foram implementadas. Executamos o projeto [2].

A saída da consola é a seguinte:

------------------------------------------------------------------------
Building mv-pam-jpa-hibernate 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
juin 21, 2012 4:22:47 PM org.hibernate.annotations.common.Version <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.1.Final}
juin 21, 2012 4:22:47 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.1.2}
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000402: Using Hibernate built-in connection pool (not for production use!)
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000115: Hibernate connection pool size: 20
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000006: Autocommit mode: true
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000401: using driver [com.mysql.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/dbpam_hibernate]
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000046: Connection properties: {user=root, autocommit=true, release_mode=auto}
juin 21, 2012 4:22:48 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
juin 21, 2012 4:22:48 PM org.hibernate.engine.jdbc.internal.LobCreatorBuilder useContextualLobCreation
INFO: HHH000423: Disabling contextual LOB creation as JDBC driver reported JDBC version [3] less than 4
juin 21, 2012 4:22:48 PM org.hibernate.engine.transaction.internal.TransactionFactoryInitiator initiateService
INFO: HHH000268: Transaction strategy: org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory
juin 21, 2012 4:22:48 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000227: Running hbm2ddl schema export
Hibernate: 
    alter table EMPLOYES 
        drop 
        foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: HHH000389: Unsuccessful: alter table EMPLOYES drop foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: Table 'dbpam_hibernate.employes' doesn't exist
Hibernate: 
    drop table if exists COTISATIONS
Hibernate: 
    drop table if exists EMPLOYES
Hibernate: 
    drop table if exists INDEMNITES
Hibernate: 
    create table COTISATIONS (
        id bigint not null auto_increment,
        CSGD double precision not null,
        CSGRDS double precision not null,
        RETRAITE double precision not null,
        SECU double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    create table EMPLOYES (
        id bigint not null auto_increment,
        SS varchar(15) not null unique,
        ADRESSE varchar(50) not null,
        CP varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(20) not null,
        VERSION integer not null,
        VILLE varchar(30) not null,
        INDEMNITE_ID bigint not null,
        primary key (id)
    )
Hibernate: 
    create table INDEMNITES (
        id bigint not null auto_increment,
        BASE_HEURE double precision not null,
        ENTRETIEN_JOUR double precision not null,
        INDEMNITES_CP double precision not null,
        INDICE integer not null unique,
        REPAS_JOUR double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    alter table EMPLOYES 
        add index FK75C8D6BC73F24A67 (INDEMNITE_ID), 
        add constraint FK75C8D6BC73F24A67 
        foreign key (INDEMNITE_ID) 
        references INDEMNITES (id)
juin 21, 2012 4:22:49 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
juin 21, 2012 4:22:49 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH000030: Cleaning up connection pool [jdbc:mysql://localhost:3306/dbpam_hibernate]
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 2.637s
Finished at: Thu Jun 21 16:22:49 CEST 2012
Final Memory: 8M/153M

A consola contém apenas registos do Hibernate, uma vez que o programa executado não faz nada além de instanciar a camada JPA. Tenha em atenção os seguintes pontos:

  • linha 43: o Hibernate tenta eliminar a chave estrangeira da tabela [EMPLOYEES],
  • linhas 51–55: eliminação das três tabelas,
  • linha 57: criação da tabela [COTISATIONS],
  • linha 67: criação da tabela [EMPLOYEES],
  • linha 80: criação da tabela [INDEMNITIES],
  • linha 91: criação da chave estrangeira para a tabela [EMPLOYEES].

No NetBeans, pode visualizar as tabelas na ligação criada anteriormente:

As tabelas criadas dependem tanto da implementação da camada JPA utilizada como do SGBD utilizado. Assim, uma implementação JPA/EclipseLink com a mesma base de dados pode gerar tabelas diferentes. É isso que vamos ver agora.

Vamos criar um novo projeto Maven no seguinte ambiente:

Seguiremos os passos da secção anterior:

  1. criar uma base de dados MySQL [dbpam_eclipselink]. Iremos utilizar o script [dbpam_eclipselink.sql] para a gerar,
  2. criar o ficheiro [persistence.xml] do projeto. Utilize a implementação EclipseLink JPA 2.0,
  3. adicionar a dependência do controlador JDBC do MySQL às dependências geradas,
  4. adicionar as entidades JPA e o programa de consola,
  5. execute os testes.

O ficheiro [persistence.xml] terá o seguinte conteúdo:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pam-jpa-eclipselinkPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="eclipselink.target-database" value="MySQL"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_eclipselink"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • As propriedades 9–13 foram geradas pelo assistente do NetBeans,
  • Linha 14: Esta propriedade permite-nos definir o nível de registo do EclipseLink. O nível FINE permite-nos ver as instruções SQL que o EclipseLink irá executar na base de dados,
  • Linha 15: Quando a camada JPA/EclipseLink é instanciada, as tabelas de entidades JPA serão eliminadas e, em seguida, criadas.

A saída da consola é a seguinte:

------------------------------------------------------------------------
Building mv-pam-jpa-eclipselink 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
[EL Config]: 2012-06-22 14:35:01.852--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Cotisation] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Employe] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The target entity (reference) class for the many to one mapping element [field indemnite] is being defaulted to: class jpa.Indemnite.
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Indemnite] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Cotisation] is being defaulted to: Cotisation.
[EL Config]: 2012-06-22 14:35:01.915--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Employe] is being defaulted to: Employe.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Indemnite] is being defaulted to: Indemnite.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Thread(Thread[main,5,main])--The primary key column name for the mapping element [field indemnite] is being defaulted to: ID.
[EL Info]: 2012-06-22 14:35:02.558--ServerSession(730572764)--Thread(Thread[main,5,main])--EclipseLink, version: Eclipse Persistence Services - 2.3.0.v20110604-r9504
[EL Config]: 2012-06-22 14:35:02.568--ServerSession(730572764)--Connection(1543921451)--Thread(Thread[main,5,main])--connecting(DatabaseLogin(
    platform=>MySQLPlatform
    user name=> "root"
    datasource URL=> "jdbc:mysql://localhost:3306/dbpam_eclipselink"
))
[EL Config]: 2012-06-22 14:35:02.738--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--Connected: jdbc:mysql://localhost:3306/dbpam_eclipselink
    User: root@localhost
    Database: MySQL  Version: 5.5.20-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
[EL Info]: 2012-06-22 14:35:02.798--ServerSession(730572764)--Thread(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU login successful
[EL Fine]: 2012-06-22 14:35:02.818--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--ALTER TABLE EMPLOYES DROP FOREIGN KEY FK_EMPLOYES_INDEMNITE_ID
[EL Fine]: 2012-06-22 14:35:03.088--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE COTISATIONS
[EL Fine]: 2012-06-22 14:35:03.118--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE COTISATIONS (ID BIGINT NOT NULL, CSGD DOUBLE NOT NULL, CSGRDS DOUBLE NOT NULL, RETRAITE DOUBLE NOT NULL, SECU DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.198--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE EMPLOYES
[EL Fine]: 2012-06-22 14:35:03.238--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE EMPLOYES (ID BIGINT NOT NULL, SS VARCHAR(15) NOT NULL UNIQUE, ADRESSE VARCHAR(50) NOT NULL, CP VARCHAR(5) NOT NULL, NOM VARCHAR(30) NOT NULL, PRENOM VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(30) NOT NULL, INDEMNITE_ID BIGINT NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.318--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE INDEMNITES
[EL Fine]: 2012-06-22 14:35:03.338--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE INDEMNITES (ID BIGINT NOT NULL, BASE_HEURE DOUBLE NOT NULL, ENTRETIEN_JOUR DOUBLE NOT NULL, INDEMNITES_CP DOUBLE NOT NULL, INDICE INTEGER NOT NULL UNIQUE, REPAS_JOUR DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.418--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--ALTER TABLE EMPLOYES ADD CONSTRAINT FK_EMPLOYES_INDEMNITE_ID FOREIGN KEY (INDEMNITE_ID) REFERENCES INDEMNITES (ID)
[EL Fine]: 2012-06-22 14:35:03.568--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Thread(Thread[main,5,main])--SELECT 1
[EL Warning]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Thread(Thread[main,5,main])--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.0.v20110604-r9504): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery(sql="CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))")
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values (SEQ_GEN, 0)
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--disconnect
[EL Info]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Thread(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU logout successful
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Connection(1543921451)--Thread(Thread[main,5,main])--disconnect
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.503s
Finished at: Fri Jun 22 14:35:03 CEST 2012
Final Memory: 8M/153M
  • linhas 26-30: ligação à base de dados MySQL,
  • linhas 31-34: confirmação de que a ligação foi bem-sucedida,
  • linha 36: eliminar a chave estrangeira da tabela [EMPLOYEES],
  • linha 37: eliminação da tabela [COTIZAÇÕES],
  • linha 38: criação da tabela [CONTRIBUTIONS]. Vale a pena notar que a chave primária ID não possui o atributo auto_increment do MySQL. Isto significa que o MySQL não gera os valores da chave primária,
  • linha 39: eliminação da tabela [EMPLOYEES],
  • linha 40: criar a tabela [EMPLOYEES]. A sua chave primária ID não possui o atributo auto_increment do MySQL,
  • linha 41: eliminação da tabela [INDEMNITIES],
  • linha 42: criação da tabela [INDEMNITIES]. A sua chave primária ID não possui o atributo auto_increment do MySQL,
  • linha 43: criar uma chave estrangeira da tabela [EMPLOYEES] para a tabela [BENEFITS],
  • linha 44: criação da tabela [SEQUENCE]. Será utilizada para gerar as chaves primárias das três tabelas anteriores,
  • linha 47: ocorre uma exceção porque esta tabela já existia,
  • linhas 51–53: inicialização da tabela [SEQUENCE].

A existência das tabelas geradas pode ser verificada no NetBeans [1]:

Portanto, com base nas mesmas entidades JPA, as implementações JPA do Hibernate e do EclipseLink não geram as mesmas tabelas. No restante deste documento, quando a implementação JPA utilizada for:

  • Hibernate, utilizaremos a base de dados [dbpam_hibernate],
  • EclipseLink, utilizaremos a base de dados [dbpam_eclipselink].

5.6.3. Trabalho a realizar

Seguindo o mesmo procedimento de antes,

  1. crie e teste um projeto [mv-pam-jpa-hibernate-oracle] utilizando uma implementação do Hibernate JPA e um SGBD Oracle,
  2. crie e teste um projeto [mv-pam-jpa-hibernate-mssql] utilizando uma implementação Hibernate JPA e um SGBD SQL Server,
  3. crie e teste um projeto [mv-pam-jpa-eclipselink-oracle] utilizando uma implementação JPA do EclipseLink e um SGBD Oracle,
  4. criar e testar um projeto [mv-pam-jpa-eclipselink-mssql] utilizando uma implementação EclipseLink JPA e um SGBD SQL Server,

5.6.4. Lazy ou Eager?

Voltemos a uma possível definição da entidade [Employee]:


package jpa;
 
...
 
@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
 
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="SS", nullable=false, unique=true, length=15)
  private String SS;
  @Column(name="NOM", nullable=false, length=30)
  private String nom;
  @Column(name="PRENOM", nullable=false, length=20)
  private String prenom;
  @Column(name="ADRESSE", nullable=false, length=50)
  private String adresse;
  @Column(name="VILLE", nullable=false, length=30)
  private String ville;
  @Column(name="CP", nullable=false, length=5)
  private String codePostal;
  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
  ...
}

As linhas 27–29 definem a chave estrangeira da tabela [EMPLOYEES] para a tabela [INDEMNITIES]. O atributo fetch na linha 27 define a estratégia de recuperação para o campo indemnity na linha 29. Existem dois modos:

  • FetchType.LAZY: Quando um funcionário é consultado, a indemnização correspondente não é recuperada. Será recuperada quando o campo [Employee].indemnity for referenciado pela primeira vez.
  • FetchType.EAGER: Quando um funcionário é pesquisado, o subsídio correspondente é recuperado. Este é o modo predefinido quando nenhum modo é especificado.

Para compreender a vantagem da opção FetchType.LAZY, considere o seguinte exemplo. Uma lista de funcionários sem a respetiva remuneração é apresentada numa página web com um link [Detalhes]. Ao clicar neste link, é apresentada a remuneração do funcionário selecionado. Observamos que:

  • Para apresentar a primeira página, não precisamos dos funcionários juntamente com os seus benefícios. O modo FetchType.LAZY é, portanto, adequado;
  • para exibir a segunda página com os detalhes, é necessário efetuar uma consulta adicional à base de dados para recuperar os benefícios do funcionário selecionado.

O modo FetchType.LAZY evita a recuperação de dados em excesso de que a aplicação não necessita imediatamente. Vejamos um exemplo.

O projeto [mv-pam-jpa-hibernate] é duplicado:

  • em [1], o projeto é copiado,
  • em [2], especificamos a pasta para a cópia e, em [3], o seu nome,
  • em [4], o novo projeto tem o mesmo nome que o antigo. Alteramos isto:
  • em [1], renomeamos o projeto,
  • em [2], renomeamos o projeto e o seu artifactId,
  • em [3], o novo projeto.

Modificamos o programa [Main.java] da seguinte forma:


package main;
 
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;
 
public class Main {
 
  // the JPQL query below brings back an employee
  // the foreign key [Employe].indemnite is in FetchType.LAZY
  public static void main(String[] args) {
    // creating the Entity Manager is enough to build the JPA layer
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
    // first attempt
    EntityManager em = emf.createEntityManager();
    Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    em.close();
    // we display the employee
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // second test
    em = emf.createEntityManager();
    employe = (Employe) em.createQuery("select e from Employe e left join fetch e.indemnite where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    // free up resources
    em.close();
    // we display the employee
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // resource release
    emf.close();
  }
}
  • linha 15: criamos o EntityManagerFactory para a camada JPA,
  • linha 17: obtemos o EntityManager, que nos permite interagir com a camada JPA,
  • linha 18: recuperamos o funcionário chamado Jouveinal,
  • linha 19: fechamos o EntityManager. Isto fecha o contexto de persistência.
  • linha 22: exibimos o funcionário recuperado.

A classe [Employee] é a seguinte:


package jpa;
 
...
 
@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
 
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="SS", nullable=false, unique=true, length=15)
  private String SS;
  @Column(name="NOM", nullable=false, length=30)
  private String nom;
  @Column(name="PRENOM", nullable=false, length=20)
  private String prenom;
  @Column(name="ADRESSE", nullable=false, length=50)
  private String adresse;
  @Column(name="VILLE", nullable=false, length=30)
  private String ville;
  @Column(name="CP", nullable=false, length=5)
  private String codePostal;
  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
 
 
  /**
   * Returns a string representation of the object.  This implementation constructs
   * that representation based on the id fields.
   * @return a string representation of the object.
   */
  @Override
  public String toString() {
    return "jpa.Employe[id=" + getId()
    + ",version="+getVersion()
    +",SS="+getSS()
    + ",nom="+getNom()
    + ",prenom="+getPrenom()
    + ",adresse="+getAdresse()
    +",ville="+getVille()
    +",code postal="+getCodePostal()
    +",indice="+getIndemnite().getIndice()
    +"]";
  }
  ...
}
  • linha 27: o campo indemnite é recuperado no modo LAZY,
  • linha 47: utiliza o campo indemnite. Se o método toString for chamado enquanto o campo indemnite ainda não tiver sido recuperado, este será recuperado nesse momento. A menos que o contexto de persistência tenha sido fechado, como no exemplo.

Voltemos ao código [Main]:

  • linhas 21–25: devemos obter uma exceção. Isto porque o método toString será chamado. Ele utilizará o campo indemnite. Este campo será procurado. Uma vez que o contexto de persistência foi fechado, a entidade [Employee] recuperada já não existe, daí a exceção.
  • linha 27: criamos um novo EntityManager,
  • linha 28: recuperamos o funcionário Jouveinal solicitando explicitamente o subsídio associado na consulta JPQL. Esta solicitação explícita é necessária porque o modo de recuperação para este subsídio é LAZY,
  • Linha 30: Fechamos o EntityManager,
  • Linhas 32–36: exibimos o funcionário novamente. Não deve ocorrer nenhuma exceção.

Para executar o projeto, é necessário dispor de uma base de dados com dados. Deverá criá-la seguindo os passos descritos na secção 5.5. Além disso, o ficheiro [persistence.xml] deve ser alterado:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
    </properties>
  </persistence-unit>
</persistence>
  • Removemos a opção que criava as tabelas. A base de dados aqui já existe e está preenchida,
  • e removemos as opções que faziam com que o Hibernate registasse as instruções SQL enviadas para a base de dados.

A execução do projeto gera as duas mensagens seguintes na consola:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
jpa.Employe[id=31,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
  • Linha 1: A exceção que ocorreu ao tentar recuperar a remuneração em falta enquanto a sessão estava encerrada. Podemos ver que a remuneração não foi recuperada devido ao modo LAZY,
  • linha 2: o funcionário com o seu subsídio recuperado através de uma consulta que contornou o modo LAZY.

5.6.5. Trabalho a realizar

Seguindo um procedimento semelhante ao que acabámos de descrever, crie um projeto [mv-pam-pa-eclipselink-lazy] que demonstre o comportamento do EclipseLink com o modo LAZY.

Os seguintes resultados são obtidos:

jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]

No modo LAZY, ambas as consultas devolveram a remuneração juntamente com o funcionário. Ao pesquisar esta anomalia online, descobrimos que a anotação [FetchType.LAZY] (linha 1):


  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;

não é um requisito, mas sim uma sugestão. O implementador do JPA não é obrigado a segui-la. Podemos ver, portanto, que o código por vezes torna-se dependente da implementação do JPA utilizada. É possível configurar o EclipseLink para se comportar como esperado no modo LAZY.

5.6.6. Seguindo em frente

A arquitetura da aplicação a ser construída é a seguinte:

No restante deste documento, iremos duplicar o projeto Maven [mv-pam-jpa-hibernate] para o projeto [mv-pam-spring-hibernate] [1, 2, 3]:

  • depois, iremos renomear o novo projeto [4, 5, 6].

Vamos alterar as dependências do novo projeto. O ficheiro [pom.xml] fica com o seguinte conteúdo:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>istia.st</groupId>
  <artifactId>mv-pam-spring-hibernate</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
 
  <name>mv-pam-spring-hibernate</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>swing-layout</id>
      <layout>default</layout>
      <name>Repository for library Library[swing-layout]</name>
    </repository>
  </repositories>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
</project>
  • linhas 25–31: a dependência para os testes JUnit,
  • linhas 32–41: dependências para o pool de conexões Apache DBCP,
  • linhas 42–65: dependências para o framework Spring,
  • linhas 67–71: dependências para a implementação JPA/Hibernate,
  • linhas 72–76: a dependência do controlador JDBC do MySQL,
  • linhas 77–81: a dependência para a interface Swing. Esta é adicionada automaticamente pelo NetBeans quando uma interface Swing é adicionada ao projeto.

Além disso, iremos gerar as duas bases de dados MySQL:

  • [dbpam_hibernate] a partir do script [dbpam_hibernate.sql],
  • [dbpam_eclipselink] a partir do script [dbpam_eclipselink.sql],

5.7. As interfaces im s para as camadas [business] e [DAO]

Voltemos à arquitetura da aplicação:

Na arquitetura acima, que interface deve a camada [DAO] fornecer à camada [business] e que interface deve a camada [business] fornecer à camada [UI]? Uma primeira abordagem para definir as interfaces das diferentes camadas consiste em examinar os vários casos de utilização da aplicação. Aqui temos dois, dependendo da interface de utilizador escolhida: consola ou formulário gráfico.

Vamos examinar como a aplicação de consola é utilizada:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
...

Informations Cotisations :
CSGRDS : 3.49 %
...

Informations Indemnités :
...

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

A aplicação recebe três informações do utilizador (ver linha 1 acima)

  • o número de segurança social do prestador de cuidados infantis
  • o número de horas trabalhadas no mês
  • o número de dias trabalhados no mês

Com base nestas informações e noutros dados armazenados em ficheiros de configuração, a aplicação apresenta as seguintes informações:

  • linhas 4–6: os valores introduzidos
  • linhas 8–10: informações relacionadas com o funcionário cujo número de segurança social foi fornecido
  • linhas 12–14: as taxas das várias contribuições para a segurança social
  • linhas 16–17: os vários subsídios pagos ao prestador de cuidados infantis
  • linhas 19–24: itens do recibo de vencimento do prestador de cuidados infantis

A camada [empresarial] deve fornecer uma determinada quantidade de informações à camada [UI]:

  1. informações relacionadas com um prestador de cuidados infantis identificado pelo seu número de segurança social. Estas informações encontram-se na tabela [EMPLOYEES]. Isto permite que as linhas 6–8 sejam apresentadas.
  2. os montantes das várias taxas de contribuição para a segurança social a deduzir do salário bruto. Estas informações encontram-se na tabela [COTISATIONS]. Isto permite que as linhas 10–12 sejam apresentadas.
  3. os montantes dos vários subsídios relacionados com a função de prestador de cuidados infantis. Esta informação encontra-se na tabela [INDEMNITES]. Isto permite que as linhas 14–15 sejam apresentadas.
  4. os componentes do salário apresentados nas linhas 18 a 22.

A partir daí, poderíamos decidir sobre uma implementação inicial da interface [IMetier] apresentada pela camada [metier] à camada [ui]:

1
2
3
4
5
6
package metier;

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • linha 1: os elementos da camada [business] são colocados no pacote [business]
  • linha 5: o método [calculatePaystub] recebe como parâmetros as três informações obtidas pela camada [ui] e devolve um objeto do tipo [Paystub] contendo as informações que a camada [ui] irá apresentar na consola. A classe [ Paystub] poderia ser a seguinte:
package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire {
   // private fields
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  ...
}
  • linha 9: o funcionário abrangido pelo recibo de vencimento - informação n.º 1 apresentada pela camada [ui]
  • linha 10: as várias taxas de contribuição - informação n.º 2 apresentada pela camada [ui]
  • linha 11: os vários subsídios associados ao índice do funcionário - informação n.º 3 apresentada pela camada [ui]
  • linha 12: os componentes do seu salário - informação n.º 4 apresentada pela camada [ui]

Um segundo caso de utilização da camada [business] surge com a interface gráfica:

Como mostrado acima, a lista suspensa [1, 2] exibe todos os funcionários. Esta lista deve ser solicitada à camada [business]. A interface ace para esta camada evolui então da seguinte forma:

package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // list of employees
  public List<Employe> findAllEmployes();
}
  • linha [10]: o método que permitirá à camada [UI] solicitar a lista de todos os funcionários à camada [Business].

A camada [business] só pode inicializar os campos [Employee, Contribution, Allowance] do objeto [Payroll] acima através de uma consulta à camada [DAO], uma vez que esta informação está armazenada nas tabelas da base de dados. O mesmo se aplica à recuperação da lista de todos os funcionários. Poderíamos criar uma única interface [DAO] para gerir o acesso às três entidades [Empregado, Contribuição, Subsídio]. No entanto, decidimos aqui criar uma interface [DAO] por entidade.

A interface [DAO] para aceder às entidades [Contribution] na tabela [CONTRIBUTIONS] será a seguinte:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // create a new contribution
  public Cotisation create(Cotisation cotisation);
       // modify an existing contribution
  public Cotisation edit(Cotisation cotisation);
       // delete an existing contribution
  public void destroy(Cotisation cotisation);
       // search for a specific contribution
  public Cotisation find(Long id);
       // get all objects Contribution
  public List<Cotisation> findAll();

}
  • Linha 6: A interface [ICotisationDao] gere o acesso à entidade [Cotisation] e, consequentemente, à tabela [COTISATIONS] na base de dados. A nossa aplicação necessita apenas do método [findAll] na linha 16, que recupera todo o conteúdo da tabela [COTISATIONS]. Aqui, pretendíamos abordar um caso mais geral em que todas as operações CRUD (Create, Read, Update, Delete) são realizadas na entidade.
  • Linha 8: O método [create] cria uma nova entidade [Cotisation]
  • Linha 10: O método [edit] modifica uma entidade [Cotisation] existente
  • Linha 12: O método [destroy] elimina uma entidade [Cotisation] existente
  • Linha 14: O método [find] recupera uma entidade [Cotisation] existente utilizando o seu id
  • Linha 16: O método [findAll] devolve uma lista de todas as entidades [Membership] existentes

Vamos analisar mais detalhadamente a assinatura do método [create]:

      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);

O método create tem um parâmetro cotisation do tipo Cotisation. O parâmetro cotisation deve ser persistido, ou seja, armazenado na tabela [COTISATIONS]. Antes desta persistência, o parâmetro cotisation tem um identificador id sem valor. Após a persistência, o campo id tem um valor que é a chave primária do registo adicionado à tabela [COTISATIONS]. O parâmetro cotisation é, portanto, um parâmetro de entrada/saída do método create. Não parece necessário que o método create devolva adicionalmente o parâmetro cotisation como resultado. Uma vez que o método de chamada mantém uma referência ao objeto [Cotisation cotisation], se este for modificado, o método de chamada tem acesso ao objeto modificado porque mantém uma referência ao mesmo. Pode, portanto, conhecer o valor que o método create atribuiu ao campo id do objeto [Cotisation cotisation]. A assinatura do método poderia, portanto, ser simplificada para:

      // créer une nouvelle cotisation
void create(Cotisation cotisation);

Ao escrever uma interface, é importante lembrar que esta pode ser utilizada em dois contextos diferentes: local e remoto . No contexto local, o método chamador e o método chamado são executados na mesma JVM:

Se a camada [business] chamar o método create da camada [DAO], ela tem, de facto, uma referência ao parâmetro [Membership membership] que passa para o método.

No contexto remoto, o método chamador e o método chamado são executados em JVMs diferentes:

No exemplo acima, a camada [business] é executada na JVM 1 e a camada [DAO] na JVM 2, em duas máquinas diferentes. As duas camadas não comunicam diretamente entre si. Entre elas existe uma camada a que chamaremos de camada de comunicação [1]. Esta consiste numa camada de transmissão [2] e numa camada de receção [3]. O programador geralmente não precisa de escrever estas camadas de comunicação. Elas são geradas automaticamente por ferramentas de software. A camada [business] é escrita como se estivesse a ser executada na mesma JVM que a camada [DAO]. Por conseguinte, não são necessárias alterações no código.

O mecanismo de comunicação entre a camada [de negócios] e a camada [DAO] é o seguinte:

  • a camada [negócio] chama o método create da camada [DAO], passando-lhe o parâmetro [Contribution contribution1]
  • este parâmetro é, na verdade, passado para a camada de transmissão [2]. Esta camada transmitirá o valor do parâmetro cotisation1 pela rede, e não a sua referência. A forma exata deste valor depende do protocolo de comunicação utilizado.
  • A camada de receção [3] recupera este valor e utiliza-o para reconstruir um objeto [Cotisation cotisation2] que espelha o parâmetro inicial enviado pela camada [business]. Temos agora dois objetos idênticos (em termos de conteúdo) em duas JVMs diferentes: cotisation1 e cotisation2.
  • A camada de apresentação passará o objeto `contribution2` para o método `create` da camada [DAO], que o persistirá na base de dados. Após esta operação, o campo `id` do objeto `contribution2` foi inicializado com a chave primária do registo adicionado à tabela [COTISATIONS]. Este não é o caso do objeto `contribution1`, ao qual a camada [business] tem uma referência. Se quisermos que a camada [business] tenha uma referência ao objeto cotisation2, temos de o passar para a camada. Por conseguinte, precisamos de alterar a assinatura do método create na camada [DAO]:
      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);
  • Com esta nova assinatura, o método create irá devolver o objeto persistido contribution2. Este resultado é devolvido à camada recetora [3] que chamou a camada [DAO]. A camada [DAO] irá devolver o valor (não a referência) de contribution2 à camada emissora [2].
  • A camada emissora [2] irá recuperar este valor e utilizá-lo para reconstruir um objeto [Membership membership3] que reflete o resultado devolvido pelo método create da camada [DAO].
  • O objeto [Contribution contribution3] é devolvido ao método na camada [business] cuja chamada ao método create da camada [DAO] tinha iniciado todo este mecanismo. A camada [business] pode, portanto, determinar o valor da chave primária atribuído ao objeto [Contribution contribution1] para o qual tinha solicitado persistência: este é o valor do campo id em contribution3.

A arquitetura anterior não é a mais comum. Mais frequentemente, as camadas [business] e [DAO] encontram-se na mesma JVM:

Nesta arquitetura, são os métodos da camada [business] que devem devolver resultados, e não os da camada [DAO]. No entanto, a seguinte assinatura do método create da camada [DAO]:

      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);

permite-nos evitar fazer suposições sobre a arquitetura efetivamente implementada. Utilizar assinaturas que funcionem independentemente da arquitetura escolhida — seja local ou remota — significa que, se um método chamado modificar alguns dos seus parâmetros:

  • estes também devem fazer parte do resultado do método chamado
  • o método chamador deve utilizar o resultado do método chamado e não as referências aos parâmetros modificados que passou para o método chamado.

Isto permite-nos fazer a transição de uma arquitetura local para uma arquitetura remota sem modificar o código. Vamos rever a interface [ICotisationDao] à luz disto:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // create a new contribution
  public Cotisation create(Cotisation cotisation);
       // modify an existing contribution
  public Cotisation edit(Cotisation cotisation);
       // delete an existing contribution
  public void destroy(Cotisation cotisation);
       // search for a specific contribution
  public Cotisation find(Long id);
       // get all objects Contribution
  public List<Cotisation> findAll();

}
  • linha 8: o caso do método create foi tratado
  • Linha 10: O método edit utiliza o seu parâmetro [Cotisation cotisation1] para atualizar o registo na tabela [COTISATIONS] que tem a mesma chave primária que o objeto cotisation. Ele devolve o objeto cotisation2, que é uma representação do registo modificado. O próprio parâmetro contribution1 não é modificado. O método deve devolver contribution2 como resultado, quer numa arquitetura remota, quer numa local.
  • Linha 12: O método destroy elimina o registo da tabela [COTISATIONS] que tem a mesma chave primária que o objeto contribution passado como parâmetro. O objeto contribution não é modificado. Por conseguinte, não precisa de ser devolvido.
  • Linha 14: O parâmetro id do método find não é modificado pelo método. Não precisa de ser incluído no resultado.
  • Linha 16: O método findAll não tem parâmetros. Por isso, não precisamos de o examinar.

Em última análise, apenas a assinatura do método create precisa de ser adaptada para ser utilizável numa arquitetura remota. O raciocínio acima aplica-se às outras interfaces [DAO]. Não o repetiremos aqui e, em vez disso, utilizaremos assinaturas que sejam utilizáveis tanto em arquiteturas remotas como locais.

A interface [DAO] para aceder a entidades [Indemnite] na tabela [INDEMNITES] será a seguinte:

package dao;

import java.util.List;
import jpa.Indemnite;

public interface IIndemniteDao {
     // create an Indemnity entity
  public Indemnite create(Indemnite indemnite);
     // modify an Indemnite entity
  public Indemnite edit(Indemnite indemnite);
     // delete an Indemnity entity
  public void destroy(Indemnite indemnite);
     // search for an Indemnite entity via its identifier
  public Indemnite find(Long id);
     // get all Indemnite entities
  public List<Indemnite> findAll();

}
  • Linha 6: A interface [IIndemniteDao] gere o acesso à entidade [Indemnite] e, consequentemente, à tabela [INDEMNITES] na base de dados. A nossa aplicação necessita apenas do método [findAll] na linha 16, que recupera todo o conteúdo da tabela [INDEMNITES]. Aqui, pretendíamos abordar um caso mais geral em que todas as operações CRUD (Create, Read, Update, Delete) são realizadas na entidade.
  • Linha 8: O método [create] cria uma nova entidade [Indemnite]
  • Linha 10: O método [edit] modifica uma entidade [Indemnite] existente
  • Linha 12: O método [destroy] elimina uma entidade [Indemnite] existente
  • Linha 14: O método [find] recupera uma entidade [Indemnite] existente utilizando o seu id
  • Linha 16: O método [findAll] devolve uma lista de todas as entidades [Indemnite] existentes

A interface [DAO] para aceder a entidades [Employe] na tabela [EMPLOYES] será a seguinte:

package dao;

import java.util.List;
import jpa.Employe;

public interface IEmployeDao {
     // create a new Employ entity
  public Employe create(Employe employe);
     // modify an existing Employe entity
  public Employe edit(Employe employe);
     // delete an Employ entity
  public void destroy(Employe employe);
     // search for an Employe entity via its id identifier
  public Employe find(Long id);
     // search for an Employe entity via its SS number
  public Employe find(String SS);
     // get all Employe entities
  public List<Employe> findAll();
}
  • Linha 6: A interface [IEmployeDao] gere o acesso à entidade [Employee] e, consequentemente, à tabela [EMPLOYEES] na base de dados. A nossa aplicação necessita apenas do método [findAll] na linha 16, que recupera todo o conteúdo da tabela [EMPLOYEES]. Aqui, pretendíamos abordar um caso mais geral em que todas as operações CRUD (Create, Read, Update, Delete) são realizadas na entidade.
  • Linha 8: O método [create] cria uma nova entidade [Employee]
  • Linha 10: O método [edit] modifica uma entidade [Employee] existente
  • Linha 12: O método [destroy] elimina uma entidade [Employee] existente
  • Linha 14: O método [find] recupera uma entidade [Employee] existente através do seu ID
  • Linha 16: O método [find(String SS)] recupera uma entidade [Employee] existente utilizando o seu número SS. Vimos que este método era necessário para a aplicação de consola.
  • Linha 18: O método [findAll] devolve uma lista de todas as entidades [Employee] existentes. Vimos que este método era necessário para a aplicação gráfica.

5.8. A classe [PamException]

A camada [DAO] irá funcionar com a API JDBC do Java. Esta API lança exceções [SQLException] controladas, que têm duas desvantagens:

  • elas sobrecarregam o código, que deve lidar com essas exceções usando blocos try/catch.
  • devem ser declaradas nas assinaturas dos métodos da interface [IDao] utilizando "throws SQLException". Isto impede a implementação desta interface por classes que lançariam uma exceção controlada de um tipo diferente de [SQLException].

Para resolver este problema, a camada [DAO] irá apenas «propagar» exceções não verificadas do tipo [PamException].

  • A camada [JDBC] lança exceções do tipo [SQLException]
  • A camada [JPA] lança exceções específicas da implementação JPA que está a ser utilizada
  • A camada [DAO] lança exceções não capturadas do tipo [PamException]

Isto tem duas consequências:

  • A camada [business] não será obrigada a tratar exceções da camada [DAO] utilizando blocos try/catch. Pode simplesmente deixá-las propagar-se até à camada [UI].
  • Os métodos da interface [IDao] não precisam de especificar a natureza da [PamException] nas suas assinaturas, o que deixa em aberto a possibilidade de implementar esta interface com classes que lançariam outro tipo de exceção não capturada.

A classe [PamException] será colocada no pacote [exception] do projeto NetBeans:

O seu código é o seguinte:

package exception;

@SuppressWarnings("serial")
public class PamException extends RuntimeException {

   // error code
  private int code;

  public PamException(int code) {
    super();
    this.code = code;
  }

  public PamException(String message, int code) {
    super(message);
    this.code = code;
  }

  public PamException(Throwable cause, int code) {
    super(cause);
    this.code = code;
  }

  public PamException(String message, Throwable cause, int code) {
    super(message, cause);
    this.code = code;
  }

   // getter and setter

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }

}
  • Linha 4: [PamException] deriva de [RuntimeException]. É, portanto, um tipo de exceção que o compilador não exige que seja tratada com um bloco try/catch nem incluída nas assinaturas de métodos. Por este motivo, [PamException] não está incluída nas assinaturas de método da interface [IDao]. Isto permite que a interface seja implementada por uma classe que lance um tipo diferente de exceção, desde que também derive de [RuntimeException].
  • Para distinguir entre os erros que podem ocorrer, utilizamos o código de erro na linha 7. Os três construtores nas linhas 14, 19 e 24 são os da classe pai [RuntimeException], aos quais adicionámos um parâmetro: o código de erro que queremos atribuir à exceção.

O comportamento da aplicação, do ponto de vista das exceções, será o seguinte:

  • A camada [DAO] encapsulará qualquer exceção encontrada numa [PamException] e a re-lançará para a camada [business].
  • A camada [business] permitirá que as exceções lançadas pela camada [DAO] se propaguem para cima. Ela encapsulará qualquer exceção que ocorra na camada [business] numa [PamException] e a relançará para a camada [UI].
  • A camada [UI] intercepta todas as exceções propagadas pelas camadas [business] e [DAO]. Ela simplesmente exibirá a exceção na consola ou na interface gráfica do utilizador.

Vamos agora examinar a implementação das camadas [DAO] e [business] sucessivamente.

5.9. A camada [DAO] da aplicação [PAM]

Estamos a trabalhar com a seguinte arquitetura:

5.9.1. Implementação

Leitura recomendada: Secção 3.1.3 de [ref1]


Questão: Utilizando a integração Spring/JPA, escreva as classes [CotisationDao, IndemniteDao, EmployeDao] para implementar as interfaces [ICotisationDao, IIndemniteDao, IEmployeDao]. Cada método de classe irá capturar qualquer exceção e envolvê-la numa [PamException] com um código de erro específico para a exceção capturada.


As classes de implementação farão parte do pacote [dao]:

  

5.9.2. Configuração

Leitura recomendada: Secção 3.1.5 de [ref1]

A integração DAO/JPA é configurada pelo ficheiro Spring [spring-config-dao.xml] e pelo ficheiro JPA [persistence.xml]:


Pergunta: Escreva o conteúdo destes dois ficheiros. Partiremos do princípio de que a base de dados utilizada é a base de dados MySQL5 [dbpam_hibernate] gerada pelo script SQL [dbpam_hibernate.sql]. O ficheiro Spring definirá os seguintes três beans: employeDao do tipo EmployeDao, indemniteDao do tipo IndemniteDao e cotisationDao do tipo CotisationDao. Além disso, a implementação JPA utilizada será o Hibernate.


5.9.3. Testes

Leitura recomendada: secções 3.1.6 e 3.1.7 de [ref1]

Agora que a camada [DAO] está escrita e configurada, podemos testá-la. A arquitetura de teste será a seguinte:

5.9.4. InitDB

Iremos criar dois programas de teste para a camada [DAO]. Estes serão colocados no pacote [dao] [2] do ramo [Test Packages] [1] do projeto NetBeans. Este ramo não está incluído no projeto gerado pela opção [Build project], o que garante que os programas de teste que colocarmos aí não serão incluídos no ficheiro .jar final do projeto.

As classes colocadas no ramo [Test Packages] têm acesso às classes do ramo [Source Packages], bem como às bibliotecas de classes do projeto. Se os testes exigirem bibliotecas diferentes das que estão no projeto, estas devem ser declaradas no ramo [Test Libraries] [2].

As classes de teste utilizam a ferramenta de testes unitários JUnit:

  • [JUnitInitDB] não executa quaisquer testes. Preenche a base de dados com alguns registos e, em seguida, apresenta-os na consola.
  • [JUnitDao] realiza uma série de testes e verifica os seus resultados.

A estrutura da classe [JUnitInitDB] é a seguinte:

package dao;

...

public class JUnitInitDB {

  private IEmployeDao employeDao = null;
  private ICotisationDao cotisationDao = null;
  private IIndemniteDao indemniteDao = null;

  @BeforeClass
  public void init(){
     // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
  }

  @Test
  public void initDB(){
     // fill the base
...
     // displays the contents of the database
...
  }

  @Before()
  public void clean(){
     // empty the base
...
  }
}
  • O método [init] é executado antes do início do conjunto de testes (anotação @BeforeClass). Ele instancia a camada [DAO].
  • O método [clean] é executado antes de cada teste (anotação @Before). Limpa a base de dados.
  • O método [initDB] é um teste (anotação @Test). É o único. Um teste deve conter instruções de asserção Assert.assertCondition. Aqui, não haverá nenhuma. O método é, portanto, um teste fictício. O seu objetivo é preencher a base de dados com algumas linhas e, em seguida, apresentar o conteúdo da base de dados na consola. Os métodos create e findAll das camadas [DAO] são utilizados aqui.

Pergunta: Complete o código para a classe [JUnitInitDB]. Use o exemplo da Secção 3.1.6 de [ref1] como guia. O código irá gerar a saída apresentada na Secção 5.1.


5.9.5. Implementação do teste

Estamos agora prontos para executar [InitDB]. Descrevemos o procedimento utilizando o SGBD MySQL5:

  • as classes [1], os ficheiros de configuração [2] e as classes de teste da camada [DAO] [3] estão configurados,
  • o projeto é compilado [4]
  • a classe [JUnitInitDB] é executada [5]. O SGBD MySQL5 é iniciado com uma base de dados [dbpam_hibernate] existente,
  • a janela [Resultados do Teste] [6] indica que os testes foram bem-sucedidos. Esta mensagem não é significativa neste contexto, uma vez que o programa [JUnitInitDB] não contém instruções de asserção Assert.assertCondition que possam causar a falha do teste. No entanto, mostra que não ocorreram exceções durante a execução do teste.

A janela [Output] contém os registos de execução, incluindo os do Spring e do próprio teste. A saída gerada pela classe [JUnitInitDB] é a seguinte:

------------- Standard Output ---------------
Employés ----------------------
jpa.Employe[id=5,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=6,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1]
Indemnités ----------------------
jpa.Indemnite[id=5,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0]
jpa.Indemnite[id=6,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0]
Cotisations ----------------------
jpa.Cotisation[id=3,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88]
------------- ---------------- ---------------

As tabelas [EMPLOYEES, ALLOWANCES, CONTRIBUTIONS] foram preenchidas. Isto pode ser verificado ligando o NetBeans à base de dados [dbpam_hibernate].

  • Em [1], no separador [serviços], pode visualizar os dados da tabela [employees] da ligação [dbpam_hibernate] [2],
  • em [3] o resultado.

5.9.6. JUnitD ao

Vamos agora analisar uma segunda classe de teste [JUnitDao]:

A estrutura da classe será a seguinte:

package dao;

import exception.PamException;
...

public class JUnitDao {

// layers DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;

  @BeforeClass
  public static void init() {
     // log
    log("init");
     // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }

  @AfterClass
  public static void terminate() {
  }

  @Before()
  public void clean() {
...
  }

   // logs
  private static void log(String message) {
    System.out.println("----------- " + message);
  }

   // tests
  @Test
  public void test01() {
    log("test01");
     // list of contributions
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // we add a contribution
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // on demand
    cotisation = cotisationDao.find(cotisation.getId());
     // check
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(3.49, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // we modify it
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
     // checks
    Assert.assertEquals(cotisation.getVersion() + 1, cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation2.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getSecu(), 1e-6);
     // the modified element is requested
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
     // checks
    Assert.assertEquals(cotisation3.getVersion(), cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation3.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getSecu(), 1e-6);
     // we delete the
    cotisationDao.destroy(cotisation3);
     // checks
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
     // we ask for the list of allowances
...
     // we add an Indemnite indemnite
..
     // fetch indemnity from base - recover indemnity1
..
     // we check that indemnity1 = indemnity
...
     // modify the indemnity obtained and persist the modification in BD. The result is indemnity2
 ...
     // check the indemnite2 version
    ...
     // search for indemnity2 in base - obtain indemnity3
    ...
     // check that compensation3 = compensation2
    ...
     // delete indemnite3 image in base
    ...
     // we'll search for indemnite3 in base
    ...
     // check that a null reference has been obtained
 ...
  }

  @Test
  public void test03(){
    log("test03");
     // we repeat a test analogous to the previous ones for Employe
 ...
  }

  @Test
  public void test04(){
    log("test04");
     // test method [IEmployeDao].find(String SS)
     // first with an existing employee
     // then with a non-existent employee
...
  }

  @Test
  public void test05(){
    log("test05");
     // we create two allowances with the same index
     // violates index uniqueness constraint
     // check for the occurrence of a PamException exception
     // and has the expected error no
...
  }

  @Test
  public void test06(){
    log("test06");
     // create two employees with the same SS number
     // violates the uniqueness constraint on n° SS
     // check for the occurrence of a PamException exception
     // and has the expected error no
...

  }

  @Test
  public void test07(){
    log("test07");
     // create two employees with the same SS number, the 1st with create, the 2nd with edit
     // violates the uniqueness constraint on n° SS
     // check for the occurrence of a PamException exception
     // and has the expected error no
...
  }

  @Test
  public void test08(){
    log("test08");
     // deleting a non-existent employee does not trigger an exception
     // it is added and then destroyed - it is checked
...
  }

  @Test
  public void test09(){
    log("test09");
     // modifying an employee without having the correct version should trigger an exception
     // we check it
...
  }

  @Test
  public void test10(){
    log("test10");
     // deleting an employee without having the correct version should trigger an exception
     // we check it
...

  }

   // getters and setters
  ...
}

Na classe de teste anterior, a base de dados é limpa antes de cada teste.


Pergunta: Escreva os seguintes métodos:

1 - test02: com base em test01

2 - test03: Um funcionário tem um campo do tipo Indemnização. Por isso, crie uma entidade Indemnização e uma entidade Funcionário

3 - test04.


Procedendo da mesma forma que para a classe de teste [JUnitInitDB], obtemos os seguintes resultados:

  • Em [1], executamos a classe de teste
  • em [2], os resultados do teste na janela [Resultados do Teste]

Vamos provocar um erro para ver como é relatado na página de resultados:

  @Test
  public void test01() {
    log("test01");
     // list of contributions
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // we add a contribution
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // on demand
    cotisation = cotisationDao.find(cotisation.getId());
     // check
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(0, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // we modify it
....
}

Linha 13: A verificação irá causar um erro, uma vez que o valor de Csgrds é 3,49 (linha 8). A execução da classe de teste produz os seguintes resultados:

  • A página de resultados [1] mostra agora que alguns testes falharam.
  • Em [2], um resumo da exceção que causou a falha do teste. Inclui o número da linha no código Java onde a exceção ocorreu.

5.10. A camada [de negócios] da aplicação [PAM]

Agora que a camada [DAO] foi escrita, passamos a estudar a camada de negócios [2]:

5.10.1. A interface Java [IMetier]

Isto foi descrito na secção 5.7. Recordamo-lo a seguir:

package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // list of employees
  public List<Employe> findAllEmployes();
}

A implementação da camada [business] será feita num pacote [business]:

 

O pacote [Negócio] incluirá, além da interface [IMetier] e da sua implementação [Metier], duas outras classes: [Payroll] e [PayrollItems]. A classe [Payroll] foi brevemente apresentada na Secção 5.7. Vamos agora revisá-la.

5.10.2. A classe [Payroll]

O método [calculatePayStub] da interface [IMetier] devolve um objeto do tipo [PayStub] que representa os vários elementos de um recibo de vencimento. A sua definição é a seguinte:

package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire implements Serializable{
   // private fields
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

   // manufacturers
  public FeuilleSalaire() {

  }

  public FeuilleSalaire(Employe employe, Cotisation cotisation, ElementsSalaire elementsSalaire) {
    setEmploye(employe);
    setCotisation(cotisation);
    setElementsSalaire(elementsSalaire);
  }

   // toString
  public String toString() {
    return "[" + employe + "," + cotisation + "," + elementsSalaire + "]";
  }

   // accessors
...  
}
  • linha 7: a classe implementa a interface Serializable porque as suas instâncias podem ser trocadas através da rede.
  • linha 9: o funcionário abrangido pelo recibo de vencimento
  • linha 10: as várias taxas de contribuição
  • linha 11: os diversos subsídios associados ao índice do trabalhador
  • linha 12: os componentes do seu salário
  • linhas 14–22: os dois construtores da classe
  • linhas 25–27: método [toString] que identifica um objeto [PayStub] específico
  • Linhas 29 e seguintes: acessores públicos aos campos privados da classe

A classe [ElementsSalaire] referenciada na linha 11 da classe [FeuilleSalaire] acima contém os elementos que compõem um recibo de vencimento. A sua definição é a seguinte:

package metier;

public class ElementsSalaire implements Serializable{

   // private fields
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;

   // manufacturers
  public ElementsSalaire() {

  }

  public ElementsSalaire(double salaireBase, double cotisationsSociales,
    double indemnitesEntretien, double indemnitesRepas,
    double salaireNet) {
    setSalaireBase(salaireBase);
    setCotisationsSociales(cotisationsSociales);
    setIndemnitesEntretien(indemnitesEntretien);
    setIndemnitesRepas(indemnitesRepas);
  }

   // toString
  public String toString() {
    return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
      + indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
      + salaireNet + "]";
  }

   // public accessors
...  
}
  • linha 3: a classe implementa a interface Serializable porque é um componente da PayrollClass, que deve ser serializável.
  • linha 6: o salário base
  • linha 7: contribuições para a segurança social pagas sobre este salário base
  • linha 8: pagamentos diários de pensão de alimentos
  • linha 9: os subsídios diários para refeições das crianças
  • linha 10: o salário líquido a pagar ao prestador de cuidados infantis
  • linhas 12–24: construtores de classe
  • linhas 27–31: método [toString] que identifica um objeto [ElementsSalaire] específico
  • Linhas 34 e seguintes: acessores públicos aos campos privados da classe

5.10.3. A classe de implementação [Metier] da camada [business]

A classe de implementação [Metier] da camada [business] poderia ser a seguinte:

package metier;

...

@Transactional
public class Metier implements IMetier {

   // reference on the [DAO] layer
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

   // list of employees
   public List<Employe> findAllEmployes() {
     ...
  }

   // getters and setters
...
 }
  • linha 5: A anotação @Transactional do Spring garante que todos os métodos da classe sejam executados dentro de uma transação.
  • linhas 9-10: referências às camadas [DAO] das entidades [Cotisation, Employe, Indemnite]
  • linhas 14–17: o método [calculatePayroll]
  • linhas 20–22: o método [findAllEmployees]
  • Linha 24 e seguintes: os acessores públicos para os campos privados da classe

Pergunta: Escreva o código para o método [findAllEmployees].



Pergunta: Escreva o código para o método [calculatePayroll].


Tenha em atenção os seguintes pontos:

  • O método para calcular o salário foi explicado na secção 5.2.
  • Se o parâmetro [SS] não corresponder a nenhum funcionário (a camada [DAO] devolveu um ponteiro nulo), o método lançará uma [PamException] com um código de erro apropriado.

5.10.4. Testar a camada [business]

Criamos dois programas de teste:

As classes de teste [3] são criadas no pacote [metier] [2] dentro do ramo [Test Packages] [1] do projeto.

A classe [JUnitMetier_1] poderia ter o seguinte aspeto:

package metier;

...

public class JUnitMetier_1 {

// business layer
  private IMetier metier;

  @BeforeClass
  public void init(){
     // log
    log("init");
     // application configuration
     // instantiation layer [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
     // layers DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
     // empty the base
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // fill it
    Indemnite indemnite1=indemniteDao.create(new Indemnite(1,1.93,2,3,12));
    Indemnite indemnite2=indemniteDao.create(new Indemnite(2,2.1,2.1,3.1,15));
    Employe employe2=employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    Employe employe1=employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    Cotisation cotisation1=cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }

   // logs
  private void log(String message) {
    System.out.println("----------- " + message);
  }

   // test
  @Test
  public void test01(){
     // wage sheet calculation
    System.out.println(metier.calculerFeuilleSalaire("260124402111742",30, 5));
    System.out.println(metier.calculerFeuilleSalaire("254104940426058",150, 20));
    try {
      System.out.println(metier.calculerFeuilleSalaire("xx", 150, 20));
    } catch (PamException ex) {
      System.err.println(String.format("PamException[Code=%d, message=%s]",ex.getCode(), ex.getMessage()));
    }
  }
}

Não existem asserções Assert.assertCondition na classe. Estamos simplesmente a tentar calcular alguns salários para que possamos verificá-los manualmente. A saída no ecrã obtida ao executar a classe anterior é a seguinte:

1
2
3
4
5
6
7
Testsuite: metier.JUnitMetier_1
----------- init
....
[jpa.Employe[id=22,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=29,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0],[salaire base=64.85,cotisations sociales=17.45,indemnités d'entretien=10.0,indemnités de repas=15.0,salaire net=72.4]]
[jpa.Employe[id=21,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=30,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0],[salaire base=362.25,cotisations sociales=97.48,indemnités d'entretien=42.0,indemnités de repas=62.0,salaire net=368.77]]
PamException[Code=101, message=L'employé de n°[xx] est introuvable]
Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 3,234 sec
  • Linha 4: Recibo de vencimento de Justine Laverti
  • linha 5: recibo de vencimento de Marie Jouveinal
  • linha 6: a exceção devido ao facto de o funcionário com o número de segurança social «xx» não existir.

Pergunta: A linha 17 de [JUnitMetier_1] utiliza o bean Spring denominado metier. Forneça a definição deste bean no ficheiro [spring-config-metier-dao.xml].


A classe [JUnitMetier_2] poderia ser a seguinte:

package metier;

...
public class JUnitMetier_2 {

// business layer
  private IMetier metier;

  @BeforeClass
  public void init(){
...
  }

   // logs
  private void log(String message) {
    System.out.println("----------- " + message);
  }

   // test
  @Test
  public void test01(){
...
  }
}

A classe [JUnitMetier_2] é uma cópia da classe [JUnitMetier_1], com a diferença de que, desta vez, as asserções foram colocadas no método test01.


Pergunta: Escreva o método test01.


Ao executar a classe [JUnitMetier_2], obtêm-se os seguintes resultados se tudo correr bem:

Image

5.11. A camada [ui] da aplicação [PAM] – versão console

Agora que a camada [business] foi escrita, ainda precisamos de escrever a camada [ui] [1]:

Iremos criar duas implementações diferentes da camada [ui]: uma versão de consola e uma versão GUI Swing:

5.11.1. A classe [ ui.console.Main]

Vamos primeiro concentrar-nos na aplicação de consola implementada pela classe [ui.console.Main] acima. O seu funcionamento foi descrito na Secção 5.3. O esqueleto da classe [Main] poderia ser o seguinte:

package ui.console;

import exception.PamException;
import metier.FeuilleSalaire;
import metier.IMetier;

import java.util.ArrayList;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

  /**
   * @param args
   */
  public static void main(String[] args) {
     // local data
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
     // check the number of parameters
...
     // error list
    ArrayList erreurs = new ArrayList();
     // the second parameter must be a real number >0
...
     // mistake?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
     // the third parameter must be an integer >0
...
     // mistake?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
     // mistakes?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // it's OK - we can ask for the payslip
    FeuilleSalaire feuilleSalaire = null;
    try {
       // instantiation layer [metier]
      ...
       // wage sheet calculation
      ...
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.toString());
      return;
    }

     // detailed display
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
    output += ajouteInfo("Nombre d'heures travaillées", args[1]);
    output += ajouteInfo("Nombre de jours travaillés", args[2]);
    output += ajouteInfo("\nInformations Employé", "");
    output += ajouteInfo("Nom", feuilleSalaire.getEmploye().getNom());
    output += ajouteInfo("Prénom", feuilleSalaire.getEmploye().getPrenom());
    output += ajouteInfo("Adresse", feuilleSalaire.getEmploye().getAdresse());
    output += ajouteInfo("Ville", feuilleSalaire.getEmploye().getVille());
    output += ajouteInfo("Code Postal", feuilleSalaire.getEmploye().getCodePostal());
    output += ajouteInfo("Indice", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndice());
    output += ajouteInfo("\nInformations Cotisations", "");
    output += ajouteInfo("CSGRDS", ""+ feuilleSalaire.getCotisation().getCsgrds() + " %");
    output += ajouteInfo("CSGD", ""+ feuilleSalaire.getCotisation().getCsgd() + " %");
    output += ajouteInfo("Retraite", ""+ feuilleSalaire.getCotisation().getRetraite() + " %");
    output += ajouteInfo("Sécurité sociale", ""+ feuilleSalaire.getCotisation().getSecu() + " %");
    output += ajouteInfo("\nInformations Indemnités", "");
    output += ajouteInfo("Salaire horaire", ""+ feuilleSalaire.getEmploye().getIndemnite().getBaseHeure() + " euro");
    output += ajouteInfo("Entretien/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getEntretienJour() + " euro");
    output += ajouteInfo("Repas/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getRepasJour() + " euro");
    output += ajouteInfo("Congés Payés", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndemnitesCP()+ " %");
    output += ajouteInfo("\nInformations Salaire", "");
    output += ajouteInfo("Salaire de base", ""+ feuilleSalaire.getElementsSalaire().getSalaireBase()+ " euro");
    output += ajouteInfo("Cotisations sociales", ""+ feuilleSalaire.getElementsSalaire().getCotisationsSociales()+ " euro");
    output += ajouteInfo("Indemnités d'entretien", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesEntretien()+ " euro");
    output += ajouteInfo("Indemnités de repas", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesRepas()+ " euro");
    output += ajouteInfo("Salaire net", ""+ feuilleSalaire.getElementsSalaire().getSalaireNet() + " euro");

    System.out.println(output);
  }

  static String ajouteInfo(String message, String valeur) {
    return message + " : " + valeur + "\n";
  }
}

Pergunta: Complete o código acima.


5.11.2. Execução

Para executar a classe [ui.console.Main], proceda da seguinte forma:

  • Em [1], selecione as propriedades do projeto,
  • em [2], selecione a propriedade [Executar] do projeto,
  • utilize o botão [3] para especificar a classe (conhecida como classe principal) a executar,
  • selecione a classe [4],
  • A classe aparece em [5]. Esta classe requer três argumentos para ser executada (número de segurança social, número de horas trabalhadas, número de dias trabalhados). Estes argumentos são introduzidos em [6],
  • assim que isso estiver feito, o projeto pode ser executado [7]. A configuração anterior significa que a classe [ui.console.Main] será executada.

Os resultados da execução são apresentados na janela [output]:

5.12. A camada [ui] da aplicação [PAM] – versão gráfica

Vamos agora implementar a camada [ui] com uma interface gráfica de utilizador:

  • em [1], a classe [PamJFrame] da interface gráfica
  • em [2]: a interface gráfica do utilizador

5.12.1. Um tutorial rápido

Para criar a interface gráfica do utilizador, proceda da seguinte forma:

  • [1]: Crie um novo ficheiro utilizando o botão [1] [Novo ficheiro...]
  • [2]: Selecione a categoria de ficheiro [Formulários GUI Swing], ou seja, formulários gráficos
  • [3]: Selecione o tipo [Formulário JFrame], um tipo de formulário vazio
  • [5]: Dê um nome ao formulário; este será também o nome da classe
  • [6]: Coloque o formulário num pacote
  • [8]: O formulário é adicionado à árvore do projeto
  • [9]: O formulário é acessível através de duas vistas: [Design] [9], que permite projetar os vários componentes do formulário, e [Source] [10 abaixo], que fornece acesso ao código Java do formulário. Em última análise, um formulário é uma classe Java como qualquer outra. A vista [Design] é uma ferramenta para projetar o formulário. Sempre que um componente é adicionado no modo [Design], o código Java é adicionado na vista [Source] para o refletir.
  • [11]: A lista de componentes Swing disponíveis para um formulário pode ser encontrada na janela [Paleta].
  • [12]: A janela [Inspector] apresenta a estrutura em árvore dos componentes do formulário. Os componentes com representação visual encontram-se no ramo [JFrame]; os restantes, no ramo [Outros Componentes].
  • Em [13], selecionamos um componente [JLabel] com um único clique
  • Em [14], arrastamo-lo para o formulário no modo [Design]
  • Em [15], definimos as propriedades do JLabel (texto, tipo de letra).
  • Em [16], o resultado.
  • Em [17], solicitamos uma pré-visualização do formulário
  • Em [18], o resultado
  • Em [19], o rótulo [JLabel1] foi adicionado à árvore de componentes na janela [Inspector]
  • Em [20] e [21]: Na vista [Source] do formulário, foi adicionado código Java para gerir o JLabel adicionado.

Está disponível um tutorial sobre a criação de formulários com o NetBeans no URL [http://www.netbeans.org/kb/trails/matisse.html].

5.12.2. A GUI [PamJFrame]

Vamos criar a seguinte interface gráfica do utilizador:

  • em [1], a interface gráfica do utilizador
  • em [2], a estrutura em árvore dos seus componentes: um JLabel e seis contentores JPanel

JLabel1

JPanel1

JPanel2

JPanel3

JPanel4

JPanel5


Exercício prático: Crie a interface gráfica anterior utilizando o tutorial [http://www.netbeans.org/kb/trails/matisse.html].


5.12.3. Eventos da Interface Gráfica do Utilizador

Leitura recomendada: o capítulo [Interfaces gráficas de utilizador] em [ref2].

Vamos tratar do clique no botão [jButtonSalaire]. Para criar o método que trata deste evento, podemos proceder da seguinte forma:

O manipulador para o clique no botão [JButtonSalaire] é gerado:

1
2
3
    private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {
       // TODO add your handling code here:
}

O código Java que associa o método anterior ao clique no botão [JButtonSalaire] também é gerado:

1
2
3
4
5
6
    jButtonSalaire.setText("Salaire");
    jButtonSalaire.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButtonSalaireActionPerformed(evt);
      }
});

As linhas 2–5 especificam que o clique (evt do tipo ActionPerformed) no botão [jButtonSalaire] (linha 2) deve ser tratado pelo método [jButtonSalaireActionPerformed] (linha 4).

Também iremos tratar do evento [caretUpdate] (movimento do cursor) no campo de entrada [jTextFieldHT]. Para criar o manipulador para este evento, procedemos como anteriormente:

O manipulador para o evento [caretUpdate] no campo de entrada [jTextFieldHT] é gerado:

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }

O código Java que vincula o método anterior ao evento [caretUpdate] no campo de texto [jTextFieldHT] também é gerado:

1
2
3
4
5
    jTextFieldHT.addCaretListener(new javax.swing.event.CaretListener() {
      public void caretUpdate(javax.swing.event.CaretEvent evt) {
        jTextFieldHTCaretUpdate(evt);
      }
});

As linhas 1–4 indicam que o evento [caretUpdate] (linha 2) no botão [jTextFieldHT] (linha 1) deve ser tratado pelo método [jTextFieldHTCaretUpdate] (linha 3).

5.12.4. Inicialização da GUI

Voltemos à arquitetura da nossa aplicação:

A camada [ui] necessita de uma referência à camada [business]. Vamos recordar como essa referência foi obtida na aplicação de consola:

1
2
3
    // instantiation layer [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
IMetier metier = (IMetier) ctx.getBean("metier");

O método é o mesmo na aplicação GUI. Quando a aplicação GUI é inicializada, a referência [IMetier metier] da linha 3 acima também deve ser inicializada. O código gerado para a GUI é atualmente o seguinte:

package ui.swing;

...
public class PamJFrame extends javax.swing.JFrame {

   /** Creates new form PamJFrame */
  public PamJFrame() {
    initComponents();
  }

  /** This method is called from within the constructor to
   * initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is
   * always regenerated by the Form Editor.
   */
   // <editor-fold defaultstate="collapsed" desc=" Generated Code ">
  private void initComponents() {
...
  }// </editor-fold>

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }                                        

  private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {                                               
...
  }                                              

  public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new PamJFrame().setVisible(true);
      }
    });
  }

  // Variables declaration - do not modify
  private javax.swing.JButton jButtonSalaire;
...
   // End of variables declaration

}
  • Linhas 29–35: o método estático [main] que inicia a aplicação
  • linha 32: é criada e tornada visível uma instância da GUI [PamJFrame].
  • linhas 7-9: o construtor da GUI.
  • linha 8: chamada ao método [initComponents] definido na linha 17. Este método é gerado automaticamente com base no trabalho realizado no modo [Design]. Não o modifique.
  • linha 21: o método que irá tratar do deslocamento do cursor de entrada no campo [jTextFieldHT]
  • Linha 25: O método que irá tratar do clique no botão [jButtonSalaire]

Para adicionar as nossas próprias inicializações ao código anterior, podemos proceder da seguinte forma:

  /** Creates new form PamJFrame */
  public PamJFrame() {
    initComponents();
    doMyInit();
  }

...

   // instance variables
  private IMetier metier=null;
  private List<Employe> employes=null;
  private String[] employesCombo=null;
  private double heuresTravaillées=0;

   // proprietary initializations
  public void doMyInit(){
     // init context
    try{
       // instantiation layer [metier]
...
     // list of employees
...
    }catch (PamException ex){
     // the exception message is placed in [jTextAreaStatus]
...
     // return
      return;
    }
     // salary button disabled
...
     // jScrollPane1 hidden
...
     // spinner days worked
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // combobox employees
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • Linha 4: Chamamos um método personalizado para realizar as nossas próprias inicializações. Estas são definidas pelo código nas linhas 10–42

Pergunta: Usando os comentários como guia, complete o código para o procedimento [doMyInit].


5.12.5. Manipuladores de eventos


Pergunta: Escreva o método [jTextFieldHTCaretUpdate]. Este método deve garantir que, se os dados no campo [jTextFieldHT] não forem um número real >=0, o botão [jButtonSalaire] seja desativado.



Pergunta: Escreva o método [jButtonSalaireActionPerformed], que deve exibir o recibo de vencimento do funcionário selecionado em [jComboBoxEmployes].


5.12.6. Executar a GUI

Para executar a GUI, modifique a configuração [Run] do projeto:

  • Em [1], introduza a classe da GUI

O projeto deve estar completo com os seus ficheiros de configuração (persistence.xml, spring-config-metier-dao.xml) e a classe GUI. Inicie o SGBD de destino antes de executar o projeto.

Estamos interessados na seguinte arquitetura, na qual a camada JPA é agora implementada pelo EclipseLink:

5.13.1. O projeto NetBeans

O novo projeto NetBeans é criado através da cópia do projeto anterior:

  • em [1]: após clicar com o botão direito do rato no projeto Hibernate, selecione Copiar
  • usando o botão [2], selecione a pasta pai para o novo projeto. O nome da pasta aparece em [3].
  • em [4], nomeie o novo projeto
  • em [5], o nome da pasta do projeto
  • em [1], o novo projeto foi criado. Tem o mesmo nome que o original,
  • em [2] e [3], renomeie-o para [mv-pam-spring-eclipselink].

O projeto deve ser modificado em dois locais para o adaptar à nova camada JPA / EclipseLink:

  1. em [4], os ficheiros de configuração do Spring devem ser modificados. É aqui que se encontra a configuração da camada JPA.
  2. Em [5], as bibliotecas do projeto devem ser modificadas: as bibliotecas do Hibernate devem ser substituídas pelas do EclipseLink.

Comecemos por este último ponto. O ficheiro [pom.xml] para o novo projeto será o seguinte:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>istia.st</groupId>
  <artifactId>mv-pam-spring-eclipselink</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
 
  <name>mv-pam-spring-eclipselink</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>swing-layout</id>
      <layout>default</layout>
      <name>Repository for library Library[swing-layout]</name>
    </repository>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>    
  </repositories>
 
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>javax.persistence</artifactId>
      <version>2.0.3</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
</project>
  • linhas 73–82: dependências para a implementação JPA do EclipseLink,
  • linhas 19–24: o repositório Maven para o EclipseLink.

Os ficheiros de configuração do Spring devem ser modificados para indicar que a implementação do JPA foi alterada. Em ambos os ficheiros, apenas a secção que configura a camada JPA é alterada. Por exemplo, no [spring-config-metier-dao.xml] temos:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

   <!-- application layers -->
   <!- - DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
   <!-- business -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="indemniteDao" ref="indemniteDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>

   <!-- configuration JPA -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <!--
          <property name="showSql" value="true" />
    -->
        <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
        <property name="generateDdl" value="true" />
   <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
  </bean>

   <!-- data source DBCP -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/dbpam_hibernate" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>
....  
</beans>

As linhas 19–36 configuram a camada JPA. A implementação JPA utilizada é o Hibernate (linha 22). Além disso, a base de dados de destino é [dbpam_hibernate] (linha 41).

Para mudar para uma implementação JPA/EclipseLink, as linhas 19–35 acima são substituídas pelas linhas abaixo:

  <!-- configuration JPA -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
        <!--
          <property name="showSql" value="true" />
  -->
        <property name="databasePlatform" value="org.eclipse.persistence.platform.database.MySQLPlatform" />
        <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
</bean>
  • Linha 5: A implementação JPA utilizada é o EclipseLink
  • linha 9: a propriedade databasePlatform define o SGBD de destino, neste caso o MySQL
  • linha 11: para gerar as tabelas da base de dados quando a camada JPA é instanciada. Aqui, a propriedade está comentada.
  • linha 7: para exibir as instruções SQL emitidas pela camada JPA na consola. Aqui, a propriedade está comentada.

Além disso, a base de dados de destino passa a ser [dbpam_eclipselink] (linha 4 abaixo):

1
2
3
4
5
6
7
8
9
<!-- data source DBCP -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/dbpam_eclipselink" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>

5.13.2. Executar os testes

Antes de testar toda a aplicação, é aconselhável verificar se os testes JUnit são bem-sucedidos com a nova implementação do JPA. Antes de os executar, começaremos por eliminar as tabelas da base de dados. Para tal, no separador [Runtime] do NetBeans, se necessário, crie uma ligação à base de dados dbpam_eclipselink / MySQL5. Uma vez ligado à base de dados dbpam_eclipselink / MySQL5, pode prosseguir com a eliminação das tabelas, conforme ilustrado abaixo:

  • [1]: antes da eliminação
  • [2]: após a eliminação

Depois de fazer isto, pode executar o primeiro teste na camada [DAO]: InitDB, que preenche a base de dados. Para garantir que as tabelas anteriormente eliminadas são recriadas pela aplicação, certifique-se de que, na configuração do Spring JPA / EclipseLink, a linha:

        <property name="generateDdl" value="true" />

exista e não esteja comentada.

Compilamos o projeto e, em seguida, executamos o teste [JUnit InitDB]:

  • Em [1], o teste InitDB é executado.
  • Em [2], o teste falha. A exceção é lançada pelo Spring e não por um teste que falhou.

Causado por: org.springframework.beans.factory.BeanCreationException: Erro ao criar o bean com o nome 'entityManagerFactory' definido no recurso do caminho de classe [spring-config-DAO.xml]: Falha na invocação do método init; a exceção aninhada é java.lang.IllegalStateException: É necessário iniciar com o agente Java para utilizar o InstrumentationLoadTimeWeaver. Consulte a documentação do Spring.

O Spring indica que existe um problema de configuração. A mensagem não é clara. A razão para a exceção foi explicada na secção 3.1.9 de [ref1]. Para que a configuração do Spring/EclipseLink funcione, a JVM que executa a aplicação deve ser iniciada com um parâmetro específico, um agente Java. O formato deste parâmetro é o seguinte:

-javaagent:C:\...\spring-agent.jar

[spring-agent.jar] é o agente Java necessário à JVM para gerir a configuração do Spring/EclipseLink.

Ao executar um projeto, é possível passar argumentos para a JVM:

  • Em [1], pode aceder às propriedades do projeto
  • Em [2], as propriedades de execução
  • Em [3], passe o parâmetro -javaagent para a JVM

5.13.3. InitDB

Agora estamos prontos para testar [InitDB] novamente. Desta vez, os resultados são os seguintes:

  • Em [1], o teste foi bem-sucedido
  • Em [2], no separador [Serviços], atualizamos a ligação do NetBeans à base de dados [dbpam_eclipselink]
  • Em [3], foram criadas quatro tabelas
  • em [5], visualizamos o conteúdo da tabela [employees]
  • em [6], o resultado.

5.13.4. JUnitDao

A execução da classe de teste [JUnitDao] pode falhar, mesmo que tenha sido bem-sucedida com a implementação JPA/Hibernate. Para compreender o motivo, vamos analisar um exemplo.

O método que está a ser testado é o seguinte método IndemniteDao.create:

package dao;

...
@Transactional(propagation=Propagation.REQUIRED)
public class IndemniteDao implements IIndemniteDao{

  @PersistenceContext
  private EntityManager em;

   // manufacturer
  public IndemniteDao() {
  }

   // create an allowance
  public Indemnite create(Indemnite indemnite) {
    try{
      em.persist(indemnite);
    }catch(Throwable th){
      throw new PamException(th,31);
    }
    return indemnite;
  }

...
}
  • linhas 15–22: o método que está a ser testado

O método de teste é o seguinte:


package dao;
 
...
 
public class JUnitDao {
 
// layers DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;
 
  @BeforeClass
  public static void init() {
    // log
    log("init");
    // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
    // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }
 
  @Before()
  public void clean() {
    // empty the base
    for (Employe employe : employeDao.findAll()) {
      employeDao.destroy(employe);
    }
    for (Cotisation cotisation : cotisationDao.findAll()) {
      cotisationDao.destroy(cotisation);
    }
    for (Indemnite indemnite : indemniteDao.findAll()) {
      indemniteDao.destroy(indemnite);
    }
  }
 
  // logs
  private static void log(String message) {
    System.out.println("----------- " + message);
  }
 
  // tests
….
  @Test
  public void test05() {
    log("test05");
    // we create two allowances with the same index
    // violates index uniqueness constraint
    boolean erreur = true;
    Indemnite indemnite1 = null;
    Indemnite indemnite2 = null;
    Throwable th = null;
    try {
      indemnite1 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
      indemnite2 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
      erreur = false;
    } catch (PamException ex) {
      th = ex;
      // checks
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // checks
    Assert.assertTrue(erreur);
    // exception chain
    System.out.println("Chaîne des exceptions --------------------------------------");
    System.out.println(th.getClass().getName());
    while (th.getCause() != null) {
      th = th.getCause();
      System.out.println(th.getClass().getName());
    }
    // the 1st allowance had to be continued
    Indemnite indemnite = indemniteDao.find(indemnite1.getId());
    // check
    Assert.assertNotNull(indemnite);
    Assert.assertEquals(1, indemnite.getIndice());
    Assert.assertEquals(1.93, indemnite.getBaseHeure(), 1e-6);
    Assert.assertEquals(2, indemnite.getEntretienJour(), 1e-6);
    Assert.assertEquals(3, indemnite.getRepasJour(), 1e-6);
    Assert.assertEquals(12, indemnite.getIndemnitesCP(), 1e-6);
    // the second indemnity should not have persisted
    List<Indemnite> indemnites = indemniteDao.findAll();
    int nbIndemnites = indemnites.size();
    Assert.assertEquals(nbIndemnites, 1);
  }
 
...
}

Pergunta: Explique o que o teste test05 faz e indique os resultados esperados.


Os resultados obtidos utilizando uma camada JPA/Hibernate são os seguintes:

----------- test05
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
ATTENTION: SQL Error: 1062, SQLState: 23000
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
GRAVE: Duplicate entry '1' for key 2
Chaîne des exceptions --------------------------------------
exception.PamException
javax.persistence.EntityExistsException
org.hibernate.exception.ConstraintViolationException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

O teste é bem-sucedido, o que significa que as asserções foram verificadas e nenhuma exceção foi lançada pelo método de teste.


Pergunta: Explique o que aconteceu.


Os resultados obtidos com uma camada JPA/EclipseLink são os seguintes:

----------- test05
[EL Warning]: 2010-06-04 16:48:26.421--UnitOfWork(749304)--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.0.0.v20091127-r5931): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 2
Error Code: 1062
Call: INSERT INTO INDEMNITES (ID, ENTRETIEN_JOUR, REPAS_JOUR, INDICE, INDEMNITES_CP, BASE_HEURE, VERSION) VALUES (?, ?, ?, ?, ?, ?, ?)
        bind => [108, 2.0, 3.0, 1, 12.0, 1.93, 1]
Query: InsertObjectQuery(jpa.Indemnite[id=108,version=1,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0])
Chaîne des exceptions --------------------------------------
org.springframework.transaction.TransactionSystemException
javax.persistence.RollbackException
org.eclipse.persistence.exceptions.DatabaseException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

Tal como no Hibernate anteriormente, o teste é bem-sucedido, o que significa que as asserções são verificadas e nenhuma exceção é lançada pelo método de teste.


Pergunta: Explique o que aconteceu.



Pergunta: A partir destes dois exemplos, o que podemos concluir sobre a intercambiabilidade das implementações JPA? É total neste caso?


5.13.5. Os outros testes

Depois de a camada [DAO] ter sido testada e considerada correta, podemos passar a testar a camada [business] e o próprio projeto na sua versão de consola ou gráfica. Alterar a implementação do JPA não tem efeito nas camadas [business] e [UI]; portanto, se estas camadas funcionavam com o Hibernate, funcionarão com o EclipseLink, com algumas exceções: o exemplo anterior mostra que as exceções lançadas pelas camadas [DAO] podem diferir. Assim, no caso do teste, o Spring / JPA / Hibernate lança uma [PamException], uma exceção específica da aplicação [pam], enquanto o Spring / JPA / EclipseLink lança uma [TransactionSystemException], uma exceção da estrutura Spring. Se, no caso de teste, a camada [ui] esperar uma [PamException] porque foi construída com o Hibernate, ela deixará de funcionar ao mudar para o EclipseLink.

5.13.6. Trabalho a realizar


Tarefa prática: Voltar a testar as aplicações de consola e Swing com diferentes SGBDs: MySQL5, Oracle XE, SQL Server.