Как разрешить исключение 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
.