Как разрешить исключение LazyInitializationException в Spring данных JPA?

У меня есть классы, которые имеют отношение "один ко многим". Когда я пытаюсь получить доступ к лениво загруженной коллекции, я получаю LazyInitializationException. Я ищу в Интернете какое-то время, и теперь я знаю, что получаю исключение, потому что сеанс, который использовался для загрузки класса, содержащего коллекцию, закрыт. Однако я не нашел решения (или, по крайней мере, не понял их). В основном у меня есть эти классы:

Пользователь

@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();
    }

}

Работа

@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;
                }
            }
        }
    }

}

приложения

@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());
    }

}

Я часто читал, что сеанс должен быть привязан к текущему потоку, но я не знаю, как это сделать с помощью Spring основанных на аннотациях конфигураций. Может кто-нибудь указать мне, как это сделать?

P.S. Я хочу использовать ленивую загрузку, поэтому загружаемая загрузка не является вариантом.

Ответ 1

В принципе, вам нужно получить ленивые данные, пока вы находитесь внутри транзакции. Если ваши классы обслуживания @Transactional, тогда все должно быть в порядке, пока вы в них. Как только вы выйдете из класса обслуживания, если вы попытаетесь get ленивую коллекцию, вы получите это исключение, которое находится в вашем методе main(), строка System.out.println(random.getCreatedJobs());.

Теперь это сводится к тому, что нужно вернуть вашим методам обслуживания. Если ожидается, что userService.getRandomUser() вернет пользователя с инициализацией заданий, чтобы вы могли ими манипулировать, то это способ ответственности за его получение. Самый простой способ сделать это с помощью Hibernate - это вызвать Hibernate.initialize(user.getCreatedJobs()).

Ответ 2

У вас есть 2 варианта.

Вариант 1: Как указано BetaRide, используйте стратегию выборки EAGER

Вариант 2:. Получив user из базы данных с помощью спящего режима, добавьте нижеприведенную строку кода для загрузки элементов коллекции:

Hibernate.initialize(user.getCreatedJobs())

Это говорит о спящем режиме для инициализации элементов коллекции

Ответ 3

Рассмотрим использование JPA 2.1, с графами Entity:

Ленивая загрузка часто была проблемой с JPA 2.0. Вы должны были определить в объекте FetchType.LAZY или FetchType.EAGER и убедиться, что отношение инициализируется в транзакции.

Это можно сделать:

  • с использованием специального запроса, который читает объект
  • или путем обращения к отношениям внутри бизнес-кода (дополнительный запрос для каждого отношения).

Оба подхода далеки от совершенства, Графы сущности JPA 2.1 являются лучшим решением для него:

Ответ 4

Изменить

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

к

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

Или используйте Hibernate.initialize внутри вашего сервиса, который имеет тот же эффект.

Ответ 5

Для тех, кто не имеет возможности использовать JPA 2.1, но хочет сохранить возможность вернуть объект в свой контроллер (а не String/JsonNode/byte []/void с записью в ответ):

все еще есть возможность построить DTO в транзакции, которая будет возвращена контроллером.

@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);
    }
}

Ответ 6

Вы должны включить диспетчер транзакций Spring, добавив аннотацию @EnableTransactionManagement к вашему классу конфигурации контекста.

Поскольку обе службы имеют аннотацию @Transactional и свойство value умолчанию, это TxType.Required, текущая транзакция будет использоваться совместно с сервисами при условии, что менеджер транзакций TxType.Required. Таким образом, сеанс должен быть доступен, и вы не LazyInitializationException.