Исключение LazyInitializationException, даже если используется @Transactional

#java #spring #spring-boot #hibernate

#java #весна #весенняя загрузка #спящий режим

Вопрос:

В настоящее время я получаю следующее исключение:

org.спящий режим.Исключение LazyInitializationException: не удалось лениво инициализировать коллекцию role: com.example.model.Link.subLinks, не удалось инициализировать прокси-сервер — нет сеанса

Я погуглил и нашел другое решение для этого исключения, но я хотел бы знать, почему @Transactional в моем случае не работает. Я уверен, что я делаю что-то не так. Что я делаю не так?

Странная часть заключается в том, что я уже использовал @Transactional где-то еще в этом проекте, и там это работает.

Заранее благодарю вас за любые советы.

Мой класс ссылок: ссылка на объект содержит коллекцию субликаций.

 @Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Link {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    
    @NotEmpty
    private String title;
    
    @NotEmpty
    private String description;
    
    @OneToMany(mappedBy = "link")
    private Collection<SubLink> subLinks;
}
  

Мой класс сублинка:

 @Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class SubLink {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    
    @NotEmpty
    private String type;
    
    @NotEmpty
    @URL
    private String url;
    
    @ManyToOne
    @JoinColumn(name = "link_id", referencedColumnName = "id")
    private Link link;
}
  

Мой класс LinkServiceImpl:

     @Service
    public class LinkServiceImpl implements LinkService {
        @Autowired
        public LinkRepository linkRepository;
        
        @Override
        @Transactional
        public Link findById(long id) {
            return linkRepository.findById(id);
        }
    }
  

В моем классе контроллера есть метод ShowLink():

     @GetMapping("/link/{linkId}")
    public String showLink(@PathVariable(value = "linkId") long linkId, Model model) {
        Link link = linkService.findById(linkId);

        
        Collection<SubLink> subLinkCollection = link.getSubLinks(); //Error
        model.addAttribute("subLinkCollection", subLinkCollection);
        return "link";
    }
  

Комментарии:

1. Не могли бы вы предоставить структуру объекта Link? Что будет полезно для понимания.

2. Привет, @SebinThomas , я добавил класс Link. Спасибо!

3. По умолчанию FetchType уже ГОТОВ, если вы не упомянули LAZY . Содержит ли ваш класс субликации какие-либо отложенные свойства?

4. Я также добавил класс SubLink. Но чтобы ответить на ваш вопрос: я думаю, что нет

5. В вашем репозитории вы использовали Query или QueryMethod? Если запрос, вы пробовали @Aman предложил? И если найти по идентификатору, в чем причина использования метода запроса? вы могли бы использовать метод по умолчанию, предоставляемый JpaRepository, т.е. findById . Просто уточняю у вас.

Ответ №1:

LazyInitializationException указывает на доступ к невыбранным данным вне контекста сеанса. Чтобы исправить это, вы должны получить все необходимые ассоциации в пределах вашего уровня обслуживания.

Я думаю, что в вашем случае вы извлекаете Link без its SubLink , но пытаетесь получить доступ к подссылкам. Итак, использование выборки СОЕДИНЕНИЯ было бы лучшей стратегией.

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

= ОБНОВИТЬ =

Спасибо crizzi за упоминание интересной функции под названием Open Session In View (OSIV) (включена по умолчанию в приложениях с весенней загрузкой). Хотя это зависит от того, кого вы можете попросить пометить его как шаблон или антипаттерн.

Основная цель — облегчить работу с отложенными ассоциациями (избегает LazyInitializationException ). Очень подробное объяснение можно найти на

Комментарии:

1. Привет, спасибо за вашу помощь. На самом деле, чтобы исправить это, я использую JOIN, но я хотел бы знать, почему «@Transactional» не работает. ‘@Transactional’ должен выполнять аналогичные действия, или я ошибаюсь?

2. Транзакция — это последовательность операций, рассматриваемых как единая единица работы. Например, в транзакции вы можете получить, а затем обновить объект. Если одна из операций завершается неудачно, считается, что обе завершились неудачно.

3. @tqnone вы сказали, что используете JOIN . это ОБЪЕДИНЕНИЕ или ОБЪЕДИНЕНИЕ ВЫБОРКИ ?

4.@tqnone, который вы помещаете @Transactional поверх findById , что означает, что контекст сохранения открыт для выполнения findById , и транзакция создается для выполнения findById . Теперь, если вы вызываете link.getSublinks() изнутри findById , ошибки не будет. Однако ваш код вызывается за link.getSublinks() пределами findById , где контекст сохранения уже закрыт. Я надеюсь, что это прояснит ситуацию

5. Большое, очень большое вам спасибо, ребята. Я понял! 🙂 @[crizzis, Aman, doctore]

Ответ №2:

Аман хорошо объяснил причину LazyInitializationException в вашем случае, а crizzis добавил дополнительную информацию о том, почему @Transactional работает не так, как вы ожидали, и как ее следует использовать.

Итак, чтобы обобщить варианты, которые вы должны решить свою проблему:


Нетерпеливая выборка

По комментариям Амана и Криззиса, это проще, но не лучше с точки зрения производительности.

Чтобы сделать это, включите fetch = FetchType.EAGER в subLinks определение свойства:

 @OneToMany(mappedBy = "link", fetch = FetchType.EAGER)
private Collection<SubLink> subLinks;
  

Однако каждый раз, когда вы получаете из базы данных экземпляры Link => SubLink , будет включена его коллекция связанных.


Пользовательский запрос

Также прокомментировано Aman (хотя в этом нет необходимости on (link.id = sublink.link.id) ). В этом случае вы можете создать новый метод в своем репозитории, чтобы получить SubLink определенную коллекцию Link .

Таким образом, вы сможете иметь дело с ситуациями, в которых вы хотите получить такую «дополнительную информацию» ( findWithSubLinkById ) или нет (при условии findById ).

При двунаправленном OneToMany достаточно чего-то вроде:

 @Query("select l from Link l left join fetch l.subLinks where l.id = :id")
Link findWithSubLinkById(Long id);
  

При однонаправленном OneToMany , помимо приведенного выше запроса, вы должны указать, как «объединить» обе таблицы:

 @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "link_id")
private Collection<SubLink> subLinks;
  

Удаление link свойства из SubLink класса.

Дополнительная информация в ассоциациях гибернации и однонаправленных советах «один ко многим»


EntityGraph

Это вариант предыдущего. Таким образом, вместо создания нового метода с помощью @Query , более простой выбор будет настроен с помощью @EntityGraph :

 @EntityGraph(attributePaths = "subLinks")
Link findWithSubLinkById(Long id);
  

Дополнительная информация здесь ( Example 77 )


Работа внутри транзакционного метода

Как прокомментировал Криззис, внутри метода, настроенного с @Transactional вами, вы не получите такого исключения. Итак, другой вариант — переместить код, который использует Sublink ваш контроллер, в вашу службу.