Skip to content

5. Wersja 1: Architektura Spring / JPA

Proponujemy napisanie aplikacji konsolowej oraz aplikacji graficznej umożliwiającej sporządzenie listy płac opiekunek zatrudnionych przez „Dom Małego Dziecka” w danej gminie. Aplikacja ta będzie miała następującą architekturę:

5.1. BD Baza danych

Dane statyczne potrzebne do sporządzenia listy płac zostaną umieszczone w bazie danych, którą w dalszej części będziemy nazywać dbpam. Baza ta może zawierać następujące tabele:

Tabela EMPLOYES: zawiera informacje o poszczególnych opiekunkach dziecięcych

Struktura:

ID
klucz główny
VERSION
numer wersji – zwiększa się przy każdej zmianie w wierszu
SS
numer ubezpieczenia społecznego pracownika – unikalny
NOM
imię i nazwisko pracownika
prenom
jego imię
ADRESSE
jego adres
VILLE
jego miasto
CODEPOSTAL
jego kod pocztowy
INDEMNITE_ID
klucz obcy w polu [ID] tabeli [INDEMNITES]

Jego zawartość mogłaby wyglądać następująco:

Image

Tabela COTISATIONS: zawiera wartości procentowe niezbędne do obliczenia składek na ubezpieczenie społeczne

Struktura:

ID
klucz główny
VERSION
numer wersji – zwiększa się przy każdej zmianie w wierszu
CSGRDS
procent: ogólna składka społeczna + składka na spłatę długu społecznego
CSGD
procent: ogólna składka na ubezpieczenie społeczne podlegająca odliczeniu
SECU
procent: ubezpieczenie społeczne, renta wdowia, emerytura
RETRAITE
procent: dodatkowa emerytura + ubezpieczenie od bezrobocia

Jego treść mogłaby wyglądać następująco:

Image

Stawki składek na ubezpieczenie społeczne są niezależne od pracownika. Powyższa tabela zawiera tylko jeden wiersz.

Tabela INDEMNITES: zawiera elementy umożliwiające obliczenie wynagrodzenia do wypłaty.
ID
klucz główny
 
VERSION
numer wersji – zwiększa się przy każdej zmianie wiersza
 
INDICE
indeks przetwarzania – unikalny
 
BASEHEURE
Cena netto w euro za godzinę dyżuru
 
ENTRETIENJOUR
dodatek na utrzymanie w euro za dzień opieki
 
REPASJOUR
dodatek na posiłki w euro za dzień opieki
 
INDEMNITESCP
dodatek urlopowy. Jest to procent, który należy zastosować do wynagrodzenia podstawowego.
 
  

Jego treść mogłaby wyglądać następująco:

Image

Należy zauważyć, że dodatki mogą się różnić w zależności od opiekunki. Są one bowiem powiązane z konkretną opiekunką poprzez jej wskaźnik wynagrodzenia. Tak więc pani Marie Jouveinal, której wskaźnik wynagrodzenia wynosi 2 (tabela EMPLOYES), otrzymuje wynagrodzenie godzinowe w wysokości 2,1 euro (tabela INDEMNITES).

5.2. Sposób obliczania wynagrodzenia opiekunki do dzieci

Poniżej przedstawiamy sposób obliczania miesięcznego wynagrodzenia opiekunki do dzieci. Nie jest to sposób stosowany w rzeczywistości. Jako przykład posłużymy się wynagrodzeniem pani Marie Jouveinal, która przepracowała 150 godzin w ciągu 20 dni w miesiącu rozliczeniowym.

Uwzględniono następujące elementy:

[TOTALHEURES]: total des heures
travaillées dans le mois

[TOTALJOURS]: total des jours travaillés
dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
Wynagrodzenie podstawowe opiekunki do dzieci
oblicza się według następującego wzoru:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+
[INDEMNITESCP]/100)
[SALAIREBASE]=
(150*[2.1])*(1+0.15)= 362,25
Pewna kwota składek na ubezpieczenie społeczne
należy potrącić z tego wynagrodzenia
:

Contribution sociale généralisée et
contribution au remboursement de la
dette sociale :
 [SALAIREBASE]*[CSGRDS/100]

Contribution sociale généralisée déductible :
 [SALAIREBASE]*[CSGD/100]

Sécurité sociale, veuvage, vieillesse :
 [SALAIREBASE]*[SECU/100]

Retraite Complémentaire + AGPF +
Assurance Chômage :
 [SALAIREBASE]*[RETRAITE/100]
CSGRDS : 12,64
CSGD : 22,28
Sécurité sociale : 34,02
Retraite : 28,55
Łączna kwota składek na ubezpieczenie społeczne:
[COTISATIONSSOCIALES]=
[SALAIREBASE]*(CSGRDS+CSGD
+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Ponadto opiekunka ma prawo do dodatku na utrzymanie oraz dodatku na posiłki za każdy przepracowany dzień. W związku z tym otrzymuje następujące dodatki:

[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
Ostatecznie wynagrodzenie netto przysługujące opiekunce nad dziećmi wynosi:
[SALAIREBASE]-[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

5.3. Działanie aplikacji konsolowej

Oto przykład uruchomienia aplikacji konsolowej w oknie 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

Napiszemy program, który będzie pobierał następujące informacje:

  1. numer ubezpieczenia społecznego opiekunki (w przykładzie 254104940426058 – wiersz 1)
  2. łączna liczba przepracowanych godzin (w przykładzie 150 – wiersz 1)
  3. łączna liczba przepracowanych dni (w przykładzie 20 – wiersz 1)

Widać, że:

  • wiersze 9–14: wyświetlają informacje dotyczące pracownika, którego numer ubezpieczenia społecznego został podany
  • wiersze 17–20: zawierają stawki różnych składek
  • wiersze 23–26: zawierają dodatki związane z indeksem wynagrodzenia pracownika (w tym przypadku indeks 2)
  • wiersze 29–33: zawierają składniki wynagrodzenia do wypłaty

Aplikacja sygnalizuje ewentualne błędy:

Wywołanie bez parametrów:


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

Wywołanie z błędnymi danymi:


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é

Wezwanie z błędnym numerem ubezpieczenia społecznego:


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. Działanie aplikacji graficznej

Aplikacja graficzna umożliwia obliczanie wynagrodzeń opiekunek do dzieci za pomocą formularza Swing:

  • informacje przekazywane wcześniej jako parametry do programu konsolowego są teraz wprowadzane za pomocą pól wprowadzania danych [1, 2, 3].
  • Przycisk [4] uruchamia obliczenie wynagrodzenia
  • formularz wyświetla różne składniki wynagrodzenia, aż do kwoty wynagrodzenia netto do wypłaty [5]

Lista rozwijana [1, 6] nie wyświetla numerów identyfikacyjnych SS pracowników, lecz ich nazwiska i imiona. Zakłada się tutaj, że nie ma dwóch pracowników o tym samym nazwisku i imieniu.

5.5. Tworzenie bazy danych

Uruchamiamy WampServer i korzystamy z narzędzia PhpMyAdmin [1]:

  • w [2] wybieramy opcję [Bases de données],
  • w [3] tworzymy bazę danych [dbpam_hibernate],
  • w [4] pojawia się utworzona baza danych. Wybieramy ją,
  • w [5], chcemy zaimportować skrypt SQL,
  • w [6], używamy przycisku [Parcourir], aby wskazać plik,
  • w [7,8] wybieramy skrypt SQL,
  • w [9] uruchamia się go,
  • w [10], tabele zostały utworzone. Ich zawartość jest następująca:

tabela EMPLOYES

Image

tabela INDEMNITES

Image

tabela COTISATIONS

Image

5.6. Implementacja JPA

5.6.1. Warstwa JPA / Hibernate

Skonfigurujemy warstwę JPA w następującym środowisku:

Program konsolowy będzie współpracował z bazą danych. W tym celu konieczne jest:

  • posiadać bazę danych,
  • posiadać sterownik JDBC dla SGBD, w tym przypadku MySQL,
  • zaimplementować warstwę JPA przy użyciu Hibernate,
  • napisać program konsolowy.

Tworzymy projekt Maven o nazwie [mv-pam-jpa-hibernate] [1]:

W architekturze naszej aplikacji potrzebujemy następujących elementów:

  • baza danych,
  • sterownik JDBC dla SGBD MySQL,
  • warstwa JPA / Hibernate (entities i konfiguracja),
  • program konsolowy do testowania.

5.6.1.1. Baza danych

Najpierw utwórzmy pustą bazę danych. Uruchamiamy WampServer i korzystamy z narzędzia PhpMyAdmin oraz [1]:

  • w [2] wybieramy opcję [Bases de données],
  • w [3] tworzy się bazę danych [dbpam_hibernate],
  • w przypadku [4] – utworzona baza danych.

5.6.1.2. Konfiguracja warstwy JPA

Połączenie między warstwą JDBC a bazą danych odbywa się za pośrednictwem pliku [persistence.xml], który konfiguruje warstwę JPA. Plik ten można utworzyć za pomocą programu NetBeans:

  • w zakładce [services] [1] nawiązuje się połączenie z bazą danych za pomocą sterownika JDBC z MySQL [2],
  • w [3] należy podać nazwę bazy danych, z którą chcemy się połączyć.
  • w [4] – nazwę bazy danych,
  • w [5] logujemy się jako root bez hasła,
  • w [6] można przetestować połączenie,
  • w [7] połączenie się powiodło.
  • połączenie pojawia się w [8] i w [9],
  • w [10] dodajemy nowy element do projektu,
  • w [11] wybiera się kategorię [Persistence], a w [12] element [Persistence Unit],
  • w [13] nadajemy nazwę tej jednostce trwałości,
  • w [14] wybieramy implementację Hibernate,
  • w [15] wskazujemy właśnie utworzone połączenie z bazą danych MySQL,
  • w [16] określamy, że podczas instancjonowania warstwy JPA ma ona utworzyć (create) tabele odpowiadające encjom JPA projektu.

Na zakończenie pracy kreatora generowany jest plik [persistence.xml]:

  • plik pojawia się w nowej gałęzi projektu, w folderze [META-INF] [1],
  • który odpowiada folderowi [src/main/resources] w projekcie [2,3].

Jego zawartość jest następująca:


<?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>
  • wiersz 3: nazwa jednostki trwałości i typ transakcji. RESOURCE_LOCAL wskazuje, że projekt samodzielnie zarządza transakcjami. W tym przypadku zadanie to musi wykonać program konsolowy,
  • wiersz 4: zastosowaną implementacją JPA jest Hibernate,
  • wiersze 6–9: parametry połączenia z bazą danych JDBC,
  • wiersz 11: żądanie utworzenia tabel odpowiadających encjom JPA. W rzeczywistości NetBeans generuje tutaj błędną konfigurację. Konfiguracja powinna wyglądać następująco:

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

Dzięki opcji „create” Hibernate, podczas instancjonowania warstwy JPA, usuwa, a następnie tworzy tabele odpowiadające encjom JPA. Opcja „create-drop” działa tak samo, ale po zakończeniu cyklu życia warstwy JPA usuwa wszystkie tabele. Istnieje jeszcze jedna opcja:


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

Ta opcja tworzy tabele, jeśli jeszcze nie istnieją, ale nie usuwa ich, jeśli już istnieją.

Dodamy trzy kolejne właściwości do konfiguracji Hibernate:


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

Wymagają one od Hibernate wyświetlenia poleceń SQL, które wysyła do bazy danych. Pełny plik wygląda zatem następująco:


<?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. Zależności

Wróćmy do architektury projektu:

Skonfigurowaliśmy warstwę JPA za pomocą pliku [persistence.xml]. Jako implementację wybrano bibliotekę Hibernate. Spowodowało to pojawienie się zależności w projekcie:

  

Zależności te wynikają z włączenia biblioteki Hibernate do projektu. Musimy dodać kolejną zależność – sterownik JDBC od MySQL, który implementuje warstwę JDBC architektury. Modyfikujemy plik [pom.xml] w następujący sposób:


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

Wiersze 8–12 dodają zależność sterownika JDBC od MySQL.

5.6.1.4. Elementy JPA


Pytanie: Postępując zgodnie z procedurą przedstawioną w przykładzie z paragrafu 4.4, wygeneruj elementy [Cotisation, Indemnite, Employe].


Uwagi:

  • entytety będą częścią pakietu o nazwie [jpa],
  • każda jednostka będzie miała numer wersji,
  • jeśli dwa elementy są powiązane relacją, utworzona zostanie wyłącznie relacja główna @ManyToOne. Relacja odwrotna @OneToMany nie zostanie utworzona.

5.6.1.5. Kod klasy głównej

Do projektu dołączamy wcześniej opracowane encje JPA i [1]:

a następnie dodajemy [2], następującą klasę [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) {
    // wystarczy utworzyć Entity Manager, aby zbudować warstwę JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
    EntityManager em=emf.createEntityManager();
    // zwolnienie zasobów
    em.close();
    emf.close();
  }
}
  • wiersz 10: tworzymy klasę EntityManagerFactory dla jednostki trwałości o nazwie [mv-pam-jpa-hibernatePU]. Nazwa ta pochodzi z pliku [persistence.xml]:

  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
  </persistence-unit>
  • wiersz 12: tworzony jest plik EntityManager. Ta operacja powoduje utworzenie warstwy JPA. Plik [persistence.xml] zostanie przetworzony, w związku z czym zostaną utworzone tabele bazy danych,
  • wiersze 14–15: zwolniono zasoby.

5.6.1.6. Tests

Wróćmy do architektury naszego projektu:

Wszystkie warstwy zostały zaimplementowane. Uruchamiamy projekt [2].

Wyniki wyświetlane w konsoli są następujące:

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

W konsoli znajdują się wyłącznie logi Hibernate, ponieważ uruchomiony program nie wykonuje żadnych innych czynności poza instancjonowaniem warstwy JPA. Należy zwrócić uwagę na następujące kwestie:

  • wiersz 43: Hibernate próbuje usunąć klucz obcy z tabeli [EMPLOYES],
  • wiersze 51–55: usunięcie trzech tabel,
  • wiersz 57: utworzenie tabeli [COTISATIONS],
  • wiersz 67: utworzenie tabeli [EMPLOYES],
  • wiersz 80: utworzenie tabeli [INDEMNITES],
  • wiersz 91: utworzenie klucza obcego tabeli [EMPLOYES].

W programie NetBeans można wyświetlić tabele w utworzonym wcześniej połączeniu:

Utworzone tabele zależą zarówno od implementacji warstwy JPA, jak i od używanego SGBD. W związku z tym implementacja JPA / EclipseLink z tą samą bazą danych może generować różne tabele. Teraz to właśnie sprawdzimy.

Stworzymy nowy projekt Maven w następującym środowisku:

Postępujemy zgodnie z instrukcjami z poprzedniego akapitu:

  1. utworzymy bazę danych o nazwie MySQL [dbpam_eclipselink]. Do jej wygenerowania użyjemy skryptu [dbpam_eclipselink.sql],
  2. utworzymy plik projektu o nazwie [persistence.xml]. Wybierzemy implementację JPA 2.0 EclipseLink,
  3. dodać do wygenerowanych zależności zależność od sterownika JDBC z MySQL,
  4. dodać encje JPA oraz program konsolowy,
  5. przeprowadzić testy.

Plik [persistence.xml] będzie wyglądał następująco:


<?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>
  • właściwości 9–13 zostały wygenerowane przez kreatora NetBeans,
  • wiersz 14: ta właściwość pozwala nam ustawić poziom logowania dla EclipseLink. Poziom FINE pozwala nam poznać polecenia SQL, które EclipseLink wyśle do bazy danych,
  • wiersz 15: podczas instancjonowania warstwy JPA / EclipseLink tabele encji JPA zostaną usunięte, a następnie utworzone.

Uzyskane wyniki w konsoli są następujące:

------------------------------------------------------------------------
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)--Wątek(Thread[main,5,main])--Typ dostępu dla klasy trwałej [class jpa.Cotisation] został ustawiony na [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Wątek(Thread[main,5,main])--Typ dostępu dla klasy trwałej [class jpa.Employe] został ustawiony na [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Wątek(Thread[main,5,main])--Klasa podmiotu docelowego (odniesienia) dla elementu mapowania „wiele do jednego” [field indemnite] jest domyślnie ustawiona na: klasę jpa.Indemnite.
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Wątek(Thread[main,5,main])--Typ dostępu dla klasy trwałej [class jpa.Indemnite] został ustawiony na [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa aliasowa klasy encji [class jpa.Cotisation] została domyślnie ustawiona na: Cotisation.
[EL Config]: 2012-06-22 14:35:01.915--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa kolumny dla elementu [id] została domyślnie ustawiona na: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa aliasu dla klasy encji [class jpa.Employe] została ustawiona domyślnie na: Employe.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa kolumny dla elementu [id] jest domyślnie ustawiona na: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa aliasu dla klasy encji [class jpa.Indemnite] została domyślnie ustawiona na: Indemnite.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa kolumny dla elementu [id] została ustawiona domyślnie na: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Wątek(Thread[main,5,main])--Nazwa kolumny klucza głównego dla elementu mapowania [field indemnite] została ustawiona domyślnie na: ID.
[EL Info]: 2012-06-22 14:35:02.558--ServerSession(730572764)--Wątek(Thread[main,5,main])--EclipseLink, wersja: Eclipse Persistence Services – 2.3.0.v20110604-r9504
[EL Config]: 2012-06-22 14:35:02.568--ServerSession(730572764)--Połączenie(1543921451)--Wątek(Thread[main,5,main])--nawiązywanie połączenia(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)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--Połączono: 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)--Wątek (Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU logowanie zakończone sukcesem
[EL Fine]: 2012-06-22 14:35:02.818--ServerSession(730572764)--Połączenie(1296716340)--Wątek(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)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--DROP TABLE COTISATIONS
[EL Fine]: 2012-06-22 14:35:03.118--ServerSession(730572764)--Połączenie(1296716340)--Wątek(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)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--DROP TABLE EMPLOYES
[EL Fine]: 2012-06-22 14:35:03.238--ServerSession(730572764)--Połączenie(1296716340)--Wątek(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)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--DROP TABLE INDEMNITES
[EL Fine]: 2012-06-22 14:35:03.338--ServerSession(730572764)--Połączenie(1296716340)--Wątek(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)--Połączenie(1296716340)--Wątek(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)--Połączenie(1296716340)--Wątek(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)--Wątek(Thread[main,5,main])--SELECT 1
[EL Warning]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Wątek(Thread[main,5,main])--Wyjątek [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)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) wartości (SEQ_GEN, 0)
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Połączenie(1296716340)--Wątek(Thread[main,5,main])--rozłączenie
[EL Info]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Wątek(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU wylogowanie zakończone sukcesem
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Połączenie(1543921451)--Wątek(Thread[main,5,main])--rozłączenie
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.503s
Finished at: Fri Jun 22 14:35:03 CEST 2012
Final Memory: 8M/153M
  • wiersze 26–30: połączenie z bazą danych MySQL,
  • wiersze 31–34: potwierdzenie pomyślnego nawiązania połączenia,
  • wiersz 36: usunięcie klucza obcego z tabeli [EMPLOYES],
  • wiersz 37: usunięcie tabeli [COTISATIONS],
  • wiersz 38: utworzenie tabeli [COTISATIONS]. Warto zauważyć, że klucz podstawowy ID nie posiada atrybutu MySQL auto_increment. Oznacza to, że to nie MySQL generuje wartości klucza głównego,
  • wiersz 39: usunięcie tabeli [EMPLOYES],
  • wiersz 40: utworzenie tabeli [EMPLOYES]. Jej klucz podstawowy ID nie posiada atrybutu MySQL auto_increment,
  • wiersz 41: usunięcie tabeli [INDEMNITES],
  • wiersz 42: utworzenie tabeli [INDEMNITES]. Jej klucz główny ID nie posiada atrybutu MySQL auto_increment,
  • wiersz 43: utworzenie klucza obcego z tabeli [EMPLOYES] do tabeli [INDEMNITES],
  • wiersz 44: utworzenie tabeli [SEQUENCE]. Będzie ona służyć do generowania kluczy głównych dla trzech poprzednich tabel,
  • wiersz 47: wystąpił wyjątek, ponieważ ta tabela już istniała,
  • wiersze 51–53: inicjalizacja tabeli [SEQUENCE].

Istnienie wygenerowanych tabel można sprawdzić w NetBeans [1]:

Zatem na podstawie tych samych encji JPA implementacje JPA, Hibernate i EclipseLink nie generują tych samych tabel. W dalszej części dokumentu, gdy stosowana jest implementacja JPA:

  • Hibernate, będzie wykorzystywana baza danych [dbpam_hibernate],
  • EclipseLink, będzie używana baza danych [dbpam_eclipselink].

5.6.3. Zadanie do wykonania

Postępując tak samo jak poprzednio,

  1. należy utworzyć i przetestować projekt [mv-pam-jpa-hibernate-oracle], wykorzystujący implementację JPA dla Hibernate oraz SGBD dla Oracle,
  2. utworzyć i przetestować projekt [mv-pam-jpa-hibernate-mssql] wykorzystujący implementację JPA z Hibernate oraz serwer SGBD i SQL,
  3. utworzenie i przetestowanie projektu [mv-pam-jpa-eclipselink-oracle] z wykorzystaniem implementacji JPA EclipseLink oraz serwera Oracle SGBD,
  4. utworzenie i przetestowanie projektu [mv-pam-jpa-eclipselink-mssql] z wykorzystaniem implementacji JPA, EclipseLink oraz serwera SGBD, SQL,

5.6.4. Lazy czy Eager?

Wróćmy do jednej z możliwych definicji encji [Employe]:


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;
  ...
}

Wiersze 27–29 definiują klucz obcy z tabeli [EMPLOYES] do tabeli [INDEMNITES]. Atrybut fetch w wierszu 27 określa strategię wyszukiwania pola indemnite z wiersza 29. Istnieją dwa tryby:

  • FetchType.LAZY: podczas wyszukiwania pracownika nie jest zwracane odpowiadające mu świadczenie. Zostanie ono zwrócone, gdy po raz pierwszy zostanie odwołane do pola [Employe].indemnite.
  • FetchType.EAGER: podczas wyszukiwania pracownika wyświetlane jest odpowiadające mu świadczenie. Jest to tryb domyślny, gdy nie określono żadnego trybu.

Aby zrozumieć zalety opcji FetchType.LAZY, można posłużyć się następującym przykładem. Lista pracowników bez informacji o dodatkach jest wyświetlana na stronie internetowej z linkiem [Details]. Kliknięcie tego linku powoduje wyświetlenie dodatków wybranego pracownika. Widać, że:

  • do wyświetlenia pierwszej strony nie są potrzebne dane pracowników wraz z ich dodatkami. W tym przypadku odpowiedni jest tryb FetchType.LAZY,
  • aby wyświetlić drugą stronę ze szczegółami, należy wykonać dodatkowe zapytanie do bazy danych w celu uzyskania dodatków wybranego pracownika.

Tryb FetchType.LAZY pozwala uniknąć pobierania zbyt dużej ilości danych, których aplikacja nie potrzebuje od razu. Spójrzmy na przykład.

Projekt [mv-pam-jpa-hibernate] jest duplikatem:

  • do [1], kopiuje się projekt,
  • w [2] wskazujemy folder, do którego ma trafić kopia, a w [3] jej nazwę,
  • w [4] nowy projekt nosi tę samą nazwę co poprzedni. Zmieniamy to:
  • w [1] zmieniamy nazwę projektu,
  • na [2], zmieniamy nazwę projektu, a jego nazwę na artifactId,
  • na [3] – nowy projekt.

Modyfikujemy program [Main.java] w następujący sposób:


package main;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;

public class Main {

  // poniższe zapytanie JPQL zwraca pracownika
  // klucz obcy [Employe].indemnite znajduje się w FetchType.LAZY
  public static void main(String[] args) {
    // wystarczy utworzyć Entity Manager, aby zbudować warstwę JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
    // pierwsza próba
    EntityManager em = emf.createEntityManager();
    Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    em.close();
    // wyświetla się pracownik
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // druga próba
    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();
    // zwolnienie zasobów
    em.close();
    // wyświetlanie pracownika
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // zwolnienie zasobów
    emf.close();
  }
}
  • wiersz 15: tworzymy plik EntityManagerFactory na podstawie warstwy JPA,
  • wiersz 17: uzyskujemy program EntityManager, który umożliwia nam komunikację z warstwą JPA,
  • wiersz 18: żądamy pracownika o nazwie Jouveinal,
  • wiersz 19: zamykamy EntityManager. Powoduje to zamknięcie kontekstu trwałości.
  • wiersz 22: wyświetlamy pobranego pracownika.

Klasa [Employe] wygląda następująco:


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()
    +"]";
  }
  ...
}
  • wiersz 27: pole indemnite jest przywracane do trybu LAZY,
  • wiersz 47: wykorzystuje pole indemnite. Jeśli metoda toString zostanie wywołana, a pole indemnite nie zostało jeszcze przywrócone, nastąpi to w tym momencie. Chyba że kontekst trwałości został zamknięty, tak jak w przykładzie.

Wróćmy do kodu metody [Main]:

  • wiersze 21–25: powinno wystąpić wyjątek. Metoda toString zostanie bowiem wywołana. Będzie ona korzystać z pola indemnite. Pole to zostanie wyszukane. Ponieważ kontekst trwałości został zamknięty, zwrócona encja [Employe] już nie istnieje, stąd wyjątek.
  • wiersz 27: tworzony jest nowy obiekt EntityManager,
  • wiersz 28: pobieramy pracownika Jouveinal, wyraźnie żądając w zapytaniu JPQL powiązanego z nim dodatku. To wyraźne żądanie jest konieczne, ponieważ trybem wyszukiwania tego dodatku jest LAZY,
  • wiersz 30: zamyka się EntityManager,
  • wiersze 32–36: ponownie wyświetla się pracownika. Nie powinno wystąpić żadnych wyjątków.

Aby uruchomić projekt, potrzebna jest wypełniona baza danych. Utworzymy ją, postępując zgodnie z instrukcjami zawartymi w paragrafie 5.5. Ponadto należy zmodyfikować plik [persistence.xml]:


<?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>
  • usunięto opcję, która tworzyła tabele. Baza danych już tutaj istnieje i jest wypełniona,
  • usunięto opcje, które powodowały, że Hibernate rejestrował polecenia SQL wysyłane do bazy danych.

Po uruchomieniu projektu w konsoli pojawiają się dwa następujące komunikaty:

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]
  • wiersz 1: wyjątek, który wystąpił podczas próby wyszukania brakującej premii, gdy sesja była zamknięta. Widać, że premia nie została pobrana z powodu trybu LAZY,
  • wiersz 2: pracownik wraz z dodatkiem uzyskanym w wyniku zapytania, które ominęło tryb LAZY.

5.6.5. Zadanie do wykonania

Postępując analogicznie do powyższego, utwórz projekt [mv-pam-pa-eclipselink-lazy], który pokazuje zachowanie projektu EclipseLink w porównaniu z trybem LAZY.

Otrzymujemy następujące wyniki:

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]

W trybie LAZY oba zapytania zwróciły odszkodowanie wraz z danymi pracownika. Szukając informacji w Internecie na temat tej nieprawidłowości, odkrywamy, że adnotacja [FetchType.LAZY] (wiersz 1):


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

nie jest poleceniem, lecz zaleceniem. Implementator JPA nie ma obowiązku się do niego stosować. Widać zatem, że kod staje się czasami zależny od używanej implementacji JPA. Możliwe jest skonfigurowanie EclipseLink tak, aby zachowywał się zgodnie z oczekiwaniami dla trybu LAZY.

5.6.6. W dalszej części

Architektura tworzonej aplikacji przedstawia się następująco:

W dalszej części dokumentu skopiujemy projekt Maven [mv-pam-jpa-hibernate] do projektu [mv-pam-spring-hibernate] [1, 2, 3]:

  • a następnie zmienimy nazwę nowego projektu na [4, 5, 6].

Zmienimy zależności nowego projektu. Plik [pom.xml] przyjmuje następującą postać:


<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>
  • wiersze 25–31: zależność od testów JUnit,
  • wiersze 32–41: zależności dotyczące puli połączeń Apache DBCP,
  • wiersze 42–65: zależności od frameworka Spring,
  • wiersze 67–71: zależności dla implementacji JPA / Hibernate,
  • wiersze 72–76: zależność od sterownika JDBC dla MySQL,
  • wiersze 77–81: zależność od interfejsu Swing. Jest ona automatycznie dodawana przez NetBeans po dodaniu interfejsu Swing do projektu.

Ponadto wygenerowane zostaną dwie bazy danych MySQL:

  • [dbpam_hibernate] na podstawie skryptu [dbpam_hibernate.sql],
  • [dbpam_eclipselink] na podstawie skryptu [dbpam_eclipselink.sql],

5.7. 'y interfejsów warstw [metier] i [DAO]

Wróćmy do architektury aplikacji:

W powyższej architekturze, jaki interfejs powinna udostępniać warstwa [DAO] warstwie [metier], a jaki interfejs powinna udostępniać warstwa [metier] warstwie [ui]? Pierwszym podejściem do zdefiniowania interfejsów poszczególnych warstw jest przeanalizowanie różnych przypadków użycia (use cases) aplikacji. W tym przypadku mamy dwa, w zależności od wybranego interfejsu użytkownika: konsola lub formularz graficzny.

Przyjrzyjmy się sposobowi korzystania z aplikacji konsolowej:

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

Aplikacja otrzymuje od użytkownika trzy informacje (patrz wiersz 1 powyżej)

  • numer ubezpieczenia społecznego opiekunki
  • liczba godzin przepracowanych w miesiącu
  • liczba dni przepracowanych w miesiącu

Na podstawie tych danych oraz innych zapisanych w plikach konfiguracyjnych aplikacja wyświetla następujące informacje:

  • wiersze 4–6: wprowadzone wartości
  • wiersze 8–10: informacje dotyczące pracownika, którego numer ubezpieczenia społecznego został podany
  • wiersze 12–14: stawki różnych składek na ubezpieczenie społeczne
  • wiersze 16–17: różne dodatki wypłacane opiekunce dziecięcej
  • wiersze 19–24: elementy listy płac opiekunki do dzieci

Warstwa [metier] musi dostarczyć warstwie [ui] pewne informacje:

  1. informacje dotyczące opiekunki do dzieci identyfikowanej na podstawie jej numeru ubezpieczenia społecznego. Informacje te znajdują się w tabeli [EMPLOYES]. Umożliwia to wyświetlenie wierszy 6–8.
  2. kwoty różnych stawek składek na ubezpieczenie społeczne, które należy potrącić z wynagrodzenia brutto. Informacje te znajdują się w tabeli [COTISATIONS]. Pozwala to na wyświetlenie wierszy 10–12.
  3. kwoty różnych dodatków związanych z wykonywaniem zawodu opiekunki do dzieci. Informacje te znajdują się w tabeli [INDEMNITES]. Pozwala to na wyświetlenie wierszy 14–15.
  4. składniki wynagrodzenia wyświetlane w wierszach 18–22.

Na tej podstawie można by zdecydować się na pierwszy zapis z interfejsu [IMetier], przedstawionego przez warstwę [metier] do warstwy [ui]:

1
2
3
4
5
6
package metier;

public interface IMetier {
   // pobierz listę płac
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • wiersz 1: elementy warstwy [metier] są umieszczane w pakiecie [metier]
  • wiersz 5: metoda [ calculerFeuilleSalaire ] przyjmuje jako parametry trzy informacje pozyskane przez warstwę [ui] i zwraca obiekt typu [FeuilleSalaire] zawierający informacje, które warstwa [ui] wyświetli na konsoli. Klasa [FeuilleSalaire] mogłaby wyglądać następująco:
package metier;

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

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

  ...
}
  • wiersz 9: pracownik, którego dotyczy lista płac – informacja nr 1 wyświetlana przez warstwę [ui]
  • wiersz 10: różne stawki składek – informacja nr 2 wyświetlana przez warstwę [ui]
  • wiersz 11: różne dodatki związane z indeksem pracownika – informacja nr 3 wyświetlana przez warstwę [ui]
  • wiersz 12: składniki wynagrodzenia – informacja nr 4 wyświetlana przez warstwę [ui]

Drugi przykład zastosowania warstwy [métier] pojawia się w interfejsie graficznym:

Jak widać powyżej, lista rozwijana [1, 2] zawiera wszystkich pracowników. Lista ta musi zostać pobrana z warstwy [métier]. Wygląd interfej y tej warstwy zmienia się wówczas w następujący sposób:

package metier;

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

public interface IMetier {
   // pobierz listę płac
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // lista pracowników
  public List<Employe> findAllEmployes();
}
  • wiersz [10]: metoda, która umożliwi warstwie [ui] zażądanie listy wszystkich pracowników od warstwy [métier].

Warstwa [metier] może zainicjować pola [Employe, Cotisation, Indemnite] powyższego obiektu [FeuilleSalaire] wyłącznie poprzez wysłanie zapytania do warstwy [DAO], ponieważ informacje te znajdują się w tabelach bazy danych. To samo dotyczy uzyskania listy wszystkich pracowników. Można utworzyć jeden interfejs [DAO] zarządzający dostępem do trzech encji [Employe, Cotisation, Indemnite]. W tym przypadku decydujemy się jednak na utworzenie osobnego interfejsu [DAO] dla każdej encji.

Interfejs [DAO], umożliwiający dostęp do encji [Cotisation] z tabeli [COTISATIONS], będzie wyglądał następująco:

package dao;

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

public interface ICotisationDao {
       // utworzyć nową składkę
  public Cotisation create(Cotisation cotisation);
       // edytuj istniejącą składkę
  public Cotisation edit(Cotisation cotisation);
       // usunięcie istniejącej składki
  public void destroy(Cotisation cotisation);
       // wyszukanie konkretnej składki
  public Cotisation find(Long id);
       // pobierz wszystkie obiekty składki
  public List<Cotisation> findAll();

}
  • W wierszu 6 interfejs [ICotisationDao] obsługuje dostęp do encji [Cotisation], a tym samym do tabeli [COTISATIONS] w bazie danych. Nasza aplikacja potrzebuje jedynie metody [findAll] z wiersza 16, która pozwala pobrać całą zawartość tabeli [COTISATIONS]. Chcieliśmy tutaj uwzględnić bardziej ogólny przypadek, w którym wszystkie operacje CRUD (Create, Read, Update, Delete) są wykonywane na tej encji.
  • wiersz 8: metoda [create] tworzy nową encję [Cotisation]
  • wiersz 10: metoda [edit] modyfikuje istniejącą encję [Cotisation]
  • wiersz 12: metoda [destroy] usuwa istniejącą encję [Cotisation]
  • wiersz 14: metoda [find] umożliwia odnalezienie istniejącej jednostki [Cotisation] na podstawie jej identyfikatora id
  • wiersz 16: metoda [findAll] zwraca listę wszystkich istniejących encji [Cotisation]

Przyjrzyjmy się sygnaturze metody [create]:

       // utworzyć nową składkę
Cotisation create(Cotisation cotisation);

Metoda create posiada parametr cotisation typu Cotisation. Parametr cotisation musi zostać zapisany w pamięci trwałej, c.a.d. który jest umieszczany w tabeli [COTISATIONS]. Przed tym zapisaniem parametr cotisation ma identyfikator id bez wartości. Po zapisaniu w bazie pole id ma wartość, która jest kluczem głównym rekordu dodanego do tabeli [COTISATIONS]. Parametr cotisation jest zatem parametrem wejściowym/wyjściowym metody create. Nie wydaje się konieczne, aby metoda create zwracała dodatkowo parametr cotisation jako wynik. Metoda wywołująca posiada odwołanie do obiektu [Cotisation cotisation], więc jeśli obiekt ten zostanie zmodyfikowany, ma ona dostęp do zmodyfikowanego obiektu, ponieważ posiada do niego odwołanie. Może zatem poznać wartość, jaką metoda create przypisała do pola id obiektu [Cotisation cotisation]. Sygnatura metody mogłaby zatem wyglądać prościej:

       // utworzyć nową składkę
void create(Cotisation cotisation);

Podczas pisania interfejsu warto pamiętać, że może on być używany w dwóch różnych kontekstach: local oraz distant. W kontekście local metoda wywołująca i metoda wywoływana są wykonywane w tym samym JVM:

Jeśli warstwa [metier] wywołuje metodę create z warstwy [DAO], to rzeczywiście posiada odwołanie do parametru [Cotisation cotisation], który przekazuje do tej metody.

W kontekście distant metoda wywołująca i metoda wywoływana są wykonywane w różnych JVM:

W powyższym przykładzie warstwa [metier] jest wykonywana w JVM 1, a warstwa [DAO] w JVM 2 na dwóch różnych maszynach. Obie warstwy nie komunikują się bezpośrednio. Pomiędzy nimi znajduje się warstwa, którą nazwiemy warstwą komunikacyjną [1]. Składa się ona z warstwy nadawczej [2] oraz warstwy odbiorczej [3]. Programista zazwyczaj nie musi samodzielnie pisać tych warstw komunikacyjnych. Są one generowane automatycznie przez narzędzia programistyczne. Warstwa [metier] jest napisana tak, jakby była wykonywana w tej samej warstwie JVM co warstwa [DAO]. Nie ma zatem żadnych zmian w kodzie.

Mechanizm komunikacji między warstwą [metier] a warstwą [DAO] wygląda następująco:

  • warstwa [metier] wywołuje metodę create warstwy [DAO], przekazując jej parametr [Cotisation cotisation1]
  • parametr ten jest w rzeczywistości przekazywany do warstwy wysyłającej [2]. Warstwa ta przekaże do sieci wartość parametru cotisation1, a nie jego odwołanie. Dokładna postać tej wartości zależy od zastosowanego protokołu komunikacyjnego.
  • Warstwa odbiorcza [3] pobierze tę wartość i na jej podstawie odtworzy obiekt [Cotisation cotisation2], będący odzwierciedleniem pierwotnego parametru wysłanego przez warstwę [metier]. Mamy teraz dwa identyczne (pod względem treści) obiekty w dwóch różnych warstwach JVM: cotisation1 i cotisation2.
  • Warstwa odbiorcza przekaże obiekt cotisation2 do metody create warstwy [DAO], która zapisze go w bazie danych. Po tej operacji pole id obiektu cotisation2 zostało zainicjowane kluczem głównym rekordu dodanego do tabeli [COTISATIONS]. Nie dotyczy to obiektu cotisation1, do którego warstwa [metier] posiada odniesienie. Jeśli chcemy, aby warstwa [metier] posiadała odwołanie do obiektu cotisation2, należy jej go przesłać. W związku z tym konieczna jest zmiana sygnatury metody create warstwy [DAO]:
       // utworzyć nową składkę
Cotisation create(Cotisation cotisation);
  • Dzięki tej nowej sygnaturze metoda create zwróci jako wynik obiekt trwały cotisation2. Wynik ten jest przekazywany do warstwy odbiorczej [3], która wywołała warstwę [DAO]. Ta z kolei zwróci wartość (a nie odwołanie) obiektu cotisation2 do warstwy wysyłającej [2].
  • Warstwa wysyłająca [2] pobierze tę wartość i na jej podstawie odtworzy obiekt [Cotisation cotisation3], będący odzwierciedleniem wyniku zwróconego przez metodę create warstwy [DAO].
  • Obiekt [Cotisation cotisation3] jest przekazywany do metody warstwy [metier], której wywołanie metody create warstwy [DAO] zainicjowało cały ten mechanizm. Warstwa [metier] może zatem poznać wartość klucza głównego przypisanego do obiektu [Cotisation cotisation1], o którego trwałość wnioskowała: jest to wartość pola id obiektu cotisation3.

Powyższa architektura nie jest najczęściej spotykana. Częściej spotyka się warstwy [metier] i [DAO] w tej samej warstwie JVM:

W tej architekturze to metody warstwy [metier] powinny zwracać wyniki, a nie metody warstwy [DAO]. Niemniej jednak następująca sygnatura metody create z warstwy [DAO]:

       // utwórz nową składkę
Cotisation create(Cotisation cotisation);

pozwala nam nie wysuwać żadnych hipotez dotyczących faktycznie wdrożonej architektury. Stosowanie sygnatur, które będą działać niezależnie od wybranej architektury – lokalnej czy zdalnej – oznacza, że w przypadku, gdy wywoływana metoda modyfikuje niektóre ze swoich parametrów:

  • muszą one również stanowić część wyniku wywołanej metody
  • metoda wywołująca musi wykorzystywać wynik wywołanej metody, a nie odwołania do zmodyfikowanych parametrów, które przekazała do wywołanej metody.

W ten sposób zachowujemy możliwość przejścia z architektury locale na architekturę distante bez konieczności modyfikacji kodu. Przyjrzyjmy się zatem ponownie, w tym kontekście, interfejsowi [ICotisationDao]:

package dao;

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

public interface ICotisationDao {
       // utwórz nową składkę
  public Cotisation create(Cotisation cotisation);
       // edytuj istniejącą składkę
  public Cotisation edit(Cotisation cotisation);
       // usunąć istniejącą składkę
  public void destroy(Cotisation cotisation);
       // wyszukaj konkretną składkę
  public Cotisation find(Long id);
       // pobierz wszystkie obiekty składki
  public List<Cotisation> findAll();

}
  • wiersz 8: omówiono przypadek metody create
  • wiersz 10: metoda edit wykorzystuje swój parametr [Cotisation cotisation1] do aktualizacji rekordu w tabeli [COTISATIONS] o tym samym kluczu głównym co obiekt cotisation. Jako wynik zwraca obiekt cotisation2, będący obrazem zmodyfikowanego rekordu. Parametr cotisation1 nie ulega natomiast zmianie. Metoda musi zwracać obiekt cotisation2 jako wynik, niezależnie od tego, czy znajduje się w kontekście architektury distante, czy locale.
  • wiersz 12: metoda destroy usuwa rekord z tabeli [COTISATIONS] posiadający ten sam klucz główny co obiekt cotisation przekazany jako parametr. Obiekt ten nie ulega zmianie. Nie ma zatem potrzeby zwracania go.
  • wiersz 14: parametr id metody find nie jest modyfikowany przez tę metodę. Nie musi on być częścią wyniku.
  • wiersz 16: metoda findAll nie ma parametrów. Nie ma zatem potrzeby jej analizowania.

Ostatecznie jedynie sygnatura metody create musi zostać dostosowana, aby można ją było wykorzystać w ramach architektury distante. Powyższe rozważania mają zastosowanie również do pozostałych interfejsów [DAO]. Nie będziemy ich powtarzać i bezpośrednio wykorzystamy sygnatury, które mogą być stosowane zarówno w ramach architektury distante, jak i locale.

Interfejs [DAO] służący do uzyskiwania dostępu do elementów [Indemnite] z tabeli [INDEMNITES] będzie wyglądał następująco:

package dao;

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

public interface IIndemniteDao {
     // utworzyć obiekt „Odszkodowanie”
  public Indemnite create(Indemnite indemnite);
     // zmiana obiektu „Indemnite”
  public Indemnite edit(Indemnite indemnite);
     // usunięcie obiektu „Indemnite”
  public void destroy(Indemnite indemnite);
     // wyszukaj jednostkę „Indemnite” na podstawie jej identyfikatora
  public Indemnite find(Long id);
     // pobierz wszystkie jednostki Indemnite
  public List<Indemnite> findAll();

}
  • W wierszu 6 interfejs [IIndemniteDao] zarządza dostępem do encji [Indemnite], a tym samym do tabeli [INDEMNITES] w bazie danych. Nasza aplikacja potrzebuje jedynie metody [findAll] z wiersza 16, która pozwala pobrać całą zawartość tabeli [INDEMNITES]. Chcieliśmy tutaj uwzględnić bardziej ogólny przypadek, w którym wszystkie operacje CRUD (Create, Read, Update, Delete) są wykonywane na tej encji.
  • wiersz 8: metoda [create] tworzy nową encję [Indemnite]
  • wiersz 10: metoda [edit] modyfikuje istniejącą encję [Indemnite]
  • wiersz 12: metoda [destroy] usuwa istniejącą encję [Indemnite]
  • wiersz 14: metoda [find] umożliwia odnalezienie istniejącej jednostki [Indemnite] na podstawie jej identyfikatora id
  • wiersz 16: metoda [findAll] zwraca listę wszystkich istniejących encji [Indemnite]

Interfejs [DAO] służący do uzyskiwania dostępu do encji [Employe] z tabeli [EMPLOYES] będzie wyglądał następująco:

package dao;

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

public interface IEmployeDao {
     // utworzyć nowy obiekt „Pracownik”
  public Employe create(Employe employe);
     // zmiana istniejącego obiektu „Pracownik”
  public Employe edit(Employe employe);
     // usunięcie podmiotu typu „Pracownik”
  public void destroy(Employe employe);
     // wyszukaj podmiot typu „Pracownik” na podstawie identyfikatora id
  public Employe find(Long id);
     // wyszukanie encji „Pracownik” na podstawie jej numeru SS
  public Employe find(String SS);
     // pobierz wszystkie rekordy typu „Pracownik”
  public List<Employe> findAll();
}
  • w wierszu 6 interfejs [IEmployeDao] obsługuje dostęp do encji [Employe], a tym samym do tabeli [EMPLOYES] w bazie danych. Nasza aplikacja potrzebuje jedynie metody [findAll] z wiersza 16, która pozwala pobrać całą zawartość tabeli [EMPLOYES]. Chcieliśmy tutaj uwzględnić bardziej ogólny przypadek, w którym wszystkie operacje CRUD (Create, Read, Update, Delete) są wykonywane na tej encji.
  • wiersz 8: metoda [create] tworzy nową encję [Employe]
  • wiersz 10: metoda [edit] modyfikuje istniejącą encję [Employe]
  • wiersz 12: metoda [destroy] usuwa istniejącą encję [Employe]
  • wiersz 14: metoda [find] umożliwia odnalezienie istniejącej jednostki [Employe] na podstawie jej identyfikatora id
  • wiersz 16: metoda [find(String SS)] pozwala odnaleźć istniejącą encję [Employe] na podstawie jej numeru SS. Widzieliśmy już, że metoda ta była niezbędna dla aplikacji konsolowej.
  • wiersz 18: metoda [findAll] zwraca listę wszystkich istniejących encji [Employe]. Widzieliśmy już, że ta metoda jest niezbędna dla aplikacji graficznej.

5.8. Klasa [PamException]

Warstwa [DAO] będzie współpracować z klasą API i JDBC w Javie. Ta klasa API generuje kontrolowane wyjątki typu [SQLException], które mają dwie wady:

  • zwiększają objętość kodu, który musi obowiązkowo obsługiwać te wyjątki za pomocą bloków try/catch;
  • muszą być zadeklarowane w sygnaturze metod interfejsu [IDao] za pomocą „throws SQLException”. W konsekwencji uniemożliwia to implementację tego interfejsu przez klasy, które zgłaszałyby wyjątek kontrolowany typu innego niż [SQLException].

Aby rozwiązać ten problem, warstwa [DAO] będzie „przekazywać dalej” wyłącznie niekontrolowane wyjątki typu [PamException].

  • warstwa [JDBC] generuje wyjątki typu [SQLException]
  • warstwa [JPA] generuje wyjątki specyficzne dla używanej implementacji JPA
  • warstwa [DAO] generuje niekontrolowane wyjątki typu [PamException]

Ma to dwie konsekwencje:

  • warstwa [metier] nie będzie miała obowiązku obsługi wyjątków z warstwy [DAO] za pomocą instrukcji try / catch. Będzie mogła po prostu pozwolić, by przepłynęły one aż do warstwy [ui].
  • metody interfejsu [IDao] nie muszą określać w swojej sygnaturze rodzaju wyjątku [PamException], co pozostawia możliwość implementacji tego interfejsu za pomocą klas, które zgłaszałyby inny typ niekontrolowanego wyjątku.

Klasa [PamException] zostanie umieszczona w pakiecie [exception] projektu NetBeans:

Jej kod wygląda następująco:

package exception;

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

   // kod błędu
  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;
  }

   // metody getter i setter

  public int getCode() {
    return code;
  }

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

}
  • wiersz 4: [PamException] wywodzi się z [RuntimeException]. Jest to zatem typ wyjątków, których kompilator nie wymaga od nas obsługi za pomocą try/catch ani umieszczania w sygnaturze metod. Z tego powodu [PamException] nie występuje w sygnaturze metod interfejsu [IDao]. Dzięki temu interfejs ten może być zaimplementowany przez klasę rzucającą inny typ wyjątków, pod warunkiem, że klasa ta również dziedziczy po [RuntimeException].
  • Aby rozróżnić błędy, które mogą wystąpić, wykorzystuje się kod błędu z linii 7. Trzy konstruktory z linii 14, 19 i 24 pochodzą z klasy nadrzędnej [RuntimeException], do których dodano jeden parametr: kod błędu, który ma zostać przypisany do wyjątku.

Działanie aplikacji z punktu widzenia wyjątków będzie wyglądało następująco:

  • warstwa [DAO] zamknie każdy napotkany wyjątek w wyjątku typu [PamException] i ponownie rzuci ten wyjątek do warstwy [métier].
  • Warstwa [métier] pozwoli na przekazanie w górę wyjątków wygenerowanych przez warstwę [DAO]. Będzie ona enkapsulować każdy wyjątek występujący w warstwie [métier] w wyjątku typu [PamException] i ponownie rzucać ten wyjątek do warstwy [ui].
  • Warstwa [ui] przechwytuje wszystkie wyjątki, które są przekazywane z warstw [métier] i [DAO]. Ogranicza się ona jedynie do wyświetlenia wyjątku na konsoli lub w interfejsie graficznym.

Przeanalizujmy teraz kolejno implementację warstw [DAO] i [metier].

5.9. Warstwa [DAO] aplikacji [PAM]

Poruszamy się w ramach następującej architektury:

5.9.1. Implementacja

Zalecana lektura: paragraf 3.1.3 dokumentu [ref1]


Pytanie: Korzystając z integracji Spring / JPA, należy napisać klasy [CotisationDao, IndemniteDao, EmployeDao] implementujące interfejsy [ICotisationDao, IIndemniteDao, IEmployeDao]. Każda metoda klasy będzie przechwytywać ewentualny wyjątek i enkapsulować go w wyjątku typu [PamException] z kodem błędu odpowiadającym przechwyconemu wyjątkowi.


Klasy implementacyjne będą częścią pakietu [dao]:

  

5.9.2. Konfiguracja

Zalecana lektura: paragraf 3.1.5 dokumentu [ref1]

Integracja DAO / JPA jest konfigurowana za pomocą pliku Spring [spring-config-dao.xml] oraz plików JPA i [persistence.xml]:


Pytanie: proszę zapisać zawartość tych dwóch plików. Zakładamy, że wykorzystywana baza danych to baza MySQL5 [dbpam_hibernate] wygenerowana przez skrypt SQL [dbpam_hibernate.sql]. Plik Spring zdefiniuje trzy następujące bean: employeDao typu EmployeDao, indemniteDao typu IndemniteDao, cotisationDao typu CotisationDao. Ponadto wykorzystana implementacja JPA będzie miała typ Hibernate.


5.9.3. Testy

Zalecana lektura: paragrafy 3.1.6 i 3.1.7 dokumentu [ref1]

Teraz, gdy warstwa [DAO] została napisana i skonfigurowana, możemy ją przetestować. Architektura testów będzie wyglądać następująco:

5.9.4. InitDB

Stworzymy dwa programy testujące warstwę [DAO]. Zostaną one umieszczone w pakiecie [dao] [2] w gałęzi [Test Packages] [1] projektu NetBeans. Ta gałąź nie jest uwzględniona w projekcie wygenerowanym przez opcję [Build project], co gwarantuje, że umieszczone w niej programy testowe nie zostaną uwzględnione w ostatecznym pliku .jar projektu.

Klasy umieszczone w gałęzi [Test Packages] mają dostęp do klas znajdujących się w gałęzi [Source Packages], a także do bibliotek klas projektu. Jeśli testy wymagają bibliotek innych niż te należące do projektu, należy je zadeklarować w gałęzi [Test Libraries] [2].

Klasy testowe korzystają z narzędzia do testów jednostkowych JUnit:

  • [JUnitInitDB] nie przeprowadza żadnych testów. Wypełnia bazę danych kilkoma rekordami, a następnie wyświetla je na konsoli.
  • [JUnitDao] przeprowadza serię testów i weryfikuje ich wyniki.

Szkielet klasy [JUnitInitDB] wygląda następująco:

package dao;

...

public class JUnitInitDB {

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

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

  @Test
  public void initDB(){
     // wypełnianie bazy danych
...
     // wyświetlanie zawartości bazy danych
...
  }

  @Before()
  public void clean(){
     // opróżnianie bazy
...
  }
}
  • metoda [init] jest wykonywana przed rozpoczęciem serii testów (adnotacja @BeforeClass). Tworzy ona instancję warstwy [DAO].
  • Metoda [clean] jest wykonywana przed każdym testem (adnotacja @Before). Opróżnia ona bazę danych.
  • Metoda [initDB] jest testem (adnotacja @Test). Jest to jedyny test. Test musi zawierać instrukcje asercji Assert.assertCondition. W tym przypadku nie będzie żadnej. Metoda ta jest zatem fałszywym testem. Jej zadaniem jest wypełnienie bazy danych kilkoma wierszami, a następnie wyświetlenie zawartości bazy na konsoli. Wykorzystywane są tutaj metody create i findAll z warstw [DAO].

Zadanie: uzupełnij kod klasy [JUnitInitDB]. Skorzystaj z przykładu z paragrafu 3.1.6 klasy [ref1]. Kod wygeneruje treść przedstawioną w paragrafie 5.1.


5.9.5. Wdrożeni owanie testów

Jesteśmy teraz gotowi do uruchomienia [InitDB]. Opisujemy procedurę na przykładzie SGBD i MySQL5:

  • klasy [1], pliki konfiguracyjne [2] oraz klasy testowe warstwy [DAO] i [3] zostały skonfigurowane,
  • projekt zostaje skompilowany [4]
  • klasa [JUnitInitDB] jest uruchamiana [5]. SGBD MySQL5 jest uruchamiany z istniejącą bazą [dbpam_hibernate],
  • okno [Test Results] i [6] informuje, że testy zakończyły się powodzeniem. Komunikat ten nie ma w tym przypadku znaczenia, ponieważ program [JUnitInitDB] nie zawiera żadnych instrukcji asercji Assert.assertCondition, które mogłyby spowodować niepowodzenie testu. Niemniej jednak wskazuje to, że podczas wykonywania testu nie wystąpił żaden wyjątek.

Okno [Output] zawiera logi wykonania, logi Springa oraz logi samego testu. Wyjścia generowane przez klasę [JUnitInitDB] są następujące:

------------- 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]
------------- ---------------- ---------------

Tabele [EMPLOYES, INDEMNITES, COTISATIONS] zostały wypełnione. Można to sprawdzić, łącząc się z bazą danych [dbpam_hibernate] za pomocą NetBeans.

  • w [1], w zakładce [services] wyświetlane są dane z tabeli [employes] z połączenia [dbpam_hibernate] [2],
  • a w [3] – wynik.

5.9.6. JUnitDao

Teraz zajmiemy się drugą klasą testów [JUnitDao]:

Szkielet tej klasy będzie wyglądał następująco:

package dao;

import exception.PamException;
...

public class JUnitDao {

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

  @BeforeClass
  public static void init() {
     // log
    log("init");
     // konfiguracja aplikacji
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // warstwy 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() {
...
  }

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

   // testy
  @Test
  public void test01() {
    log("test01");
     // lista składek
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // dodajemy składkę
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // na żądanie
    cotisation = cotisationDao.find(cotisation.getId());
     // weryfikacja
    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);
     // zmiana
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
     // weryfikacje
    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);
     // żądanie zmodyfikowanego elementu
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
     // weryfikacje
    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);
     // usuwamy element
    cotisationDao.destroy(cotisation3);
     // weryfikacje
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
     // żądana jest lista odszkodowań
...
     // dodaje się świadczenie „Indemnite”
..
     // pobieramy świadczenie z bazy danych – pobieramy świadczenie1
..
     // sprawdzamy, czy odszkodowanie1 = odszkodowanie
...
     // modyfikuje się uzyskaną rekompensatę i zapisuje zmianę w BD. Otrzymuje się rekompensatę2
 ...
     // sprawdzamy wersję odszkodowania2
    ...
     // pobieramy indemnite2 z bazy danych – otrzymujemy indemnite3
    ...
     // sprawdza się, czy indemnite3 = indemnite2
    ...
     // usuwamy z bazy obraz „indemnite3”
    ...
     // pobieramy „indemnite3” z bazy danych
    ...
     // sprawdzamy, czy otrzymaliśmy odwołanie null
 ...
  }

  @Test
  public void test03(){
    log("test03");
     // powtarza się test analogiczny do poprzednich dla „Employe”
 ...
  }

  @Test
  public void test04(){
    log("test04");
     // testujemy metodę [IEmployeDao].find(String SS)
     // najpierw z istniejącym pracownikiem
     //, a następnie z nieistniejącym pracownikiem
...
  }

  @Test
  public void test05(){
    log("test05");
     // tworzymy dwa świadczenia o tym samym indeksie
     // narusza ograniczenie unikalności indeksu
     // sprawdzamy, czy występuje wyjątek typu PamException
     // oraz czy ma on oczekiwany numer błędu
...
  }

  @Test
  public void test06(){
    log("test06");
     // tworzymy dwóch pracowników o tym samym numerze SS
     //, co narusza ograniczenie unikalności numeru SS
     // sprawdzamy, czy występuje wyjątek typu PamException
     // oraz że ma on oczekiwany numer błędu
...

  }

  @Test
  public void test07(){
    log("test07");
     // tworzymy dwóch pracowników o tym samym numerze SS, pierwszego za pomocą funkcji create, drugiego za pomocą funkcji edit
     // narusza ograniczenie unikalności numeru SS
     // sprawdzamy, czy występuje wyjątek typu PamException
     // oraz że ma ona oczekiwany numer błędu
...
  }

  @Test
  public void test08(){
    log("test08");
     // usunięcie nieistniejącego pracownika nie powoduje wyjątku
     // jest on dodawany, a następnie usuwany – należy to sprawdzić
...
  }

  @Test
  public void test09(){
    log("test09");
     // modyfikacja pracownika bez posiadania właściwej wersji powinna spowodować wyjątek
     // sprawdzamy to
...
  }

  @Test
  public void test10(){
    log("test10");
     // Usunięcie pracownika bez posiadania właściwej wersji powinno spowodować wyjątek
     // sprawdzamy to
...

  }

   // metody pobierające i ustawiające
  ...
}

W poprzedniej klasie testów baza danych jest czyszczona przed każdym testem.


Zadanie: napisz następujące metody:

1 – test02: należy wzorować się na test01

2 – test03: pracownik posiada pole typu Indemnite. Należy zatem utworzyć encję Indemnite oraz encję Employe

3 – test04.


Postępując w taki sam sposób jak w przypadku klasy testów [JUnitInitDB], otrzymujemy następujące wyniki:

  • w [1] uruchamia się klasę testów
  • w przypadku klasy testowej [2] wyniki testów w oknie [Test Results]

Wywołajmy błąd, aby sprawdzić, jak zostanie on zgłoszony na stronie wyników:

  @Test
  public void test01() {
    log("test01");
     // lista składek
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // dodajemy składkę
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // pobieranie
    cotisation = cotisationDao.find(cotisation.getId());
     // weryfikacja
    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);
     // modyfikacja
....
}

Wiersz 13 – asercja spowoduje błąd, ponieważ wartość Csgrds wynosi 3,49 (wiersz 8). Wykonanie klasy testów daje następujące wyniki:

  • Strona wyników [1] pokazuje teraz, że niektóre testy zakończyły się niepowodzeniem.
  • W pliku [2] znajduje się podsumowanie wyjątku, który spowodował niepowodzenie testu. Zawiera ono numer linii kodu Java, w której wystąpił wyjątek.

5.10. Warstwa [metier] aplikacji [PAM]

Teraz, gdy warstwa [DAO] została już napisana, przechodzimy do analizy warstwy biznesowej [2]:

5.10.1. Interfejs Java [IMetier]

Został on opisany w punkcie 5.7. Przytaczamy go poniżej:

package metier;

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

public interface IMetier {
   // pobieranie listy płac
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // lista pracowników
  public List<Employe> findAllEmployes();
}

Implementacja warstwy [metier] zostanie zrealizowana w pakiecie [metier]:

 

Pakiet [metier] będzie zawierał, oprócz interfejsu [IMetier] i jego implementacji [Metier], dwie inne klasy: [FeuilleSalaire] i [ElementsSalaire]. Klasa [FeuilleSalaire] została pokrótce omówiona w paragrafie 5.7. Powrócimy do niej teraz.

5.10.2. Klasa [FeuilleSalaire]

Metoda [calculerFeuilleSalaire] interfejsu [IMetier] zwraca obiekt typu [FeuilleSalaire], który reprezentuje różne elementy listy płac. Jej definicja jest następująca:

package metier;

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

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

   // konstruktorzy
  public FeuilleSalaire() {

  }

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

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

   // akcesory
...  
}
  • wiersz 7: klasa implementuje interfejs Serializable, ponieważ jej instancje mogą być przesyłane przez sieć.
  • wiersz 9: pracownik, którego dotyczy lista płac
  • wiersz 10: różne stawki składek
  • wiersz 11: różne dodatki związane z indeksem pracownika
  • wiersz 12: składniki jego wynagrodzenia
  • wiersze 14–22: dwa konstruktory klasy
  • wiersze 25–27: metoda [toString] identyfikująca konkretny obiekt [FeuilleSalaire]
  • wiersze 29 i kolejne: publiczne akcesory do pól prywatnych klasy

Klasa [ElementsSalaire], do której odwołuje się wiersz 11 powyższej klasy [FeuilleSalaire], zawiera elementy składające się na kartę płacową. Jej definicja jest następująca:

package metier;

public class ElementsSalaire implements Serializable{

   // pola prywatne
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;

   // producenci
  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 + "]";
  }

   // akcesory publiczne
...  
}
  • wiersz 3: klasa implementuje interfejs Serializable, ponieważ jest składnikiem klasy FeuilleSalaire, która musi być serializowalna.
  • wiersz 6: wynagrodzenie podstawowe
  • wiersz 7: składki na ubezpieczenie społeczne odprowadzane od tego wynagrodzenia podstawowego
  • wiersz 8: dzienne zasiłki na utrzymanie dziecka
  • wiersz 9: dzienne dodatki na wyżywienie dziecka
  • wiersz 10: wynagrodzenie netto należne opiekunce
  • wiersze 12–24: elementy składowe klasy
  • wiersze 27–31: metoda [toString] identyfikująca konkretny obiekt [ElementsSalaire]
  • wiersze 34 i kolejne: publiczne akcesory do pól prywatnych klasy

5.10.3. Klasa implementacyjna [Metier] warstwy [metier]

Klasa implementacyjna [Metier] warstwy [metier] mogłaby wyglądać następująco:

package metier;

...

@Transactional
public class Metier implements IMetier {

   // odniesienie do warstwy [DAO]
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


   // pobierz odcinek wypłaty
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

   // lista pracowników
   public List<Employe> findAllEmployes() {
     ...
  }

   // metody pobierające i ustawiające
...
 }
  • wiersz 5: adnotacja Spring @Transactional sprawia, że każda metoda tej klasy będzie wykonywana w ramach transakcji.
  • wiersze 9–10: odwołania do warstw [DAO] w encjach [Cotisation, Employe, Indemnite]
  • wiersze 14–17: metoda [calculerFeuilleSalaire]
  • wiersze 20–22: metoda [findAllEmployes]
  • wiersz 24 i kolejne: publiczne akcesory do pól prywatnych klasy

Zadanie: napisz kod metody [findAllEmployes].



Zadanie: napisz kod metody [calculerFeuilleSalaire].


Należy zwrócić uwagę na następujące kwestie:

  • sposób obliczania wynagrodzenia został wyjaśniony w paragrafie 5.2.
  • jeśli parametr [SS] nie odpowiada żadnemu pracownikowi (warstwa [DAO] zwróciła wskaźnik null), metoda wygeneruje wyjątek typu [PamException] z odpowiednim kodem błędu.

5.10.4. Testy warstwy [metier]

Tworzymy dwa programy testowe:

Klasy testowe [3] są tworzone w pakiecie [metier] [2] w gałęzi [Test Packages] [1] projektu.

Klasa [JUnitMetier_1] mogłaby wyglądać następująco:

package metier;

...

public class JUnitMetier_1 {

// warstwa biznesowa
  private IMetier metier;

  @BeforeClass
  public void init(){
     // log
    log("init");
     // konfiguracja aplikacji
     // instancjonowanie warstwy [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
     // warstwy DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
     // opróżnianie bazy
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // wypełnianie bazy
    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));
  }

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

   // test
  @Test
  public void test01(){
     // obliczanie list płac
    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()));
    }
  }
}

W klasie nie ma asercji o nazwie Assert.assertCondition. Chodzi po prostu o obliczenie kilku wynagrodzeń, aby następnie zweryfikować je ręcznie. Wynik wyświetlany na ekranie po uruchomieniu poprzedniej klasy wygląda następująco:

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
  • wiersz 4: lista płac Justine Laverti
  • wiersz 5: zestawienie wynagrodzenia Marie Jouveinal
  • wiersz 6: wyjątek spowodowany tym, że pracownik o numerze SS „xx” nie istnieje.

Pytanie: w wierszu 17 pliku [JUnitMetier_1] wykorzystywany jest bean Spring o nazwie metier. Podaj definicję tego beana w pliku [spring-config-metier-dao.xml].


Klasa [JUnitMetier_2] mogłaby wyglądać następująco:

package metier;

...
public class JUnitMetier_2 {

// warstwa biznesowa
  private IMetier metier;

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

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

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

Klasa [JUnitMetier_2] jest kopią klasy [JUnitMetier_1], z tą różnicą, że tym razem w metodzie test01 umieszczono asercje.


Zadanie: napisz metodę test01.


Podczas wykonywania klasy [JUnitMetier_2], jeśli wszystko przebiegnie pomyślnie, otrzymujemy następujące wyniki:

Image

5.11. Warstwa [ui] aplikacji [PAM] – wersja console

Teraz, gdy warstwa [metier] została już napisana, pozostaje nam jeszcze napisać warstwę [ui] [1]:

Stworzymy dwie różne implementacje warstwy [ui]: wersję tekstową console oraz wersję graficzną swing:

5.11.1. Klasa [ui.console.Main]

W pierwszej kolejności zajmiemy się aplikacją konsolową zaimplementowaną przez powyższą klasę [ui.console.Main]. Jej działanie opisano w punkcie 5.3. Szkielet klasy [Main] mógłby wyglądać następująco:

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) {
     // dane lokalne
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
     // sprawdzanie liczby parametrów
...
     // lista błędów
    ArrayList erreurs = new ArrayList();
     // drugi parametr musi być liczbą rzeczywistą >0
...
     // błąd?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
     // trzeci parametr musi być liczbą całkowitą większą od 0
...
     // błąd?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
     // błędy?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // w porządku – można poprosić o odcinek wypłaty
    FeuilleSalaire feuilleSalaire = null;
    try {
       // instancja warstwy [metier]
      ...
       // obliczenie listy płac
      ...
    } 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;
    }

     // wyświetlanie szczegółów
    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";
  }
}

Zadanie: uzupełnij powyższy kod.


5.11.2. Wykonanie

Aby uruchomić klasę [ui.console.Main], należy postępować w następujący sposób:

  • w [1], należy wybrać właściwości projektu,
  • w [2] należy wybrać właściwość projektu [Run],
  • użyj przycisku [3], aby wskazać klasę (tzw. klasę główną) do wykonania,
  • wybrać klasę [4],
  • klasa pojawi się w polu [5]. Do uruchomienia tej klasy potrzebne są trzy argumenty (nr SS, liczba przepracowanych godzin, liczba przepracowanych dni). Argumenty te należy umieścić w polu [6],
  • po wykonaniu tej czynności można uruchomić projekt [7]. Poprzednia konfiguracja powoduje, że uruchomiona zostanie klasa [ui.console.Main].

Wyniki wykonania są wyświetlane w oknie [output]:

5.12. Warstwa [ui] aplikacji [PAM] – wersja graficzna

Teraz wdrażamy warstwę [ui] z interfejsem graficznym:

  • w [1], klasa [PamJFrame] interfejsu graficznego
  • w [2]: interfejs graficzny

5.12.1. Krótki samouczek

Aby utworzyć interfejs graficzny, można postępować w następujący sposób:

  • [1]: tworzymy nowy plik za pomocą przycisku [1] [New File...]
  • [2]: wybieramy kategorię pliku [Swing GUI Forms], c.a.d. formularze graficzne
  • [3]: wybieramy typ [JFrame Form], czyli pusty typ formularza
  • [5]: nadajemy formularzowi nazwę, która będzie jednocześnie nazwą klasy
  • [6]: umieszczamy formularz w pakiecie
  • [8]: formularz zostaje dodany do drzewa projektu
  • [9]: formularz jest dostępny w dwóch perspektywach: [Design] i [9], które umożliwiają projektowanie różnych komponentów formularza, oraz [Source] i [10 ci-dessous], które zapewniają dostęp do kodu Java formularza. Ostatecznie formularz jest klasą Java, tak jak każda inna. Perspektywa [Design] ułatwia projektowanie formularza. Przy każdym dodaniu komponentu w trybie [Design] do perspektywy [Source] dodawany jest kod Java, aby uwzględnić ten komponent.
  • [11]: lista komponentów Swing dostępnych dla formularza znajduje się w oknie [Palette].
  • [12]: okno [Inspector] przedstawia drzewo komponentów formularza. Komponenty posiadające reprezentację wizualną znajdują się w gałęzi [JFrame], pozostałe w gałęzi [Other Components].
  • w formacie [13], wybieramy komponent [JLabel] jednym kliknięciem
  • w [14], umieszczamy go w formularzu w trybie [Design]
  • w [15] definiujemy właściwości JLabel (tekst, czcionka).
  • w [16] – uzyskany wynik.
  • w pliku [17] wyświetlamy podgląd formularza
  • w pliku [18] – wynik
  • w [19] etykieta [JLabel1] została dodana do drzewa komponentów w oknie [Inspector]
  • w [20] i [21]: w perspektywie formularza [Source] dodano kod Java do obsługi dodanego elementu JLabel.

Samouczek dotyczący tworzenia formularzy w programie NetBeans jest dostępny pod adresem [http://www.netbeans.org/kb/trails/matisse.html].

5.12.2. Interfejs graficzny [PamJFrame]

Stworzymy następujący interfejs graficzny:

  • w pliku [1], interfejs graficzny
  • w [2], drzewo jego komponentów: jeden JLabel i sześć kontenerów JPanel

JLabel1

JPanel1

JPanel2

JPanel3

JPanel4

JPanel5


Zadanie praktyczne: zbuduj powyższy interfejs graficzny, korzystając z samouczka [http://www.netbeans.org/kb/trails/matisse.html].


5.12.3. Zdarzenia w interfejsie graficznym

Zalecana lektura: rozdział [Interfaces graphiques] z [ref2].

Zajmiemy się obsługą kliknięcia przycisku [jButtonSalaire]. Aby utworzyć metodę obsługującą to zdarzenie, można postępować w następujący sposób:

Generowana jest procedura obsługi kliknięcia przycisku [JButtonSalaire]:

1
2
3
    private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {
       // TODO dodaj tutaj swój kod obsługi:
}

Wygenerowany zostaje również kod Java, który powiązuje powyższą metodę z kliknięciem przycisku [JButtonSalaire]:

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);
      }
});

Wiersze 2–5 wskazują, że kliknięcie (zdarzenie typu ActionPerformed) przycisku [jButtonSalaire] (wiersz 2) musi być obsługiwane przez metodę [jButtonSalaireActionPerformed] (wiersz 4).

Będziemy również obsługiwać zdarzenie [caretUpdate] (przesunięcie kursora) w polu wprowadzania danych [jTextFieldHT]. Aby utworzyć procedurę obsługi tego zdarzenia, postępujemy tak samo jak poprzednio:

Generowany jest procedor obsługi zdarzenia [caretUpdate] dla pola wprowadzania danych [jTextFieldHT]:

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

Wygenerowano również kod Java, który powiązuje powyższą metodę ze zdarzeniem [caretUpdate] w polu wprowadzania danych [jTextFieldHT]:

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

Wiersze 1–4 wskazują, że zdarzenie [caretUpdate] (wiersz 2) dotyczące przycisku [jTextFieldHT] (wiersz 1) ma być obsługiwane przez metodę [ jTextFieldHTCaretUpdate] (wiersz 3).

5.12.4. Inicjalizacja interfejsu graficznego

Wróćmy do architektury naszej aplikacji:

Warstwa [ui] wymaga odwołania do warstwy [metier]. Przypomnijmy, w jaki sposób uzyskano to odwołanie w aplikacji console:

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

W aplikacji graficznej metoda jest taka sama. Konieczne jest, aby podczas jej inicjalizacji odwołanie [IMetier metier] z wiersza 3 powyżej również zostało zainicjowane. Kod wygenerowany dla interfejsu graficznego wygląda na razie następująco:

package ui.swing;

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

   /** Tworzy nowy formularz PamJFrame */
  public PamJFrame() {
    initComponents();
  }

   /** Ta metoda jest wywoływana z poziomu konstruktora w celu
   * 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=" Kod wygenerowany ">
  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);
      }
    });
  }

   // Deklaracja zmiennych – nie modyfikować
  private javax.swing.JButton jButtonSalaire;
...
   // Koniec deklaracji zmiennych

}
  • wiersze 29–35: statyczna metoda [main], która uruchamia aplikację
  • wiersz 32: tworzona jest instancja interfejsu graficznego [PamJFrame] i jest ona wyświetlana.
  • wiersze 7–9: konstruktor interfejsu graficznego.
  • wiersz 8: wywołanie metody [initComponents] zdefiniowanej w wierszu 17. Metoda ta jest generowana automatycznie na podstawie pracy wykonanej w trybie [Design]. Nie należy jej modyfikować.
  • wiersz 21: metoda, która będzie obsługiwać przesuwanie kursora wprowadzania danych w polu [jTextFieldHT]
  • wiersz 25: metoda obsługująca kliknięcie przycisku [jButtonSalaire]

Aby dodać do powyższego kodu nasze własne inicjalizacje, możemy postępować w następujący sposób:

  /** Tworzy nowy formularz PamJFrame */
  public PamJFrame() {
    initComponents();
    doMyInit();
  }

...

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

   // Inicjalizacje własne
  public void doMyInit(){
     // inicjalizacja kontekstu
    try{
       // instancjonowanie warstwy [metier]
...
     // lista pracowników
...
    }catch (PamException ex){
     // komunikat o wyjątku jest umieszczany w [jTextAreaStatus]
...
     // powrót
      return;
    }
     // przycisk „wynagrodzenie” zablokowany
...
     // jScrollPane1 ukryty
...
     // przewijacz dni przepracowanych
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // lista rozwijana pracowników
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • wiersz 4: wywołujemy metodę własną, aby przeprowadzić nasze własne inicjalizacje. Są one zdefiniowane w kodzie w wierszach 10–42

Pytanie: Korzystając z komentarzy, uzupełnij kod procedury [doMyInit].


5.12.5. Obsługa zdarzeń


Zadanie: Napisz metodę [jTextFieldHTCaretUpdate]. Metoda ta musi sprawić, że jeśli dane w polu [jTextFieldHT] nie są liczbą rzeczywistą >=0, wówczas przycisk [jButtonSalaire] musi być nieaktywny.



Zadanie: napisz metodę [jButtonSalaireActionPerformed], która ma wyświetlać listę płac pracownika wybranego w polu [jComboBoxEmployes].


5.12.6. Uruchomienie interfejsu graficznego

Aby uruchomić interfejs graficzny, należy zmodyfikować konfigurację [Run] projektu:

  • na [1], należy ustawić klasę interfejsu graficznego

Projekt musi być kompletny wraz z plikami konfiguracyjnymi (persistence.xml, spring-config-metier-dao.xml) oraz klasą interfejsu graficznego. Przed uruchomieniem projektu należy uruchomić cel SGBD.

Interesuje nas następująca architektura, w której warstwa JPA jest teraz implementowana przez EclipseLink:

5.13.1. Projekt NetBeans

Nowy projekt NetBeans powstaje poprzez skopiowanie poprzedniego projektu:

  • na [1]: po kliknięciu prawym przyciskiem myszy na projekt Hibernate należy wybrać Copy
  • za pomocą przycisku [2] należy wybrać folder nadrzędny nowego projektu. Nazwa folderu pojawia się w polu [3].
  • w polu [4] nadać nazwę nowemu projektowi
  • w polu [5] wprowadź nazwę folderu projektu
  • w [1] nowy projekt został utworzony. Ma on taką samą nazwę jak oryginał,
  • w [2] i [3] zmieniamy jego nazwę na [mv-pam-spring-eclipselink].

Aby dostosować projekt do nowej warstwy JPA / EclipseLink, należy wprowadzić dwie zmiany:

  1. w warstwie [4] należy zmodyfikować pliki konfiguracyjne Springa. Znajduje się w nich bowiem konfiguracja warstwy JPA.
  2. w warstwie [5] należy zmodyfikować biblioteki projektu: biblioteki Hibernate należy zastąpić bibliotekami z warstwy EclipseLink.

Zacznijmy od tej ostatniej kwestii. Plik [pom.xml] dla nowego projektu będzie wyglądał następująco:


<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>
  • wiersze 73–82: zależności dla implementacji JPA EclipseLink,
  • wiersze 19–24: repozytorium Maven dla EclipseLink.

Pliki konfiguracyjne Springa należy zmodyfikować, aby uwzględnić zmianę implementacji JPA. W obu plikach zmienia się jedynie sekcja konfigurująca warstwę JPA. Na przykład w pliku [spring-config-metier-dao.xml] mamy:

<?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">

   <!-- warstwy aplikacyjne -->
   <!- - DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
   <!-- dział biznesowy -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="indemniteDao" ref="indemniteDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>

   <!-- konfiguracja 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>

   <!-- źródło danych 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>

Wiersze 19–36 konfigurują warstwę JPA. Wykorzystywana implementacja JPA to Hibernate (wiersz 22). Ponadto docelową bazą danych jest [dbpam_hibernate] (wiersz 41).

Aby przejść na implementację JPA / EclipseLink, powyższe wiersze 19–35 należy zastąpić poniższymi wierszami:

   <!-- konfiguracja 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>
  • wiersz 5: stosowana implementacja JPA to EclipseLink
  • wiersz 9: właściwość databasePlatform określa docelową implementację SGBD, w tym przypadku MySQL
  • wiersz 11: służy do generowania tabel bazy danych podczas instancjonowania warstwy JPA. W tym przypadku właściwość jest skomentowana.
  • wiersz 7: służy do wyświetlania na konsoli poleceń SQL wysyłanych przez warstwę JPA. Tutaj właściwość jest skomentowana.

Ponadto docelową bazą danych staje się [dbpam_eclipselink] (wiersz 4 poniżej):

1
2
3
4
5
6
7
8
9
<!-- źródło danych 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. Przeprowadzenie testów

Przed przetestowaniem całej aplikacji warto sprawdzić, czy testy JUnit przebiegają pomyślnie z nową implementacją JPA. Przed ich uruchomieniem należy najpierw usunąć tabele z bazy danych. W tym celu w zakładce [Runtime] programu NetBeans, w razie potrzeby, należy utworzyć połączenie z bazą dbpam_eclipselink / MySQL5. Po nawiązaniu połączenia z bazą danych dbpam_eclipselink / MySQL5 można przystąpić do usuwania tabel, jak pokazano poniżej:

  • [1]: przed usunięciem
  • [2]: po usunięciu

Po wykonaniu tej czynności można przeprowadzić pierwszy test na warstwie [DAO]: InitDB, która wypełnia bazę danych. Aby aplikacja odtworzyła wcześniej usunięte tabele, należy upewnić się, że w konfiguracji JPA / EclipseLink w Springu znajduje się wiersz:

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

istnieje i nie jest skomentowana.

Kompilujemy projekt (Build), a następnie uruchamiamy test [JUnitInitDB] :

  • w [1] uruchamiany jest test InitDB.
  • W przypadku testu [2] test kończy się niepowodzeniem. Wyjątek jest generowany przez Spring, a nie przez test, który zakończył się niepowodzeniem.

Przyczyna: org.springframework.beans.factory.BeanCreationException: Błąd podczas tworzenia obiektu typu bean o nazwie „entityManagerFactory” zdefiniowanego w zasobie ścieżki klas [spring-config-DAO.xml]: Nie udało się wywołać metody init; wyjątkiem zagnieżdżonym jest java.lang.IllegalStateException: Aby korzystać z InstrumentationLoadTimeWeaver, należy uruchomić agenta Java. Zobacz dokumentację Spring.

Spring wskazuje, że występuje problem z konfiguracją. Komunikat nie jest jasny. Przyczyna wyjątku została wyjaśniona w paragrafie 3.1.9 dokumentu [ref1]. Aby konfiguracja Spring / EclipseLink działała, JVM, który uruchamia aplikację, musi zostać uruchomiony z określonym parametrem – agentem Java. Parametr ten ma następującą postać:

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

[spring-agent.jar] to agent Java, którego potrzebuje JVM do obsługi konfiguracji Spring / EclipseLink.

Podczas uruchamiania projektu można przekazać argumenty do agenta JVM:

  • w [1] uzyskuje się dostęp do właściwości projektu
  • w [2] wyświetlane są właściwości Run
  • w [3] przekazujemy parametr -javaagent do JVM

5.13.3. InitDB

Teraz jesteśmy gotowi do ponownego przetestowania [InitDB]. Tym razem uzyskano następujące wyniki:

  • w [1] test zakończył się powodzeniem
  • w przypadku [2], w zakładce [Services] odświeżamy połączenie NetBeans z bazą danych [dbpam_eclipselink]
  • w [3] utworzono cztery tabele
  • w [5] wyświetla się zawartość tabeli [employes]
  • w [6] – wynik.

5.13.4. JUnitDao

Wykonanie klasy testowej [JUnitDao] może zakończyć się niepowodzeniem, nawet jeśli w przypadku implementacji JPA / Hibernate zakończyło się sukcesem. Aby zrozumieć dlaczego, przeanalizujmy przykład.

Testowaną metodą jest następująca metoda IndemniteDao.create:

package dao;

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

  @PersistenceContext
  private EntityManager em;

   // producent
  public IndemniteDao() {
  }

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

...
}
  • wiersze 15–22: testowana metoda

Metoda testowa wygląda następująco:


package dao;

...

public class JUnitDao {

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

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

  @Before()
  public void clean() {
    // opróżnianie bazy
    for (Employe employe : employeDao.findAll()) {
      employeDao.destroy(employe);
    }
    for (Cotisation cotisation : cotisationDao.findAll()) {
      cotisationDao.destroy(cotisation);
    }
    for (Indemnite indemnite : indemniteDao.findAll()) {
      indemniteDao.destroy(indemnite);
    }
  }

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

  // testy
….
  @Test
  public void test05() {
    log("test05");
    // tworzymy dwa świadczenia z tym samym indeksem
    // narusza ograniczenie unikalności wskaźnika
    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;
      // weryfikacje
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // weryfikacje
    Assert.assertTrue(erreur);
    // łańcuch wyjątków
    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());
    }
    // pierwsze odszkodowanie musiało zostać zapisane
    Indemnite indemnite = indemniteDao.find(indemnite1.getId());
    // weryfikacja
    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);
    // druga rekompensata nie powinna była zostać zapisana
    List<Indemnite> indemnites = indemniteDao.findAll();
    int nbIndemnites = indemnites.size();
    Assert.assertEquals(nbIndemnites, 1);
  }

...
}

Pytanie: wyjaśnij, na czym polega test test05 i podaj oczekiwane wyniki.


Wyniki uzyskane przy użyciu warstwy JPA / Hibernate są następujące:

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

Test zakończył się powodzeniem, c.a.d. Asercje zostały zweryfikowane i nie wystąpił żaden wyjątek w metodzie testowej.


Pytanie: proszę wyjaśnić, co się stało.


Wyniki uzyskane przy użyciu warstwy JPA / EclipseLink są następujące:

----------- test05
[EL Warning]: 2010-06-04 16:48:26.421--UnitOfWork(749304)--Wyjątek [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

Podobnie jak wcześniej w przypadku Hibernate, test zakończył się powodzeniem (c.a.d). Sprawdzono asercje i nie wystąpił żaden wyjątek w metodzie testowej.


Pytanie: wyjaśnij, co się stało.



Pytanie: jakie wnioski można wyciągnąć z tych dwóch przykładów na temat zamienności implementacji JPA? Czy jest ona w tym przypadku całkowita?


5.13.5. Pozostałe testy

Po przetestowaniu warstwy [DAO] i uznaniu jej za poprawną można przejść do testowania warstwy [metier] oraz samego projektu w wersji konsolowej lub graficznej. Zmiana implementacji JPA nie ma żadnego wpływu na warstwy [metier] i [ui], a zatem jeśli warstwy te działały z Hibernate, będą działać z EclipseLink z kilkoma wyjątkami: poprzedni przykład pokazuje bowiem, że wyjątki rzucane przez warstwy [DAO] mogą się różnić. Tak więc w przypadku wykorzystania testu, Spring / JPA / Hibernate zgłasza wyjątek typu [PamException], czyli wyjątek specyficzny dla aplikacji [pam], podczas gdy Spring / JPA / EclipseLink generuje wyjątek typu [TransactionSystemException], czyli wyjątek właściwy dla frameworka Spring. Jeśli w przypadku testowym warstwa [ui] oczekuje wyjątku typu [PamException], ponieważ została zbudowana przy użyciu Hibernate, przestanie działać po przejściu na EclipseLink.

5.13.6. Zadanie do wykonania


Zadanie praktyczne: ponowne przeprowadzenie testów aplikacji console i swing z wykorzystaniem różnych instancji SGBD: MySQL5, Oracle XE, SQL Server.