Persistenza Java con JPA e Hibernate, Parte 1: entità e relazioni

Java Persistence API (JPA) è una specifica Java che colma il divario tra database relazionali e programmazione orientata agli oggetti. Questo tutorial in due parti introduce JPA e spiega come gli oggetti Java vengono modellati come entità JPA, come vengono definite le relazioni di entità e come utilizzare JPA EntityManagercon il pattern Repository nelle applicazioni Java.

Tieni presente che questo tutorial utilizza Hibernate come provider JPA. La maggior parte dei concetti può essere estesa ad altri framework di persistenza Java.

Cos'è JPA?

Vedere "Cos'è JPA? Introduzione all'API Java Persistence" per conoscere l'evoluzione di JPA e dei framework correlati, incluso EJB 3.0. e JDBC.

Relazioni oggettive nell'APP

I database relazionali esistono come mezzo per memorizzare i dati dei programmi sin dagli anni '70. Sebbene oggi gli sviluppatori abbiano molte alternative al database relazionale, questo tipo di database è scalabile e ben compreso ed è ancora ampiamente utilizzato nello sviluppo di software su piccola e larga scala.

Gli oggetti Java in un contesto di database relazionale sono definiti come entità . Le entità vengono collocate nelle tabelle in cui occupano colonne e righe. I programmatori utilizzano chiavi esterne e uniscono le tabelle per definire le relazioni tra le entità, vale a dire le relazioni uno a uno, uno a molti e molti a molti. Possiamo anche utilizzare SQL (Structured Query Language) per recuperare e interagire con i dati in singole tabelle e su più tabelle, utilizzando vincoli di chiave esterna. Il modello relazionale è semplice, ma gli sviluppatori possono scrivere query per recuperare i dati e costruire oggetti da quei dati.

Mancata corrispondenza dell'impedenza delle relazioni tra oggetti

Potresti avere familiarità con il termine disadattamento di impedenza delle relazioni tra oggetti , che si riferisce alla sfida di mappare oggetti di dati a un database relazionale. Questa mancata corrispondenza si verifica perché la progettazione orientata agli oggetti non è limitata alle relazioni uno-a-uno, uno-a-molti e molti-a-molti. Invece, nella progettazione orientata agli oggetti, pensiamo agli oggetti, ai loro attributi e comportamenti e al modo in cui gli oggetti si relazionano. Due esempi sono l'incapsulamento e l'ereditarietà:

  • Se un oggetto contiene un altro oggetto, definiamo questo attraverso incapsulamento --a ha-un rapporto.
  • Se un oggetto è una specializzazione di un altro oggetto, lo definiamo attraverso l' ereditarietà : una è una relazione.

Associazione, aggregazione, composizione, astrazione, generalizzazione, realizzazione e dipendenze sono tutti concetti di programmazione orientata agli oggetti che possono essere difficili da mappare a un modello relazionale.

ORM: Mappatura relazionale di oggetti

La mancata corrispondenza tra la progettazione orientata agli oggetti e la modellazione di database relazionali ha portato a una classe di strumenti sviluppati specificamente per la mappatura relazionale a oggetti (ORM). Strumenti ORM come Hibernate, EclipseLink e iBatis traducono modelli di database relazionali, comprese le entità e le loro relazioni, in modelli orientati agli oggetti. Molti di questi strumenti esistevano prima della specifica JPA, ma senza uno standard le loro caratteristiche dipendevano dal fornitore.

Rilasciato per la prima volta come parte di EJB 3.0 nel 2006, Java Persistence API (JPA) offre un modo standard per annotare gli oggetti in modo che possano essere mappati e archiviati in un database relazionale. La specifica definisce anche un costrutto comune per l'interazione con i database. Avere uno standard ORM per Java conferisce coerenza alle implementazioni dei fornitori, consentendo anche flessibilità e componenti aggiuntivi. Ad esempio, mentre la specifica JPA originale è applicabile ai database relazionali, alcune implementazioni di fornitori hanno esteso JPA per l'utilizzo con i database NoSQL.

Evoluzione dell'APP

La prima versione di JPA, versione 1.0, è stata pubblicata nel 2006 tramite Java Community Process (JCP) come Java Specification Request (JSR) 220. La versione 2.0 (JSR 317) è stata pubblicata nel 2009, la versione 2.1 (JSR 338) nel 2013, e la versione 2.2 (una versione di manutenzione di JSR 338) è stata pubblicata nel 2017. JPA 2.2 è stato selezionato per l'inclusione e lo sviluppo in corso in Jakarta EE.

Iniziare con JPA

La Java Persistence API è una specifica, non un'implementazione: definisce un'astrazione comune che puoi utilizzare nel tuo codice per interagire con i prodotti ORM. Questa sezione esamina alcune delle parti importanti della specifica JPA.

Imparerai come:

  • Definisci entità, campi e chiavi primarie nel database.
  • Crea relazioni tra entità nel database.
  • Lavora con EntityManagere i suoi metodi.

Definizione di entità

Per definire un'entità, è necessario creare una classe annotata con l' @Entityannotazione. L' @Entityannotazione è un'annotazione marcatore , che viene utilizzata per scoprire entità persistenti. Ad esempio, se si desidera creare un'entità libro, la si annoterà come segue:

 @Entity public class Book { ... } 

Per impostazione predefinita, questa entità verrà mappata alla Booktabella, come determinato dal nome della classe specificato. Se si desidera mappare questa entità su un'altra tabella (e, facoltativamente, uno schema specifico), è possibile utilizzare l' @Tableannotazione per farlo. Ecco come associare la Bookclasse a una tabella LIBRI:

 @Entity @Table(name="BOOKS") public class Book { ... } 

Se la tabella BOOKS era nello schema PUBLISHING, è possibile aggiungere lo schema @Tableall'annotazione:

 @Table(name="BOOKS", schema="PUBLISHING") 

Mappatura dei campi alle colonne

With the entity mapped to a table, your next task is to define its fields. Fields are defined as member variables in the class, with the name of each field being mapped to a column name in the table. You can override this default mapping by using the @Column annotation, as shown here:

 @Entity @Table(name="BOOKS") public class Book { private String name; @Column(name="ISBN_NUMBER") private String isbn; ... } 

In this example, we've accepted the default mapping for the name attribute but specified a custom mapping for the isbn attribute. The name attribute will be mapped to the name column, but the isbn attribute will be mapped to the ISBN_NUMBER column.

The @Column annotation allows us to define additional properties of the field/column, including length, whether it is nullable, whether it must be unique, its precision and scale (if it's a decimal value), whether it is insertable and updatable, and so forth.

Specifying the primary key

One of the requirements for a relational database table is that it must contain a primary key, or a key that uniquely identifies a specific row in the database. In JPA, we use the @Id annotation to designate a field to be the table's primary key. The primary key is required to be a Java primitive type, a primitive wrapper, such as Integer or Long, a String, a Date, a BigInteger, or a BigDecimal.

In this example, we map the id attribute, which is an Integer, to the ID column in the BOOKS table:

 @Entity @Table(name="BOOKS") public class Book { @Id private Integer id; private String name; @Column(name="ISBN_NUMBER") private String isbn; ... } 

It is also possible to combine the @Id annotation with the @Column annotation to overwrite the primary key's column-name mapping.

Relationships between entities

Now that you know how to define an entity, let's look at how to create relationships between entities. JPA defines four annotations for defining entities:

  • @OneToOne
  • @OneToMany
  • @ManyToOne
  • @ManyToMany

One-to-one relationships

The @OneToOne annotation is used to define a one-to-one relationship between two entities. For example, you may have a User entity that contains a user's name, email, and password, but you may want to maintain additional information about a user (such as age, gender, and favorite color) in a separate UserProfile entity. The @OneToOne annotation facilitates breaking down your data and entities this way.

The User class below has a single UserProfile instance. The UserProfile maps to a single User instance.

 @Entity public class User { @Id private Integer id; private String email; private String name; private String password; @OneToOne(mappedBy="user") private UserProfile profile; ... } 
 @Entity public class UserProfile { @Id private Integer id; private int age; private String gender; private String favoriteColor; @OneToOne private User user; ... } 

The JPA provider uses UserProfile's user field to map UserProfile to User. The mapping is specified in the mappedBy attribute in the @OneToOne annotation.

One-to-many and many-to-one relationships

The @OneToMany and @ManyToOne annotations facilitate both sides of the same relationship. Consider an example where a Book can have only one Author, but an Author may have many books. The Book entity would define a @ManyToOne relationship with Author and the Author entity would define a @OneToMany relationship with Book.

 @Entity public class Book { @Id private Integer id; private String name; @ManyToOne @JoinColumn(name="AUTHOR_ID") private Author author; ... } 
 @Entity public class Author { @Id @GeneratedValue private Integer id; private String name; @OneToMany(mappedBy = "author") private List books = new ArrayList(); ... } 

In this case, the Author class maintains a list of all of the books written by that author and the Book class maintains a reference to its single author. Additionally, the @JoinColumn specifies the name of the column in the Book table to store the ID of the Author.

Many-to-many relationships

Finally, the @ManyToMany annotation facilitates a many-to-many relationship between entities. Here's a case where a Book entity has multiple Authors:

 @Entity public class Book { @Id private Integer id; private String name; @ManyToMany @JoinTable(name="BOOK_AUTHORS", [email protected](name="BOOK_ID"), [email protected](name="AUTHOR_ID")) private Set authors = new HashSet(); ... } 
 @Entity public class Author { @Id @GeneratedValue private Integer id; private String name; @ManyToMany(mappedBy = "author") private Set books = new HashSet(); ... } 

In this example, we create a new table, BOOK_AUTHORS, with two columns: BOOK_ID and AUTHOR_ID. Using the joinColumns and inverseJoinColumns attributes tells your JPA framework how to map these classes in a many-to-many relationship. The @ManyToMany annotation in the Author class references the field in the Book class that manages the relationship; namely the authors property.

That's a quick demo for a fairly complex topic. We'll dive further into the @JoinTable and @JoinColumn annotations in the next article.

Working with the EntityManager

EntityManager is the class that performs database interactions in JPA. It is initialized through a configuration file named persistence.xml. This file is found in the META-INF folder in your CLASSPATH, which is typically packaged in your JAR or WAR file. The persistence.xml file contains:

  • The named "persistence unit," which specifies the persistence framework you're using, such as Hibernate or EclipseLink.
  • A collection of properties specifying how to connect to your database, as well as any customizations in the persistence framework.
  • A list of entity classes in your project.

Let's look at an example.

Configuring the EntityManager

First, we create an EntityManager using the EntityManagerFactory retrieved from the Persistence class:

 EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("Books"); EntityManager entityManager = entityManagerFactory.createEntityManager(); 

In this case we've created an EntityManager that is connected to the "Books" persistence unit, which we've configured in the persistence.xml file.

The EntityManager class defines how our software will interact with the database through JPA entities. Here are some of the methods used by EntityManager:

  • find retrieves an entity by its primary key.
  • createQuery creates a Query instance that can be used to retrieve entities from the database.
  • createNamedQuery loads a Query that has been defined in a @NamedQuery annotation inside one of the persistence entities. Named queries provide a clean mechanism for centralizing JPA queries in the definition of the persistence class on which the query will execute.
  • getTransaction defines an EntityTransaction to use in your database interactions. Just like database transactions, you will typically begin the transaction, perform your operations, and then either commit or rollback your transaction. The getTransaction() method lets you access this behavior at the level of the EntityManager, rather than the database.
  • merge() adds an entity to the persistence context, so that when the transaction is committed, the entity will be persisted to the database. When using merge(), objects are not managed.
  • persist adds an entity to the persistence context, so that when the transaction is committed, the entity will be persisted to the database. When using persist(), objects are managed.
  • refresh refreshes the state of the current entity from the database.
  • flush synchronizes the state of the persistence context with the database.

Non preoccuparti di integrare tutti questi metodi contemporaneamente. Li conoscerai lavorando direttamente con EntityManager, cosa che faremo di più nella prossima sezione.