Search code examples
javahibernatejpalazy-initialization

How does merge avoid LazyInitException?


This is my PersistenceUtil class..

package biz.tugay.learningjpa.persistence;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class PersistenceUtilImpl implements PersistenceUtil {

    private EntityManagerFactory entityManagerFactory;

    public PersistenceUtilImpl(final String persistenceUnit) {
        entityManagerFactory = Persistence.createEntityManagerFactory(persistenceUnit);
    }

    public EntityManager getEntityManager() {
        final EntityManager entityManager = entityManagerFactory.createEntityManager();
        return entityManager;
    }

    public void closeFactory() {
        entityManagerFactory.close();
    }
}

And these are my Entities..

Actor.java

package biz.tugay.learningjpa.model;

import javax.persistence.*;
import java.util.List;

@Entity
public class Actor {

    private Integer id;
    private String firstname;
    private String lastname;

    private List<Film> films;

    @Id
    @Column(name = "actor_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Integer getId() {
        return id;
    }

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

    @Basic
    @Column(name = "first_name")
    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String first_name) {
        this.firstname = first_name;
    }

    @Basic
    @Column(name = "last_name")
    public String getLastname() {
        return lastname;
    }

    public void setLastname(String last_name) {
        this.lastname = last_name;
    }

    @ManyToMany
    @JoinTable(name = "film_actor",
            joinColumns = @JoinColumn(name = "actor_id"),
            inverseJoinColumns = @JoinColumn(name = "film_id")
    )
    public List<Film> getFilms() {
        return films;
    }

    public void setFilms(List<Film> films) {
        this.films = films;
    }
}

and Film.java

package biz.tugay.learningjpa.model;

import javax.persistence.*;
import java.util.List;

@Entity
public class Film {

    private Integer id;
    private String title;

    private List<Actor> actors;

    @Id
    @Column(name = "film_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Integer getId() {
        return id;
    }

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

    @Basic
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @ManyToMany
    @JoinTable(name = "film_actor",
            joinColumns = @JoinColumn(name = "film_id"),
            inverseJoinColumns = @JoinColumn(name = "actor_id"))
    public List<Actor> getActors() {
        return actors;
    }

    public void setActors(List<Actor> actors) {
        this.actors = actors;
    }
}

And I have 2 Services for my Models:

ActorServiceImpl.java

public final class ActorServiceImpl implements ActorService {

    private final PersistenceUtil persistenceUtil;

    public ActorServiceImpl(PersistenceUtil persistenceUtil) {
        this.persistenceUtil = persistenceUtil;
    }

    @Override
    public List<Actor> getActorsOfFilm(Film film) {
        final EntityManager entityManager = persistenceUtil.getEntityManager();
        final Film mergedFilm = entityManager.merge(film);
        final ArrayList<Actor> actors = new ArrayList<Actor>();
        actors.addAll(mergedFilm.getActors());
        entityManager.close();
        return actors;
    }
}

and FilmServiceImpl.java

public final class FilmServiceImpl implements FilmService {

    private final PersistenceUtil persistenceUtil;

    public FilmServiceImpl(final PersistenceUtil persistenceUtil) {
        this.persistenceUtil = persistenceUtil;
    }

    @Override
    public Film filmById(int filmId) {
        final EntityManager entityManager = persistenceUtil.getEntityManager();
        final Film film = entityManager.find(Film.class, filmId);
        entityManager.close();
        return film;
    }
}

I have 2 tests as seen below:

public class ActorServiceImplTest {

    private PersistenceUtilImpl persistenceUtil;
    private ActorServiceImpl actorService;

    @Before
    public void init() {
        persistenceUtil = new PersistenceUtilImpl("sakiladb");
        actorService = new ActorServiceImpl(persistenceUtil);
    }

    @After
    public void tearDown() {
        persistenceUtil.closeFactory();
    }

    @Test(expected = LazyInitializationException.class)
    public void testGetActorsOfFilmLazyInit() {
        final FilmServiceImpl filmService = new FilmServiceImpl(persistenceUtil);
        final Film film = filmService.filmById(1);
        for (Actor actor : film.getActors()) {
            System.out.println(actor.getFirstname());
        }
    }

    @Test
    public void testGetActorsOfFilm() {
        final FilmServiceImpl filmService = new FilmServiceImpl(persistenceUtil);
        final Film film = filmService.filmById(1);
        final List<Actor> actorsOfFilm = actorService.getActorsOfFilm(film);
        for (Actor actor : actorsOfFilm) {
            System.out.println(actor.getFirstname());
        }
    }
}

So the first test throws a LazyInitException, which is fine.. EntityManager is closed by the time I am getting the actors of the film..

Regarding the second test, the one called "testGetActorsOfFilm".. As it is seen, it passes and prints the Actors in the console from the database.. This is accomplished by entityManager.merge(film); as far as I understand. We are passing a detached film object, we merge it into the PersistenceContext, then we get the actors of the film in the implementation.

But, what I want to ask is, why does this test fail:

@Test
public void testGetActorsOfFilm() {
    final Film film = new Film();
    film.setId(1);
    final List<Actor> actorsOfFilm = actorService.getActorsOfFilm(film);
    for (Actor actor : actorsOfFilm) {
        System.out.println(actor.getFirstname());
    }
}

My understanding is, I am creating a Film object where ID is 1 and passing it into the service method. So entityManager.merge(film) should load the film from Database and synchronize it to the Persistence Context? However all I am seeing is:

java.lang.NullPointerException
    at java.util.ArrayList.addAll(ArrayList.java:559)
    at biz.tugay.learningjpa.service.ActorServiceImpl.getActorsOfFilm(ActorServiceImpl.java:57)

So I am confused, what is the difference between the detached entity I have in first situation and the second situation? Does not merge just work with the id?


Solution

  • merge(film) gets the film with the given film ID from the database (let's call it managedFilm), copies all the state from film to managedFilm, and returns managedFim.

    Since the actors list is null by default, and since this null field is copied to managedFilm, iterating on it throws a NullPointerException.

    You shouldn't use merge(), unless your intention is really to copy the state of the detached film to the managed one (and thus potentially modify the film by calling getActorsOfFilm(), which would then be really badly named and have two very different responsibilities). Just pass the film ID instead of the film and get the managed film from the database using EntityManager.find().