Java persistence with JPA and Hibernate, Part 1: Entities and relationships

Modeling entities and relationships for Java data persistence

1 2 3 4 Page 3
Page 3 of 4

Finally, the author field is annotated with the @ManyToOne and @JoinColumn annotations. Recall 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.geekcap.javaworld.jpa.model;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.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 +
                '}';
    }
}

The Author class 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 Author.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, detach the entities on the other side of the operation, as well.
  • MERGE: When an entity is merged into the EntityManager, merge the entities on the other side of the operation, as well.
  • PERSIST: When an entity is persisted to the EntityManager, persist the entities on the other side of the operation, as well.
  • 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, 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 source code for the BookRepository.

Listing 5. BookRepository.java

package com.geekcap.javaworld.jpa.repository;
import com.geekcap.javaworld.jpa.model.Book;
import javax.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> 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();
    }
    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();
    }
}

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 more 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().

A closer look at 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 query, using JPQL, 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 we'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 Author repository

Listing 6 shows the source code for the AuthorRepository.

Listing 6. AuthorRepository.java

package com.geekcap.javaworld.jpa.repository;
import com.geekcap.javaworld.jpa.model.Author;
import javax.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> 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();
    }
    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();
    }
}

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

Example application for Hibernate JPA

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.geekcap.javaworld.jpa;
import com.geekcap.javaworld.jpa.model.Author;
import com.geekcap.javaworld.jpa.model.Book;
import com.geekcap.javaworld.jpa.repository.AuthorRepository;
import com.geekcap.javaworld.jpa.repository.BookRepository;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.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 our sample application does is create an EntityManagerFactory:


EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("Books");
1 2 3 4 Page 3
Page 3 of 4