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 identifiesAuthor
as a JPA entity. - The
@Table
annotation tells Hibernate that this entity should be stored in the AUTHOR table. - The
@Table
annotation also defines anAuthor.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. CascadeType
s include the following:
DETACH
: When an entity is detached from theEntityManager
, detach the entities on the other side of the operation, as well.MERGE
: When an entity is merged into theEntityManager
, merge the entities on the other side of the operation, as well.PERSIST
: When an entity is persisted to theEntityManager
, persist the entities on the other side of the operation, as well.REFRESH
: When an entity is refreshed from theEntityManager
, also refresh the entities on the other side of the operation.FLUSH
: When an entity is flushed to theEntityManager
, 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 theJPQL SELECT
statement and returns the results as aList
; if no results are found then it returns an empty list.getSingleResult
executes theJPQL SELECT
statement and returns a single result; if no results are found then it throws aNoResultException
.
In the findAll()
method, we execute the Query
's getResultList()
method and return the list of Book
s 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 Author
s instead of Book
s.
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");