Java persistence with JPA and Hibernate: Persisting data to a database

how-to
Dec 27, 202322 mins

Write, build, and run an example application that persists data to and from a relational database using Hibernate, JPA, and the repository pattern.

Books with shopping cart
Credit: Galina-Photo/Shutterstock

In this second half of the Java persistence with JPA and Hibernate tutorial, we move past concepts and start writing code that persists data to and from a relational database using JPA with Hibernate. We’ll start by configuring an example application to use Hibernate as the JPA provider, then quickly configure the EntityManager and write two classes that we want to persist to the database: Book and Author. Finally, we’ll write a simple application that pulls together all the application components and successfully persists our two entities to the database.

You’ll learn how to:

  • Configure a Java application to use Hibernate as your JPA provider.
  • Configure JPA’s EntityManager.
  • Create a simple JPA domain model representing two classes with a one-to-many relationship.
  • Use repositories to cleanly separate persistence logic from your application code.
  • Write, build, and run the example application using JPA to connect with a relational database.

You’ll also get started with JPA Query Language (JPQL) and use it to execute a few simple database operations on the example application.

The complete source code for the example application can be found here. If you have not read the first half of this tutorial, we recommend doing that before you continue.

Configuring Hibernate

To keep things simple, we’re going to use the embedded H2 database for both development and runtime examples. You can change the JDBC URL in the EntityManager to point to any database you wish.

Start by reviewing the Maven POM file for the project, shown in Listing 1.

Listing 1. pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.infoworld</groupId>
    <artifactId>jpa-example</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>jpa-example</name>
    <url>http://maven.apache.org</url>
    <properties>
        <java.version>21</java.version>
        <hibernate.version>6.3.1.Final</hibernate.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>com.infoworld.jpa.JpaExample</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>install</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>${hibernate.version}</version>
</dependency>
<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-api</artifactId>
    <version>9.1.0</version>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>jakarta.json.bind</groupId>
    <artifactId>jakarta.json.bind-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.eclipse</groupId>
    <artifactId>yasson</artifactId>
    <version>3.0.3</version>
    <scope>runtime</scope>
</dependency>

    </dependencies>
</project>

We’ll build this project using Java 21 and Hibernate version 6.3, which was the latest version as of this writing. Plugins in the build node will set the Java compilation version, make the resulting JAR file executable, and ensure that all dependencies are copied to a lib folder so that the executable JAR can run. We include the following dependencies:

  • Hibernate’s core functionality: hibernate-core
  • The JPA API: hibernate-jpa-2.1-api
  • The embedded H2 database: h2

Note that H2’s scope is set to runtime so that we can use it when we run our code.

Configuring the EntityManager

Recall that our JPA’s EntityManager is driven by the persistence.xml file. Listing 2 shows the contents of this file.

Listing 2. /resources/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="Books" transaction-type="RESOURCE_LOCAL">
        <!-- Persistence provider -->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="jakarta.persistence.jdbc.url"    value="jdbc:h2:mem:bookstore" />
            <property name="jakarta.persistence.jdbc.user" value="sa" />
            <property name="jakarta.persistence.jdbc.password" value="" />

            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="show_sql" value="true"/>
            <property name="hibernate.temp.use_jdbc_metadata_defaults" value="false"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Although we are using persistence.xml, you could achieve a similar configuration with an annotated EntityManager. In that case, you would use the @PersistenceContext(unitName = "Books") annotation.

The persistence.xml file begins with a persistence node that can contain one or more persistence units. A persistence-unit has a name, which we’ll use later when we create the EntityManager, and it defines the attributes of that unit. In this case, we configure properties in this unit to do the following:

  • Specify HibernatePersistenceProvider, so the application knows we’re using Hibernate as our JPA provider.
  • Define the database configuration via JDBC. In this case, we’re using an in-memory H2 instance.
  • Configure Hibernate, including setting the Hibernate dialect to H2Dialect, so that Hibernate knows how to communicate with the H2 database.

Note that JPA will automatically scan and discover your annotated entity classes.

The domain model

For this application, we’re modeling a Book class and an Author class. These entities have a one-to-many relationship, meaning that a book can only be written by a single author, but an author can write many books. Figure 1 shows this domain model.

JPA and Hibernate--Example of a domain model with two entities. Steven Haines

Figure 1. Domain model for a JPA/Hibernate application with two entities.

When we talk about database tables, we typically speak about a “data model,” but when we talk about Java entities and their relationships, we typically refer to it as a “domain model.”

Modeling the Book class

Let’s begin with our entities. Listing 3 shows the source code for the Book class.

Listing 3. Book.java


package com.infoworld.jpa.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;

@Entity
@Table(name = "BOOK")
@NamedQueries({
        @NamedQuery(name = "Book.findByName",
                query = "SELECT b FROM Book b WHERE b.name = :name"),
        @NamedQuery(name = "Book.findAll",
                query = "SELECT b FROM Book b")
})
public class Book {
    @Id
    @GeneratedValue
    private Integer id;
    private String name;

    @ManyToOne
    @JoinColumn(name="AUTHOR_ID")
    private Author author;

    public Book() {
    }

    public Book(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Book(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + ''' +
                ", author=" + author.getName() +
                '}';
    }
}

This Book is a simple POJO (plain old Java object) that manages three properties:

  • id: The primary key, or identifier, of the book.
  • name: The name, or title, of the book.
  • author: The author who wrote the book.

The class itself is annotated with three annotations:

  • @Entity: Identifies the Book as a JPA entity.
  • @Table: Overrides the name of the table to which this entity will be persisted. In this case we define the table name as BOOK.
  • @NamedQueries: Allows you to define JPA Query Language queries that can later be retrieved and executed by the EntityManager.

About JPA Query Language (JPQL)

JPQL is similar to SQL, but rather than operating on database column names, it operates on entities, their fields, and their relationships. JPA refers to these queries as running on top of an “abstract schema,” which gets translated to a proper database schema. Entities are mapped to that database schema. Here’s the format for a simple JPQL query:


SELECT returnedEntity FROM entityName var WHERE whereClause

The Book.findAll query is defined as SELECT b FROM Book b, in which “Book” is the name of the entity and “b” is the variable name assigned to “Book.” This is equivalent to the SQL SELECT * FROM BOOK.

The Book.findByName uses a named parameter, “name,” as in:


SELECT b FROM Book b WHERE b.name = :name

Parameters can be referenced by name or by position. When referencing a parameter by name, you specify the name your query expects, such as “name” or “bookName,” prefaced by a “:“. If, instead, you wanted to reference the name by position, you could replace “:name” with “?1“. When executing the query, you would set the parameter with position of “1”. Binding the book name to “MyBook” is equivalent to the following SQL:


SELECT * FROM BOOK WHERE name='MyBook'

The Book‘s id attribute is annotated with both @Id and @GeneratedValue. The @Id annotation identifies the id as the primary key of the Book, which will resolve to the primary key of the underlying database. The @GeneratedValue annotation tells JPA that the database should generate the primary key when the entity is persisted to the database. Because we have not specified a @Column annotation, the id will be mapped to the same column name, “id.”

The Book‘s name attribute will be mapped to the “name” column in the BOOK table.

Finally, the author field is annotated with the @ManyToOne and @JoinColumn annotations. Recall from Part 1 that the @ManyToOne is one side of a one-to-many relationship. This annotation tells the JPA provider that there can be many books to one author.

Modeling the Author class

Listing 4 shows the source code for the Author class.

Listing 4. Author.java


package com.infoworld.jpa.model;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name="AUTHOR")
@NamedQueries({
        @NamedQuery(name = "Author.findByName",
                query = "SELECT a FROM Author a WHERE a.name = :name")
})
public class Author {
    @Id
    @GeneratedValue
    private Integer id;
    private String name;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    public Author(String name) {
        this.name = name;
    }

    public Author(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Book> getBooks() {
        return books;
    }

    public void addBook(Book book) {
        books.add(book);
        book.setAuthor(this);
    }

    @Override
    public String toString() {
        return "Author{" +
                "id=" + id +
                ", name='" + name + ''' +
                ", books=" + books +
                '}';
    }
}

Author isn’t much different from the Book class:

  • The @Entity annotation identifies Author as a JPA entity.
  • The @Table annotation tells Hibernate that this entity should be stored in the AUTHOR table.
  • The @Table annotation also defines an findByName named query.

The Author class maintains a list of books written by the given author, which is annotated with the @OneToMany annotation. Author‘s @OneToMany annotation matches the @ManyToOne annotation on the Book class. The mappedBy field tells Hibernate that this field is stored in the Book‘s author property.

CascadeType

You might note the CascadeType in the @OneToMany annotation. CascadeType is an enumerated type that defines cascading operations to be applied in a given relationship. In this case, CascadeType defines operations performed on the author that should be propagated to the book. CascadeTypes include the following:

  • DETACH: When an entity is detached from the EntityManager, also detach the entities on the other side of the operation.
  • MERGE: When an entity is merged into the EntityManager, also merge the entities on the other side of the operation.
  • PERSIST: When an entity is persisted to the EntityManager, also persist the entities on the other side of the operation.
  • REFRESH: When an entity is refreshed from the EntityManager, also refresh the entities on the other side of the operation.
  • FLUSH: When an entity is flushed to the EntityManager, also flush its corresponding entities.
  • ALL: Includes all of the aforementioned operation types.

When any operation is performed on an author, its books should be updated. This makes sense because a book cannot exist without its author.

Repositories in JPA

We could create an EntityManager and do everything inside the sample application class, but using external repository classes will make the code cleaner. As defined by the Repository pattern, creating a BookRepository and AuthorRepository isolates the persistence logic for each entity. Listing 5 shows the source for the BookRepository.

Listing 5. BookRepository.java


package com.infoworld.jpa.repository;

import com.infoworld.jpa.model.Book;

import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class BookRepository {
    private EntityManager entityManager;

    public BookRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public Optional<Book> save(Book book) {
        try {
            entityManager.getTransaction().begin();
            entityManager.persist(book);
            entityManager.getTransaction().commit();
            return Optional.of(book);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }

    public Optional<Book> findById(Integer id) {
        Book book = entityManager.find(Book.class, id);
        return book != null ? Optional.of(book) : Optional.empty();
    }

    public List<Book> findAll() {
        return entityManager.createQuery("from Book").getResultList();
    }

    public Optional<Book> findByName(String name) {
        Book book = entityManager.createQuery("SELECT b FROM Book b WHERE b.name = :name", Book.class)
                .setParameter("name", name)
                .getSingleResult();
        return book != null ? Optional.of(book) : Optional.empty();
    }

    public Optional<Book> findByNameNamedQuery(String name) {
        Book book = entityManager.createNamedQuery("Book.findByName", Book.class)
                .setParameter("name", name)
                .getSingleResult();
        return book != null ? Optional.of(book) : Optional.empty();
    }
}

The BookRepository is initialized with an EntityManager, which we’ll create in our sample application. The first method, findById(), invokes EntityManager‘s find() method, which retrieves an entity of a given class with its given primary key. If, for example, we add a new book and its primary key is generated as “1,” then entityManager.find(Book.class, 1) will return the Book with an ID of 1. If a Book with the requested primary key is not found in the database, then the find() method returns null. Because we want our code to be resilient and not pass nulls around, it checks the value for null and returns either a valid book wrapped in an Optional or Optional.empty().

More about EntityManager’s methods

Looking at the code in Listing 5, the find() method is probably the easiest to understand. The slightly more complex findAll() method creates a new JPQL query to retrieve all books. As shown earlier, we could have written this as SELECT b FROM Book b, but from Book is a shorthand way of doing it. The createQuery() method creates a Query instance, which supports a host of setter methods (such as setParameter(), which you’ll see next) to make building a query look a little more elegant. It has two methods that execute the query of interest to us:

  • getResultList executes the JPQL SELECT statement and returns the results as a List; if no results are found then it returns an empty list.
  • getSingleResult executes the JPQL SELECT statement and returns a single result; if no results are found then it throws a NoResultException.

In the findAll() method, we execute the Query‘s getResultList() method and return the list of Books back to the caller.

The findByName() and findByNameNamedQuery() methods both find a Book by its name, but the first method executes a JPQL query and the second retrieves the named query defined in the Book class. Because these queries define a named parameter, “:name“, they call the Query::setParameter method to bind the method’s name argument to the query before executing.

We expect a single Book to be returned, so we execute the Query::getSingleResult method, which either returns a Book or null. We check the response. If it is not null, we return the Book wrapped in an Optional; otherwise we return Optional.empty().

Finally, the save() method saves a Book to the database. Both the persist and merge operations, which update the database, need to run in a transaction. We retrieve the resource-level EntityTransaction by invoking the EntityManager::getTransaction method and wrap the persist call in begin() and commit() calls. We opt to persist() the book to the database so that the book will be “managed” and saved to the database.

This way, the book we return will have the generated primary key. If we used merge() instead, then our book would be copied into the entity context. When the transaction was committed we would not see the auto-generated primary key.

The AuthorRepository

Listing 6 shows the source for the AuthorRepository.

Listing 6. AuthorRepository.java


package com.infoworld.jpa.repository;

import com.infoworld.jpa.model.Author;

import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class AuthorRepository {
    private EntityManager entityManager;

    public AuthorRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public Optional<Author> save(Author author) {
        try {
            entityManager.getTransaction().begin();
            entityManager.persist(author);
            entityManager.getTransaction().commit();
            return Optional.of(author);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }

    public Optional<Author> findById(Integer id) {
        Author author = entityManager.find(Author.class, id);
        return author != null ? Optional.of(author) : Optional.empty();
    }

    public List<Author> findAll() {
        return entityManager.createQuery("from Author").getResultList();
    }

    public Optional<Author> findByName(String name) {
        Author author = entityManager.createNamedQuery("Author.findByName", Author.class)
                .setParameter("name", name)
                .getSingleResult();
        return author != null ? Optional.of(author) : Optional.empty();
    }


}

The AuthorRepository is identical to the BookRepository, only it persists and queries for authors instead of books.

Hibernate and JPA example application

Listing 7 presents a sample application that creates an EntityManager, creates our repositories, and then executes some operations to demonstrate how to use the repositories.

Listing 7. JpaExample.java


package com.infoworld.jpa;

import com.infoworld.jpa.model.Author;
import com.infoworld.jpa.model.Book;
import com.infoworld.jpa.repository.AuthorRepository;
import com.infoworld.jpa.repository.BookRepository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import java.util.List;
import java.util.Optional;

public class JpaExample {

    public static void main(String[] args) {
        // Create our entity manager
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("Books");
        EntityManager entityManager = entityManagerFactory.createEntityManager();

        // Create our repositories
        BookRepository bookRepository = new BookRepository(entityManager);
        AuthorRepository authorRepository = new AuthorRepository(entityManager);

        // Create an author and add 3 books to his list of books
        Author author = new Author("Author 1");
        author.addBook(new Book("Book 1"));
        author.addBook(new Book("Book 2"));
        author.addBook(new Book("Book 3"));
        Optional<Author> savedAuthor = authorRepository.save(author);
        System.out.println("Saved author: " + savedAuthor.get());

        // Find all authors
        List<Author> authors = authorRepository.findAll();
        System.out.println("Authors:");
        authors.forEach(System.out::println);

        // Find author by name
        Optional<Author> authorByName = authorRepository.findByName("Author 1");
        System.out.println("Searching for an author by name: ");
        authorByName.ifPresent(System.out::println);

        // Search for a book by ID
        Optional<Book> foundBook = bookRepository.findById(2);
        foundBook.ifPresent(System.out::println);

        // Search for a book with an invalid ID
        Optional<Book> notFoundBook = bookRepository.findById(99);
        notFoundBook.ifPresent(System.out::println);

        // List all books
        List<Book> books = bookRepository.findAll();
        System.out.println("Books in database:");
        books.forEach(System.out::println);

        // Find a book by name
        Optional<Book> queryBook1 = bookRepository.findByName("Book 2");
        System.out.println("Query for book 2:");
        queryBook1.ifPresent(System.out::println);

        // Find a book by name using a named query
        Optional<Book> queryBook2 = bookRepository.findByNameNamedQuery("Book 3");
        System.out.println("Query for book 3:");
        queryBook2.ifPresent(System.out::println);

        // Add a book to author 1
        Optional<Author> author1 = authorRepository.findById(1);
        author1.ifPresent(a -> {
            a.addBook(new Book("Book 4"));
            System.out.println("Saved author: " + authorRepository.save(a));
        });

        // Close the entity manager and associated factory
        entityManager.close();
        entityManagerFactory.close();
    }
}

The first thing the application does is to create an EntityManagerFactory:


EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("Books");

The Persistence class has a createEntityManagerFactory method to which we pass the name of a persistence unit. Recall that in our persistence.xml file, we defined a single persistence-unit with the name “Books.” The call to createEntityManagerFactory creates an EntityManagerFactory that will connect to this database with its configuration, including the Book and Author entity classes. We then use the EntityManagerFactory to create an EntityManager:


EntityManager entityManager = entityManagerFactory.createEntityManager();

Next, we create our repositories, passing the EntityManager to their constructors.

Now we’re ready to exercise the repositories.

Running CRUD operations in JPA

First, we create an author and add three books to the author, then save it by invoking the AuthorRepository::save method. Here’s the resulting output:


Saved author: Author{id=1, name='Author 1', books=[Book{id=2, name='Book 1', author=Author 1}, Book{id=3, name='Book 2', author=Author 1}, Book{id=4, name='Book 3', author=Author 1}]}

Recall that we set the Author‘s @OneToMany annotation to use a CascadeType of ALL. Because of this, when we save the author its books will also all be saved. As the output shows, the author is saved first. It gets an auto-generated primary key value of 1, then the author’s three books are saved.

Next, we retrieve all authors by executing the AuthorRepository::findAll method. We have just one author, so this code yields the following output:


Authors:
Author{id=1, name='Author 1', books=[Book{id=2, name='Book 1', author=Author 1}, Book{id=3, name='Book 2', author=Author 1}, Book{id=4, name='Book 3', author=Author 1}]}

We search for an author by name by executing the AuthorRepository::findByName method, passing it the name “Author 1“. This yields the following output:


Searching for an author by name:
Author{id=1, name='Author 1', books=[Book{id=2, name='Book 1', author=Author 1}, Book{id=3, name='Book 2', author=Author 1}, Book{id=4, name='Book 3', author=Author 1}]}

We execute two book queries by ID, one that should be found, namely the book with ID 2, and one that should not be found, namely the one with ID 99. As expected, we see only one record printed out:


Book{id=2, name='Book 1', author=Author 1}

We query for all books by executing the BookRepository::findAll method, which successfully shows all books:


Books in database:
Book{id=2, name='Book 1', author=Author 1}
Book{id=3, name='Book 2', author=Author 1}
Book{id=4, name='Book 3', author=Author 1}

We exercise our queries to find books by name, one using the raw JPQL query, BookRepository::findByName, and one using the named query, BookRepository::findByNameNamedQuery:


Query for book 2:
Book{id=3, name='Book 2', author=Author 1}
Query for book 3:
Book{id=4, name='Book 3', author=Author 1}

Finally, we retrieve the author with ID 1, add a new book to the author’s list of books, save the author, and output the saved author, expecting to now see four books in the list:


Saved author: Optional[Author{id=1, name='Author 1', books=[Book{id=2, name='Book 1', author=Author 1}, Book{id=3, name='Book 2', author=Author 1}, Book{id=4, name='Book 3', author=Author 1}, Book{id=5, name='Book 4', author=Author 1}]}]

When we’re finished with all our queries, we need to close both the EntityManager and the EntityManagerFactory; otherwise their threads will live on and our application will never complete:


entityManager.close();
entityManagerFactory.close();

Run the application

Running the application is a simple two-step process.

Step 1: Build with Maven:


mvn clean install

Step 2: Change directories into the target directory and execute the JAR file:


cd target
java -jar jpa-example-1.0-SNAPSHOT.jar

Conclusion

This tutorial has been a general introduction to JPA. We’ve reviewed JPA as a standard for ORM in Java and looked at how entities, relationships, and the EntityManager work together in your Java applications.

Steven Haines

Steven Haines is a senior technologist, accomplished architect, author, and educator. He currently is a principal software engineer at Veeva Systems, where he builds Spring-based Java applications. Steven previously worked on two startups: Chronosphere, where he helped customers design and implement large-scale observability strategies, and Turbonomic, where he was a principal software architect for cloud optimization products. He's also worked for Disney as a technical architect and lead solution architect, building out the next generation of Disney's guest experience and other Disney solutions. Steven specializes in performance and scalability, cloud-based architectures, high-availability, fault tolerance, business analytics, and integration with new and emerging technologies.

As an author, he has written two books on Java programming and more than 500 articles for publications such as InfoWorld, InformIT.com (Pearson Education), JavaWorld, and Dr. Dobb's Journal. He has also written over a dozen white papers and ebooks on performance management and cloud-based architectures.

Steven has taught computer science and Java programming at Learning Tree University and the University of California, Irvine. He also maintains a personal website dedicated to helping software developers and architects grow in their knowledge: www.geekcap.com (by Geeks for Geeks).

More from this author

Matthew Tyson
Contributing Writer

Matthew Tyson is a contributing writer at InfoWorld. A seasoned technology journalist and expert in enterprise software development, Matthew has written about programming, programming languages, language frameworks, application platforms, development tools, databases, cryptography, information security, cloud computing, and emerging technologies such as blockchain and machine learning for more than 15 years. His work has appeared in leading publications including InfoWorld, CIO, CSO Online, and IBM developerWorks. Matthew also has had the privilege of interviewing many tech luminaries including Brendan Eich, Grady Booch, Guillermo Rauch, and Martin Hellman.

Matthewโ€™s diverse background encompasses full-stack development (Java, JVM languages such as Kotlin, JavaScript, Python, .NET), front-end development (Angular, React, Vue, Svelte) and back-end development (Spring Boot, Node.js, Django), software architecture, and IT infrastructure at companies ranging from startups to Fortune 500 enterprises. He is a trusted authority in critical technology areas such as database design (SQL and NoSQL), AI-assisted coding, agentic AI, open-source initiatives, enterprise integration, and cloud platforms, providing insightful analysis and practical guidance rooted in real-world experience.

More from this author