21

I have two classes that have a one-to-many relation. When I try to access the lazily loaded collection I get the LazyInitializationException. I have been searching the web for a while and now I know that I get the exception because the session that was used to load the class which holds the collection is closed. However, I did not find a solution (or at least I did not understand them). Basically I have these classes:

User

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private long id;

    @OneToMany(mappedBy = "creator")
    private Set<Job> createdJobs = new HashSet<>();

    public long getId() {
        return id;
    }

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

    public Set<Job> getCreatedJobs() {
        return createdJobs;
    }

    public void setCreatedJobs(final Set<Job> createdJobs) {
        this.createdJobs = createdJobs;
    }

}

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {}

UserService

@Service
@Transactional
public class UserService {

    @Autowired
    private UserRepository repository;

    boolean usersAvailable = false;

    public void addSomeUsers() {
        for (int i = 1; i < 101; i++) {
            final User user = new User();

            repository.save(user);
        }

        usersAvailable = true;
    }

    public User getRandomUser() {
        final Random rand = new Random();

        if (!usersAvailable) {
            addSomeUsers();
        }

        return repository.findOne(rand.nextInt(100) + 1L);
    }

    public List<User> getAllUsers() {
        return repository.findAll();
    }

}

Job

@Entity
@Table(name = "job")
@Inheritance
@DiscriminatorColumn(name = "job_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Job {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private long id;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User creator;

    public long getId() {
        return id;
    }

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

    public User getCreator() {
        return creator;
    }

    public void setCreator(final User creator) {
        this.creator = creator;
    }

}

JobRepository

public interface JobRepository extends JpaRepository<Job, Long> {}

JobService

@Service
@Transactional
public class JobService {

    @Autowired
    private JobRepository repository;

    public void addJob(final Job job) {
        repository.save(job);
    }

    public List<Job> getJobs() {
        return repository.findAll();
    }

    public void addJobsForUsers(final List<User> users) {
        final Random rand = new Random();

        for (final User user : users) {
            for (int i = 0; i < 20; i++) {
                switch (rand.nextInt(2)) {
                case 0:
                    addJob(new HelloWorldJob(user));
                    break;
                default:
                    addJob(new GoodbyeWorldJob(user));
                    break;
                }
            }
        }
    }

}

App

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class App {

    public static void main(final String[] args) {
        final ConfigurableApplicationContext context = SpringApplication.run(App.class);
        final UserService userService = context.getBean(UserService.class);
        final JobService jobService = context.getBean(JobService.class);

        userService.addSomeUsers();                                 // Generates some users and stores them in the db
        jobService.addJobsForUsers(userService.getAllUsers());      // Generates some jobs for the users

        final User random = userService.getRandomUser();            // Picks a random user

        System.out.println(random.getCreatedJobs());
    }

}

I have often read that the session has to be bound to the current thread, but I don't know how to do this with Spring's annotation based configurations. Can someone point me out how to do that?

P.S. I want to use lazy loading, thus eager loading is no option.

Morteza Bandi
  • 429
  • 2
  • 12
stevecross
  • 5,130
  • 6
  • 37
  • 79

6 Answers6

23

Basically, you need to fetch the lazy data while you are inside of a transaction. If your service classes are @Transactional, then everything should be ok while you are in them. Once you get out of the service class, if you try to get the lazy collection, you will get that exception, which is in your main() method, line System.out.println(random.getCreatedJobs());.

Now, it comes down to what your service methods need to return. If userService.getRandomUser() is expected to return a user with jobs initialized so you can manipulate them, then it's that method's responsibility to fetch it. The simplest way to do it with Hibernate is by calling Hibernate.initialize(user.getCreatedJobs()).

Predrag Maric
  • 21,996
  • 4
  • 45
  • 64
  • 1
    So every interaction with the databse has to be done in a transactional context and a transactional context ensures that a session is available? Is it possible to associate a session with the current thread so that the session is always available? – stevecross Oct 22 '14 at 13:08
  • 1
    @feuerball, what do you mean by `to associate a session with the current thread`? Internally hibernate uses the session object to get the database connection and once the transaction is completed then the session is also closed along with the DB connection. So even if you try to hold the session outside the transaction context then it is of no use. – Chaitanya Oct 22 '14 at 13:26
  • 1
    @feuerball You can put `@Transactional` on other levels, instead of service classes, but service layer is the natural place for them. Service methods encapsulate business operations, which should be committed to database as a whole, or rolled back as a whole. Any other "associating session to the current thread", IMHO, while probably possible, is a misuse of this framework and can lead to breaking boundaries between application layers. – Predrag Maric Oct 22 '14 at 13:35
  • 1
    Thanks for clarifying this. But imagine I have a webapp where a user can log in write some posts. When the user logged in succesfully, some information about him is associated with the http session. Now assume that there is a page where all his posts are listed. The posts were not needed before so his `posts` collection is empty and accessing it would again result in an exception. Would I instead have to call a service method like `getPostsByUser(User user)`? Wouldn't lazy loading be completely useless then? – stevecross Oct 23 '14 at 06:32
  • 1
    If there is a high probability that you will need user's posts, then it makes sense to fetch them eagerly when you fetch the user. But then you can have a problem with stale data, posts created in the meantime won't be visible (even if fetching them lazily would work, you still wouldn't get the latest posts after the initial getter call). That's why I think a service call is a better option. I agree that lazy loading isn't exactly useful in this use case, but there are many others where it is. – Predrag Maric Oct 23 '14 at 08:11
6

Consider using JPA 2.1, with Entity graphs:

Lazy loading was often an issue with JPA 2.0. You had to define at the entity FetchType.LAZY or FetchType.EAGER and make sure the relation gets initialized within the transaction.

This could be done by:

  • using a specific query that reads the entity
  • or by accessing the relation within business code (additional query for each relation).

Both approaches are far from perfect, JPA 2.1 entity graphs are a better solution for it:

Pleymor
  • 1,874
  • 1
  • 21
  • 35
4

You have 2 options.

Option 1 : As mentioned by BetaRide, use the EAGER fetching strategy

Option 2 : After getting the user from database using hibernate, add the below line in of code to load the collection elements:

Hibernate.initialize(user.getCreatedJobs())

This tells hibernate to initialize the collection elements

Chaitanya
  • 14,187
  • 31
  • 92
  • 131
  • 6
    But he does not use Hibernate directly, he uses "repository.findAll();". When that method returns the session is already gone and the lazyinitializationexception is thrown. I'm having this problem right now and I don't know how to fix it. I don't want to use eager, I want to make lazy loading work with the repository methods. – Bruno Negrão Zica Apr 17 '18 at 21:37
  • @BrunoNegrãoZica did you ever figure out how to solve this? I'm going through the same issue right now. – dyslexit Jul 13 '20 at 07:16
0

Change

@OneToMany(mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();

to

@OneToMany(fetch = FetchType.EAGER, mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();

Or use Hibernate.initialize inside your service, which has the same effect.

BetaRide
  • 14,756
  • 26
  • 84
  • 153
  • I think the downvotes for this answer are unfair. It is lacking in some context, but it is otherwise entirely correct. This is necessary because the default `FetchType` for `@OneToMany` is Lazy. Changing the `FetchType` to eager causes the child object to be included in the object graph before the session is closed, thus curing the problem (performance hit acknowledged). – 8bitjunkie Mar 08 '18 at 18:28
  • 4
    he wants to make lazy loading work so he can load when necessary, he does not want to use eager fetch. – Bruno Negrão Zica Apr 17 '18 at 21:40
0

For those who have not the possibility to use JPA 2.1 but want to keep the possibility to return a entity in their controller (and not a String/JsonNode/byte[]/void with write in response):

there is still the possibility to build a DTO in the transaction, that will be returned by the controller.

@RestController
@RequestMapping(value = FooController.API, produces = MediaType.APPLICATION_JSON_VALUE)
class FooController{

    static final String API = "/api/foo";

    private final FooService fooService;

    @Autowired
    FooController(FooService fooService) {
        this.fooService= fooService;
    }

    @RequestMapping(method = GET)
    @Transactional(readOnly = true)
    public FooResponseDto getFoo() {
        Foo foo = fooService.get();
        return new FooResponseDto(foo);
    }
}
Pleymor
  • 1,874
  • 1
  • 21
  • 35
  • `readOnly` is not a valid attribute for `javax.transaction.Transactional`. When I try to use it my IDE underlines it in read and tells me that this is not valid. – 8bitjunkie Mar 08 '18 at 18:41
  • @8bitjunkie, I believe it's org.springframework.transaction.annotation.Transactional that is used in this example. – dpelisek May 06 '20 at 16:12
0

You should enable Spring transaction manager by adding @EnableTransactionManagement annotation to your context configuration class.

Since both services have @Transactional annotation and default value property of it is TxType.Required, current transaction will be shared among the services, provided that transaction manager is on. Thus a session should be available, and you won't be getting LazyInitializationException.

oak
  • 139
  • 6