Facoltà di Ingegneria dell'informazione, informatica e statistica Ingegneria informatica Tesi di Laurea Studio e Sperimentazione di Framework Java per la Persistenza Relatore : Prof. Massimo Mecella Laureando : Giulio Gullà matricola : 1144509 Anno Accademico : 2011-2012 1 2 3 Sommario 1. INTRODUZIONE...................................................................................................................................... 7 1.1 Hibernate ................................................................................................................................................ 8 Hibernate Tools ........................................................................................................................................ 8 1.2 Java Persistence API ............................................................................................................................... 8 2. SVILUPPARE CON HIBERNATE E JPA ............................................................................................... 9 2.1 Generale ................................................................................................................................................. 9 2.2 Scenari di sviluppo .................................................................................................................................. 9 2.2.1 Top Down ........................................................................................................................................ 9 2.2.2 Bottom up........................................................................................................................................ 9 2.2.3 Middle out ..................................................................................................................................... 10 2.2.4 Meet in the middle ........................................................................................................................ 10 2.3 Classi di dominio ................................................................................................................................... 10 2.4 File di configurazione............................................................................................................................ 13 2.5 Mapping ............................................................................................................................................... 17 2.5.1 Mapping di proprietà identificatore .............................................................................................. 20 2.5.2 Mapping di proprietà con molteplicità massima 1 ........................................................................ 25 2.5.3 Tipi Hibernate ................................................................................................................................ 27 2.5.4 Mapping di proprietà con molteplicità massima maggiore di 1 ..................................................... 28 2.5.5 Mapping di proprietà istanze di una classe che realizza un tipo UML ........................................... 31 2.5.6 Mapping di associazioni ................................................................................................................. 35 2.5.7 Mapping di ereditarietà ................................................................................................................. 78 2.6 Interfacce di Hibernate ......................................................................................................................... 88 2.6.1 SessionFactory (org.hibernate.SessionFactory) ................................................ 88 2.6.2 Session (org.hibernate.Session) ................................................................................. 88 2.6.3 Transaction (org.hibernate.Transaction) .............................................................. 89 2.6.4 Query (org.hibernate.Query) ........................................................................................... 89 2.7 Java Persistence API ............................................................................................................................. 90 2.7.1 EntityManagerFactory (javax.persistence.EntityManagerFactory)........... 90 2.7.2 EntityManager (javax.persistence.EntityManager)............................................ 90 2.7.3 EntityTransaction (javax.persistence.EntityTransaction) ......................... 91 4 2.7.4 Query (javax.persistence.Query) ................................................................................. 92 2.8 Hibernate Query Language e Java Persistence Query Language .......................................................... 92 2.8.1 Creazione di un oggetto query ...................................................................................................... 92 2.8.2 Binding dei parametri .................................................................................................................... 94 2.8.3 Gestione dei risultati ..................................................................................................................... 95 2.8.4 Operazioni di aggiornamento ed eliminazione .............................................................................. 96 2.8.5 Query ............................................................................................................................................. 97 2.8 Generazione dello schema della base di dati ........................................................................................ 98 2.9 Creazione e funzionamento di un progetto .......................................................................................... 99 3. APPLICAZIONE DI ESEMPIO ........................................................................................................... 106 3.1 Requisiti .............................................................................................................................................. 106 3.2 Classi di dominio ................................................................................................................................. 107 3.3 Eccezioni ............................................................................................................................................. 116 3.4 Persistenza ......................................................................................................................................... 117 3.5 Attività ................................................................................................................................................ 120 3.5.1 AttivitàComplesse ........................................................................................................................ 123 3.5.2 Attività atomiche ......................................................................................................................... 131 3.6 Interfaccia grafica ............................................................................................................................... 144 3.7 Main ................................................................................................................................................... 165 4. CONSIDERAZIONI FINALI ................................................................................................................ 166 Riferimenti bibliografici ................................................................................................................................ 167 5 6 1. INTRODUZIONE La gestione della persistenza è un argomento delicato nella progettazione di applicazioni software. Lo sviluppo di applicazioni Object-Oriented che utilizzano un DBMS relazionale per immagazinare i dati in memoria secondaria è uno scenario molto comune. Ciò che rende complessa la costruzione dello strato di persistenza è il problema dell’object/relational impedence mismatch, la discrepanza tra il paradigma Object-Oriented e il paradigma relazionale. Questo problema è causato dal fatto che il paradigma ObjectOriented è basato su principi di ingegneria del software mentre il paradigma relazionale è basato su principi matematici. La progettazione e la realizzazione delle classi di dominio e della base di dati mostrano questa differenza: le classi di dominio vengono progettate con UML, un linguaggio di modellazione ObjectOriented, mentre per la progettazione della base di dati viene inizialmente utilizzato il modello EntitàRelazione per creare uno schema concettuale che successivamente viene indebolito per permettere la traduzione dello schema nel modello relazionale della base di dati. Il problema dell’impedence mismatch deve essere analizzato e risolto nel modo opportuno in fase di progettazione. Un altro aspetto molto importante nella gestione della persistenza è l’interazione tra lo strato di logica applicativa e lo strato di persistenza. In applicazioni Java la comunicazione con la base di dati avviene utilizzando API specifiche come ODB (Open Database Connectivity) o JDBC (Java Database Connectivity). Una delle strategie più valide e utilizzate è l’uso del pattern Data Access Object (DAO) per la creazione di oggetti che costituiscono uno strato di comunicazione con la base di dati. La realizzazione dei DAO avviene usando API JDBC per rendere disponibili le operazioni CRUD (Create Read Update Delete) allo strato di logica applicativa. I DAO incapsulano l’accesso alla base di dati e permettono di gestire la maggior parte degli scenari ma la loro realizzazione necessita di una conoscenza dettagliata della base di dati. Questo lavoro ha lo scopo di presentare una strategia alternativa per la gestione della persistenza in applicazioni Java: l’Object-Relational Mapping (ORM). Questa tecnica consiste nel mappare le classi di dominio dell’applicazione su una base di dati relazionale. Il mapping avviene utilizzando appositi framework di persistenza che, dopo una fase iniziale di configurazione, permettono di generare una base di dati relazionale partendo dalle classi di dominio dell’applicazione e di gestirne l’accesso. In particolare vengono analizzati due importanti framework java per la persistenza: Hibernate e Java Persistence API. La trattazione prosegue con una introduzione ai due framework esaminati e con un lungo capitolo dedicato alle tecniche di sviluppo in applicazioni Hibernate e Java Persistence. Questo capitolo è il cuore dell’elaborato, vengono spiegati i principali costrutti utili in differenti scenari. Ogni tecnica illustrata è stata sperimentata e viene accompagnata da un esempio che ne mostra l’utilizzo. Nel terzo capitolo viene presentata un’applicazione Java Persistence completa e in conclusione ci sono le considerazioni finali. 7 1.1 Hibernate Hibernate è un framework open source con servizi Object-Relational Mapping (ORM) in Java. Hibernate ORM (fino alla versione 4.0 conosciuto come Hibernate Core), composto dalle API native di Hibernate e il suo motore, è disponibile alla versione 4.1, utilizzata in questa trattazione. Il software è composto da API Java che permettono di gestire la persistenza con il mapping delle classi sulla base di dati e forniscono interfacce per l’accesso ai dati persistenti. Hibernate Tools Hibernate Tools è un insieme di strumenti per Hibernate racchiusi in una suite di plugin Eclipse e dei task Ant per l'integrazione a stadio di compilazione. Le funzioni messe a disposizione sono: Mapping Editor: un editor per i file di mapping XML; Console: permette di configurare le connessioni alla base di dati ed eseguire query interattivamente; Reverse Engineering: la funzione più importante di Hibernate Tools. Consente di generare le classi di dominio dell'applicazione Java dalle tabelle della base di dati; Wizards: procedure fornite per la generazione dei file di configurazione di Hibernate e dei file di configurazione Console; Task Ant: utilizzato per avviare la generazione dello schema della base di dati, la generazione dei file di mapping e la generazione del codice Java durante la compilazione. 1.2 Java Persistence API Le Java Persistence API (JPA) sono un framework Java per la gestione della persistenza. Attualmente le Java Persistence API sono alla versione 2.0 (JSR 317: Java Persistence 2.0). Come Hibernate, le Java Persistence API permettono di effettuare il mapping delle classi di dominio sulle tabelle di una base di dati relazionale e di interagire con esse. Per utilizzare le Java Persistence API è necessaria un’implementazione. In questo studio verrà utilizzata l’implementazione di Hibernate analizzando le strategie di mapping appartenenti allo standard delle Java Persistence API e tralasciando costrutti specifici del software. L’implementazione di Hibernate delle Java Persistence API è composta da: Hibernate Annotations: sistema di metadati per il mapping delle classi di dominio; Hibernate EntityManager: implementazione delle interfacce di programmazione e delle regole riguardanti il ciclo di vita delle entità definito dalle specifiche JPA 2.0. 8 2. SVILUPPARE CON HIBERNATE E JPA 2.1 Generale Un’applicazione sviluppata con Hibernate o con le Java Persistence API è composta da quattro elementi: Classi di dominio Java File di configurazione Metadati per il mapping Base di dati relazionale Questi elementi vengono realizzati secondo diverse modalità in base allo scenario di sviluppo. 2.2 Scenari di sviluppo Vengono ora presentati i possibili scenari di sviluppo con Hibernate e le Java Persistence API. In questo studio verrà analizzato a fondo solo lo scenario top down. 2.2.1 Top Down Nello sviluppo top down si parte dalle classi di dominio dell'applicazione e si effettua il mapping su tabelle della base di dati relazionale. Lo sviluppo top down è molto comodo in assenza di uno schema della base di dati. Strumenti principali di Hibernate utilizzati: <hbm2ddl> : permette di creare, aggiornare e convalidare lo schema della base di dati utilizzando i file di mapping e configurazione di Hibernate. 2.2.2 Bottom up Scenario di reverse engineering in cui si ha una base di dati legacy e vengono generate le classi di dominio e i metadati per il mapping. La generazione delle classi con questo sviluppo è rapida ma lo schema con cui sono organizzate le classi di dominio non potrà mai avvicinarsi alla costruzione ideale del diagramma delle classi. Il mapping è condizionato dalle tabelle dello schema e genererà un modello delle classi di dominio degradato. Strumenti principali di Hibernate utilizzati: <hbm2java> : permette la generazione delle classi di dominio Java utilizzando i file di reverse engineering e configurazione di Hibernate. <hbm2hbmxml> : permette la generazione dei file di mapping delle classi di dominio Java utilizzando I file di reverse engineering e configurazione di Hibernate. 9 2.2.3 Middle out Scenario che punta ad utilizzare a fondo le funzioni di Hibernate per lo sviluppo di una nuova applicazione Java. Il punto di partenza è la scrittura “a mano” dei file XML di mapping dai quali verranno create le tabelle per la base di dati utilizzando lo strumento <hbm2ddl> e generate le classi Java con <hbm2java>. Lo scenario di sviluppo middle out non è possibile utilizzando le Java Persistence API. Questo approccio è consigliabile solo a sviluppatori con una profonda conoscenza di Hibernate. 2.2.4 Meet in the middle Lo scenario più complesso e difficile. Si tratta di combinare le classi di dominio con uno schema di base di dati legacy. Mappare classi di dominio Java arbitrarie su uno schema dato non è possibile, sarà quindi necessario effettuare refactoring delle classi Java, dello schema della base di dati o di entrambi. E’ uno scenario piuttosto raro. 2.3 Classi di dominio Le classi di dominio Java dovranno essere realizzate secondo l’approccio Plain Old Java Object (POJO). Verranno ora elencate le principali caratteristiche della realizzazione delle classi di dominio, in particolare sottolineando le differenze tra la realizzazione ideale e l'approccio POJO. Caratteristiche di una classe POJO: - Costruttore senza argomenti E’ richiesto da Hibernate e deve essere almeno visibile all’interno del package. Solitamente viene lasciato vuoto e vengono definiti ulteriori costruttori per l’utilizzo all’interno dell’applicazione. - Proprietà identificatore La classe deve fornire una proprietà con funzione di identificatore. - Classe non-final Le classi non-final sono da preferire per garantire il funzionamento delle operazioni di lazy loading di Hibernate. - Metodi get e set per le proprietà degli oggetti della classe E’ importante che siano definiti per tutte le proprietà della classe. I nomi dei metodi devo essere necessariamente nella forma get (o set) seguito dal nome della proprietà con la prima lettera maiuscola (es. getProprietà() e setProprietà()). Note: 1 - Il metodo set di proprietà immutabili come quella che ha ruolo di identificatore dell’oggetto può essere definito privato; 10 2 - Proprietà persistenti che sono collezioni avranno il metodo set privato e il metodo get che non restituisce un clone della collezione ma la collezione stessa. - Metodi di business - Metodi speciali I metodi speciali toString(), equals(), hashCode() e clone() devono essere ridefiniti in alcune circostanze: 1 – è necessario ridefinire equals() e hashCode() se si vogliono inserire istanze di una classe persistente in un set; 2 – è necessario ridefinire toString(), equals() e hashCode() se la classe realizza un nuovo tipo di valore; 3 - è necessario ridefinire clone() se nella classe sono definiti metodi che effettuano side-effect. Associazioni: Per la realizzazione di associazioni secondo l'approccio POJO non vengono utilizzate classi TipoLink. Associazioni a responsabilità singola senza attributi - molteplicità 0..1 (o 1..1): la classe con responsabilità sull'associazione ha una proprietà riferimento ad un'istanza della classe persistente a cui è associata; - molteplicità 0..* (o 1..*): la classe con responsabilità sull'associazione ha una proprietà insieme di riferimenti ad istanze delle classi persistenti a cui è associata; Associazioni a responsabilità doppia senza attributi Come per la realizzazione ideale del diagramma delle classi, viene utilizzata una classe manger che gestisce le modifiche dell'associazione. La classe manager dovrà però gestire direttamente riferimenti a istanze delle classi conivolte e non istanze di classi TipoLink. Nota: la classe manager non deve essere resa persistente. Associazioni con attributi Hibernate e JPA non forniscono costrutti specifici per il mapping di associazioni con attributi. In caso di associzioni con molteplicità massima pari a 1, è possibile utilizzare un approccio “naif”, trasformando gli attributi dell'associazione in nuove proprietà della classe che ha responsabilità sull’associazione. Se l'associazione con attributi ha molteplicità massima superiore a 1, si può considerare di effettuare refactoring aggiungendo una nuova classe di dominio per rappresentare l’associazione. Una possibile strategia è spiegata nell’apposito paragrafo. 11 Esempio : Classe POJO File Utente.java package domain; import java.util.*; public class Utente { private private private private private String username; String password; Set<String> numTelefono; Indirizzo indirizzo; Set<Acquisto> acquisti; protected Utente() { } public Utente(String username, String password) { this.username = username; this.password = password; numTelefono = new HashSet<String>(); effettua = new HashSet<Acquisto>(); } public String getUsername() { return this.username; } private void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } 12 public Set<String> getNumTelefono() { return (Set<String>) this.numTelefono; } private void setNumTelefono(Set<String> numTelefono) { this.numTelefono = numTelefono; } public Indirizzo getIndirizzo() { return this.indirizzo; } public void setIndirizzo(Indirizzo indirizzo) { this.indirizzo = indirizzo; } public Set<Acquisto> getAcquisti() { return (Set<Acquisto>) this.acquisti; } private void setAcquisti(Set<Acquisto> acquisti) { this.acquisti = acquisti; } public double calcolaCostoSpedizione(Indirizzo posizione) { ... } } 2.4 File di configurazione I file di configurazione contengono informazioni riguardo le impostazioni di connessione al DBMS e la posizione dei file di mapping che verranno utilizzati per l’applicazione. Hibernate Il file di configurazione principale di Hibernate è hibernate.cfg.xml. Questo file deve essere salvato nella radice del classpath dell’applicazione. Esempio : File di configurazione di Hibernate File hibernate.cfg.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="connection.driver_class"> org.postgresql.Driver 13 </property> <property name="connection.url"> jdbc:postgresql://localhost:5432/test </property> <property name="connection.username">postgres</property> <property name="connection.password">password</property> <!-- Use the C3P0 connection pool provider --> <property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.max_size">20</property> <property name="hibernate.c3p0.timeout">300</property> <property name="hibernate.c3p0.max_statements">50</property> <property name="hibernate.c3p0.idle_test_period">3000</property> <!-- SQL dialect --> <property name="dialect"> org.hibernate.dialect.PostgreSQLDialect </property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <property name="format_sql">true</property> <!-- Mapping files --> <mapping resource="domain/Utente.hbm.xml"/> <mapping resource="domain/Acquisto.hbm.xml"/> </session-factory> </hibernate-configuration> L'elemento principale è <hibernate-configuration> che contiene al suo interno l'elemento <session-factory> che definisce tutte le impostazioni per la connessione ad una base di dati. Descrizione delle proprietà hibernate.connection: hibernate.connection.driver_class Nome del driver JDBC specifico per il DBMS utilizzato. hibernate.connection.url Indirizzo JDBC della base di dati per la connessione. hibernate.connection.username Nome utente per la connessione al DBMS. hibernate.connection.password Password per la connessione al DBMS. Seguono proprietà per la gestione di un pool di connessioni JDBC. Nell'esempio viene utilizzato un software di terze parti, c3p0, per avere migliori performance e stabilità. In alternativa può essere utilizzato il gestore interno ad Hibernate che utilizza un algoritmo piuttosto semplice. Per fare ciò si utilizzerà la proprietà hibernate.connection.pool_size specificando il numero massimo di connessioni come valore. 14 Descrizione delle proprietà hibernate.c3p0: hibernate.c3p0.min_size Numero minimo di connessioni JDBC che c3p0 mantiene pronte. hibernate.c3p0.max_size Numero massimo di connessioni JDBC nel pool. Se le connessioni vengono esaurite viene lanciata un'eccezione a runtime. hibernate.c3p0.timeout Periodo di timeout dopo il quale una connessione inattiva viene rimossa dal pool. hibernate.c3p0.max_statements Numero massimo di prepared statement in cache. Il caching è molto importante per migliorare le performance di Hibernate. hibernate.c3p0.idle_test_period Periodo di tempo in secondi prima che una connessione venga automaticamente convalidata. La proprietà dialect specifica la variante di SQL utilizzata dal DBMS. Gli elementi <mapping> specificano i file di mapping che Hibernate dovrà utilizzare. Java Persistence API Il file di configurazione di JPA è chiamato persistence.xml e contiene le stesse informazioni contenute in hibernate.cfg.xml con alcune differenze. persistence.xml deve essere salvato in una directory nominata META-INF posizionata nel classpath dell’applicazione. Esempio : File di configurazione di JPA File persistence.xml <?xml version="1.0" encoding="UTF-8" ?> <persistence 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" version="2.0"> <persistence-unit name="testPU"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <!-- Not needed, Hibernate supports auto-detection in JSE <class>domain.Utente</class> <class>domain.Acquisto</class> --> <properties> <property name="hibernate.archive.autodetection" 15 value="class, hbm"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.connection.driver_class" value="org.postgresql.Driver"/> <property name="hibernate.connection.url" value="jdbc:postgresql://localhost:5432/test"/> <property name="hibernate.connection.username" value="postgres"/> <property name="hibernate.connection.password" value="password"/> <property name="hibernate.c3p0.min_size" value="5"/> <property name="hibernate.c3p0.max_size" value="20"/> <property name="hibernate.c3p0.timeout" value="300"/> <property name="hibernate.c3p0.max_statements" value="50"/> <property name="hibernate.c3p0.idle_test_period" value="3000"/> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="create"/> </properties> </persistence-unit> </persistence> L’elemento principale è <persistence> che contiene la definizione della grammatica, al cui interno ci possono essere uno o più elementi <persistence-unit> che contengono le informazioni per la connessione al DBMS. Ogni elemento <persistence-unit> ha un nome, definito dall’attributo name, che verrà utilizzato per la creazione di EntityManager per l’accesso alla base di dati. Il primo elemento all’interno di <persistence-unit> è <provider> con il quale viene specificata l’implementazione di JPA utilizzata all’interno dell’applicazione, in questo caso l’implementazione di Hibernate. La specifica delle classi Java contenenti le annotazioni per il mapping può avvenire utilizzando elementi <class> oppure con la proprietà di Hibernate hibernate.archive.autodetection che permette di determinare quali file verranno rilevati automaticamente da Hibernate per il mapping. Nell’esempio il valore dell’elemento hibernate.archive.autodetection è class,hbm, il valore di default, che permette il rilevamento automatico di classi Java con annotazioni e file di mapping di Hibernate. A seguire ci sono tutte le proprietà che specificano le impostazioni per la connessione JDBC alla base di dati, analoghe a quelle contenute nel file di configurazione di Hibernate. 16 2.5 Mapping Il mapping delle classi Java di dominio in tabelle di una base di dati relazionale in Hibernate e JPA viene effettuato utilizzando metadati differenti: Hibernate utilizza file di mapping XML mentre JPA utilizza annotazioni inserite all’interno del codice delle classi stesse. Entrambi i metodi specificano esattamente come una classe deve essere resa persistente. Hibernate Il file di mapping Hibernate di una classe Classe.java è chiamato Classe.hbm.xml. E' composto da un elemento principale di nome <hibernate-mapping> che contiene al suo interno un elemento <class> con tutte le informazioni per il mapping della classe java. Esempio: File di mapping per la classe Utente.java File Utente.hbm.xml <?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="domain.Utente" table="utente"> <id name="username" column="username" type="string"/> <property name="password" column="password" type="string" not-null="true"/> <set name="effettua"> <key column="username" /> <one-to-many class="domain.Acquisto"/> </set> </class> </hibernate-mapping> Descrizione di alcuni attributi dell'elemento <class> : name="ClassName" Il nome completo della classe persistente. table="tableName" Il nome della tabella in cui verrà mappata la classe. Di default è il nome della classe. schema="schemaName" Il nome dello schema della base di dati. 17 Java Persistence API In JPA i metadati utilizzati per il mapping sono annotazioni indivisibili dal codice Java delle classi di dominio. Esempio: Annotazioni per il mapping della classe Utente.java File Utente.java package domain; import javax.persistence.*; import java.util.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<Acquisto> effettua; protected Utente() { effettua = new HashSet<Acquisto>(); } public Utente(String username, String password) { this.username = username; this.password = password; effettua = new HashSet<Acquisto>(); } @Id public String getUsername() { return this.username; } private void setUsername(String username) { this.username = username; } @Column(name = "password", nullable = false) public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } @OneToMany(targetEntity = domain.Acquisto.class) @JoinColumn(name = "username", nullable = false) public Set<Acquisto> getEffettua() { return this.effettua; } private void setEffettua(Set<Acquisto> effettua) { this.effettua = effettua; } 18 public void inserisciAcquisto(Acquisto acquisto) throws EccezionePrecondizioni { if(acquisto == null) throw new EccezionePrecondizioni("L'oggetto deve essere inizializzato"); else effettua.add(acquisto); } public void rimuoviAcquisto(Acquisto acquisto) throws EccezionePrecondizioni { if(acquisto == null) throw new EccezionePrecondizioni("L'oggetto deve essere inizializzato"); else effettua.remove(acquisto); } } Per utilizzare le annotazioni in una classe Java è necessario importare il contenuto del package javax.persistence contenente tutte le classi e interfacce necessarie. La prima annotazione è @Entity in corrispondenza della dichiarazione di classe che la definisce come entità, un oggetto Java che verrà reso persistente. A seguire può essere utilizzata l’annotazione @Table che permette di definire il nome della tabella in cui verrà mappata la classe e altre informazioni opzionali come lo schema, il catalogo o anche vincoli di chiave. L’annotazione @Table è opzionale, il valore di default della proprietà name è il nome della classe Java. Le successive annotazioni sono utilizzate per il mapping delle proprietà e delle associazioni. Sviluppando con JPA è importante impostare l’accesso ad un’entità. L’accesso può essere effettuato secondo due modalità: - field-based : il provider accede allo stato dell’entità direttamente dalle variabili d’istanza. Per effettuare questo tipo di accesso è necessario inserire le annotazioni prima dei campi delle proprietà a cui si riferiscono; - property-based : il provider utilizza i metodi get e set per accedere alle proprietà persistenti dell’entità. Per effettuare l’accesso property-based è necessario inserire le annotazioni prima dei metodi get delle proprietà. La tipologia di accesso per una entità viene decisa in base all’impostazione di accesso della proprietà identificatore. Nota: Anche sviluppando con Hibernate è possibile impostare la tipologia di accesso alle proprietà di un’entità. Il comportamento di default di Hibernate è l’accesso property-based, però questa impostazione può essere cambiata con l’utilizzo dell’attributo access nei file di mapping. 19 2.5.1 Mapping di proprietà identificatore Le classi mappate devono dichiarare la colonna che è chiave primaria della tabella. Esempio: proprietà identificatore File Acquisto.java package domain; public class Acquisto { private long id; ... public long getId() { return this.id; } private void setId(long id) { this.id = id; } ... } Hibernate Viene utilizzato l'elemento <id>. Descrizione di alcuni attributi dell'elemento <id> : name="propertyName" Il nome della proprietà identificatore. type="typename" Nome che indica il tipo Hibernate. column="column_name" Il nome della colonna che è chiave primaria. Di default è il nome della proprietà. L'elemento <id> può avere l'elemento figlio <generator> con cui viene nominata una classe usata per generare identificatori unici per le istanze della classe persistente. Se la classe generator richiede parametri, questi verranno passati con elementi <param> figli di <generator>. Le classi generator implementano l'interfaccia org.hibernate.id.identifiergenerator. E' possibile fornire una propria implementazione dell'interfaccia o utilizzare le classi built-in. 20 Alcune delle classi generator: increment : genera identificatori di tipo long, short o int che sono unici solo quando nessun altro processo sta inserendo dati nella stessa tabella. Non usare in cluster. identity : supporta colonne identity in DB2, MySQL, MS SQL Server, Sybase e HypersonicSQL. L'identificatore ritornato è di tipo long, short o int. sequence : usa sequence in DB2, PostgreSQL, Oracle, SAP DB, McKoi o generator in Interbase. L'identificatore ritornato è di tipo long, short o int. assigned : lascia che l'applicazione assegni un identificatore all'oggetto prima che il metodo save() venga chiamato. E' la strategia di default se l'elemento <generator> non viene specificato. native : utilizza identity, sequence o hilo a seconda del DBMS utilizzato. Esempio : utilizzo dell'elemento <id> con <generator> File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> <id name="id" column="id_acquisto"> <generator class="native"/> </id> ... </class> </hibernate-mapping> Java Persistence API In JPA viene utilizzata l’annotazione @Id in corrispondenza della proprietà identificatore. Per sfruttare una classe di identificatori unici si usa l’annotazione @GeneratedValue per la quale si può impostare la proprietà strategy per scegliere una classe identificatore specifica, altrimenti assumerà il valore di default AUTO, analogo alla classe native di Hibernate. Le strategie di generazione più comuni sono IDENTITY e SEQUENCE, anch’esse analoghe ai corrispettivi Hibernate. Esempio : utilizzo dell'annotazione @id con @GeneratedValue File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { 21 private long id; ... @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) public long getId() { return this.id; } ... } 2.4.1.1 Identificatore composto Gli identificatori composti sono utilizzati per mappare identificatori formati da più di una proprietà. In questo caso l’identificatore dell’entità è una classe apposita, le cui proprietà diventeranno colonne della tabella che identificano. E’ importante che la classe che rappresenta l’identificatore implementi java.io.Serializable e ridefinisca i metodi equals() e hashCode(). Esempio: identificatore composto File Utente.java package domain; public class Utente { private IdUtente id; ... protected Utente() { } public IdUtente getId() { return this.id; } private void setId(IdUtente id) { this.id = id; } ... } File IdUtente.java package domain; public class IdUtente implements java.io.Serializable { 22 private String username; private String password; protected IdUtente() { } public IdUtente(String username, String password) { this.username = username; this.password = password; } public boolean equals(Object o) { if (o != null && getClass().equals(this.getClass())) { IdUtente id = (IdUtente) o; return id.username == username && id.password == password; } else { return false; } } public int hashCode() { return username.hashCode() + password.hashCode(); } } Hibernate Viene utilizzato l’elemento <composite-id> specificando l’attributo name per indicare la proprietà identificatore e l’attributo class per indicare la classe che rapprensenta l’identificatore. L’elemento <composite-id> ha come figli un numero di elementi <property-key> pari al numero delle proprietà della classe identificatore. <composite-id name="propertyName" class="ClassName"> <key-property name="property1"/> ... <key-property name="propertyN"/> </composite-id> Esempio: mapping identificatore composto File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> <composite-id name="id" class="domain.IdUtente"> <key-property name="username" access="field"/> <key-property name="password" access="field"/> </composite-id> ... </class> </hibernate-mapping> 23 SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username , password ) ) Java Persistence API Il mapping di un identificatore composto con JPA viene effettuato in modo molto simile alle classi component. La classe che rappresente l’identificatore viene annotata con @Embeddable mentre per la proprietà id della classe entità viene utilizzata l’annotazione @EmbeddedId. Esempio: mapping identificatore composto File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { @EmbeddedId private IdUtente id; ... protected Utente() { } public IdUtente getId() { return this.id; } private void setId(IdUtente id) { this.id = id; } ... } File IdUtente.java package domain; import javax.persistence.*; 24 @Embeddable public class IdUtente implements java.io.Serializable { private String username; private String password; protected IdUtente() { } ... } 2.5.2 Mapping di proprietà con molteplicità massima 1 Le proprietà delle classi persistenti vengono mappate su colonne della tabella corrispondente. Esempio: proprietà di una classe Java File Utente.java package domain; public class Utente { private String username; private String password; ... public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } ... } Hibernate Hibernate utilizza l’elemento <property> per mappare le proprietà della classe persistente. Descrizione di alcuni attributi dell'elemento <property> : name="propertyName" Il nome della proprietà che deve avere la prima lettera minuscola. type="typename" Nome che indica il tipo Hibernate. column="column_name" Il nome della colonna su cui verrà mappata la proprietà. unique="true|false" Attiva la generazione di un vincolo unique per la colonna. 25 not-null="true|false" Attiva la generazione di un vincolo not null per la colonna. Esempio: mapping di proprietà File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <property name="password" column="password" type="string" not-null="true"/> ... </class> </hibernate-mapping> Java Persistence API JPA considera tutte le proprietà non statiche e non transitorie come proprietà persistenti a meno che non vengano annotate come @Transient. Non inserire annotazioni in corrispondenza di una proprietà equivale ad avere l’annotazione @Basic che definisce una proprietà persistente. Per proprietà di tipo data o tempo viene utilizzata l’annotazione @Temporal con cui viene specificata la precisione del dato tra DATE, TIME e TIMESTAMP (data attuale, solo ora, o entrambi). Un’altra annotazione importante è @Column che permette di definire la colonna della tabella in cui verrà mappata la proprietà. Il tipo di dato della colonna in cui verrà mappata la proprietà verrà definito in automatico a partire dal tipo Java. Esempio: mapping di proprietà File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private long id; private Date data; private String info; ... @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) @Column(name = "id_acquisto") public long getId() { return this.id; } @Temporal(TemporalType.DATE) 26 public Date getData() { return this.data; } public String getInfo() { return this.info; } ... } 2.5.3 Tipi Hibernate I tipi Hibernate sono utilizzati per il mapping del sistema di tipi Java e dei tipi definiti dagli sviluppatori nel sistema di tipi SQL della base di dati. I tipi Hibernate da utilizzare possono essere definiti con l'attributo type degli elementi XML <id>, <property>, <component>, altrimenti Hibernate utilizza impostazioni di default. E' possibile utilizzare tipi Hibernate built-in oppure si possono definire strategie di conversione personalizzate. Tipi Hibernate di base: Tipo Hibernate Tipo Java Tipo ANSI SQL integer int or java.lang.Integer INTEGER long long or java.lang.Long BIGINT short short or java.lang.Short SMALLINT float float or java.lang.Float FLOAT double double or java.lang.Double DOUBLE character java.lang.String CHAR(1) byte byte or java.lang.Byte TINYINT boolean boolean or java.lang.Boolean BIT string java.lang.String VARCHAR big_decimal java.math.BigDecimal NUMERIC big_integer java.math.BigInteger NUMERIC 27 Tipi Hibernate per date e tempo: Tipo Hibernate Tipo Java Tipo ANSI SQL date java.util.Date o java.sql.Date DATE time java.util.Date o java.sql.Time TIME timestamp java.util.Date o java.sql.Timestamp TIMESTAMP calendar java.util.Calendar TIMESTAMP calendar_date java.util.Calendar DATE Per convertire un tipo Java in un tipo della base di dati differente, è necessario costruire un tipo Hibernate personalizzato. Hibernate mette a disposizione alcune interfacce per lo scopo. Tra queste, org.hibernate.UserType è l'interfaccia di base, ma possono anche essere utilizzate CompositeUserType, EnhancedUserType, UserCollectionType e UserVersionType per avere supporto a situazioni specifiche. Per definire un nuovo tipo sarà quindi sufficiente costruire una nuova classe Java che implementi una delle interfacce specificando all'interno dei vari metodi come la conversione dovrà avvenire e passarla come valore all'attributo type dell'elemento XML per la proprietà. 2.5.4 Mapping di proprietà con molteplicità massima maggiore di 1 Le proprietà con molteplicità massima maggiore di 1 sono collezioni omogenee di tipi base che non vengono mappate sulla tabella che rende persistente la classe di cui sono proprietà, ma su un'altra tabella. Esempio : proprietà Set File Utente.java package domain; public class Utente { private String username; private String password; private Set<String> numTelefono; ... public Set<String> getNumTelefono() { return (Set<String>) this.numTelefono; } private void setNumTelefono(Set<String> numTelefono) { this.numTelefono = numTelefono; 28 } ... } Hibernate Nel file di mapping Hibernate della classe verrà inserito un elemento set figlio dell'elemento <class>. <set name="propertyName" table="table_name"> <key column="column_a"/> <element type="typename" column="column_b" not-null="true"/> </set> Gli attributi dell'elemento <set> specificano il nome della proprietà collezione da mappare (name) e il nome della tabella in cui verrà mappata (table). L'elemento <key> permette di specificare la colonna della tabella che rappresenta la collezione (attributo column) per la quale sarà definito un vincolo di foreign key verso la chiave primaria della tabella che rende persistente la classe. Infine l'elemento <element> definisce il tipo di dato per la collezione e il nome della colonna della tabella per il mapping dei valori. L'attributo not-null di <element> viene utilizzato per definire la chiave primaria composta da column_a e column_b. Esempio : mapping di una proprietà Set File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <set name="numTelefono" table="num_telefono"> <key column="username"/> <element type="string" column="numero" not-null="true"/> </set> ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ) ) CREATE TABLE num_telefono ( username character varying(255) NOT NULL, 29 numero character varying(255) NOT NULL, CONSTRAINT num_telefono_pkey PRIMARY KEY (username , numero ), CONSTRAINT fk126d878df2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Per mappare collezioni ordinate si può utilizzare l'elemento <list>, con una struttura molto simile a <set> ma con un elemento figlio <list-index> per la generazione di una colonna per il numero d'ordine. <list name="propertyName" table="table_name"> <key column="column_a"/> <list-index column="position_column"/> <element type="typename" column="column_b"/> </list> Per il mapping di diversi tipi di collezione vengono utilizzati elementi specifici. Interfaccia Java Implementazione Elemento Hibernate java.util.Set java.util.HashSet set java.util.SortedSet java.util.TreeSet set java.util.List java.util.ArrayList list java.util.Collection java.util.ArrayList bag java.util.Map java.util.HashMap map java.util.SortedMap java.util.TreeMap map Per il mapping di array vengono invece utilizzati gli elementi <primitive-array>, per array di dati primitivi Java, e <array> per array di oggetti. L'utilizzo di array nelle classi di dominio è però sconsigliabile perchè Hibernate non gestisce le proprietà degli array e questo si traduce in un pesante calo delle performance per collezioni persistenti. Java Persistence API Con JPA viene utilizzata l’annotazione @ElementCollection in corrispondenza della collezione: @ElementCollection @CollectionTable(name="table_name", joinColumns=@JoinColumn(name="column_a") ) @Column(name="column_b") 30 L’annotazione @CollectionTable definisce la tabella in cui verrà mappata la collezione con nome table_name e la colonna (joinColumn) della tabella che rappresenta la collezione con nome column_a. L’annotazione @Column che segue, definisce il nome della colonna per il mapping dei valori della collezione. Esempio : mapping JPA di una proprietà Set File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<String> numTelefono; ... @ElementCollection @CollectionTable(name = "num_telefono", joinColumns = @JoinColumn(name = "username") ) @Column(name = "numero") public Set<String> getNumTelefono() { return (Set<String>) this.numTelefono; } ... } Il mapping di collezioni ordinate con JPA avviene aggiungendo l’annotazione @OrderColumn per definire la colonna per il numero d’ordine. 2.5.5 Mapping di proprietà istanze di una classe che realizza un tipo UML Alcune proprietà di una classe Java possono essere classi che realizzano tipi UML. Queste ultime, non rappresentano entità, cioè non hanno un'identità individuale, ma hanno lo scopo di caratterizzare una classe entità. Una classe che realizza un tipo UML viene mappata sulla tabella della classe entità a cui è collegata ed ogni sua istanza è identificata dalla chiave primaria della classe entità. 31 Esempio: proprietà istanza di una classe che realizza un tipo UML File Utente.java package domain; public class Utente { private String username; private String password; private Indirizzo indirizzo; ... public Indirizzo getIndirizzo() { return this.indirizzo; } public void setIndirizzo(Indirizzo indirizzo) { this.indirizzo = indirizzo; } ... } File Indirizzo.java package domain; public class Indirizzo { private private private private String via; int numeroCivico; String cap; String citta; public Indirizzo() { } public String getVia() { return via; } 32 public void setVia(String via) { this.via = via; } public int getNumeroCivico() { return numeroCivico; } public void setNumeroCivico(int numeroCivico) { this.numeroCivico = numeroCivico; } public String getCap() { return cap; } public void setCap(String cap) { this.cap = cap; } public String getCitta() { return citta; } public void setCitta(String citta) { this.citta = citta; } } Hibernate Con Hibernate si utilizza l'elemento XML <component>, specificato nel file di mapping della classe entità per includere nella tabella le colonne che mapperanno le proprietà della classe che realizza il tipo UML (classe component). <component name="propertyName" class="ClassName"> <property name="property1" column="column_1"/> ... <property name="propertyN" column="column_n"/> </component> L'elemento <component> avrà come attributi il nome della proprietà istanza della classe component all'interno della classe entità (name) e il nome della classe component (class). Gli elementi figli <property> permettono il mapping delle proprietà della classe component. 33 Esempio: mapping di una classe component File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <component name="indirizzo" class="domain.Indirizzo"> <property name="via" column="via"/> <property name="numeroCivico" column="numero_civico"/> <property name="cap" column="cap"/> <property name="citta" column="citta"/> </component> ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, via character varying(255), numero_civico integer, cap character varying(255), citta character varying(255), CONSTRAINT utente_pkey PRIMARY KEY (username ) ) Java Persistence API In applicazioni JPA, una classe che realizza un tipo UML non viene definita con @Entity ma con l’annotazione @Embeddable. Nella classe entità, in corrispondenza della proprietà istanza della classe embeddable verrà inserità l’annotazione @Embedded. Esempio: mapping di una classe embedded File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Indirizzo indirizzo; 34 ... @Embedded public Indirizzo getIndirizzo() { return this.indirizzo; } ... } File Indirizzo.java package domain; import javax.persistence.*; @Embeddable public class Indirizzo { private String via; @Column(name = "numero_civico") private int numeroCivico; private String cap; private String citta; ... } 2.5.6 Mapping di associazioni Il mapping di associazioni con Hibernate e JPA può essere effettuato secondo due modalità: Accorpamento della relazione ER Table join per la relazione ER Verranno ora presentate in dettaglio le diverse metodologie per il mapping dei più importanti tipi di associazione. 2.5.6.1 Mapping di associazioni senza attributi a responsabilità singola con molteplicità 1..1 e 0..1 Le classi di dominio ClassA e ClassB sono legate da un'associazione senza attributi con molteplicità 1..1 sulla quale ClassA ha responsabilità. ClassA ha una proprietà propertyB riferimento ad un'istanza di ClassB. 35 Esempio: associazione senza attributi a responsabilità singola con molteplicità 1..1 File Acquisto.java package domain; public class Acquisto { private long id; private Date data; private Utente effettuatoDa; ... public Utente getEffettuatoDa() { return this.effettuatoDa; } public void setEffettuatoDa(Utente effettuatoDa) { this.effettuatoDa = effettuatoDa; } ... } Accorpamento della relazione ER Nella tabella table_A viene inserita una colonna column_B e viene definito un vincolo di foreign key tra la colonna column_B e la chiave primaria di table_B. Hibernate Viene utilizzato l’elemento <many-to-one>. <many-to-one name=”propertyB” class=”ClassB” column=”column_B” not-null=”true”/> L'attributo not-null=”true” definisce un vincolo NOT NULL per la colonna column_B, in questo modo si garantisce che ogni istanza di ClassA sia collegata a un'istanza di ClassB. Se questo attributo non viene definito, si avrà un'associazione con molteplicità 0..1, perchè sarà possibile introdurre istanze di ClassA con valore null alla colonna column_B, quindi non collegate a nessuna istanza di ClassB. 36 Questo approccio non è consigliato perchè introdurrebbe valori nulli che degraderebbero la qualità dei dati persistenti. Esempio: mapping associazione senza attributi a responsabilità singola con molteplicità 1..1 File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> ... <many-to-one name="effettuatoDa" class="domain.Utente" column="username" not-null="true"/> ... </class> </hibernate-mapping> SQL output CREATE TABLE acquisto ( id bigint NOT NULL, data date, username character varying(255) NOT NULL, CONSTRAINT acquisto_pkey PRIMARY KEY (id ), CONSTRAINT fk98d0bfebf2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ) ) In caso di associazione one-to-one, la strategia è identica al caso di mapping di associazione senza attributi con molteplicità 1..1 con l'unica differenza dell'attributo unique=”true” per definire un vincolo di chiave sulla colonna column_B. <many-to-one name=”propertyB” class=”ClassB” column=”column_B” not-null=”true” unique=”true”/> 37 Esempio: Mapping di associazione senza attributi a responsabilità singola tipo one-to-one File Utente.java package domain; public class Utente { private String username; private String password; private Indirizzo indirizzo; public Indirizzo getIndirizzo() { return this.indirizzo; } public void setIndirizzo(Indirizzo indirizzo) { this.indirizzo = indirizzo; } } File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente"> ... <many-to-one name="indirizzo" class="domain.Indirizzo" column="id_indirizzo" unique="true" not-null="true"/> ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( 38 username character varying(255) NOT NULL, password character varying(255) NOT NULL, id_indirizzo bigint NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ), CONSTRAINT fkce393bf9c9c5d612 FOREIGN KEY (id_indirizzo) REFERENCES indirizzo (id_indirizzo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT utente_indirizzo_key UNIQUE (id_indirizzo ) ) CREATE TABLE indirizzo ( id_indirizzo bigint NOT NULL, via character varying(255), numero_civico integer, cap character varying(255), citta character varying(255), CONSTRAINT indirizzo_pkey PRIMARY KEY (id_indirizzo ) ) Java Persistence API Sviluppando con JPA, si potranno avere i medesimi risultati avuti con Hibernate utilizzando l’annotazione @ManyToOne con @JoinColumn per specificare la colonna column_B per l’accorpamento. @ManyToOne @JoinColumn(name="column_B", nullable=false) La proprietà nullable=false definisce un vincolo NOT NULL per la colonna propB per garantire che l’associazione sia di molteplicità 1..1. Esempio: Mapping di associazione senza attributi a responsabilità singola con molteplicità 1..1 File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private long id; private Date data; private Utente effettuatoDa; ... @ManyToOne @JoinColumn(name = "username", nullable = false) public Utente getEffettuatoDa() { return this.effettuatoDa; } ... 39 } Per quanto riguarda le associazioni one-to-one, viene utilizzato il metodo spiegato per le associazioni generiche di tipo 1..1 con l’aggiunta della proprietà unique=true in modo analogo ad Hibernate. @ManyToOne @JoinColumn(name="column_B", unique=true, nullable=false) Esempio: Mapping di associazione senza attributi a responsabilità singola tipo one-to-one File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<String> numTelefono; private Indirizzo indirizzo; ... @ManyToOne @JoinColumn(name = "id_indirizzo", unique = true, nullable = false) public Indirizzo getIndirizzo() { return this.indirizzo; } ... } File Indirizzo.java package domain; import javax.persistence.*; @Entity @Table(name = "indirizzo") public class Indirizzo { private private private private private long id; String via; int numeroCivico; String cap; String citta; public Indirizzo() { } 40 @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) @Column(name = "id_indirizzo") public long getId() { return this.id; } ... } Table join per la relazione ER Viene creata una nuova tabella table_AB per rappresentare la relazione ER. Questa tabella avrà due colonne, column_A e column_B, ognuna con un vincolo di foreign key verso la chiave primaria di una tabella dell'associazione. Hibernate Viene utilizzato l’elemento <join> con l’elemento <many-to-one>. <join table=”table_AB”> <key column=”column_A”/> <many-to-one name=”propertyB” class=”ClassB” column=”column_B” not-null=”true”/> </join> Come nel caso di accorpamento della relazione ER, viene definito l'attributo not-null=”true” per definire un vincolo NOT NULL sulla colonna column_B di table_AB. Questa modalità di mapping è spesso utilizzata per le associazioni con molteplicità 0..1 a responsabilità singola. Quando un'istanza di ClassA viene resa persistente, viene inserita una tupla in table_A e una tupla in table_AB. Nel rendere persistente un'istanza di ClassA non associata a nessuna istanza di ClassB avremo la tupla da inserire in table_AB con il campo di column_B nullo che violerà il vincolo NOT NULL generando un errore. Per effettuare il mapping di un'associazione 0..1 a responsabilità singola sarà quindi necessario definire l'attributo optional con valore true per l'elemento <join> che non inserirà tuple contenenti valori nulli in table_AB. Per l'elemento <join> è anche possibile definite l'attributo optional con valore true che non permette l'inserimento di valori nulli in table_AB. 41 Esempio: Mapping di associazione senza attributi a responsabilità singola con molteplicità 1..1 File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> ... <join table="acquisto_utente"> <key column="id_acquisto"/> <many-to-one name="effettuatoDa" class="domain.Utente" column="username" not-null="true"/> </join> ... </class> </hibernate-mapping> SQL output CREATE TABLE acquisto ( id_acquisto bigint NOT NULL, data date, CONSTRAINT acquisto_pkey PRIMARY KEY (id_acquisto ) ) CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ) ) CREATE TABLE acquisto_utente ( id_acquisto bigint NOT NULL, username character varying(255) NOT NULL, CONSTRAINT acquisto_utente_pkey PRIMARY KEY (id_acquisto ), CONSTRAINT fka907cacde7ffd084 FOREIGN KEY (id_acquisto) REFERENCES acquisto (id_acquisto) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT fka907cacdf2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) 42 Java Persistence API La crazione di table join per la relazione ER con JPA avviene utilizzando l’annotazione @ManyToOne con @JoinTable. @ManyToOne @JoinTable(name="table_AB", joinColumns = @JoinColumn(name="column_A"), inverseJoinColumns = @JoinColumn(name="column_B", nullable=false) ) La proprietà inversejoinColumns definisce la colonna con vincolo di foreign key verso la tabella dell’entità associata. Questa modalità di mapping genererà lo stesso output SQL di Hibernate, però saranno presenti alcune importanti differenze riscontrabili solo lavorando con gli oggetti. Il mapping JPA con join table mappa un’associazione 0..1 e non 1..1. Questa differenza si ha perché lavorando con Hibernate, quando viene resa persistente un’istanza di ClassA, questa viene mappata su una tupla per la tabella table_A e su una tupla per la join table table_AB mentre con JPA l’istanza di ClassA non verrà mappata su table_AB e quindi il vincolo NOT NULL sulla colonna column_B non verrà violato in caso di mancata associazione. Esempio: Mapping di associazione senza attributi a responsabilità singola con molteplicità 0..1 File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private long id; private Date data; private Utente effettuatoDa; ... @ManyToOne @JoinTable(name = "acquisto_utente", joinColumns = @JoinColumn(name="id_acquisto"), inverseJoinColumns = @JoinColumn(name="username", nullable=false) ) public Utente getEffettuatoDa() { return this.effettuatoDa; } ... } Una soluzione per il mapping di associazioni con molteplicità 1..1 è di aggiungere un ulteriore vincolo utilizzando Hibernate Validator, implementazione di JSR 303 – Bean Validation, che definisce un ulteriore modello di metadati e API per convalidare le classi Java. 43 Basterà inserire l’annotazione @NotNull sul campo che definisce l’associazione per avere un vincolo NOT NULL che controllerà che il campo dell’istanza della classe Java non sia nullo quando si tenterà di renderla persistente. Esempio: Mapping di associazione senza attributi a responsabilità singola con molteplicità 1..1 File Acquisto.java package domain; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name = "acquisto") public class Acquisto { private long id; private Date data; @NotNull private Utente effettuatoDa; ... @ManyToOne @JoinTable(name = "acquisto_utente", joinColumns = @JoinColumn(name = "id_acquisto"), inverseJoinColumns = @JoinColumn(name = "username") ) public Utente getEffettuatoDa() { return this.effettuatoDa; } ... } 2.5.6.2 Mapping di associazioni senza attributi a responsabilità singola con molteplicità 1..* e 0..* Le classi di dominio ClassA e ClassB sono legate da un'associazione senza attributi con molteplicità 1..* sulla quale ClassA ha responsabilità. ClassA ha una proprietà propertyB insieme di riferimenti ad istanze di ClassB. Esempio: associazione senza attributi a responsabilità singola con molteplicità 1..* 44 File Utente.java package domain; public class Utente { private String username; private String password; private Set<Acquisto> effettua; ... public Set<Acquisto> getEffettua() { return (Set<Acquisto>) this.effettua; } private void setEffettua(Set<Acquisto> effettua) { this.effettua = effettua; } ... } Accorpamento della relazione ER Nella tabella table_B viene inserita una colonna column_A e viene definito un vincolo di foreign key tra column_A e la chiave primaria di table_B. Hibernate Viene utilizzato l’elemento <set> con l’elemento <one-to-many>. <set name=”propertyB”> <key column=”column_A”/> <one-to-many class=”ClassB”/> </set> Il mapping di un'associazione 1..* a responsabilità singola con l'elemento <one-to-many> è inusuale e non raccomandata. Esempio: mapping di associazione senza attributi a responsabilità singola con molteplicità 1..* File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <set name="effettua"> <key column="username" not-null="true"/> <one-to-many class="domain.Acquisto"/> </set> ... </class> 45 </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ) ) CREATE TABLE acquisto ( id bigint NOT NULL, data date, username character varying(255) NOT NULL, CONSTRAINT acquisto_pkey PRIMARY KEY (id ), CONSTRAINT fk98d0bfebf2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API Questo mapping utilizza l’annotazione @OneToMany con @JoinColumn. @OneToMany @JoinColumn(name = "colonnaA", nullable = false) Come per Hibernate, il mapping di un'associazione 1..* a responsabilità singola con accorpamento è inusuale e non raccomandato. Esempio: mapping di associazione senza attributi a responsabilità singola con molteplicità 1..* File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<Acquisto> effettua; ... @OneToMany @JoinColumn(name = "username", nullable = false) public Set<Acquisto> getEffettua() { return this.effettua; 46 } ... } Table join per la relazione ER Viene creata una nuova tabella per rappresentare la relazione ER. Questa tabella avrà due colonne, ognuna con un vincolo di foreign key verso la chiave primaria della tabella dell'associazione corrispondente. Questa modalità di mapping è utilizzata per associazioni con molteplicità 0..* a responsabilità singola. Hibernate Viene utilizzato l’elemento <set> insieme all’elemento <many-to-many>. <set name=”propertyB” table=”table_AB”> <key column=”column_A”/> <many-to-many class=”ClassB” column=”column_B” unique=”true”/> </set> L'utilizzo dell'attributo unique con valore true per l'elemento <many-to-many> definisce un vincolo di chiave per column_B, cioè dichiara che l'associazione è di tipo many-to-one. Esempio: Mapping di associazione senza attributi a responsabilità singola con molteplicità 1..* File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <set name="effettua" table="utente_acquisto"> <key column="username"/> <many-to-many class="domain.Acquisto" column="id_acquisto" unique="true"/> </set> ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, 47 CONSTRAINT utente_pkey PRIMARY KEY (username ) ) CREATE TABLE acquisto ( id_acquisto bigint NOT NULL, data date, CONSTRAINT acquisto_pkey PRIMARY KEY (id_acquisto ) ) CREATE TABLE utente_acquisto ( username character varying(255) NOT NULL, id_acquisto bigint NOT NULL, CONSTRAINT utente_acquisto_pkey PRIMARY KEY (username , id_acquisto ), CONSTRAINT fk489e1d71e7ffd084 FOREIGN KEY (id_acquisto) REFERENCES acquisto (id_acquisto) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT fk489e1d71f2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT utente_acquisto_id_acquisto_key UNIQUE (id_acquisto ) ) Il mapping di associazioni many-to-many a responsabilità singola viene effettuato in modo analogo al caso precedente ma eliminando la definizione dell'attributo unique per l'elemento <many-to-many>. <set name=”propB” table=”tabellaAB”> <key column=”colonnaA”/> <many-to-many class=”ClasseB” column=”colonnaB”/> </set> Java Persistence API Per mappare associazioni 1..* e 0..* con join table si utilizza l’annotazione @OneToMany con @JoinTable. @OneToMany @JoinTable(name = "tabellaAB", joinColumns = @JoinColumn(name="colonnaA"), inverseJoinColumns = @JoinColumn(name="colonnaB", nullable=false) ) Come per il caso di associazioni con molteplicità 1..1 con join table, per le associazioni 1..* è necessario aggiungere un vincolo @NotNull di Hibernate Validator. 48 Esempio: Mapping di associazione senza attributi a responsabilità singola con molteplicità 0..* File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<Acquisto> effettua; ... @OneToMany @JoinTable(name = "utente_acquisto", joinColumns = @JoinColumn(name = "username"), inverseJoinColumns = @JoinColumn(name = "id_acquisto", nullable = false) ) public Set<Acquisto> getEffettua() { return this.effettua; } ... } 2.5.6.3 Mapping di associazione senza attributi a responsabilità doppia tipo one-to-many / many-to-one Il mapping di associazioni a responsabilità doppia viene effettuato utilizzando i costrutti analizzati per il mapping di associazioni a responsabilità singola per entrambe le classi che partecipano. L’esempio mostra anche la struttura della classe manager. Esempio: Classi java per il mapping File Utente.java package domain; public class Utente { 49 private String username; private String password; private Set<Acquisto> effettua; ... public Set<Acquisto> getEffettua() { return (Set<Acquisto>) this.effettua; } private void setEffettua(Set<Acquisto> effettua) { this.effettua = effettua; } public void inserisciAcquisto(Acquisto acquisto) throws EccezionePrecondizioni { if(acquisto == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerEffettua.inserisci(this, acquisto); } public void rimuoviAcquisto(Acquisto acquisto) throws EccezionePrecondizioni { if(acquisto == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerEffettua.elimina(this, acquisto); } public void inserisciPerManagerEffettua(ManagerEffettua manager) { if(manager != null) this.effettua.add(manager.getAcquisto()); } public void eliminaPerManagerEffettua(ManagerEffettua manager) { if(manager != null) this.effettua.remove(manager.getAcquisto()); } ... } File Acquisto.java package domain; public class Acquisto { private long id; private Date data; private Utente effettuatoDa; ... 50 public Utente getEffettuatoDa() { return this.effettuatoDa; } private void setEffettuatoDa(Utente effettuatoDa) { this.effettuatoDa = effettuatoDa; } public void inserisciUtente(Utente utente) throws EccezionePrecondizioni { if(utente == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerEffettua.inserisci(utente, this); } public void rimuoviUtente(Utente utente) throws EccezionePrecondizioni { if(utente == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerEffettua.elimina(utente, this); } public void inserisciPerManagerEffettua(ManagerEffettua manager) { if(manager != null) this.effettuatoDa = manager.getUtente(); } public void eliminaPerManagerEffettua(ManagerEffettua manager) { if(manager != null) this.effettuatoDa = null; } public void inserisciArticolo(Articolo articolo) throws EccezionePrecondizioni { if(articolo == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else comprende.add(articolo); } public void rimuoviArticolo(Articolo articolo) throws EccezionePrecondizioni { if(articolo == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else comprende.remove(articolo); } ... 51 } File ManagerEffettua.java package domain; public final class ManagerEffettua { private Utente utente; private Acquisto acquisto; private ManagerEffettua(Utente utente, Acquisto acquisto) { this.utente = utente; this.acquisto = acquisto; } public Utente getUtente() { return this.utente; } public Acquisto getAcquisto() { return this.acquisto; } public static void inserisci(Utente utente, Acquisto acquisto) { if(utente != null && acquisto != null && acquisto.getEffettuatoDa() == null) { ManagerEffettua manager = new ManagerEffettua(utente, acquisto); utente.inserisciPerManagerEffettua(manager); acquisto.inserisciPerManagerEffettua(manager); } } public static void elimina(Utente utente, Acquisto acquisto) { if(utente != null && acquisto == null && acquisto.getEffettuatoDa() != null) { ManagerEffettua manager = new ManagerEffettua(utente, acquisto); utente.eliminaPerManagerEffettua(manager); acquisto.eliminaPerManagerEffettua(manager); } } } Accorpamento della relazione ER L'accorpamento della relazione ER prevede l'aggiunta di una colonna username alla tabella acquisto per associare ad ogni tupla di acquisto una tupla di utente. 52 Hibernate L'utilizzo dell'attributo not-null=”true” nell'elemento <many-to-one> garantisce che per ogni tupla di acquisto venga specificato un utente, quindi che l'associazione sia di tipo 1..*. Inoltre viene utilizzato l’attributo inverse con valore true che viene definito solo per una delle due classi che partecipano all’associazione. Questo attributo viene utilizzato perché alla modifica dell’associazione vengono modificate entrambe le classi coinvolte che corrispondono ad un unico valore nella base di dati. Il mapping con inverse non genererà alcuno statement SQL, in questo modo si evita che entrambe le classi modifichino i valori della base di dati. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo one-to-many/many-to-one File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <set name="effettua" inverse="true"> <key column="username" /> <one-to-many class="domain.Acquisto"/> </set> ... </class> </hibernate-mapping> File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> ... <many-to-one name="effettuatoDa" class="domain.Utente" column="username" not-null="true"/> ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ) ) 53 CREATE TABLE acquisto ( id_acquisto bigint NOT NULL, data date, username character varying(255) NOT NULL, CONSTRAINT acquisto_pkey PRIMARY KEY (id_acquisto ), CONSTRAINT fk98d0bfebf2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API Per questo mapping vengono utilizzate le annotazioni @OneToMany e @ManyToOne. La proprietà mappedBy è equivalente all’attributo inverse=true utilizzato nel mapping con Hibernate. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo one-to-many/many-to-one File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<Acquisto> effettua; ... @OneToMany(mappedBy = "effettuatoDa") public Set<Acquisto> getEffettua() { return (Set<Acquisto>) this.effettua; } ... } File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private long id; 54 private Date data; private Utente effettuatoDa; ... @ManyToOne @JoinColumn(name = "username", nullable = false) public Utente getEffettuatoDa() { return this.effettuatoDa; } ... } Table join per la relazione ER In questo caso una tabella utente_acquisto associa tuple di acquisto a tuple di utente. Hibernate Viene utilizzato l’elemento <set> con l’elemento <many-to-many>. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo one-to-many/many-to-one File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <set name="effettua" table="utente_acquisto" inverse="true"> <key column="username"/> <many-to-many class="domain.Acquisto" column="id_acquisto" unique="true"/> </set> ... </class> </hibernate-mapping> File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> ... <join table="utente_acquisto"> <key column="id_acquisto"/> <many-to-one name="effettuatoDa" class="domain.Utente" column="username" not-null="true"/> </join> 55 ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ) ) CREATE TABLE acquisto ( id_acquisto bigint NOT NULL, data date, CONSTRAINT acquisto_pkey PRIMARY KEY (id_acquisto ) ) CREATE TABLE utente_acquisto ( id_acquisto bigint NOT NULL, username character varying(255) NOT NULL, CONSTRAINT utente_acquisto_pkey PRIMARY KEY (username , id_acquisto ), CONSTRAINT fk489e1d71e7ffd084 FOREIGN KEY (id_acquisto) REFERENCES acquisto (id_acquisto) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT fk489e1d71f2625d59 FOREIGN KEY (username) REFERENCES utente (username) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API Anche per questo mapping vengono utilizzate le annotazioni @OneToMany e @ManyToOne e la proprietà mappedBy su un lato dell’associazione. L’output SQL generato sarà leggermente diverso da quello dell’esempio Hibernate perché la tabella utente_acquisto avrà la chiave primaria composta solo dalla colonna id_acquisto. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo one-to-many/many-to-one File Utente.java package domain; import javax.persistence.*; 56 @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Set<String> numTelefono; private Indirizzo indirizzo; private Set<Acquisto> effettua; ... @OneToMany(mappedBy = "effettuatoDa") public Set<Acquisto> getEffettua() { return (Set<Acquisto>) this.effettua; } ... } File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private long id; private Date data; private Utente effettuatoDa; ... @ManyToOne @JoinTable(name = "utente_acquisto", joinColumns = @JoinColumn(name="id_acquisto"), inverseJoinColumns = @JoinColumn(name="username", nullable=false) ) public Utente getEffettuatoDa() { return this.effettuatoDa; } ... } 2.5.6.4 Mapping di associazione senza attributi a responsabilità doppia tipo one-to-one Questo tipo di associazione viene spesso mappato con l’utilizzo dell’elemento <component>, però nel caso in cui si ha interesse a mantenere una tabella per ogni classe associata, viene utilizzato questo tipo di mapping. In questo caso è possibile solo il mapping con accorpamento della relazione ER. 57 Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo one-to-one File Utente.java package domain; public class Utente { private String username; private String password; private Indirizzo indirizzo; ... public Indirizzo getIndirizzo() { return this.indirizzo; } private void setIndirizzo(Indirizzo indirizzo) { this.indirizzo = indirizzo; } public void inserisciIndirizzo(Indirizzo indirizzo) throws EccezionePrecondizioni { if(indirizzo == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerResidenza.inserisci(this, indirizzo); } public void rimuoviIndirizzo(Indirizzo indirizzo) throws EccezionePrecondizioni { if(indirizzo == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerResidenza.elimina(this, indirizzo); } public void inserisciPerManagerResidenza(ManagerResidenza manager) { if(manager != null) this.indirizzo = manager.getIndirizzo(); } 58 public void eliminaPerManagerResidenza(ManagerResidenza manager) { if(manager != null) this.indirizzo = null; } ... } File Indirizzo.java package domain; public class Indirizzo { private long id; private String via; private int numeroCivico; private String cap; private String citta; private Utente utente; ... public Utente getUtente() { return this.utente; } private void setUtente(Utente utente) { this.utente=utente; } public void inserisciUtente(Utente utente) throws EccezionePrecondizioni { if(utente == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerResidenza.inserisci(utente, this); } public void rimuoviUtente(Utente utente) throws EccezionePrecondizioni { if(utente == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerResidenza.elimina(utente, this); } public void inserisciPerManagerResidenza(ManagerResidenza manager) { if(manager != null) this.utente = manager.getUtente(); } 59 public void eliminaPerManagerResidenza(ManagerResidenza manager) { if(manager != null) this.utente = null; } ... } File ManagerResidenza.java package domain; public class ManagerResidenza { private Utente utente; private Indirizzo indirizzo; private ManagerResidenza(Utente utente, Indirizzo indirizzo) { this.utente = utente; this.indirizzo = indirizzo; } public Utente getUtente() { return this.utente; } public Indirizzo getIndirizzo() { return this.indirizzo; } public static void inserisci(Utente utente, Indirizzo indirizzo) { if(utente != null && indirizzo != null && utente.getIndirizzo() == null && indirizzo.getUtente() == null) { ManagerResidenza manager = new ManagerResidenza(utente, indirizzo); utente.inserisciPerManagerResidenza(manager); indirizzo.inserisciPerManagerResidenza(manager); } } public static void elimina(Utente utente, Indirizzo indirizzo) { if(utente != null && indirizzo != null && utente.getIndirizzo() != null && indirizzo.getUtente() != null) { ManagerResidenza manager = new ManagerResidenza(utente, indirizzo); utente.eliminaPerManagerResidenza(manager); indirizzo.eliminaPerManagerResidenza(manager); } } } 60 Hibernate Il mapping per la classe Utente.java è identico a quello presentato per associazioni a responsabilità singola, mentre per il mapping della classe Indirizzo.java è stato utilizzato l’elemento <one-toone>, presente solo in questi casi, per cui è specificato l’attributo property-ref per identificare la proprietà della classe Utente.java che rappresenta l’associazione. File Utente.hbm.xml <hibernate-mapping> <class name="domain.Utente" table="utente"> ... <many-to-one name="indirizzo" class="domain.Indirizzo" column="indirizzo" unique="true" not-null="true"/> ... </class> </hibernate-mapping> File Indirizzo.hbm.xml <hibernate-mapping> <class name="domain.Indirizzo" table="indirizzo"> ... <one-to-one name="utente" class="domain.Utente" property-ref="indirizzo"/> ... </class> </hibernate-mapping> SQL output CREATE TABLE utente ( username character varying(255) NOT NULL, password character varying(255) NOT NULL, id_indirizzo bigint NOT NULL, CONSTRAINT utente_pkey PRIMARY KEY (username ), CONSTRAINT fkce393bf9c9c5d612 FOREIGN KEY (id_indirizzo) REFERENCES indirizzo (id_indirizzo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT utente_indirizzo_key UNIQUE (id_indirizzo ) ) CREATE TABLE indirizzo ( 61 id_indirizzo bigint NOT NULL, via character varying(255), numero_civico integer, cap character varying(255), citta character varying(255), CONSTRAINT indirizzo_pkey PRIMARY KEY (id_indirizzo ) ) Java Persistence API Per questo mapping viene inserita in entrambe le classi l’annotazione @OneToOne, utilizzata solo in questi casi particolari. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo one-to-one File Utente.java package domain; import javax.persistence.*; @Entity @Table(name = "utente") public class Utente { private String username; private String password; private Indirizzo indirizzo; ... @OneToOne @JoinColumn(name = "id_indirizzo", unique = true, nullable = false) public Indirizzo getIndirizzo() { return this.indirizzo; } ... } File Indirizzo.java package domain; import javax.persistence.*; @Entity @Table(name = "indirizzo") public class Indirizzo { private long id; private String via; private int numeroCivico; 62 private String cap; private String citta; private Utente utente; ... @OneToOne(mappedBy = "indirizzo") public Utente getUtente() { return this.utente; } ... } 2.5.6.5 Mapping di associazione senza attributi a responsabilità doppia tipo many-tomany Il Mapping di associazioni di questo tipo può essere effettuato solo con table join. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo many-to-many File Acquisto.java package domain; public class Acquisto { private long id; private Date data; private Set<Articolo> comprende; ... public Set<Articolo> getComprende() { return (Set<Articolo>)this.comprende; } private void setComprende(Set<Articolo> comprende) { this.comprende = comprende; } public void inserisciArticolo(Articolo articolo) throws EccezionePrecondizioni { if(articolo == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); 63 else ManagerComprende.inserisci(this, articolo); } public void rimuoviArticolo(Articolo articolo) throws EccezionePrecondizioni { if(articolo == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerComprende.elimina(this, articolo); } public void inserisciPerManagerComprende(ManagerComprende manager) { if(manager != null) this.comprende.add(manager.getArticolo()); } public void eliminaPerManagerComprende(ManagerComprende manager) { if(manager != null) this.comprende.remove(manager.getArticolo()); } ... } File Articolo.java package domain; public class Articolo { private String codice; private String descrizione; private double prezzo; private Set<Acquisto> compreso; ... public Set<Acquisto> getCompreso() { return (Set<Acquisto>) this.compreso; } private void setCompreso(Set<Acquisto> compreso) { this.compreso = compreso; } public void inserisciAcquisto(Acquisto acquisto) throws EccezionePrecondizioni { if(acquisto == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerComprende.inserisci(acquisto, this); } 64 public void rimuoviAcquisto(Acquisto acquisto) throws EccezionePrecondizioni { if(acquisto == null) throw new EccezionePrecondizioni( "L'oggetto deve essere inizializzato"); else ManagerComprende.elimina(acquisto, this); } public void inserisciPerManagerComprende(ManagerComprende manager) { if(manager != null) this.compreso.add(manager.getAcquisto()); } public void eliminaPerManagerComprende(ManagerComprende manager) { if(manager != null) this.compreso.remove(manager.getAcquisto()); } ... } File ManagerComprende.java package domain; public final class ManagerComprende { private Acquisto acquisto; private Articolo articolo; private ManagerComprende(Acquisto acquisto, Articolo articolo) { this.acquisto = acquisto; this.articolo = articolo; } public Acquisto getAcquisto() { return this.acquisto; } public Articolo getArticolo() { return this.articolo; } public static void inserisci(Acquisto acquisto, Articolo articolo) { if(acquisto != null && articolo != null) { ManagerComprende manager = new ManagerComprende(acquisto, articolo); acquisto.inserisciPerManagerComprende(manager); articolo.inserisciPerManagerComprende(manager); } } public static void elimina(Acquisto acquisto, Articolo articolo) { if(acquisto != null && articolo != null) { 65 ManagerComprende manager = new ManagerComprende(acquisto, articolo); acquisto.eliminaPerManagerComprende(manager); articolo.eliminaPerManagerComprende(manager); } } } Hibernate In entrambi i file di mapping vengono utilizzato l’elemento <set> con <many-to-many>. File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> ... <set name="comprende" table="acquisto_articolo"> <key column="id_acquisto"/> <many-to-many class="domain.Articolo" column="cod_articolo"/> </set> ... </class> </hibernate-mapping> File Articolo.hbm.xml <hibernate-mapping> <class name="domain.Articolo" table="articolo"> ... <set name="compreso" table="acquisto_articolo" inverse="true"> <key column="cod_articolo"/> <many-to-many class="domain.Acquisto" column="id_acquisto"/> </set> ... </class> </hibernate-mapping> SQL output CREATE TABLE acquisto ( id_acquisto bigint NOT NULL, data date, CONSTRAINT acquisto_pkey PRIMARY KEY (id_acquisto ) ) CREATE TABLE articolo ( 66 cod_articolo character varying(255) NOT NULL, descrizione character varying(255), prezzo double precision, CONSTRAINT articolo_pkey PRIMARY KEY (cod_articolo ) ) CREATE TABLE acquisto_articolo ( cod_articolo character varying(255) NOT NULL, id_acquisto bigint NOT NULL, CONSTRAINT acquisto_articolo_pkey PRIMARY KEY (id_acquisto , cod_articolo ), CONSTRAINT fk182708299cdd2f60 FOREIGN KEY (cod_articolo) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT fk18270829e7ffd084 FOREIGN KEY (id_acquisto) REFERENCES acquisto (id_acquisto) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API Viene utilizzata l’annotazione @ManyToMany per entrambe le classi. Esempio: Mapping di associazione senza attributi a responsabilità doppia tipo many-to-many File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private long id; private Date data; private Set<Articolo> comprende; ... @ManyToMany @JoinTable(name = "acquisto_articolo", joinColumns = @JoinColumn(name = "id_acquisto"), inverseJoinColumns = @JoinColumn(name = "cod_articolo") ) public Set<Articolo> getComprende() { return (Set<Articolo>)this.comprende; } ... } 67 File Articolo.java package domain; import javax.persistence.*; @Entity @Table(name = "articolo") public class Articolo { private String codice; private String descrizione; private double prezzo; private Set<Acquisto> compreso; ... @ManyToMany(mappedBy = "comprende") public Set<Acquisto> getCompreso() { return (Set<Acquisto>) this.compreso; } ... } 2.5.6.6 Mapping di associazioni con attributi Come è stato scritto nel paragrafo sulla realizzazione delle classi di dominio, Hibernate e JPA non forniscono costrutti specifici per il mapping di associazioni con attributi. In caso di associzioni con molteplicità massima pari a 1, trasformando gli attributi dell'associazione in nuove proprietà della classe che ha responsabilità, queste verranno mappate come normali proprietà. Viene ora presentata una possibile strategia per la costruzione di una classe che rappresenti l’associazione in caso di associazioni con molteplicità massima maggiore di 1. L’identificatore della classe è composto dagli identificatori delle classi che partecipano all’associazione ed è incapsulato in una classe statica annidata. Le altre proprietà della classe sono i riferimenti alle classi associate e gli attributi dell’associazione stessa. Particolarmente importante è il costruttore della classe che oltre ad impostare i valori delle proprietà e i valori della classe identificatore annidata, gestisce anche l’inserimento dei valori nei campi della classe o delle classi che hanno responsabilità sull’associazione, garantendo l’integrità referenziale. I riferimenti alle classi che partecipano all’associazione sono mappati su colonne in cui non potranno essere effettuate operazioni di inserimento o aggiornamento, in questo modo le istanze della classe che rappresenta l’associazione non saranno modificabili e verranno gestire unicamente dal costruttore. 68 Esempio: Mapping di associazione con attributi File Acquisto.java package domain; import javax.persistence.*; public class Acquisto { private Long id; private Date data; private Set<ArticoloAcquistato> comprende; public Acquisto() { this.data = new Date(); this.comprende = new HashSet(); } ... public void inserisciArticolo(ArticoloAcquistato articoloAcquistato) { comprende.add(articoloAcquistato); } ... } File Articolo.java package domain; public abstract class Articolo { protected String codice; protected String descrizione; protected double prezzo; protected Articolo() { } ... } 69 File ArticoloAcquistato.java package domain; public class ArticoloAcquistato { public static class Id implements java.io.Serializable { private Long idAcquisto; private String codiceArticolo; protected Id() { } protected Id(Long idAcquisto, String codiceArticolo) { this.idAcquisto = idAcquisto; this.codiceArticolo = codiceArticolo; } public boolean equals(Object o) { if (o != null && getClass().equals(this.getClass())) { Id id = (Id) o; return id.idAcquisto == idAcquisto && id.codiceArticolo == codiceArticolo; } else { return false; } } public int hashCode() { return idAcquisto.hashCode() + codiceArticolo.hashCode(); } } private private private private Id id = new Id(); Acquisto acquisto; Articolo articolo; int quantita; protected ArticoloAcquistato() { } public ArticoloAcquistato(Acquisto acquisto, Articolo articolo, int quantita) throws EccezionePrecondizioni { if(acquisto == null || articolo == null) throw new EccezionePrecondizioni ("L'oggetto deve essere inizializzato"); if(quantita <= 0) throw new EccezionePrecondizioni ("La quantità deve essere positiva"); // Set fields this.acquisto = acquisto; this.articolo = articolo; this.quantita = quantita; 70 // Set identifier values this.id.idAcquisto = acquisto.getId(); this.id.codiceArticolo = articolo.getCodice(); // Guarantee referential integrity acquisto.inserisciArticolo(this); } ... } Hibernate Nel file di mapping ArticoloAcquistato.hbm.xml viene utilizzato l’elemento <composite-id> per mappare la classe annidata come chiave primaria e gli elementi <many-to-one> specificano gli attributi insert=false e update=false che rendono i valori delle colonne non modificabili. File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> <id name="id" column="id_acquisto"> <generator class=""/> </id> <property name="data" column="data" type="date"/> <set name="comprende" inverse="true"> <key column="id_acquisto"/> <one-to-many class="domain.ArticoloAcquistato"/> </set> </class> </hibernate-mapping> File Articolo.hbm.xml <hibernate-mapping> <class name="domain.Articolo" table="articolo"> <id name="codice" column="cod_articolo" type="string"/> <property name="descrizione" column="descrizione" type="string"/> <property name="prezzo" column="prezzo" type="double"/> </class> </hibernate-mapping> File ArticoloAcquistato.hbm.xml <hibernate-mapping> <class name="domain.ArticoloAcquistato" 71 table="articolo_acquistato" mutable="false"> <composite-id name="id" class="domain.ArticoloAcquistato$Id"> <key-property name="idAcquisto" access="field" column="id_acquisto"/> <key-property name="codiceArticolo" access="field" column="cod_articolo"/> </composite-id> <property name="quantita" column="quantita" type="int" not-null="true"/> <many-to-one name="acquisto" column="id_acquisto" not-null="true" insert="false" update="false"/> <many-to-one name="articolo" column="cod_articolo" not-null="true" insert="false" update="false"/> </class> </hibernate-mapping> Java Persistence API Nella classe che rappresenta l’associazione viene utilizzata l’annotazione @EmbeddedId per mappare l’identificatore composto e due annotazioni @ManyToOne con proprietà insertable=false e updatable=false in modo analogo al mapping Hibernate. File Acquisto.java package domain; import javax.persistence.*; @Entity @Table(name = "acquisto") public class Acquisto { private Long id; private Date data; private Set<ArticoloAcquistato> comprende; public Acquisto() { this.data = new Date(); this.comprende = new HashSet(); } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 72 @Column(name = "id_acquisto") public Long getId() { return this.id; } @Temporal(TemporalType.DATE) @Column(name = "data") public Date getData() { return this.data; } @OneToMany(mappedBy = "acquisto") public Set<ArticoloAcquistato> getComprende() throws EccezioneCardMin { return comprende; } ... } File Articolo.java package domain; import javax.persistence.*; @Entity @Table(name = "articolo") public abstract class Articolo { protected protected protected protected String codice; String descrizione; double prezzo; Articolo() { } public Articolo(String codice, String descrizione, double prezzo) { this.codice = codice; this.descrizione = descrizione; this.prezzo = prezzo; } @Id @Column(name = "cod_articolo") public String getCodice() { return codice; } @Column(name = "descrizione") public String getDescrizione() { return descrizione; } @Column(name = "prezzo") public double getPrezzo() { return prezzo; } ... 73 } File ArticoloAcquistato.java package domain; import javax.persistence.*; @Entity @Table(name = "articolo_acquistato") public class ArticoloAcquistato { @Embeddable public static class Id implements java.io.Serializable { @Column(name = "id_acquisto") private Long idAcquisto; @Column(name = "cod_articolo") private String codiceArticolo; protected Id() { } protected Id(Long idAcquisto, String codiceArticolo) { this.idAcquisto = idAcquisto; this.codiceArticolo = codiceArticolo; } public boolean equals(Object o) { if (o != null && getClass().equals(this.getClass())) { Id id = (Id) o; return id.idAcquisto == idAcquisto && id.codiceArticolo == codiceArticolo; } else { return false; } } public int hashCode() { return idAcquisto.hashCode() + codiceArticolo.hashCode(); } } @EmbeddedId private Id id = new Id(); @ManyToOne @JoinColumn(name = "id_acquisto", insertable = false, updatable = false) private Acquisto acquisto; @ManyToOne @JoinColumn(name = "cod_articolo", insertable = false, updatable = false) private Articolo articolo; 74 @Column(name = "quantita") private int quantita; protected ArticoloAcquistato() { } public ArticoloAcquistato(Acquisto acquisto, Articolo articolo, int quantita) throws EccezionePrecondizioni { if(acquisto == null || articolo == null) throw new EccezionePrecondizioni ("L'oggetto deve essere inizializzato"); if(quantita <= 0) throw new EccezionePrecondizioni ("La quantità deve essere positiva"); this.acquisto = acquisto; this.articolo = articolo; this.quantita = quantita; this.id.idAcquisto = acquisto.getId(); this.id.codiceArticolo = articolo.getCodice(); acquisto.inserisciArticolo(this); } ... } 2.5.6.7 Cascading L'opzione cascade permette di decidere se operazioni effettuate su istanze una classe persitente che ha responsabilità su un'associazione avranno effetto anche sulle istanze di classi persistenti ad essa associate. Hibernate Il cascading viene attivato con l'attributo cascade per gli elementi XML che definiscono associazioni. Valori più importanti per l’attributo cascade: cascade=”none” Comportamento di default. Nessuna operazione ha effetto sulle istanze di classi associate. cascade=”save-update” Le operazioni di salvataggio o aggiornamento hanno effetto sulle istanze di classi associate cascade=”delete” Le operazioni di eliminazione hanno effetto sulle istanze di classi associate cascade=”all” Tutte le operazioni hanno effetto sulle istanze di classi associate 75 Java Persistence API Le strategie di cascading utilizzabili con JPA sono simili a quelle utilizzabili con Hibernate ma con sintassi leggermente differente. La modalità di cascading viene impostata con la proprietà cascade. Valori più importanti per la proprietà cascade: CascadeType.PERSIST CascadeType.REFRESH CascadeType.REMOVE CascadeType.ALL Le operazioni di salvataggio hanno effetto sulle istanze di classi associate quando viene chiamato il metodo persist() Le operazioni di aggiornamento hanno effetto sulle istanze di classi associate quando viene chiamato il metodo refresh() Le operazioni di eliminazione hanno effetto sulle istanze di classi associate quando viene chiamato il metodo delete() Tutte le operazioni hanno effetto sulle istanze di classi associate 2.5.6.8 Strategie di fetching Le strategie di fetching definiscono il comportamento dinamico del framework di persistenza, in particolare le modalità di caricamento in memoria principale di oggetti associati. La scelta della strategia di fetching è molto importante per migliorare le performance del software. In questo paragrafo verranno spiegate le differenti strategie di Hibernate e JPA, mentre alcune considerazioni riguardo l'utilizzo di esse sono inserite nel paragrafo 2.9. Hibernate Hibernate mette a disposizione diverse strategie di fetching suddivise in base a quando e come esso viene effettuato. Strategie che definiscono quando viene effettuato il fetching: Lazy collection fetching: una collezione viene caricata in memoria quando l'applicazione invoca un'operazione su quella collezione. Questo è il comportamento di default, il valore dell'attributo lazy per l'elemento che definisce il mapping dell'associazione è "true". Immediate fetching: un'associazione o collezione viene caricata immediatamente quando viene caricato l'oggetto associato. Questa strategia di mapping viene impostata specificando lazy="false" nell'elemento che definisce il mapping dell'associazione. "Extra-lazy" collection fetching: singoli elementi della collezione vengono acceduti dalla base di dati secondo necessità. L'intera collezione viene caricata in memoria solo se strettamente necessario. Questa strategia può essere utile in caso di grandi collezioni, viene impostata specificando lazy="extra" nell'elemento che definisce il mapping per la collezione. Proxy fetching: questa strategia utilizza proxy, dei segnaposto creati da Hibernate a tempo di esecuzione per la gestione di associazioni a valore singolo. Quando viene richiesta l'istanza di una entità, Hibernate può creare un proxy che identifica l'istanza e rimandare l'effettivo caricamento 76 dell'oggetto a quando verrà invocato un metodo su di esso diverso dal metodo get della proprietà identificatore. Esempio: Acquisto acquisto = (Acquisto) session.load( Acquisto.class, new Long(123)); acquisto.getId(); acquisto.getData(); Nell'esempio, il caricamento in memoria dell'oggetto acquisto viene effettuato quando viene invocato il metodo getData(), prima viene utilizzato un proxy. Il proxy fetching è impostato quando il valore dell'attributo lazy per l'elemento che definisce il mapping dell'associazione è "true". "No-proxy" fetching: il caricamento in memoria di associazioni a valore singolo viene effettuato quando la variabile dell'istanza viene acceduta. Questa strategia di mapping viene impostata specificando lazy="no-proxy" nell'elemento che definisce il mapping per l'associazione. Strategie che definiscono come viene effettuato il fetching: Select fetching: l'istanza associata o collezione viene recuperata con una seconda query. A differenza dell'immediate fetching, la seconda query verrà eseguita quando l'associazione verrà acceduta. Questo è il comportamento di default, il valore dell'attributo fetch per l'elemento che definisce il mapping dell'associazione è "select". Join fetching: l'istanza associata o collezione viene recuperata con la stessa query, usando un OUTER JOIN. Questa strategia di mapping viene impostata specificando fetch="join" nell'elemento che definisce il mapping per l'associazione. Subselect fetching: quando viene impostata questa strategia su una collezione, verrà utilizzata una seconda query per recuperare la collezione e tutte le collezioni delle entità caricate con la prima query. Questa strategia di mapping viene impostata specificando fetch="subselect" nell'elemento che definisce il mapping per l'associazione. Batch fetching: è un'ottimizzazione del select fetching. La seconda select può essere utilizzata per il recupero di N istanze di entità o collezioni. Questa strategia di mapping viene impostata specificando batch-size="N" nell'elemento che definisce il mapping per l'associazione. Esempio: definizione della strategia di fetching File Acquisto.hbm.xml <hibernate-mapping> <class name="domain.Acquisto" table="acquisto"> ... 77 <set name="comprende" inverse="true" lazy="false"> <key column="id_acquisto"/> <one-to-many class="domain.ArticoloAcquistato"/> </set> ... </class> </hibernate-mapping> Java Persistence API Le API Java Persistence mettono a disposizione solo due strategie di mapping: lazy o eager. Il lazy fetching è l'impostazione di default mentre l'eager fetching è equivalente all'immediate fetching di Hibernate. La strategia di fetching viene definita con la proprietà fetch per l'annotazione che definisce il mapping dell'associazione impostandone il valore a FetchType.LAZY o FetchType.EAGER. Esempio: definizione della strategia di fetching File Acquisto.java package dominio; @Entity @Table(name = "acquisto") public class Acquisto { private Long id; private Date data; @OneToMany(mappedBy = "acquisto", fetch = FetchType.EAGER) private Set<ArticoloAcquistato> comprende; ... } 2.5.7 Mapping di ereditarietà Hibernate e JPA permettono il mapping di gerarchie di ereditarietà disgiunte e disgiunte complete. Le strategie di base per il mapping di ereditarietà sono tre: una tabella per gerarchia di classi una tabella per sottoclasse una tabella per classe concreta 78 Esempio: Classi java per mapping di ereditarietà File Articolo.java package domain.articolo; public abstract class Articolo { protected String codice; protected String descrizione; protected double prezzo; protected Articolo() { } ... } File Abbigliamento.java package domain.abbigliamento; import domain.articolo.*; public class Abbigliamento extends Articolo { protected Set<String> materiali; protected Abbigliamento() { } ... } File Calzature.java package domain.calzature; 79 import domain.articolo.*; public class Calzature extends Articolo { protected String paese; protected Calzature() { } ... } 2.5.7.1 Una tabella per gerarchia di classi Viene generata una sola tabella per tutta la gerarchia di classi. Alla tabella della superclasse vengono aggiunte colonne per mappare le proprietà delle sottoclassi e una colonna con funzione di discriminante. Hibernate Il file di mapping della superclasse contiene anche i metadati di mapping delle sottoclassi. Viene utilizzato l’elemento <discriminator> per mappare la colonna con funzione di discriminante ed elementi <subclass> per mappare le sottoclassi. Questa strategia ha una limitazione: le colonne dichiarate dalle sottoclassi, non possono avere vincoli NOT NULL. Nota: è importante che l'elemento <discriminator> sia definito subito dopo l'elemento <id>. File Articolo.hbm.xml <hibernate-mapping> <class name="domain.articolo.Articolo" table="articolo"> <id name="codice" column=" cod_articolo" type="string"/> <discriminator column="tipo" type="string"/> <property name="descrizione" column="descrizione" type="string"/> <property name="prezzo" column="prezzo" type="double"/> <subclass name="domain.abbigliamento.Abbigliamento" discriminator-value="A"> <set name="materiali" table="materiali"> <key column="abbigliamento"/> <element type="string" column="materiale"/> </set> </subclass> <subclass name="domain.calzature.Calzature" discriminator-value="C"> <property name="paese" column="paese" type="string"/> </subclass> </class> </hibernate-mapping> 80 SQL output CREATE TABLE articolo ( cod_articolo character varying(255) NOT NULL, tipo character varying(255) NOT NULL, descrizione character varying(255), prezzo double precision, paese character varying(255), CONSTRAINT articolo_pkey PRIMARY KEY (cod_articolo ) ) CREATE TABLE materiali ( abbigliamento character varying(255) NOT NULL, materiale character varying(255), CONSTRAINT fk289940225a93a6c FOREIGN KEY (abbigliamento) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API La superclasse contiene l’annotazione @Inheritance con cui viene specificata la strategia di mapping della gerarchia di classi con l’attributo strategy con valore InheritanceType.SINGLE_TABLE. Le annotazioni @DiscriminatorColumn e @DiscriminatorValue servono rispettivamente a definire la colonna discriminante e il valore che avranno le istanze delle sottoclassi per quella colonna. JPA permette di utilizzare questa strategia anche per mappare gerarchie non complete. In questo caso verrà aggiunta l’annotazione @DiscriminatorValue alla superclasse (concreta). File Articolo.java package domain.articolo; import javax.persistence.*; @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "tipo", discriminatorType = DiscriminatorType.STRING ) public abstract class Articolo { protected String codice; protected String descrizione; protected double prezzo; ... 81 } File Abbigliamento.java package domain.abbigliamento; import domain.articolo.*; import javax.persistence.*; @Entity @DiscriminatorValue("A") public class Abbigliamento extends Articolo { protected Set<String> materiali; ... } File Calzature.java package domain.calzature; import domain.articolo.*; import javax.persistence.*; @Entity @DiscriminatorValue("C") public class Calzature extends Articolo { protected String paese; ... } 2.5.7.2 Una tabella per sottoclasse Viene generata una tabella per la superclasse e una tabella per ogni sottoclasse. Alle sottoclassi verrà aggiunta una colonna per la chiave primaria e un vincolo di foreign key verso la chiave primaria della superclasse. Hibernate Le tabelle per le sottoclassi hanno un'associazione di chiave primaria con la tabella della superclasse. All’interno del file di mapping della superclasse vengono inseriti elementi <joined-subclass> per mappare le sottoclassi. 82 File Articolo.hbm.xml <hibernate-mapping> <class name="domain.articolo.Articolo" table="articolo"> <id name="codice" column="cod_articolo" type="string"/> <property name="descrizione" column="descrizione" type="string"/> <property name="prezzo" column="prezzo" type="double"/> <joined-subclass name="domain.abbigliamento.Abbigliamento" table="abbigliamento"> <key column="cod_articolo"/> <set name="materiali" table="materiali"> <key column="abbigliamento"/> <element type="string" column="materiale"/> </set> </joined-subclass> <joined-subclass name="domain.calzature.Calzature" table="calzature"> <key column="cod_articolo"/> <property name="paese" column="paese" type="string"/> </joined-subclass> </class> </hibernate-mapping> SQL output CREATE TABLE articolo ( cod_articolo character varying(255) NOT NULL, descrizione character varying(255), prezzo double precision, CONSTRAINT articolo_pkey PRIMARY KEY (cod_articolo ) ) CREATE TABLE abbigliamento ( cod_articolo character varying(255) NOT NULL, CONSTRAINT abbigliamento_pkey PRIMARY KEY (cod_articolo ), CONSTRAINT fk5bfe290c17adf557 FOREIGN KEY (cod_articolo) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) CREATE TABLE calzature ( cod_articolo character varying(255) NOT NULL, paese character varying(255), CONSTRAINT calzature_pkey PRIMARY KEY (cod_articolo ), 83 CONSTRAINT fkd9a6916917adf557 FOREIGN KEY (cod_articolo) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) CREATE TABLE materiali ( abbigliamento character varying(255) NOT NULL, materiale character varying(255), CONSTRAINT fk289940225a93a6c FOREIGN KEY (abbigliamento) REFERENCES abbigliamento (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API Questa strategia di mapping viene specificata dal valore InheritanceType.JOINED della proprietà strategy per l’annotazion@Inheritance. File Articolo.java package domain.articolo; import javax.persistence.*; @Entity @Inheritance(strategy = InheritanceType.JOINED) public abstract class Articolo { protected String codice; protected String descrizione; protected double prezzo; ... } File Abbigliamento.java package domain.abbigliamento; import domain.articolo.*; import javax.persistence.*; @Entity public class Abbigliamento extends Articolo { protected Set<String> materiali; ... } 84 File Calzature.java package domain.calzature; import domain.articolo.*; import javax.persistence.*; @Entity public class Calzature extends Articolo { protected String paese; ... } 2.5.7.3 Una tabella per classe concreta Viene generata una tabella per ogni classe concreta della gerarchia. In ogni tabella vengono definite tutte le proprietà della classe, comprese quelle ereditate. Questa strategia può essere utilizzata per mappare gerarchie di ereditarietà disgiunte non complete. Hibernate E' l'unica strategia che permette di mappare gerarchie disgiunte non complete con Hibernate. Se la gerarchia di ereditarietà da mappare è disgiunta completa, la superclasse sarà astratta e verrà definito l'attributo abstract=”true” per l'elemento <class>. Se la gerarchia da mappare è disgiunta non completa, la superclasse verrà mappata su una tabella aggiuntiva, necessaria per rendere persistenti le istanze della superclasse. Come nelle strategie precedenti, i metadati per il mapping delle sottoclassi viene specificato nel file di mapping della superclasse. Vengono utilizzati elementi <union-subclass> per le sottoclassi. Una limitazione di questa strategia è il fatto che non è possibile utilizzare generatori di identificatori. File Articolo.hbm.xml <hibernate-mapping> <class name="domain.articolo.Articolo" table="articolo" abstract="true"> <id name="codice" column="cod_articolo" type="string"/> <property name="descrizione" column="descrizione" type="string"/> <property name="prezzo" column="prezzo" type="double"/> <union-subclass name="domain.abbigliamento.Abbigliamento" table="abbigliamento"> <set name="materiali" table="materiali"> <key column="abbigliamento"/> <element type="string" column="materiale"/> </set> </union-subclass> 85 <union-subclass name="domain.calzature.Calzature" table="calzature"> <property name="paese" column="paese" type="string"/> </union-subclass> </class> </hibernate-mapping> SQL output CREATE TABLE abbigliamento ( cod_articolo character varying(255) NOT NULL, descrizione character varying(255), prezzo double precision, CONSTRAINT abbigliamento_pkey PRIMARY KEY (cod_articolo ) ) CREATE TABLE calzature ( cod_articolo character varying(255) NOT NULL, descrizione character varying(255), prezzo double precision, paese character varying(255), CONSTRAINT calzature_pkey PRIMARY KEY (cod_articolo ) ) CREATE TABLE materiali ( abbigliamento character varying(255) NOT NULL, materiale character varying(255), CONSTRAINT fk289940225a93a6c FOREIGN KEY (abbigliamento) REFERENCES abbigliamento (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java Persistence API L’annotazione @Inheritance specifica l’attributo strategy con valore InheritanceType.TABLE_PER_CLASS. In caso di gerarchie disgiunte non complete, i metadati di mapping non hanno bisogno di modifiche. File Articolo.java package domain.articolo; import javax.persistence.*; 86 @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class Articolo { protected String codice; protected String descrizione; protected double prezzo; ... } File Abbigliamento.java package domain.abbigliamento; import domain.articolo.*; import javax.persistence.*; @Entity public class Abbigliamento extends Articolo { protected Set<String> materiali; protected Abbigliamento() { } ... } File Calzature.java package domain.calzature; import domain.articolo.*; import javax.persistence.*; @Entity public class Calzature extends Articolo { protected String paese; protected Calzature() { } ... } 87 2.6 Interfacce di Hibernate L'accesso alla base di dati avviene utilizzando quattro principali interfacce fornite da Hibernate: SessionFactory, Session, Transaction e Query. 2.6.1 SessionFactory (org.hibernate.SessionFactory) Con questa interfaccia è possibile la creazione di istanze di Session. Solitamente in un'applicazione viene creata una singola istanza di SessionFactory per la creazione di istanze di Session che vengono passate ai thread che si occupano delle richieste dei client. Metodi principali: void close() Distrugge la SessionFactory e rilascia le risorse. Session openSession() Apre una Session. 2.6.2 Session (org.hibernate.Session) Session permette di gestire una sessione di comunicazione con la base di dati durante la quale è possibile utilizzare metodi specifici per la modifica dei dati persistenti lavorando con oggetti. Metodi principali: Transaction beginTransaction() Crea un nuovo oggetto Transaction. Connection close() Termina la sessione restituendo la connessione JDBC. Query createQuery(String queryString) Crea un nuovo oggetto Query dalla query in Hibernate Query Language (HQL) passata come argomento. SQLQuery createSQLQuery(String queryString) Crea un nuovo oggetto SQLQuery dalla query in SQL nativo passata come argomento. void delete(Object object) Rimuove un'istanza persistente dalla base di dati. Object get(Class clazz, Serializable id) Come load()carica una tupla della base di dati, però se non viene trovata, non lancia eccezioni ma ritorna null. 88 Object load(Class theClass, Serializable id) Carica una tupla di una tabella della base di dati in un'istanza della classe corrispondente utilizzando il suo identificatore. Se non viene trovata una tupla corrispondente all'identificatore passato come argomento, viene lanciata un'eccezione non recuperabile. Serializable save(Object object) Rende persistente l'oggetto passato come argomento e ritorna un identificatore per l'oggetto. void update(Object object) Aggiorna un'istanza persistente con l'identificatore dell'oggetto passato come argomento. 2.6.3 Transaction (org.hibernate.Transaction) Con l'interfaccia Transaction vengono gestite transizioni associate a sessioni. Una nuova istanza di Transaction viene solitamente creata con il metodo beginTransaction() di Session che imposta automaticamente l'autocommit a false. Metodi principali: void begin() Inizia una nuova transazione. void commit() Esegue il commit della transazione e termina l'unità di lavoro. void rollback() Esegue il roll back della transazione. 2.6.4 Query (org.hibernate.Query) E' una rappresentazione object-oriented di una query Hibernate. Una nuova istanza di Query viene creata dal metodo createQuery() di Session e permette la creazione di una query in HQL, il linguaggio di Hibernate per le query, da una stringa passata come argomento. Metodi principali: int executeUpdate() Esegue uno statement di aggiornamento o eliminazione. List list() Ritorna il risultato della query come istanza di List. Iterator iterate() Ritorna il risultato della query come istanza di Iterator. 89 Object uniqueResult() Esegue la query e ritorna il singolo risultato come istanza di Object. L'interfaccia Query è estesa dall'interfaccia SQLQuery che permette di creare query in SQL nativo. Una nuova istanza di SQLQuery viene creata dal metodo createSQLQuery() di Session. 2.7 Java Persistence API Nelle applicazioni Java Persistence l’interazione con la base di dati è possibile utilizzando quattro principali interfacce equivalenti a quelle di Hibernate: EntityManagerFactory, EntityManager, EntityTransaction, Query. 2.7.1 EntityManagerFactory (javax.persistence.EntityManagerFactory) Permette la creazione di istanze di EntityManager per iniziare unità di lavoro, in modo equivalente a SessionFactory con Hibernate. Una volta chiusa la EntityManagerFactory tutte le sue istanze di EntityManager verranno considerate chiuse. Metodi principali: void close() Chiude la EntityManagerFactory e rilascia le risorse. EntityManager createEntityManager() Apre un EntityManager. 2.7.2 EntityManager (javax.persistence.EntityManager) L’interfaccia EntityManager fornisce metodi per interagire con il contesto persistente. Ad ogni istanza di EntityManager è associato un contesto persistente, un insieme di istanze di entità nel quale per ogni entità persistente c’è un’unica istanza di entità. Metodi principali: void close() Chiude l’EntityManager. Query createQuery(java.lang.String qlString) Crea un’istanza di Query per eseguire una query in Java Persistence Query Language (JPQL). 90 <T> TypedQuery<T> createQuery(java.lang.String qlString, java.lang.Class resultClass) Crea un’istanza di TypedQuery, sottointerfaccia di Query, per eseguire una query in Java Persistence Query Language (JPQL). resultClass specifica la classe del risultato della query. Query createNativeQuery(java.lang.String sqlString) Crea un nuovo oggetto Query per eseguire uno statement in SQL nativo. Query createNativeQuery(java.lang.String sqlString, java.lang.Class resultClass) Crea un nuovo oggetto Query per eseguire una query in SQL nativo. Con resultClass viene specificata la classe del risultato della query. <T> T find(java.lang.Class<T> entityClass, java.lang.Object primaryKey) Carica una tuple dalla base di dati utilizzando la chiave primaria. void flush() Sincronizza il contesto di persistenza con la base di dati sottostante. EntityTransaction getTransaction() Crea un nuovo oggetto EntityTransaction. void persist(java.lang.Object entity) Rende persistente un oggetto. void refresh(java.lang.Object entity) Aggiorna lo stato di un oggetto dal database, sovrascrivendo eventuali modifiche all’entità. void remove(java.lang.Object entity) Rimuove l’istanza di entità. 2.7.3 EntityTransaction (javax.persistence.EntityTransaction) EntityTransaction permette la gestione di transazioni. Una nuova istanza di EntityTransaction viene creata con il metodo getTransaction() di EntityManager. Metodi principali: void begin() Inizia una nuova transazione. void commit() Esegue il commit della transazione e termina l'unità di lavoro. 91 void rollback() Esegue il roll back della transazione. 2.7.4 Query (javax.persistence.Query) Query è l’interfaccia per il controllo e l’esecuzione di query. Con questa interfaccia possono essere eseguite istanze di query JPQL, il linguaggio di JPA per le query, e istanze di SQL nativo. Metodi principali: int executeUpdate() Esegue uno statement di aggiornamento o eliminazione. java.util.List getResultList() Esegue la query e ritorna il risultato come istanza di List. java.lang.Object getSingleResult() Esegue la query e ritorna il singolo risultato come istanza di Object. 2.8 Hibernate Query Language e Java Persistence Query Language Hibernate Query Language (HQL) e Java Persistence Query Language (JPQL) sono due linguaggi utilizzati rispettivamente in applicazioni Hibernate e Java Persistence per ottimizzare l’interazione con la base di dati. HQL e JPQL sono strutturati in maniera molto simile a SQL ma sono completamente object-oriented, quindi utilizzano i nomi delle classi di dominio invece dei nomi delle tabelle della base di dati e i nomi delle proprietà invece dei nomi delle colonne. Permettono inoltre di utilizzare concetti come l’ereditarietà e le associazioni tra classi. HQL è un’estensione dello standard definito per JPQL nelle Java Persistence API, quindi ogni statement JPQL è anche valido per HQL. L’utilizzo di HQL e JPQL aumenta la portabilità del software perché il linguaggio è indipendente dalla piattaforma: quando viene eseguito uno statement, questo viene convertito in uno o più statement in SQL nativo per la base di dati configurata. Le operazioni disponibili con HQL e JPQL sono l’interrogazione della base di dati mediante query e operazioni di aggiornamento ed eliminazione. 2.8.1 Creazione di un oggetto query Hibernate La creazione di istanze di org.hibernate.Query viene effettuata utilizzando istanze di Session. 92 Il metodo dell’interfaccia Session per la creazione di istanze di Query è createQuery(String queryString) che prende come argomento una stringa che rappresenta la query HQL. Esempio: Creazione di un’istanza di Query Query hqlQuery = session.createQuery("from Utente"); La creazione di una query HQL è molto più compatta ed immedita rispetto alla creazione di una query in SQL nativo. In questo caso verrà utilizzato il metodo createSQLQuery(String queryString). Esempio: Creazione di un’istanza di SQLQuery Query sqlQuery = session.createSQLQuery( "select {utente.*} from utente {utente}" ).addEntity("utente", Utente.class); E’ importante notare come per la crezione di una query in SQL nativo sia necessario chiamare il metodo addEntity(String tableAlias, Class entityType) per specificare l’entità che verrà gestita nella query. In particolare esistono due più importanti metodi addEntity(): - SQLQuery addEntity(Class entityType) Imposta la classe dell’entità coinvolta nella query. SQLQuery addEntity(String tableAlias, Class entityType) Imposta la classe dell’entità specificando l’alias utilizzato nella query. Si ricorda che SQLQuery è sottointerfaccia di Query. Java Persistence API Per la creazione di istanze di javax.persistence.Query è necessario utilizzare un oggetto EntityManager. Viene utilizzato il metodo createQuery(String qlString) che prende come argomento una stringa che rappresenta la query JPQL. Esempio: Creazione di un’istanza di Query Query ejbQuery = em.createQuery("select u from Utente u"); E’ anche disponibile il metodo createQuery(java.lang.String qlString java.lang.Class resultClass) per specificare la classe del risultato della query. 93 Le query in SQL nativo vengono create con il metodo createNativeQuery( java.lang.String sqlString) , disponibile anche con la possibilità di specificare la classe risultante della query createNativeQuery(java.lang.String sqlString, java.lang.Class resultClass). Esempio: Creazione di istanze di Query in SQL nativo Query sqlQuery = em.createNativeQuery( "select u.id_utente, u.nome, u.cognome from utente u", Utente.class ); 2.8.2 Binding dei parametri Il binding dei parametri a runtime permette alla base di dati di gestire in modo efficiente prepared statement precompilate, migliorando le performance. Il binding di parametri può essere di tipo posizionale o utilizzare parametri con nome. I parametri con nome vengono specificati da due punti seguiti dal nome del parametro. Esempio: Binding di parametri con nome String queryString = "from Articolo articolo where articolo.descrizione like :search"; I parametri per il binding di tipo posizionale vengono specificati da un punto interrogativo in HQL e da un punto interrogativo seguito dal numero del parametro in JPQL. Esempio: Binding di parametri di tipo posizionale con HQL String queryString = "from Articolo articolo where articolo.descrizione like ?"; Esempio: Binding di parametri di tipo posizionale con JPQL String queryString = "select articolo from Articolo articolo where articolo.descrizione like ?1"; Hibernate Il binding di un valore nel parametro con nome viene effettuato con metodi setXXX() che prendono come argomento un tipo specifico. Query q = session.createQuery(queryString) .setString("search", searchString); 94 Per il binding di tipo posizionale si utilizzano metodi analoghi al binding di parametri con nome che prendono però come argomento la posizione del parametro e il valore da assegnare. Query q = session.createQuery(queryString) .setString(0, searchString); Java Persistence API Il binding avviene in modo leggermente diverso rispetto ad Hibernate. Viene utilizzato il metodo generico setParameter(java.lang.String name, java.lang.Object value) che riconosce automaticamente il tipo di dato. Si potrebbero avere problemi con tipi che rappresentano date o tempo, in questo caso si utilizzerà una variante più specifica del metodo, per esempio setParameter(java.lang.String name, java.util.Date value, TemporalType temporalType). Query q = em.createQuery(queryString) .setParameter("search", searchString); Il binding di parametri di tipo posizionale viene effettuato utilizzando il metodo setParameter(int position, java.lang.Object value). Query q = em.createQuery(queryString) .setParameter(1, searchString); Nota: Anche Hibernate supporta il metodo setParameter() per entrambi i tipi di binding. 2.8.3 Gestione dei risultati Il metodo più utilizzato per gestire il risultato di una query è di caricare in memoria l’intero risultato. Hibernate In applicazioni Hibernate viene utilizzato il metodo list() di Query che esegue la query e ritorna il risultato come java.util.List. Esempio: Utilizzo del metodo list() List result = hqlQuery.list(); Hibernate mette a disposizione altre due strategie per l’accesso ai risultati di una query: - Iteratore: chiamando il metodo iterate() della classe Query viene ritornato un oggetto Iterator per la lettura della collezione di oggetti che è risultato della query. Utilizzando il metodo iterate() vengono caricati dalla base di dati solo le chiavi primarie degli oggetti e successivamente viene cercata la parte restante dello stato degli oggetti nella cache del contesto di persistenza. 95 - Scrollable resultset: questa strategia permette di creare un iteratore gestito dal DBMS per accedere ai dati senza caricare tutto il risultato della query in memoria. Viene utilizzato il metodo scroll() di Query che ritorna un oggetto ScrollableResults con cui è possibile utilizzare metodi specifici per accedere alla base di dati. Java Persistence API In applicazioni Java Persistence il metodo getResultList() di Query restituisce il risultato come istanza di java.util.List in modo analogo al metodo list() di Hibernate. Esempio: Utilizzo del metodo getResultList() List result = jpqlQuery.getResultList(); Nel caso in cui si è a conoscenza che il risultato della query è un singolo oggetto, si potrà utilizzare il metodo uniqueResult(). 2.8.4 Operazioni di aggiornamento ed eliminazione Le operazioni di aggiornamento ed eliminazione con HQL e JPQL vengono effettuate tramite update statement e delete statement, molto simili a statement SQL ma utilizzando nomi di entità e proprietà. Sono presenti alcune regole da seguire per la scrittura di update statement e delete statement: - Non è possibile effettuare join; È possibile eseguire statement per un’unica entità; L’uso di alias è opzionale; Sono permesse le sottoquery nella clausola where. L’esecuzione di statement HQL e JPQL avviene chiamando il metodo executeUpdate() di Query che ritorna il numero di oggetti entità modificati. Negli esempi presentati in seguito verranno mostrati update statement e delete statement che agiscono su una gerarchia di classi disgiunta e completa già presentata nel paragrafo dedicato al mapping di ereditarietà: le classi Abbigliamento e Calzature sono sottoclassi della classe astratta Articolo. Hibernate Nell’esempio di update statement HQL viene modificata la proprietà descrizione di un oggetto Articolo. Viene utilizzato il binding del parametro con nome descr. Esempio: update statement HQL Query hqlquery = session.createQuery("update Articolo a " + "set a.descrizione = :descr " + "where a.codice = 'A0001'"); hqlquery.setString("descr", "Articolo A0001"); 96 int updatedItems = hqlquery.executeUpdate(); In questo caso viene eliminato l’oggetto istanza di Abbigliamento con codice “A0001”. Esempio: delete statement HQL Query hqlquery = session.createQuery("delete Abbigliamento a " + "where a.codice = :code"); hqlquery.setString("code", "A0001"); int updatedItems = hqlquery.executeUpdate(); Java Persistence API Come sarà possibile notare, gli esempi di statement JPQL non presentano alcuna differenza rispetto ai corrispettivi esempi HQL. Esempio: update statement JPQL Query jpqlquery = em.createQuery("update Articolo a " + "set a.descrizione = :descr " + "where a.codice = 'A0001'"); jpqlquery.setParameter("descr", "Articolo A0001"); int updatedItems = jpqlquery.executeUpdate(); Esempio: delete statement JPQL Query jpqlquery = em.createQuery("delete Abbigliamento a " + "where a.codice = :code"); jpqlquery. setParameter("code", "A0001"); int updatedItems = jpqlquery.executeUpdate(); 2.8.5 Query Le query scritte utilizzando HQL e JPQL, come già scritto in precedenza, non sono molto diverse nella struttura rispetto alle query SQL. I select statement sono composti dalla target list, dalla clausola from e dalla clausola where. In HQL la target list è opzionale ma questo non crea una grande differenza perché sarà possibile ometterla solo nel caso in cui non si vorrà effettuare una proiezione (select *). Esempio: Query HQL from Utente Esempio: Query JPQL select u from Utente u 97 Per una spiegazione approfondita sulle regole di scrittura di query HQL e JPQL si consiglia di consultare la documentazione ufficiale. 2.8 Generazione dello schema della base di dati Hibernate La generazione automatica dello schema della base di dati avviene utilizzando lo strumento hbm2ddl, in particolare la classe org.hibernate.tool.hbm2ddl.SchemaExport. Metodi di utilizzo dello strumento hbm2ddl: 1. Eseguire <hbm2ddl> in un target Ant per generare lo schema della base di dati durante la procedura di compilazione. 2. Eseguire SchemaExport all’interno del codice dell’applicazione. Esempio: Configuration cfg = new Configuration().configure(); SchemaExport schemaExport = new SchemaExport(cfg); schemaExport.create(false, true); 3. Impostare la proprietà di configurazione hibernate.hbm2ddl.auto a create per avviare la generazione automatica dello schema alla creazione di SessionFactory. Esempio: File hibernate.hbm.xml <hibernate-configuration> <session-factory> ... <property name="hibernate.hbm2ddl.auto">create</property> ... </session-factory> </hibernate-configuration> Valori disponibili per la proprietà hibernate.hbm2ddl.auto: create create-drop update validate Genera lo schema distruggendo eventuali dati presenti. Genera lo schema e lo distrugge quando viene chiusa la SessionFactory. Aggiorna lo schema. Convalida lo schema. Non effettua cambiamenti. 98 Java Persistence API Le applicazioni Java Persistence che utilizzano Hibernate come provider fanno anch’esse uso di hbm2ddl come strumento per la creazione dello schema della base di dati. In particolare i metodi più utilizzati sono i target Ant e l’impostazione della proprietà hibernate.hbm2ddl.auto nel file di configurazione persistence.xml. Esempio: File persistence.xml <persistence 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" version="2.0"> <persistence-unit name="testPU"> ... <properties> ... <property name="hibernate.hbm2ddl.auto" value="create"/> ... </properties> </persistence-unit> </persistence> 2.9 Creazione e funzionamento di un progetto Hibernate Il primo passo per iniziare un progetto Hibernate è effettuare il download dell’ultima versione di Hibernate nel formato release bundle dal sito ufficiale www.hibernate.org. Nell’archivio è presente la directory lib all’interno della quale sono presenti le API Hibernate. Per la creazione dell’applicazione verranno inclusi tra le librerie del progetto i file jar presenti nella directory requied all’interno di lib. Tra le librerie del progetto dovrà anche essere incluso il driver JDBC della base di dati utilizzata e il software per la creazione di un pool di connessioni (es. c3p0). A questo punto si potrà procedere con la realizzazione delle classi di dominio secondo l’approccio POJO e la creazione dei file XML per il mapping delle classi stesse. Infine verrà creato il file XML di configurazione di Hibernate, hibernate.cfg.xml. L’inizializzazione di Hibernate avviene istanziando un oggetto della classe Configuration (org.hibernate.cfg.Configuration) e richiamando i metodi configure(), che configura le impostazioni dal file hibernate.cfg.xml, e buildSessionFactory() per la creazione di un’istanza di SessionFactory che potrà essere utilizzata per l’intera applicazione. La creazione e la distruzione di SessionFactory può essere affidata ad una classe HibernateUtil. 99 Esempio: HibernateUtil per la creazione di SessionFactory File HibernateUtil.java : package persistence; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class HibernateUtil { private static final SessionFactory sessionFactory = buildSessionFactory(); private static SessionFactory buildSessionFactory() { try { // Create the SessionFactory from hibernate.cfg.xml return new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) { // Make sure you log the exception, as it might be swallowed System.err.println("Initial SessionFactory creation failed." + ex); throw new ExceptionInInitializerError(ex); } } public static SessionFactory getSessionFactory() { return sessionFactory; } public static void shutdown() { //Close caches and connection pools getSessionFactory().close(); } } Con SessionFactory si potranno creare istanza di Session con il metodo openSession() e richiamare i vari metodi per la comunicazione con la base di dati. Ogni unità di lavoro utilizzerà un’istanza di Session e verrà conclusa dal metodo close(). Durante la fase di progettazione è importante considerare anche il comportamento dinamico di Hibernate per decidere la strategia di fetching adeguata ad ognuna delle associazioni mappate. Alcune strategie potrebbero migliorare le prestazioni del software, altre peggiorarle o provocare la generazione di eccezioni. La scelta di una strategia di fetching dovrà essere effettuata tenendo conto della quantità di dati da caricare in memoria principale, del numero e della struttura delle query utilizzate da Hibernate e di come i dati verranno in seguito gestiti dall'applicazione. L'utilizzo di lazy fetching è sempre consigliato anche se potrebbe creare problemi nella gestione dei dati generando l'eccezione LazyInitializationException. In generale, in caso di lazy fetching, bisogna sempre ricordare che l'accesso ai dati deve essere effettuato all'interno di una session. In alcuni casi sarà necessario utilizzare immediate fetching, bisognerà fare attenzione alla quantità di dati che verranno caricati con una songola richiesta al framework perchè un'errato utilizzo di questa strategia può causare una reazione a 100 catena che porta al caricamento una gran quantità di tuple della base di dati peggiorando molto le prestazioni del software. Ogni situazione deve quindi essere analizzata dal punto di vista dell'intero modello della base di dati. Le strategie che definiscono come viene effettuato il fetching possono spesso migliorare le performance diminuendo il numero di query che il framework utilizza per il recupero dei dati. La scelta di queste strategie di ottimizzazione necessita di uno studio specifico degli scenari in cui può trovarsi l'applicazione. Esempio: main per un’applicazione File Main.java package app; import domain.*; import org.hibernate.*; import persistence.HibernateUtil; public class Main { public static void main(String[] args) { //First unit of work Session session = HibernateUtil.getSessionFactory().openSession(); Transaction tx = session.beginTransaction(); Utente utente1 = new Utente("utente1", "password1"); nuovoUtente.inserisciNumTelefono("123456789"); Articolo a1 = new Articolo("A1", "articolo1", 1.00); Articolo a2 = new Articolo("A2", "articolo2", 2.50); Articolo a3 = new Articolo("A3", "articolo3", 5.00); String cod1 = (String) session.save(a1); session.save(a2); session.save(a3); session.save(utente1); tx.commit(); session.close(); //Second unit of work session = HibernateUtil.getSessionFactory().openSession(); tx = session.beginTransaction(); Acquisto ac1 = new Acquisto(); try { ac1.inserisciArticolo(a1); ac1.inserisciArticolo(a2); utente1.inserisciAcquisto(ac1); } 101 catch(EccezionePrecondizioni e) { e.printStackTrace(); } session.save(ac1); session.update(utente1); a1 = (Articolo) session.get(Articolo.class, cod1); a1.setPrezzo(1.10); tx.commit(); session.close(); //Third unit of work session = HibernateUtil.getSessionFactory().openSession(); tx = session.beginTransaction(); List articoli = session.createSQLQuery( "SELECT * " + "FROM articolo " + "ORDER BY cod_articolo;").addEntity(Articolo.class).list(); System.out.println( "Numero articoli: " + articoli.size() + "\n"); for (Iterator iter= articoli.iterator(); iter.hasNext(); ) { Articolo a = (Articolo) iter.next(); System.out.println("codice: " + a.getCodice() + "\ndescrizione: " + a.getDescrizione() + "\nprezzo: " + a.getPrezzo() + "\n"); } List acquisti = session.createSQLQuery( "SELECT * " + "FROM acquisto " + "ORDER BY id_acquisto;").addEntity(Acquisto.class).list(); System.out.println("Numero acquisti: " + acquisti.size()); for (Iterator iter= acquisti.iterator(); iter.hasNext(); ) { Acquisto ac = (Acquisto) iter.next(); System.out.println("id: " + ac.getId() + "\ndata: " + ac.getData() + "\n\n"); } tx.commit(); session.close(); //Shutting down the application HibernateUtil.shutdown(); } } 102 Java Persistence API Le librerie necessarie alla creazione di un progetto Java Persistence con Hibernate sono contenute all’interno del release bundle di Hibernate. Tra le librerie del progetto verranno inclusi i file jar contenuti nella directory required e il file hibernate-entitymanager contenuto nella directory jpa, oltre al driver JDBC della base di dati utilizzata e il software per la creazione di un pool di connessioni. E’ ora possibile sviluppare l’applicazione scrivendo le classi di dominio POJO con annotazioni e il file di configurazione persistence.xml contenuto in una directory nominata META-INF. L’inizializzazione avverrà alla creazione dell’istanza di EntityManagerFactory per l’applicazione. Per fare ciò verrà utilizzato il metodo statico createEntityManagerFactory() della classe javax.persistence.Persistence che prende come argomento il nome della persistence unit per la EntityManagerFactory. Avendo a disposizione un’istanza di EntityManagerFactory sarà possibile creare diverse istanze di EntityManager con il metodo createEntityManager() per poter interagire con la base di dati. Al termine dell’applicazione verrà chiusa la EntityManagerFactory con il metodo close(). Le considerazioni fatte per Hibernate riguardanti la scelta delle strategie di fetching per le associazioni valgono anche in questo caso, con la differenza che lo standard JPA non fornisce una gran varietà di strategie. In base all'implementazione delle API Java Persistence potranno essere disponibili strategie alternative. Esempio: main per un’applicazione File Main.java package app; import domain.*; import javax.persistence.*; public class Main { public static void main(String[] args) { /// Start EntityManagerFactory EntityManagerFactory emf = Persistence.createEntityManagerFactory("testPU"); // First unit of work EntityManager em1 = emf.createEntityManager(); EntityTransaction tx1 = em1.getTransaction(); tx1.begin(); Utente utente1 = new Utente("utente1", "password1"); nuovoUtente.inserisciNumTelefono("123456789"); Articolo a1 = new Articolo("A1", "articolo1", 1.00); Articolo a2 = new Articolo("A2", "articolo2", 2.50); Articolo a3 = new Articolo("A3", "articolo3", 5.00); 103 em1.persist(a1); String cod1 = a1.getCodice(); em1.persist(a2); em1.persist(a3); em1.persist(utente1); tx1.commit(); em1.close(); //Second unit of work EntityManager em2 = emf.createEntityManager(); EntityTransaction tx2 = em2.getTransaction(); tx2.begin(); Acquisto ac1 = new Acquisto(); try { ac1.inserisciArticolo(a1); ac1.inserisciArticolo(a2); utente1.inserisciAcquisto(ac1); } catch(EccezionePrecondizioni e) { e.printStackTrace(); } em2.persist(ac1); em2.refresh(utente1) a1 = (Abbigliamento) em2.getReference(Abbigliamento.class, cod1); a1.setPrezzo(55.5); tx2.commit(); em2.close(); //Third unit of work EntityManager em3 = emf.createEntityManager(); EntityTransaction tx3 = em3.getTransaction(); tx3.begin(); List articoli = em3.createNativeQuery( "SELECT * " + "FROM articolo " + "ORDER BY cod_articolo;", Articolo.class).getResultList(); System.out.println( "Numero articoli: " + articoli.size() + "\n"); for (Iterator iter= articoli.iterator(); iter.hasNext(); ) { Articolo a = (Articolo) iter.next(); System.out.println("codice: " + a.getCodice() + "\ndescrizione: " + a.getDescrizione() + "\nprezzo: " + a.getPrezzo() + "\n"); } List acquisti = em3.createNativeQuery( "SELECT * " + "FROM acquisto " + "ORDER BY id_acquisto;", 104 Acquisto.class).getResultList(); System.out.println("Numero acquisti: " + acquisti.size()); for (Iterator iter= acquisti.iterator(); iter.hasNext(); ) { Acquisto ac = (Acquisto) iter.next(); System.out.println("id: " + ac.getId() + "\ndata: " + ac.getData() + "\n\n"); } tx3.commit(); em3.close(); // Shutting down the application emf.close(); } } 105 3. APPLICAZIONE DI ESEMPIO Viene ora presentata una applicazione di esempio realizzata utilizzando le Java Persistence API. L’applicazione è composta dalle classi di dominio che contengono le annotazioni per il mapping, una base di dati generata automaticamente dal software Hibernate che implementa le Java Persistence API, le classi delle attività per le operazioni e una interfaccia grafica realizzata con il framework Java Swing. 3.1 Requisiti L’applicazione riguarda la gestione di un negozio di scarpe ed abbigliamento. Degli articoli in vendita presso il negozio interessa conoscere il codice (una stringa), una descrizione (una stringa) ed il prezzo per singola unità (un reale positivo). Gli articoli sono solo di due tipi: articoli di abbigliamento e calzature. Dei primi interessa sapere con quali materiali sono realizzati (un insieme non vuoto di stringhe), mentre delle alzature interessa conoscere il paese in cui sono state fabbricate (una stringa). Sul prezzo di una calzatura può essere effettuato uno sconto quando questa è acquistata insieme a determinati capi di abbigliamento. Per ogni calzatura interessa conoscere quindi quali sono gli articoli di abbigliamento che danno diritto allo sconto sul prezzo della calzatura se acquistati insieme ad essa, e l'entità di tale sconto in percentuale (un intero compreso fra 5 e 80). Si noti che per ogni calzatura esiste sempre almeno un articolo di abbigliamento associato che dà diritto allo sconto, e che tale sconto dipende dall'articolo a cui la calzatura è associata (ad esempio, se si acquista la calzatura X insieme all'articolo di abbigliamento Y si ha diritto ad uno sconto del 10% sul prezzo di X, mentre se la si acquista insieme all'articolo di abbigliamento Z si ha diritto ad uno sconto del 20%, sempre sul prezzo di X). Per ogni acquisto che viene effettuato presso il negozio interessa conoscere la data, l'importo complessivo effettivamente pagato (un reale positivo), quali articoli sono stati acquistati (almeno uno), indicando per ciascuno di questi la quantità acquistata (un intero positivo). Inoltre, per ogni calzatura acquistata, si vuole anche conoscere lo sconto praticato nell'acquisto (un intero compreso fra 0 e 100, dove 0 indica in effetti che non c'è stato sconto). L’applicazione è composta da tre attività. La prima attività permette di inserire nuovi acquisti nella base di dati. Inizialmente è possibile scegliere gli articoli da inserire nell’acquisto, successivamente viene inserito in nuovo acquisto nella base di dati e alla fine viene stampato l’importo totale dell’acquisto. La seconda attività permette di visualizzare una lista completa degli acquisti contenuti nella base di dati che comprende l’ID degli acquisti e la data di creazione. L’ultima è l’attività di controllo della correttezza degli acquisti e produzione di informazioni statistiche, che prende come parametro un acquisto a. L'attività inizia verificando che l'importo complessivo effettivamente pagato per a sia minore od uguale del totale che si ottiene sommando i costi dei singoli articoli acquistati tramite a (ovviamente considerando per ogni articolo la quantità di articoli acquistati). Se il risultato è false si stampa un messaggio di errore e si termina. Se il risultato è true, allora concorrentemente partono le seguenti sottoattività: (i) dato l'acquisto a, si restituisce l'insieme di calzature acquistate tramite a per le quali è stato praticato uno sconto (maggiore di 0); (ii) dato l'acquisto a, si restituisce l'insieme dei materiali con cui sono realizzati gli articoli facenti parte di a. Una volta che tali sottoattività sono completate, si stampa il contenuto dei due insiemi risultato delle sottoattività. 106 3.2 Classi di dominio Le classi di dominio devono essere realizzate secondo l’approccio POJO. Le fasi di analisi e progetto vengono effettuate senza particolari accorgimenti. Diagramma UML delle classi: Responsabilità sulle associazioni: Associazione comprende sconto Classe Acquisto Articolo Calzature Abbigliamento ha responsabilità SI NO SI NO Dal diagramma UML delle classi è possibile notare che sono presenti quattro entità fondamentali: Acquisto, Articolo, Abbigliamento e Calzature. Le classi Articolo, Abbigliamento e Calzature compongono una gerarchia di classi disgiunta e completa. Sono inoltre presenti due associazioni con attributi: comprende e sconto. Alcune considerazioni riguardo la realizzazione delle classi POJO e il mapping. Le associazioni con attributi verranno realizzare utilizzando la strategia spiegata nel paragrafo 2.5.6.6. 107 L’associazione comprende viene creata utilizzando la classe ArticoloAcquistato che contiene un riferimento ad un’istanza di Acquisto, un riferimento ad un’istanza di Articolo e l’attributo dell’associazione. Avendo la responsabilità su comprende, la classe Acquisto avrà un campo collezione di riferimenti a istanze di ArticoloAcquistato. L’associazione sconto viene realizzata in modo analogo all’associazione comprende, utilizzando la classe ScontoInfo. E’ importante spiegare un aspetto del mapping di associazioni. Per poter utilizzare i metodi della classe Acquisto, totale() e calcolaSconto(), che coinvolgono buona parte delle tabelle della base di dati, è stato necessario mappare alcune collezioni con la proprietà fetch=FetchType.EAGER che imposta la strategia di eager fetching, cioè caricamento in memoria immeditato dei dati delle collezioni. Questa strategia peggiora molto le performance rispetto alla strategia di default lazy fetching, ma è utile per notare il livello di isolamento che JPA può fornire dalla base di dati. La gerarchia di classi composta da Articolo, Abbigliamento e Calzature è stata mappata con la strategia una tabella per sottoclasse. File Acquisto.java package dominio; import java.util.*; import javax.persistence.*; import dominio.calzature.Calzature; import dominio.abbigliamento.Abbigliamento; @Entity @Table(name = "acquisto") public class Acquisto { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id_acquisto") private Long id; @Temporal(TemporalType.DATE) @Column(name = "data") private Date data; @OneToMany(mappedBy = "acquisto", fetch = FetchType.EAGER) private Set<ArticoloAcquistato> comprende; public Acquisto() { this.data = new Date(); this.comprende = new HashSet(); } public Long getId() { return this.id; } private void setId(Long id) { this.id = id; } public Date getData() { return this.data; } 108 private void setData(Date data) { this.data = data; } public Set<ArticoloAcquistato> getComprende() throws EccezioneCardMin { if(comprende.size() == 0) throw new EccezioneCardMin("Cardinalità minima violata"); else return (Set<ArticoloAcquistato>) comprende; } private void setComprende(Set<ArticoloAcquistato> comprende) { this.comprende = comprende; } public void inserisciArticolo(ArticoloAcquistato articoloAcquistato) { comprende.add(articoloAcquistato); } public double totale() throws EccezioneCardMin { double result = 0; Iterator<ArticoloAcquistato> it = comprende.iterator(); while(it.hasNext()) { ArticoloAcquistato ai = (ArticoloAcquistato) it.next(); if(ai.getArticolo() instanceof Calzature) { Calzature c = (Calzature) ai.getArticolo(); int sconto = this.calcolaSconto(c); result += ((c.getPrezzo()*(100-sconto))/100)*ai.getQuantita(); } else result += ai.getArticolo().getPrezzo()*ai.getQuantita(); } return result; } public int calcolaSconto(Calzature calzature) throws EccezioneCardMin { int sconto = 0; Iterator<ScontoInfo> it1 = calzature.getSconto().iterator(); while (it1.hasNext()) { ScontoInfo si = (ScontoInfo) it1.next(); Abbigliamento a = si.getAbbigliamento(); Iterator<ArticoloAcquistato> it2 = comprende.iterator(); while (it2.hasNext()) { ArticoloAcquistato aa = (ArticoloAcquistato) it2.next(); if (aa.getArticolo().equals(a)) { int percentuale = si.getPercentuale(); if(percentuale > sconto) sconto = percentuale; } } } return sconto; } } 109 File Articolo.java package dominio.articolo; import javax.persistence.*; @Entity @Table(name = "articolo") @Inheritance(strategy = InheritanceType.JOINED) public abstract class Articolo { @Id @Column(name = "cod_articolo") protected String codice; @Column(name = "descrizione") protected String descrizione; @Column(name = "prezzo") protected double prezzo; protected Articolo() { } public Articolo(String codice, String descrizione, double prezzo) { this.codice = codice; this.descrizione = descrizione; this.prezzo = prezzo; } public String getCodice() { return codice; } private void setCodice(String codice) { this.codice = codice; } public String getDescrizione() { return descrizione; } public void setDescrizione(String descrizione) { this.descrizione = descrizione; } public double getPrezzo() { return prezzo; } public void setPrezzo(double prezzo) { this.prezzo = prezzo; } } 110 File Abbigliamento.java package dominio.abbigliamento; import java.util.*; import javax.persistence.*; import dominio.articolo.Articolo; import dominio.EccezioneCardMin; @Entity public class Abbigliamento extends Articolo { @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "materiali", joinColumns = @JoinColumn(name = "cod_abbigliamento")) @Column(name = "materiale") protected Set<String> materiali; protected Abbigliamento() { } public Abbigliamento(String codice, String descrizione, double prezzo) { super(codice, descrizione, prezzo); this.materiali = new HashSet(); } public Set<String> getMateriali() throws EccezioneCardMin { if(materiali.size() == 0) throw new EccezioneCardMin("Cardinalità minima violata"); else return materiali; } private void setMateriali(Set<String> materiali) { this.materiali = materiali; } public void inserisciMateriali(String materiale) { this.materiali.add(materiale); } public void eliminaMateriali(String materiale) { this.materiali.remove(materiale); } } File Calzature.java package dominio.calzature; import java.util.HashSet; import java.util.Set; import javax.persistence.*; import dominio.EccezioneCardMin; import dominio.articolo.Articolo; 111 import dominio.ScontoInfo; @Entity public class Calzature extends Articolo { @Column(name = "paese") protected String paese; @OneToMany(mappedBy = "calzature", fetch = FetchType.EAGER) protected Set<ScontoInfo> sconto; protected Calzature() { } public Calzature(String codice, String descrizione, double prezzo, String paese) { super(codice, descrizione, prezzo); this.paese = paese; this.sconto = new HashSet(); } public String getPaese() { return paese; } public void setPaese(String paese) { this.paese = paese; } public Set<ScontoInfo> getSconto() throws EccezioneCardMin { if(sconto.size() == 0) throw new EccezioneCardMin("Cardinalità minima violata"); else return this.sconto; } private void setSconto(Set<ScontoInfo> sconto) { this.sconto = sconto; } public void inserisciSconto(ScontoInfo scontoInfo) { sconto.add(scontoInfo); } } File ArticoloAcquistato.java package dominio; import javax.persistence.*; import dominio.articolo.Articolo; @Entity @Table(name = "articolo_acquistato") public class ArticoloAcquistato { 112 @Embeddable public static class Id implements java.io.Serializable { @Column(name = "id_acquisto") private Long idAcquisto; @Column(name = "cod_articolo") private String codiceArticolo; protected Id() { } protected Id(Long idAcquisto, String codiceArticolo) { this.idAcquisto = idAcquisto; this.codiceArticolo = codiceArticolo; } public boolean equals(Object o) { if (o != null && getClass().equals(this.getClass())) { Id id = (Id) o; return id.idAcquisto == idAcquisto && id.codiceArticolo == codiceArticolo; } else { return false; } } public int hashCode() { return idAcquisto.hashCode() + codiceArticolo.hashCode(); } } @EmbeddedId private Id id = new Id(); @ManyToOne @JoinColumn(name = "id_acquisto", insertable = false, updatable = false) private Acquisto acquisto; @ManyToOne @JoinColumn(name = "cod_articolo", insertable = false, updatable = false) private Articolo articolo; @Column(name = "quantita") private int quantita; protected ArticoloAcquistato() { } public ArticoloAcquistato(Acquisto acquisto, Articolo articolo, int quantita) throws EccezionePrecondizioni { if(acquisto == null || articolo == null) throw new EccezionePrecondizioni ("Gli oggetti devono essere inizializzati"); if(quantita <= 0) throw new EccezionePrecondizioni ("La quantità deve essere positiva"); this.acquisto = acquisto; this.articolo = articolo; this.quantita = quantita; 113 this.id.idAcquisto = acquisto.getId(); this.id.codiceArticolo = articolo.getCodice(); acquisto.inserisciArticolo(this); } private Id getId() { return this.id; } private void setId(Id id) { this.id = id; } public Acquisto getAcquisto() { return this.acquisto; } private void setAcquisto(Acquisto acquisto) { this.acquisto = acquisto; } public Articolo getArticolo() { return this.articolo; } private void setArticolo(Articolo articolo) { this.articolo = articolo; } public int getQuantita() { return this.quantita; } private void setQuantita(int quantita) { this.quantita = quantita; } } File ScontoInfo.java package dominio; import javax.persistence.*; import dominio.calzature.Calzature; import dominio.abbigliamento.Abbigliamento; @Entity @Table(name = "sconto_info") public class ScontoInfo { @Embeddable public static class Id implements java.io.Serializable { 114 @Column(name = private String @Column(name = private String "cod_calzature") codiceCalzature; "cod_abbigliamento") codiceAbbigliamento; protected Id() { } protected Id(String codiceCalzature, String codiceAbbigliamento) { this.codiceCalzature = codiceCalzature; this.codiceAbbigliamento = codiceAbbigliamento; } public boolean equals(Object o) { if (o != null && getClass().equals(this.getClass())) { Id id = (Id) o; return id.codiceCalzature == codiceCalzature && id.codiceAbbigliamento == codiceAbbigliamento; } else { return false; } } public int hashCode() { return codiceCalzature.hashCode() + codiceAbbigliamento.hashCode(); } } @EmbeddedId private Id id = new Id(); @ManyToOne @JoinColumn(name = "cod_calzature", insertable = false, updatable = false) private Calzature calzature; @ManyToOne @JoinColumn(name = "cod_abbigliamento", insertable = false, updatable = false) private Abbigliamento abbigliamento; @Column(name = "percentuale") private int percentuale; protected ScontoInfo() { } public ScontoInfo(Calzature calzature, Abbigliamento abbigliamento, int percentuale) throws EccezionePrecondizioni { if(calzature == null || abbigliamento == null) throw new EccezionePrecondizioni ("L'oggetto deve essere inizializzato"); if(percentuale < 5 || percentuale > 80) throw new EccezionePrecondizioni ("La percentuale di sconto deve essere compresa tra 5 e 80"); this.calzature = calzature; this.abbigliamento = abbigliamento; this.percentuale = percentuale; this.id.codiceCalzature = calzature.getCodice(); this.id.codiceAbbigliamento = abbigliamento.getCodice(); 115 calzature.inserisciSconto(this); } private Id getId() { return this.id; } private void setId(Id id) { this.id = id; } public Calzature getCalzature() { return this.calzature; } private void setCalzature(Calzature calzature) { this.calzature = calzature; } public Abbigliamento getAbbigliamento() { return this.abbigliamento; } private void setAbbigliamento(Abbigliamento abbigliamento) { this.abbigliamento = abbigliamento; } public int getPercentuale() { return this.percentuale; } private void setPercentuale(int percentuale) { this.percentuale = percentuale; } } 3.3 Eccezioni Per la gestione delle eccezioni generate dalle classi di dominio vengono utilizzate due classi che estendono Exception. EccezionePrecondizioni viene utilizzata per eccezioni riguardanti la violazione delle precondizioni, mentre EccezioneCardMin viene utilizzata per le eccezioni generate in caso di violazione dei vincoli di cardinalità minima. File EccezionePrecondizioni.java package dominio; public class EccezionePrecondizioni extends Exception { private String messaggio; public EccezionePrecondizioni(String m) { messaggio = m; } 116 public EccezionePrecondizioni() { messaggio = "Si e’ verificata una violazione delle precondizioni"; } public String toString() { return messaggio; } } File EccezioneCardMin.java package dominio; public class EccezioneCardMin extends Exception { private String messaggio; public EccezioneCardMin(String m) { messaggio = m; } public String toString() { return messaggio; } } 3.4 Persistenza Lo strato di persistenza viene configurato in fase di sviluppo e successivamente gestito dal software JPA. Il file di configurazione di JPA, persistence.xml, contiene una persistence unit chiamata testPU contenente i dati per la connessione ad una base di dati PostgreSQL. Il valore della proprietà hibernate.hbm2ddl.auto viene impostato sul valore create in fase di sviluppo per la creazione della base di dati e successivamente impostato su update per l’esecuzione dell’applicazione completa. File persistence.xml <?xml version="1.0" encoding="UTF-8" ?> <persistence 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" version="2.0"> <persistence-unit name="testPU"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <!-- Not needed, Hibernate supports auto-detection in JSE <class>dominio.Acquisto</class> <class>dominio.ArticoloAcquistato</class> <class>dominio.ScontoInfo</class> <class>dominio.abbigliamento.Abbigliamento</class> <class>dominio.articolo.Articolo</class> 117 <class>dominio.calzature.Calzature</class> --> <properties> <property name="hibernate.archive.autodetection" value="class, hbm"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.connection.driver_class" value="org.postgresql.Driver"/> <property name="hibernate.connection.url" value="jdbc:postgresql://localhost:5432/test"/> <property name="hibernate.connection.username" value="postgres"/> <property name="hibernate.connection.password" value="password"/> <property name="hibernate.c3p0.min_size" value="5"/> <property name="hibernate.c3p0.max_size" value="20"/> <property name="hibernate.c3p0.timeout" value="300"/> <property name="hibernate.c3p0.max_statements" value="50"/> <property name="hibernate.c3p0.idle_test_period" value="3000"/> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> </properties> </persistence-unit> </persistence> Per l’interazione con la base di dati viene utilizzata una classe JPAUtil, analoga alla classe HibernateUtil mostrata nel paragrafo 2.9. JPAUtil permette di utilizzare un’unica istanza di EntityManagerFactory per creare istanze di EntityManager nelle attività atomiche. File JPAUtil.java package persistence; import javax.persistence.*; public class JPAUtil { private static EntityManagerFactory entityManagerFactory = initJPAUtil(); private static EntityManagerFactory initJPAUtil(){ // Start EntityManagerFactory return Persistence.createEntityManagerFactory("testPU"); } public static EntityManagerFactory getEntityManagerFactory() { return entityManagerFactory; } 118 public static void shutdown() { //Close caches and connection pools getEntityManagerFactory().close(); } } Lo strato di persistenza è costituito da una base di dati PostgreSQL generata automaticamente dal software JPA utilizzando le annotazioni inserite all’interno delle classi di dominio. SQL output CREATE TABLE acquisto ( id_acquisto bigint NOT NULL, data date, CONSTRAINT acquisto_pkey PRIMARY KEY (id_acquisto ) ); CREATE TABLE articolo_acquistato ( cod_articolo character varying(255) NOT NULL, id_acquisto bigint NOT NULL, quantita integer, CONSTRAINT articolo_acquistato_pkey PRIMARY KEY (cod_articolo , id_acquisto ), CONSTRAINT fk1522eb4254099cc9 FOREIGN KEY (cod_articolo) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT fk1522eb42bbef361b FOREIGN KEY (id_acquisto) REFERENCES acquisto (id_acquisto) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE articolo ( cod_articolo character varying(255) NOT NULL, descrizione character varying(255), prezzo double precision, CONSTRAINT articolo_pkey PRIMARY KEY (cod_articolo ) ); CREATE TABLE abbigliamento ( cod_articolo character varying(255) NOT NULL, CONSTRAINT abbigliamento_pkey PRIMARY KEY (cod_articolo ), 119 CONSTRAINT fk8b9d58ec54099cc9 FOREIGN KEY (cod_articolo) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE calzature ( paese character varying(255), cod_articolo character varying(255) NOT NULL, CONSTRAINT calzature_pkey PRIMARY KEY (cod_articolo ), CONSTRAINT fk5118b14954099cc9 FOREIGN KEY (cod_articolo) REFERENCES articolo (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE materiali ( cod_abbigliamento character varying(255) NOT NULL, materiale character varying(255), CONSTRAINT fk28994022da5688ee FOREIGN KEY (cod_abbigliamento) REFERENCES abbigliamento (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE sconto_info ( cod_abbigliamento character varying(255) NOT NULL, cod_calzature character varying(255) NOT NULL, percentuale integer, CONSTRAINT sconto_info_pkey PRIMARY KEY (cod_abbigliamento , cod_calzature ), CONSTRAINT fk2b958e38a179445 FOREIGN KEY (cod_calzature) REFERENCES calzature (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT fk2b958e3da5688ee FOREIGN KEY (cod_abbigliamento) REFERENCES abbigliamento (cod_articolo) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); 3.5 Attività Le classi che realizzano le attività non presentano particolari costrutti. In alcuni casi le informazioni utili alle attività vengono scambiate con istanze di classi RecordAcquisto, RecordArticolo e RecordCalzature. File RecordAcquisto.java package attivita; import java.util.Date; 120 public class RecordAcquisto { private Long id; private Date data; public RecordAcquisto(Long id, Date data) { this.id = id; this.data = data; } public Long getId() { return this.id; } public Date getData() { return this.data; } } File RecordArticolo.java package attivita; public class RecordArticolo { private String codice; private String categoria; private int quantita; public RecordArticolo(String codice, String categoria, int quantita) { this.codice = codice; this.categoria = categoria; this.quantita = quantita; } public String getCodice() { return this.codice; } public String getCategoria() { return this.categoria; } public int getQuantita() { return this.quantita; } } File RecordCalzature.java package attivita; public class RecordCalzature { 121 private String codice; private int sconto; public RecordCalzature(String codice, int sconto) { this.codice = codice; this.sconto = sconto; } public String getCodice() { return this.codice; } public int getSconto() { return this.sconto; } } 122 3.5.1 AttivitàComplesse La prima attività complessa è AttivitàInserisciAcquisto che permette di inserire nuovi acquisti nella base di dati. Viene eseguita inizialmente SelezionaArticoli, che visualizza un’interfaccia grafica per selezionare gli articoli da inserire nell’acquisto. Una volta premuto il bottone Calcola importo totale, vengono letti gli articoli inseriti e viene creato il nuovo acquisto e inserito nella base di dati. Successivamente viene calcolato l’importo totale dell’acquisto e l’insieme delle calzature inserite con lo sconto applicato. Infine viene eseguito VisualizzaReportAcquisto che visualizza una finestra contenete l’ID dell’acquisto, gli articoli inseriti con lo sconto applicato e l’importo totale. Diagramma UML dell’attività AttivitaInserisciAcquisto: File AttivitaInserisciAcquisto.java package attivita.complesse; import dominio.Acquisto; import java.util.*; 123 import javax.swing.SwingUtilities; import javax.swing.JOptionPane; import java.lang.reflect.InvocationTargetException; import import import import import import import import _framework.*; attivita.atomiche.*; attivita.*; dominio.EccezioneCardMin; dominio.abbigliamento.Abbigliamento; dominio.calzature.Calzature; gui.SelezionaArticoli; gui.VisualizzaReportAcquisto; public class AttivitaInserisciAcquisto implements Runnable { private private private private private private private private boolean eseguita = false; List<Abbigliamento> abbigliamento; List<Calzature> calzature; SelezionaArticoli selezionaArticoli = null; Acquisto acquisto; VisualizzaReportAcquisto visualizzaReport = null; Set<RecordCalzature> calzatureAcquistate; double totaleAcquisto; public synchronized void run() { if (eseguita) { return; } eseguita = true; CaricaArticoli caricaArticoli = new CaricaArticoli(); Executor.perform(caricaArticoli); abbigliamento = caricaArticoli.getRisultatoAbbigliamento(); calzature = caricaArticoli.getRisultatoCalzature(); Runnable target1 = new Runnable() { @Override public void run() { selezionaArticoli = new SelezionaArticoli( abbigliamento, calzature); } }; try { SwingUtilities.invokeAndWait(target1); } catch (InterruptedException ex) { ex.printStackTrace(); } catch (InvocationTargetException ex) { ex.printStackTrace(); } selezionaArticoli.aspettaOK(); List<RecordArticolo> listaArticoli = selezionaArticoli.leggiArticoli(); if (!listaArticoli.isEmpty()) { 124 CreaNuovoAcquisto creaNuovoAcquisto = new CreaNuovoAcquisto(listaArticoli); Executor.perform(creaNuovoAcquisto); acquisto = creaNuovoAcquisto.getRisultato(); CalcolaImportoTotale calcolaTotale = new CalcolaImportoTotale(acquisto); Executor.perform(calcolaTotale); totaleAcquisto = calcolaTotale.getRisultato(); CalcolaSconto calcolaSconto = new CalcolaSconto(acquisto); Executor.perform(calcolaSconto); calzatureAcquistate = calcolaSconto.getRisultato(); Runnable target2 = new Runnable() { @Override public void run() { visualizzaReport = new VisualizzaReportAcquisto( acquisto.getId(), selezionaArticoli.leggiTabella(), calzatureAcquistate, totaleAcquisto); } }; SwingUtilities.invokeLater(target2); } else { JOptionPane.showMessageDialog( null, "ERRORE: E' necessario inserire " + "almeno un articolo per creare un nuovo acquisto"); } } public synchronized boolean estEseguita() { return eseguita; } } La seconda attività complessa è AttivitaVisualizzaAcquisti. Questa attività esegue l’attivita atomica CaricaAcquisti che restituisce una lista di RecordArticolo che vengono utilizzati successivamente per costruire la tabella contenuta nell’interfaccia grafica VisualizzaAcquisti. 125 Diagramma UML dell’attività AttivitaVisualizzaAcquisti: File AttivitaVisualizzaAcquisti.java package attivita.complesse; import java.util.*; import import import import _framework.*; attivita.*; attivita.atomiche.*; gui.VisualizzaAcquisti; public class AttivitaVisualizzaAcquisti implements Runnable { private boolean eseguita = false; private List<RecordAcquisto> acquisti; public synchronized void run() { if (eseguita) return; eseguita = true; CaricaAcquisti caricaAcquisti = new CaricaAcquisti(); Executor.perform(caricaAcquisti); acquisti = caricaAcquisti.getRisultato(); VisualizzaAcquisti finestra = new VisualizzaAcquisti(acquisti); } public synchronized boolean estEseguita() { return eseguita; } } 126 L’attività AttivitaEffettuaControlli è composta da una prima parte simile ad AttivitaVisualizzaAcquisti, vengono caricate dalla base di dati le informazioni di tutti gli acquisti per costruire una tabella visualizzata da SelezionaAcquisto. Dopo aver selezionato un Acquisto dalla tabella e premuto il bottone Inizia controlli, viene eseguita inizialmente l’attività atomica VerificaImporto che controlla che l’importo totale dell’acquisto sia corretto. Successivamente se VerificaImporto ha avuto esito positivo, vengono eseguite in concorrenza le attività SottoramoAbbigliamento e SottoramoCalzature per calcolare l’insieme di materiali utilizzati per i capi di abbigliamento e le calzature per le quali è stato effettuato uno sconto. Diagramma UML dell’attività AttivitaEffettuaControlli: 127 File AttivitaEffettuaControlli.java package attivita.complesse; import import import import java.util.*; javax.swing.SwingUtilities; javax.swing.JOptionPane; java.lang.reflect.InvocationTargetException; import import import import import import _framework.Executor; attivita.*; attivita.atomiche.*; dominio.Acquisto; gui.SelezionaAcquisto; gui.VisualizzaRisultatoControllo; public class AttivitaEffettuaControlli implements Runnable { private private private private private boolean eseguita = false; List<RecordAcquisto> acquisti; Object[] calzatureScontate; Object[] materiali; SelezionaAcquisto selezionaAcquisto = null; public synchronized void run() { if (eseguita) { return; } eseguita = true; CaricaAcquisti caricaAcquisti = new CaricaAcquisti(); Executor.perform(caricaAcquisti); acquisti = caricaAcquisti.getRisultato(); Runnable target1 = new Runnable() { @Override public void run() { selezionaAcquisto = new SelezionaAcquisto(acquisti); } }; try { SwingUtilities.invokeAndWait(target1); } catch (InterruptedException ex) { ex.printStackTrace(); } catch (InvocationTargetException ex) { ex.printStackTrace(); } selezionaAcquisto.aspettaOK(); Long idAcquisto = selezionaAcquisto.leggiIdAcquisto(); if (idAcquisto != null) { CaricaAcquisto caricaAcquisto = new CaricaAcquisto(idAcquisto.longValue()); Executor.perform(caricaAcquisto); 128 Acquisto acquisto = caricaAcquisto.getRisultato(); VerificaImporto verificaImporto = new VerificaImporto(acquisto); Executor.perform(verificaImporto); boolean importoCorretto = verificaImporto.getRisultato(); if (importoCorretto == true) { SottoramoCalzature sottoramoCalzature = new SottoramoCalzature(acquisto); SottoramoAbbigliamento sottoramoAbbigliamento = new SottoramoAbbigliamento(acquisto); Thread t1 = new Thread(sottoramoCalzature); Thread t2 = new Thread(sottoramoAbbigliamento); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException ex) { ex.printStackTrace(); } calzatureScontate = sottoramoCalzature.getRisultato(); materiali = sottoramoAbbigliamento.getRisultato(); VisualizzaRisultatoControllo visualizzarisultato = new VisualizzaRisultatoControllo( calzatureScontate, materiali); } else { JOptionPane.showMessageDialog( null, "ERRORE: Verifica dell'importo " + "totale non superata"); } } else { JOptionPane.showMessageDialog( null, "ERRORE: E' necessario selezionare " + "un acquisto per effettuare i controlli"); } } public synchronized boolean estEseguita() { return eseguita; } } 129 File SottoramoAbbigliamento.java package attivita.complesse; import dominio.Acquisto; import javax.persistence.*; import java.util.*; import import import import _framework.*; attivita.atomiche.RestituisciMaterialiAbbigliamento; dominio.abbigliamento.Abbigliamento; persistence.JPAUtil; public class SottoramoAbbigliamento implements Runnable { private boolean eseguita = false; private Acquisto acquisto; private Object[] materiali; public SottoramoAbbigliamento(Acquisto acquisto) { this.acquisto = acquisto; } public synchronized void run() { if (eseguita) { return; } eseguita = true; RestituisciMaterialiAbbigliamento restituisciMateriali = new RestituisciMaterialiAbbigliamento(acquisto); Executor.perform(restituisciMateriali); materiali = restituisciMateriali.getRisultato(); } public synchronized boolean estEseguita() { return eseguita; } public Object[] getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return materiali; } } File SottoramoCalzature.java package attivita.complesse; import dominio.Acquisto; import javax.persistence.*; 130 import java.util.*; import import import import _framework.*; attivita.RecordCalzature; attivita.atomiche.RestituisciCalzatureScontate; persistence.JPAUtil; public class SottoramoCalzature implements Runnable { private boolean eseguita = false; private Acquisto acquisto; private Object[] calzatureScontate; public SottoramoCalzature(Acquisto acquisto) { this.acquisto = acquisto; } public synchronized void run() { if (eseguita) { return; } eseguita = true; RestituisciCalzatureScontate restituisciCalzature = new RestituisciCalzatureScontate(acquisto); Executor.perform(restituisciCalzature); calzatureScontate = restituisciCalzature.getRisultato(); } public synchronized boolean estEseguita() { return eseguita; } public Object[] getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return calzatureScontate; } } 3.5.2 Attività atomiche Per la realizzazione delle attività atomiche viene utilizzato il pattern funtore. Le attività atomiche implementano l’interfaccia Task e i task vengono eseguiti dalla classe Executor. 131 File Task.java package _framework; public interface Task { public void esegui(Executor e); } File Executor.java package _framework; public class Executor { private Executor() { } public synchronized static void perform(Task t) { t.esegui(new Executor()); } } Le attività atomiche che interagiscono con la base di dati ricevono l’istanza di EntityManagerFactory gestita da JPAUtil per creare una istanza di EntityManager e successivamente iniziare transazioni per la comunicazione. Le altre attività atomiche non presentano soluzioni particolari. File CaricaArticoli.java package attivita.atomiche; import import import import dominio.calzature.Calzature; dominio.abbigliamento.Abbigliamento; java.util.*; javax.persistence.*; import _framework.*; import persistence.JPAUtil; import attivita.*; public class CaricaArticoli implements Task { private boolean eseguita = false; private LinkedList<Abbigliamento> listaAbbigliamento = new LinkedList<Abbigliamento>(); private LinkedList<Calzature> listaCalzature = new LinkedList<Calzature>(); public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; 132 } eseguita = true; EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); List abbigliamento = em.createNativeQuery( "SELECT * " + "FROM articolo, abbigliamento " + "WHERE articolo.cod_articolo = abbigliamento.cod_articolo;", Abbigliamento.class).getResultList(); List calzature = em.createNativeQuery( "SELECT * " + "FROM articolo, calzature " + "WHERE articolo.cod_articolo = calzature.cod_articolo;", Calzature.class).getResultList(); tx.commit(); em.close(); Iterator it1 = abbigliamento.iterator(); while (it1.hasNext()) { Abbigliamento a = (Abbigliamento) it1.next(); listaAbbigliamento.add(a); } Iterator it2 = calzature.iterator(); while (it2.hasNext()) { Calzature c = (Calzature) it2.next(); listaCalzature.add(c); } } public synchronized boolean estEseguita() { return eseguita; } public List<Abbigliamento> getRisultatoAbbigliamento() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return (List<Abbigliamento>) listaAbbigliamento.clone(); } public List<Calzature> getRisultatoCalzature() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return (List<Calzature>) listaCalzature.clone(); } } 133 File CreaNuovoAcquisto.java package attivita.atomiche; import import import import dominio.Acquisto; dominio.EccezionePrecondizioni; dominio.ArticoloAcquistato; java.util.*; import import import import import import import _framework.*; javax.persistence.*; persistence.JPAUtil; attivita.RecordArticolo; dominio.abbigliamento.Abbigliamento; dominio.calzature.Calzature; javax.swing.JOptionPane; public class CreaNuovoAcquisto implements Task { private boolean eseguita = false; private List<RecordArticolo> listaArticoli; private Acquisto nuovoAcquisto; public CreaNuovoAcquisto(List<RecordArticolo> listaArticoli) { this.listaArticoli = listaArticoli; } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); nuovoAcquisto = new Acquisto(); em.persist(nuovoAcquisto); Iterator<RecordArticolo> it = listaArticoli.iterator(); while(it.hasNext()) { RecordArticolo ra = (RecordArticolo) it.next(); String codice = ra.getCodice(); String categoria = ra.getCategoria(); int quantita = ra.getQuantita(); if(categoria.equals("Abbigliamento")) { Abbigliamento abbigliamento = (Abbigliamento) em.getReference(Abbigliamento.class, codice); ArticoloAcquistato articoloAcquistato = null; try { articoloAcquistato = new ArticoloAcquistato( nuovoAcquisto, abbigliamento, quantita); } catch(EccezionePrecondizioni ex) { 134 JOptionPane.showMessageDialog(null, ex.getMessage()); } if(articoloAcquistato != null) { em.persist(articoloAcquistato); } } else if(categoria.equals("Calzature")) { Calzature calzature = (Calzature) em.getReference(Calzature.class, codice); ArticoloAcquistato articoloAcquistato = null; try { articoloAcquistato = new ArticoloAcquistato( nuovoAcquisto, calzature, quantita); } catch(EccezionePrecondizioni ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } if(articoloAcquistato != null) { em.persist(articoloAcquistato); } } } tx.commit(); em.close(); } public synchronized boolean estEseguita() { return eseguita; } public Acquisto getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return this.nuovoAcquisto; } } File CalcolaImportoTotale.java package attivita.atomiche; import java.util.*; import javax.swing.JOptionPane; import _framework.*; import dominio.Acquisto; import dominio.EccezioneCardMin; public class CalcolaImportoTotale implements Task { private boolean eseguita = false; private Acquisto acquisto; private double totale; 135 public CalcolaImportoTotale(Acquisto acquisto) { this.acquisto = acquisto; } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; try { totale = acquisto.totale(); } catch (EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } } public synchronized boolean estEseguita() { return eseguita; } public double getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return totale; } } File CalcolaSconto.java package attivita.atomiche; import import import import import import dominio.Acquisto; dominio.EccezioneCardMin; dominio.ArticoloAcquistato; java.util.*; javax.persistence.*; javax.swing.JOptionPane; import import import import _framework.*; attivita.RecordCalzature; dominio.calzature.Calzature; persistence.JPAUtil; public class CalcolaSconto implements Task { private boolean eseguita = false; private Acquisto acquisto; private HashSet<RecordCalzature> insiemeCalzature; public CalcolaSconto(Acquisto acquisto) { this.acquisto = acquisto; 136 this.insiemeCalzature = new HashSet<RecordCalzature>(); } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; long idAcquisto = acquisto.getId(); Set<ArticoloAcquistato> articoli = null; EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { articoli = (Set<ArticoloAcquistato>) acquisto.getComprende(); } catch(EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } Iterator<ArticoloAcquistato> it = articoli.iterator(); while(it.hasNext()) { ArticoloAcquistato aa = (ArticoloAcquistato) it.next(); if(aa.getArticolo() instanceof Calzature) { Calzature c = (Calzature) aa.getArticolo(); String codice = c.getCodice(); int sconto = 0; try { sconto = acquisto.calcolaSconto(c); } catch(EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } RecordCalzature rc = new RecordCalzature(codice, sconto); insiemeCalzature.add(rc); } } tx.commit(); em.close(); } public synchronized boolean estEseguita() { return eseguita; } public Set<RecordCalzature> getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return (Set<RecordCalzature>) this.insiemeCalzature.clone(); 137 } } File CaricaAcquisti.java package attivita.atomiche; import java.util.*; import javax.persistence.*; import import import import _framework.*; persistence.JPAUtil; attivita.RecordAcquisto; dominio.Acquisto; public class CaricaAcquisti implements Task { private boolean eseguita = false; private LinkedList<RecordAcquisto> listaAcquisti = new LinkedList<RecordAcquisto>(); public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); List acquisti = em.createNativeQuery( "SELECT id_acquisto, data " + "FROM acquisto " + "ORDER BY id_acquisto;", Acquisto.class).getResultList(); tx.commit(); em.close(); Iterator it = acquisti.iterator(); while (it.hasNext()) { Acquisto ac = (Acquisto) it.next(); Long id = ac.getId(); Date data = ac.getData(); RecordAcquisto ra = new RecordAcquisto(id, data); listaAcquisti.add(ra); } } public synchronized boolean estEseguita() { return eseguita; 138 } public List<RecordAcquisto> getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return (List<RecordAcquisto>) listaAcquisti.clone(); } } File CaricaAcquisto.java package attivita.atomiche; import javax.persistence.*; import _framework.*; import dominio.Acquisto; import persistence.JPAUtil; public class CaricaAcquisto implements Task { public boolean eseguita; private long idAcquisto; private Acquisto acquisto; public CaricaAcquisto(long idAcquisto) { this.idAcquisto = idAcquisto; } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); acquisto = em.find(Acquisto.class, idAcquisto); tx.commit(); em.close(); } public synchronized boolean estEseguita() { return eseguita; } public Acquisto getRisultato() { if (!eseguita) { 139 throw new RuntimeException("Risultato non pronto!"); } return this.acquisto; } } File VerificaImporto.java package attivita.atomiche; import import import import import import dominio.Acquisto; dominio.EccezioneCardMin; dominio.ArticoloAcquistato; java.util.*; javax.persistence.*; javax.swing.JOptionPane; import _framework.*; import persistence.JPAUtil; public class VerificaImporto implements Task { public boolean eseguita; private Acquisto acquisto; private boolean importoCorretto; public VerificaImporto(Acquisto acquisto) { this.acquisto = acquisto; } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; Set<ArticoloAcquistato> articoli = null; double totale = 0.0; EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { articoli = (Set<ArticoloAcquistato>) acquisto.getComprende(); } catch(EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } try { totale = acquisto.totale(); 140 } catch (EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } double somma = 0.0; Iterator<ArticoloAcquistato> it = articoli.iterator(); while(it.hasNext()) { ArticoloAcquistato aa = (ArticoloAcquistato) it.next(); double prezzo = aa.getArticolo().getPrezzo(); somma += prezzo*aa.getQuantita(); } tx.commit(); em.close(); if(somma >= totale) { importoCorretto = true; } else importoCorretto = false; } public synchronized boolean estEseguita() { return eseguita; } public boolean getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return importoCorretto; } } File RestituisciCalzatureScontate.java package attivita.atomiche; import import import import import import dominio.Acquisto; dominio.EccezioneCardMin; dominio.ArticoloAcquistato; java.util.*; javax.persistence.*; javax.swing.JOptionPane; import _framework.*; import dominio.calzature.Calzature; import persistence.JPAUtil; public class RestituisciCalzatureScontate implements Task { private boolean eseguita = false; private Acquisto acquisto; private Object[] calzatureScontate; 141 public RestituisciCalzatureScontate(Acquisto acquisto) { this.acquisto = acquisto; } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; Set<ArticoloAcquistato> articoli = null; HashSet<String> appoggio = new HashSet<String>(); EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { articoli = (Set<ArticoloAcquistato>) acquisto.getComprende(); } catch (EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } Iterator<ArticoloAcquistato> it = articoli.iterator(); while (it.hasNext()) { ArticoloAcquistato aa = (ArticoloAcquistato) it.next(); if (aa.getArticolo() instanceof Calzature) { Calzature c = (Calzature) aa.getArticolo(); int sconto = 0; try { sconto = acquisto.calcolaSconto(c); } catch (EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } if (sconto > 0) { appoggio.add(c.getCodice()); } } } tx.commit(); em.close(); calzatureScontate = appoggio.toArray(); } public synchronized boolean estEseguita() { return eseguita; } public Object[] getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } 142 return calzatureScontate; } } File RestituisciMaterialiAbbigliamento.java package attivita.atomiche; import import import import import import dominio.Acquisto; dominio.EccezioneCardMin; dominio.ArticoloAcquistato; java.util.*; javax.swing.JOptionPane; javax.persistence.*; import _framework.*; import dominio.abbigliamento.Abbigliamento; import persistence.JPAUtil; public class RestituisciMaterialiAbbigliamento implements Task { private boolean eseguita = false; private Acquisto acquisto; private Object[] materiali; public RestituisciMaterialiAbbigliamento(Acquisto acquisto) { this.acquisto = acquisto; } public synchronized void esegui(Executor e) { if (eseguita || e == null) { return; } eseguita = true; Set<ArticoloAcquistato> articoli = null; HashSet<String> appoggio = new HashSet<String>(); EntityManagerFactory emf = JPAUtil.getEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { articoli = (Set<ArticoloAcquistato>) acquisto.getComprende(); } catch(EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } Iterator<ArticoloAcquistato> it1 = articoli.iterator(); while(it1.hasNext()) { ArticoloAcquistato aa = it1.next(); 143 if(aa.getArticolo() instanceof Abbigliamento) { Abbigliamento a = (Abbigliamento) aa.getArticolo(); Set<String> mat = null; try { mat = a.getMateriali(); } catch(EccezioneCardMin ex) { JOptionPane.showMessageDialog(null, ex.getMessage()); } Iterator<String> it2 = mat.iterator(); while(it2.hasNext()) { String materiale = it2.next(); appoggio.add(materiale); } } } tx.commit(); em.close(); materiali = appoggio.toArray(); } public synchronized boolean estEseguita() { return eseguita; } public Object[] getRisultato() { if (!eseguita) { throw new RuntimeException("Risultato non pronto!"); } return materiali; } } 3.6 Interfaccia grafica L’interfaccia grafica dell’applicazione è costruita utilizzando il framework Java Swing. FinestraPrincipale viene eseguita all’avvio dell’applicazione e visualizza una finestra con il menù principale composto da tre bottoni che permettono di selezionare l’attività da eseguire. Interaccia grafica FinestraPrincipale: 144 File FinestraPrincipale.java package gui; import java.awt.GridLayout; import javax.swing.*; import java.util.*; import attivita.*; public class FinestraPrincipale extends JFrame { private private private private final final final final JPanel jPanel1; JButton jButton1 = new JButton("Inserisci nuovo acquisto"); JButton jButton2 = new JButton("Visualizza acquisti"); JButton jButton3 = new JButton("Effettua controlli"); public FinestraPrincipale() { super("Test JPA - Menù Principale"); jPanel1 = new JPanel(new GridLayout(3, 1)); jPanel1.add(jButton1); jPanel1.add(jButton2); jPanel1.add(jButton3); FinestraPrincipaleListener listener = new FinestraPrincipaleListener(); jButton1.addActionListener(listener); jButton1.setActionCommand(listener.jButton1); jButton2.addActionListener(listener); jButton2.setActionCommand(listener.jButton2); jButton3.addActionListener(listener); jButton3.setActionCommand(listener.jButton3); this.getContentPane().add(jPanel1); this.setSize(350, 150); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); this.setVisible(true); } } File FinestraPrincipaleListener.java package gui; import java.awt.event.*; import javax.swing.*; import attivita.complesse.*; public class FinestraPrincipaleListener implements ActionListener { final static String jButton1 = "Inserisci nuovo acquisto"; 145 final static String jButton2 = "Visualizza acquisti"; final static String jButton3 = "Effettua controlli"; public void actionPerformed(ActionEvent ev) { String com = ev.getActionCommand(); if(com == jButton1) { inserisciAcquisto(); } else if(com == jButton2) { visualizzaAcquisti(); } else if(com == jButton3) { effettuaControlli(); } } private void visualizzaAcquisti() { AttivitaVisualizzaAcquisti visualizza = new AttivitaVisualizzaAcquisti(); Thread t = new Thread(visualizza); t.start(); } private void inserisciAcquisto() { AttivitaInserisciAcquisto inserisci = new AttivitaInserisciAcquisto(); Thread t = new Thread(inserisci); t.start(); } private void effettuaControlli() { AttivitaEffettuaControlli controlli = new AttivitaEffettuaControlli(); Thread t = new Thread(controlli); t.start(); } } La finestra SelezionaArticoli viene utilizzata nell’attività AttivitaInserisciAcquisto per selezionare gli articoli da inserire nel nuovo acquisto. I dati vengono visualizzati utilizzando componenti JTable. E’ presente un metodo leggiArticoli() che resituisce una lista di RecordArticolo costruita dalla tabella degli articoli inseriti, da utilizzare per la creazione di un nuovo acquisto e un metodo aspettaOK() che mette l’attività AttivitaInserisciAcquisto in attesa che l’utente prema il bottone Calcola importo totale. Dopo la pressione del bottone il listener eseguirà il metodo notify() per permettere all’attività di continuare la sua esecuzione e leggere la lista di RecordArticolo. La classe listener si occupa di inserire i dati nella tabella degli articoli inseriti. 146 Interaccia grafica SelezionaArticoli: File SelezionaArticoli.java package gui; import import import import import import dominio.calzature.Calzature; dominio.abbigliamento.Abbigliamento; java.awt.*; java.util.*; javax.swing.*; attivita.*; public class SelezionaArticoli extends JFrame { private final JButton jButton1 private final JButton jButton2 private final JButton jButton3 private final JButton jButton4 private final JLabel jLabel1 = private final JLabel jLabel2 = JTable jTable1 = new JTable(); JTable jTable2 = new JTable(); JTable jTable3 = new JTable(); private final JPanel jPanel1; = new JButton("Inserisci"); = new JButton("Inserisci"); = new JButton("Rimuovi"); = new JButton("Calcola importo totale"); new JLabel("Lista articoli"); new JLabel("Articoli inseriti"); 147 private private private private private private private private private private private private private private final final final final final final final final final final final final final final JPanel jPanel2; JPanel jPanel3; JPanel jPanel4; JPanel jPanel5; JPanel jPanel6; JPanel jPanel7; JPanel jPanel8; JPanel jPanel9; JPanel jPanel10; JPanel jPanel11; JPanel jPanel12; JScrollPane jScrollPane1; JScrollPane jScrollPane2; JScrollPane jScrollPane3; public SelezionaArticoli(java.util.List<Abbigliamento> listaAbbigliamento, java.util.List<Calzature> listaCalzature) { super("Test JPA - Inserisci nuovo acquisto"); jPanel1 = new JPanel(new GridLayout(1, 2, 10, 10)); jPanel1.setBorder( BorderFactory.createTitledBorder("Inserisci acquisto")); jPanel2 = new JPanel(); jPanel2.setLayout(new BoxLayout(jPanel2, BoxLayout.Y_AXIS)); jPanel2.setBorder(BorderFactory.createLineBorder(Color.black)); jPanel3 = new JPanel(); jPanel3.setLayout(new BoxLayout(jPanel3, BoxLayout.Y_AXIS)); jPanel4 = new JPanel(); jPanel4.setLayout(new BoxLayout(jPanel4, BoxLayout.Y_AXIS)); jPanel4.setBorder( BorderFactory.createTitledBorder("Abbigliamento")); jPanel5 = new JPanel(); jPanel5.setLayout(new BoxLayout(jPanel5, BoxLayout.Y_AXIS)); jPanel5.setBorder( BorderFactory.createTitledBorder("Calzature")); jPanel6 = new JPanel(); jPanel6.setLayout(new BoxLayout(jPanel6, BoxLayout.Y_AXIS)); jPanel6.setBorder(BorderFactory.createLineBorder(Color.black)); jPanel7 = new JPanel(new FlowLayout()); jPanel8 = new JPanel(new FlowLayout()); jPanel9 = new JPanel(new FlowLayout()); jPanel10 = new JPanel(new FlowLayout()); jPanel11 = new JPanel(new FlowLayout()); jPanel12 = new JPanel(new FlowLayout()); Object[][] tabellaAbbigliamento = new Object[listaAbbigliamento.size()][3]; Iterator<Abbigliamento> it1 = listaAbbigliamento.iterator(); int i = 0; while (it1.hasNext()) { Abbigliamento a = (Abbigliamento) it1.next(); tabellaAbbigliamento[i][0] = a.getCodice(); tabellaAbbigliamento[i][1] = a.getDescrizione(); tabellaAbbigliamento[i][2] = (Double) a.getPrezzo(); i = i + 1; } 148 jTable1.setModel(new javax.swing.table.DefaultTableModel( tabellaAbbigliamento, new String[]{ "Codice articolo", "Descrizione", "Prezzo" }) { Class[] types = new Class[]{ java.lang.String.class, java.lang.String.class, java.lang.Double.class }; boolean[] canEdit = new boolean[]{ false, false, false }; public Class getColumnClass(int columnIndex) { return types[columnIndex]; } public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit[columnIndex]; } }); Object[][] tabellaCalzature = new Object[listaCalzature.size()][3]; Iterator<Calzature> it2 = listaCalzature.iterator(); int j = 0; while (it2.hasNext()) { Calzature c = (Calzature) it2.next(); tabellaCalzature[j][0] = c.getCodice(); tabellaCalzature[j][1] = c.getDescrizione(); tabellaCalzature[j][2] = (Double) c.getPrezzo(); j = j + 1; } jTable2.setModel(new javax.swing.table.DefaultTableModel( tabellaCalzature, new String[]{ "Codice articolo", "Descrizione", "Prezzo" }) { Class[] types = new Class[]{ java.lang.String.class, java.lang.String.class, java.lang.Double.class }; boolean[] canEdit = new boolean[]{ false, false, false }; public Class getColumnClass(int columnIndex) { return types[columnIndex]; } public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit[columnIndex]; } }); 149 jTable3.setModel(new javax.swing.table.DefaultTableModel( new Object[][]{}, new String[]{ "Codice articolo", "Categoria", "Descrizione", "Prezzo", "Quantità" }) { Class[] types = new Class[]{ java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.Double.class, java.lang.Integer.class }; boolean[] canEdit = new boolean[]{ false, false, false, false, true, false }; public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit[columnIndex]; } }); jScrollPane1 = new JScrollPane(jTable1); jTable1.setFillsViewportHeight(true); jScrollPane2 = new JScrollPane(jTable2); jTable2.setFillsViewportHeight(true); jScrollPane3 = new JScrollPane(jTable3); jTable3.setFillsViewportHeight(true); jPanel12.add(jLabel2); jPanel11.add(jLabel1); jPanel10.add(jButton4); jPanel9.add(jButton3); jPanel8.add(jButton2); jPanel7.add(jButton1); jPanel6.add(jPanel12); jPanel6.add(jScrollPane3); jPanel6.add(jPanel9); jPanel4.add(jScrollPane1); jPanel4.add(jPanel7); jPanel5.add(jScrollPane2); jPanel5.add(jPanel8); jPanel3.add(jPanel6); jPanel3.add(jPanel10); jPanel2.add(jPanel11); jPanel2.add(jPanel4); jPanel2.add(jPanel5); jPanel1.add(jPanel2); jPanel1.add(jPanel3); SelezionaArticoliListener listener = new SelezionaArticoliListener(this); jButton1.addActionListener(listener); 150 jButton1.setActionCommand(listener.jButton1); jButton2.addActionListener(listener); jButton2.setActionCommand(listener.jButton2); jButton3.addActionListener(listener); jButton3.setActionCommand(listener.jButton3); jButton4.addActionListener(listener); jButton4.setActionCommand(listener.jButton4); this.getContentPane().add(jPanel1); this.setSize(1000, 700); this.setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); this.setVisible(true); } public java.util.List<RecordArticolo> leggiArticoli() { LinkedList<RecordArticolo> listaArticoli = new LinkedList<RecordArticolo>(); for (int i=0; i<jTable3.getRowCount(); i++) { String codice = (String) jTable3.getValueAt(i, 0); String categoria = (String) jTable3.getValueAt(i, 1); Integer quantita = (Integer) jTable3.getValueAt(i, 4); RecordArticolo ra = new RecordArticolo(codice, categoria, quantita); listaArticoli.add(ra); } return listaArticoli; } public javax.swing.JTable leggiTabella() { return this.jTable3; } public void aspettaOK() { synchronized (getContentPane()) { try { getContentPane().wait(); } catch (InterruptedException e) { e.printStackTrace(); System.exit(1); } } } } File SelezionaArticoliListener.java package gui; import java.awt.event.*; import java.lang.reflect.InvocationTargetException; import javax.swing.*; public class SelezionaArticoliListener implements ActionListener { 151 private SelezionaArticoli frame; final static String jButton1 = "Inserisci1"; final static String jButton2 = "Inserisci2"; final static String jButton3 = "Rimuovi"; final static String jButton4 = "Calcola totale"; public SelezionaArticoliListener(SelezionaArticoli frame) { this.frame = frame; } public void actionPerformed(ActionEvent ev) { String com = ev.getActionCommand(); if (com == jButton1) { inserisciAbbigliamento(); } else if (com == jButton2) { inserisciCalzature(); } else if (com == jButton3) { rimuovi(); } else if (com == jButton4) { calcolaTotale(); } } public void inserisciAbbigliamento() { int[] righeSelezionate = frame.jTable1.getSelectedRows(); javax.swing.table.DefaultTableModel model = (javax.swing.table.DefaultTableModel) frame.jTable3.getModel(); for (int i=0; i<righeSelezionate.length; i++) { boolean inserito = false; for(int j=0; j<frame.jTable3.getRowCount(); j++) { if(frame.jTable3.getValueAt(j, 0) == frame.jTable1.getValueAt(righeSelezionate[i], 0)) { Integer quantita = (Integer) frame.jTable3.getValueAt(j, 4); int n = quantita.intValue() + 1; frame.jTable3.setValueAt(new Integer(n), j, 4); inserito = true; } } if(inserito == false) { String codice = (String) frame.jTable1.getValueAt( righeSelezionate[i], 0); String categoria = "Abbigliamento"; String descrizione = (String) frame.jTable1.getValueAt( righeSelezionate[i], 1); Double prezzo = (Double) frame.jTable1.getValueAt( righeSelezionate[i], 2); model.addRow(new Object[]{ codice, categoria, descrizione, prezzo, 1}); } } } public void inserisciCalzature() { int[] righeSelezionate = frame.jTable2.getSelectedRows(); javax.swing.table.DefaultTableModel model = (javax.swing.table.DefaultTableModel) frame.jTable3.getModel(); for (int i=0; i<righeSelezionate.length; i++) { 152 boolean inserito = false; for(int j=0; j<frame.jTable3.getRowCount(); j++) { if(frame.jTable3.getValueAt(j, 0) == frame.jTable2.getValueAt(righeSelezionate[i], 0)) { Integer quantita = (Integer) frame.jTable3.getValueAt(j, 4); int n = quantita.intValue() + 1; frame.jTable3.setValueAt(new Integer(n), j, 4); inserito = true; } } if(inserito == false) { String codice = (String) frame.jTable2.getValueAt( righeSelezionate[i], 0); String categoria = "Calzature"; String descrizione = (String) frame.jTable2.getValueAt( righeSelezionate[i], 1); Double prezzo = (Double) frame.jTable2.getValueAt( righeSelezionate[i], 2); model.addRow(new Object[]{ codice, categoria, descrizione, prezzo, 1}); } } } public void rimuovi() { int[] righeSelezionate = frame.jTable3.getSelectedRows(); if(righeSelezionate.length==0) return; javax.swing.table.DefaultTableModel model = (javax.swing.table.DefaultTableModel) frame.jTable3.getModel(); for (int i=righeSelezionate.length-1; i>=0; i--) { model.removeRow(righeSelezionate[i]); } } public void calcolaTotale() { synchronized (frame.getContentPane()) { frame.getContentPane().notify(); frame.dispose(); } } } 153 VisualizzaReportAcquisto è la finestra che visualizza i dati dell’acquisto inserito nella base di dati. Vengono visualizzati l’ID dell’acquisto, una tabella contenente gli articoli inseriti e l’importo totale. Interaccia grafica VisualizzaReportArticolo: File VisualizzaReportAcquisto.java package gui; import import import import import javax.swing.*; javax.swing.table.TableColumn; java.awt.FlowLayout; java.awt.GridLayout; java.util.*; import attivita.RecordArticolo; import attivita.RecordCalzature; import gui.VisualizzaReportAcquistoListener; public class VisualizzaReportAcquisto extends JFrame { private final JLabel jLabel1 = new JLabel("Acquisto inserito"); private final JLabel jLabel2; private final JLabel jLabel3 = new JLabel("Articoli inseriti"); 154 private private private private private private private private private private private private final final final final final final final final final final final final JLabel jLabel4 = new JLabel("Totale: "); JTextField jTextField1; JPanel jPanel1; JPanel jPanel2; JPanel jPanel3; JPanel jPanel4; JPanel jPanel5; JPanel jPanel6; JPanel jPanel7; JScrollPane jScrollPane1; JButton jButton1 = new JButton("Torna al menù principale"); JTable jTable1; public VisualizzaReportAcquisto(long idAcquisto, JTable tabellaArticoli, Set<RecordCalzature> calzatureAcquistate, double totale) { super("Test JPA - Inserisci nuovo acquisto"); jPanel1 = new JPanel(); jPanel1.setLayout(new BoxLayout(jPanel1, BoxLayout.Y_AXIS)); jPanel1.setBorder( BorderFactory.createTitledBorder("Visualizza totale")); jPanel2 = new JPanel(new FlowLayout()); jPanel3 = new JPanel(new FlowLayout()); jPanel4 = new JPanel(new FlowLayout()); jPanel5 = new JPanel(new FlowLayout()); jPanel6 = new JPanel(new FlowLayout()); jPanel7 = new JPanel(new FlowLayout()); jLabel2 = new JLabel("ID Acquisto: " + idAcquisto); jTable1 = tabellaArticoli; javax.swing.table.DefaultTableModel model = (javax.swing.table.DefaultTableModel) jTable1.getModel(); Object[] colonnaSconto = new Object[tabellaArticoli.getRowCount()]; model.addColumn("Sconto", colonnaSconto); model.setColumnCount(6); Iterator<RecordCalzature> it = calzatureAcquistate.iterator(); while (it.hasNext()) { RecordCalzature rc = (RecordCalzature) it.next(); String codice = rc.getCodice(); int sconto = rc.getSconto(); for (int i = 0; i < jTable1.getRowCount(); i++) { if (jTable1.getValueAt(i, 0).equals(codice)) { jTable1.setValueAt(sconto + "%", i, 5); } } } jTable1.setFocusable(false); jTextField1 = new JTextField(String.valueOf(totale), 5); jTextField1.setFocusable(false); jScrollPane1 = new JScrollPane(jTable1); jTable1.setFillsViewportHeight(true); 155 jPanel7.add(jLabel1); jPanel6.add(jLabel2); jPanel2.add(jLabel3); jPanel4.add(jLabel4); jPanel4.add(jTextField1); jPanel5.add(jButton1); jPanel1.add(jPanel7); jPanel1.add(new JSeparator(SwingConstants.HORIZONTAL)); jPanel1.add(jPanel6); jPanel1.add(jPanel2); jPanel1.add(jScrollPane1); jPanel1.add(jPanel3); jPanel1.add(jPanel4); jPanel1.add(jPanel5); VisualizzaReportAcquistoListener listener = new VisualizzaReportAcquistoListener(this); jButton1.addActionListener(listener); this.getContentPane().add(jPanel1); this.setSize(700, 600); this.setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); this.setVisible(true); } } File VisualizzaReportAcquistoListener.java package gui; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class VisualizzaReportAcquistoListener implements ActionListener { private VisualizzaReportAcquisto frame; public VisualizzaReportAcquistoListener(VisualizzaReportAcquisto frame) { this.frame = frame; } public void actionPerformed(ActionEvent ev) { frame.dispose(); } } 156 La finestra VisualizzaAcquisti è composta da una tabella per visualizzare l’ID e la data di tutti gli acquisti. Interaccia grafica VisualizzaAcquisti: File VisualizzaAcquisti.java package gui; import java.awt.FlowLayout; import javax.swing.*; import java.util.*; import attivita.RecordAcquisto; public class VisualizzaAcquisti extends JFrame { private private private private private final final final final final JButton jButton1 = new JButton("Torna al menù principale"); JPanel jPanel1; JPanel jPanel2; JScrollPane jScrollPane1; JTable jTable1 = new JTable(); public VisualizzaAcquisti(List<RecordAcquisto> listaAcquisti) { super("Test JPA - Visualizza acquisti"); jPanel1 = new JPanel(); jPanel1.setLayout(new BoxLayout(jPanel1, BoxLayout.Y_AXIS)); 157 jPanel1.setBorder( BorderFactory.createTitledBorder("Visualizza acquisti")); jPanel2 = new JPanel(new FlowLayout()); Object[][] tabellaAcquisti = new Object[listaAcquisti.size()][2]; Iterator<RecordAcquisto> it = listaAcquisti.iterator(); int i = 0; while(it.hasNext()) { RecordAcquisto ra = (RecordAcquisto) it.next(); tabellaAcquisti[i][0] = ra.getId(); tabellaAcquisti[i][1] = ra.getData(); i = i+1; } jTable1.setModel(new javax.swing.table.DefaultTableModel( tabellaAcquisti, new String [] { "ID acquisto", "Data" } ) { Class[] types = new Class [] { java.lang.Long.class, java.lang.String.class }; boolean[] canEdit = new boolean [] { false, false }; public Class getColumnClass(int columnIndex) { return types [columnIndex]; } public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit [columnIndex]; } }); jTable1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); jTable1.setFocusable(false); jScrollPane1 = new JScrollPane(jTable1); jTable1.setFillsViewportHeight(true); jPanel2.add(jButton1); jPanel1.add(jScrollPane1); jPanel1.add(jPanel2); jButton1.addActionListener(new VisualizzaAcquistiListener(this)); this.getContentPane().add(jPanel1); this.setSize(500,500); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); this.setVisible(true); } } 158 File VisualizzaAcquistiListener.java package gui; import java.awt.event.*; public class VisualizzaAcquistiListener implements ActionListener { private VisualizzaAcquisti frame; public VisualizzaAcquistiListener(VisualizzaAcquisti frame) { this.frame = frame; } public void actionPerformed(ActionEvent ev) { frame.dispose(); } } La finestra SelezionaAcquisto è visualizzata durante l’esecuzione dell’attività AttivitaEffettuaControlli. E’ molto simile alla finestra VisualizzaAcquisti, con la differenza che è presente un metodo leggiIdAcquisto() per leggere l’ID dell’acquisto alla riga selezionata della tabella, permettendo di selezionare un acquisto sul quale verranno effettuati i controlli. E’ inoltre presente un metodo aspettaOK() che mette l’attività AttivitaEffettuaControlli in attesa che l’utente prema il bottone Inizia controlli per poter successivamente leggere l’ID dell’acquisto selezionato dalla tabella. Interaccia grafica SelezionaAcquisto: 159 File SelezionaAcquisto.java package gui; import java.awt.FlowLayout; import javax.swing.*; import java.util.*; import attivita.RecordAcquisto; public class SelezionaAcquisto extends JFrame { private private private private private final final final final final JButton jButton1 = new JButton("Inizia controlli"); JPanel jPanel1; JPanel jPanel2; JScrollPane jScrollPane1; JTable jTable1 = new JTable(); public SelezionaAcquisto(List<RecordAcquisto> listaAcquisti) { super("Test JPA - Effettua controlli"); jPanel1 = new JPanel(); jPanel1.setLayout(new BoxLayout(jPanel1, BoxLayout.Y_AXIS)); jPanel1.setBorder( BorderFactory.createTitledBorder("Effettua controlli")); jPanel2 = new JPanel(new FlowLayout()); Object[][] tabellaAcquisti = new Object[listaAcquisti.size()][2]; Iterator<RecordAcquisto> it = listaAcquisti.iterator(); int i = 0; while(it.hasNext()) { RecordAcquisto ra = (RecordAcquisto) it.next(); tabellaAcquisti[i][0] = ra.getId(); tabellaAcquisti[i][1] = ra.getData(); i = i+1; } jTable1.setModel(new javax.swing.table.DefaultTableModel( tabellaAcquisti, new String [] { "ID acquisto", "Data" } ) { Class[] types = new Class [] { java.lang.Long.class, java.lang.String.class }; boolean[] canEdit = new boolean [] { false, false }; public Class getColumnClass(int columnIndex) { return types [columnIndex]; } public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit [columnIndex]; } }); 160 jTable1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); jScrollPane1 = new JScrollPane(jTable1); jTable1.setFillsViewportHeight(true); jPanel2.add(jButton1); jPanel1.add(jScrollPane1); jPanel1.add(jPanel2); jButton1.addActionListener(new SelezionaAcquistoListener(this)); this.getContentPane().add(jPanel1); this.setSize(500,500); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); this.setVisible(true); } public Long leggiIdAcquisto() throws RuntimeException { int[] righeSelezionate = jTable1.getSelectedRows(); if(righeSelezionate.length == 0) return null; else { Long idAcquisto = (Long) jTable1.getValueAt(righeSelezionate[0], 0); return idAcquisto; } } public void aspettaOK() { synchronized (getContentPane()) { try { getContentPane().wait(); } catch (InterruptedException e) { e.printStackTrace(); System.exit(1); } } } } File SelezionaAcquistoListener.java package gui; import java.awt.event.*; public class SelezionaAcquistoListener implements ActionListener { private SelezionaAcquisto frame; public SelezionaAcquistoListener(SelezionaAcquisto frame) { this.frame = frame; } 161 public void actionPerformed(ActionEvent ev) { synchronized (frame.getContentPane()) { frame.getContentPane().notify(); frame.dispose(); } } } La finestra VisualizzaRisultatoControllo visualizza i risultati delle attività atomiche di controllo. Interaccia grafica VisualizzaRisultatoControllo: File VisualizzaRisultatoControllo.java package gui; import import import import javax.swing.*; java.util.*; java.awt.GridLayout; java.awt.FlowLayout; import attivita.RecordCalzature; public class VisualizzaRisultatoControllo extends JFrame { private private private private final final final final JButton jButton1 JLabel jLabel1 = JLabel jLabel2 = JLabel jLabel3 = = new JButton("Torna al menù principale"); new JLabel("Risultato Controlli"); new JLabel("Controllo importo totale: OK"); new JLabel("Calzature scontate"); 162 private final JLabel jLabel4 = new JLabel("Materiali utilizzati per l'abbigliamento"); private final JList jList1; private final JList jList2; private final JScrollPane jScrollPane1; private final JScrollPane jScrollPane2; private final JPanel jPanel1; private final JPanel jPanel2; private final JPanel jPanel3; private final JPanel jPanel4; private final JPanel jPanel5; private final JPanel jPanel6; private final JPanel jPanel7; private final JPanel jPanel8; private final JPanel jPanel9; public VisualizzaRisultatoControllo( Object[] calzatureScontate, Object[] materiali) { super("Test JPA - Effettua controlli"); jPanel1 = new JPanel(); jPanel1.setLayout(new BoxLayout(jPanel1, BoxLayout.Y_AXIS)); jPanel1.setBorder( BorderFactory.createTitledBorder("Effettua controlli")); jPanel2 = new JPanel(new GridLayout(1, 2, 10, 10)); jPanel3 = new JPanel(); jPanel3.setLayout(new BoxLayout(jPanel3, BoxLayout.Y_AXIS)); jPanel4 = new JPanel(); jPanel4.setLayout(new BoxLayout(jPanel4, BoxLayout.Y_AXIS)); jPanel5 = new JPanel(new FlowLayout()); jPanel6 = new JPanel(new FlowLayout()); jPanel7 = new JPanel(new FlowLayout()); jPanel8 = new JPanel(new FlowLayout()); jPanel9 = new JPanel(new FlowLayout()); jList1 = new JList(calzatureScontate); jList1.setFocusable(false); jList2 = new JList(materiali); jList2.setFocusable(false); jScrollPane1 = new JScrollPane(jList1); jScrollPane2 = new JScrollPane(jList2); jPanel9.add(jLabel1); jPanel8.add(jLabel2); jPanel7.add(jButton1); jPanel6.add(jLabel4); jPanel5.add(jLabel3); jPanel4.add(jPanel6); jPanel4.add(jScrollPane2); jPanel3.add(jPanel5); jPanel3.add(jScrollPane1); 163 jPanel2.add(jPanel3); jPanel2.add(jPanel4); jPanel1.add(jPanel9); jPanel1.add(new JSeparator(SwingConstants.HORIZONTAL)); jPanel1.add(jPanel8); jPanel1.add(jPanel2); jPanel1.add(jPanel7); VisualizzaRisultatoControlloListener listener = new VisualizzaRisultatoControlloListener(this); jButton1.addActionListener(listener); this.getContentPane().add(jPanel1); this.setSize(500, 400); this.setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); this.setVisible(true); } } File VisualizzaRisultatoControlloListener.java package gui; import java.awt.event.*; public class VisualizzaRisultatoControlloListener implements ActionListener { private VisualizzaRisultatoControllo frame; public VisualizzaRisultatoControlloListener( VisualizzaRisultatoControllo frame) { this.frame = frame; } public void actionPerformed(ActionEvent ev) { frame.dispose(); } } 164 3.7 Main La classe Main avvia l’applicazione. Al suo interno è presente il metodo main() che richiama getEntityManager() della classe JPAUtil per inizializzare la EntityManagerFactory. Successivamente esegue FinestraPrincipale. File Main.java package applicazione; import javax.swing.SwingUtilities; import gui.FinestraPrincipale; import persistence.JPAUtil; public class Main { public static void main(String[] args) { JPAUtil.getEntityManagerFactory(); Runnable target = new Runnable() { @Override public void run() { new FinestraPrincipale(); } }; SwingUtilities.invokeLater(target); } } 165 4. CONSIDERAZIONI FINALI Per concludere questa analisi sui framework Java per la persistenza è necessario fare alcune considerazioni. Hibernate e le Java Persistence API sono due software molto validi che forniscono gli stessi servizi. Oggi le hibernate annotations vengono utilizzate maggiormente rispetto ai file di mapping XML perché il codice sviluppato con annotazioni è più leggibile, più rapido da scrivere e meno ridondante. Bisogna però notare che i software sviluppati con JPA non permettono una piena distinzione tra le classi di dominio e i metadati per il mapping. Analizzando le procedure di Object-Relational Mapping, bisogna considerare il fatto che i framework di persistenza possono essere sfruttati al meglio in scenari top down in cui si ha totale libertà sulla base di dati. Gli altri scenari sono spesso troppo laboriosi e producono risultati insoddisfacenti come nel caso di scenari bottom up in cui le classi di dominio generate dal framework compongono un modello derivante dal modello delle tabelle della base di dati che non sfrutterà funzioni importanti della programmazione ObjectOriented. E’ interessante confrontare l’utilizzo di framework per la persistenza con un approccio Data Access Object (DAO) più tradizionale. Le classi DAO permettono di costruire uno strato di persistenza con cui interagire con la base di dati liberamente a differenza dei framework di persistenza in cui il mapping condiziona la costruzione della base di dati. Lo sviluppo con Hibernate e le Java Persistence API ha però diversi vantaggi: 1. Lo sviluppo ed eventuali modifiche al software sono molto più rapide; 2. Il programmatore deve essere a conoscenza dei dettagli della base di dati solo nella fase di configurazione, successivamente potrà dedicarsi completamente alla logica applicativa del software; 3. Permette di non lavorare con librerie di basso livello Java DataBase Connectivity (JDBC); 4. Sfruttando i linguaggi HQL e JPQL il software sarà molto portabile perché indipendente dalla tipologia di SQL nativa del DBMS. In conclusione, è possibile affermare che un approccio alla persistenza con Data Access Object permette una gestione più libera e una progettazione del software più precisa, mentre i framework per la persistenza lavorano ad un livello più alto permettendo spesso uno sviluppo più comodo e veloce che potrebbe essere utile in alcune situazioni. 166 Riferimenti bibliografici Christian Bauer, Gavin King, Java Persistence with Hibernate, Manning Publications, 2007 Documentazione ufficiale Hibernate www.hibernate.org/docs The Java EE 6 Tutorial – Part VI Persistence docs.oracle.com 167 168