问题 如何解决Spring Data JPA中的LazyInitializationException?


我必须拥有一对多关系的类。当我尝试访问延迟加载的集合时,我得到了 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的基于注释的配置来完成此操作。 有人可以指出我该怎么做?

附:我想使用延迟加载,因此急切加载是没有选择的。


2101
2017-10-22 12:28


起源



答案:


基本上,您需要在事务内部获取惰性数据。如果你的服务类是 @Transactional,当你在他们身边时,一切都应该没问题。一旦你离开了服务类,如果你试图 get 懒惰的集合,你会得到你的异常 main() 方法,线 System.out.println(random.getCreatedJobs());

现在,它归结为您的服务方法需要返回的内容。如果 userService.getRandomUser() 预计会返回一个初始化作业的用户,这样你就可以操作它们,然后这个方法负责获取它。使用Hibernate执行此操作的最简单方法是调用 Hibernate.initialize(user.getCreatedJobs())


6
2017-10-22 12:44



因此,与数据库的每次交互都必须在事务上下文中完成,事务上下文确保会话可用?是否可以将会话与当前线程相关联,以便会话始终可用? - stevecross
@feuerball,你是什么意思 to associate a session with the current thread?内部hibernate使用会话对象来获取数据库连接,一旦事务完成,会话也会与数据库连接一起关闭。因此,即使您尝试在事务上下文之外保持会话,也没有用处。 - Chaitanya
@feuerball你可以放 @Transactional 在其他级别,而不是服务类,但服务层是他们的自然场所。服务方法封装了业务操作,应该将其作为一个整体提交给数据库,或者作为一个整体回滚。任何其他“与当前线程关联的会话”,恕我直言,尽管可能,但是滥用这个框架并且可能导致应用层之间的界限突破。 - Predrag Maric
谢谢你澄清这一点。但想象一下,我有一个webapp用户可以登录写一些帖子。当用户成功登录时,有关他的一些信息与http会话相关联。现在假设有一个页面列出了他的所有帖子。他之前不需要这些帖子 posts 集合为空,访问它将再次导致异常。我是否必须调用类似的服务方法 getPostsByUser(User user)?懒惰加载不会完全没用吗? - stevecross
如果您很可能需要用户的帖子,那么当您获取用户时,急切地获取它们是有意义的。但是你可能会遇到陈旧数据的问题,同时创建的帖子将不可见(即使懒惰地提取它们也行不通,你仍然不会在最初的getter调用之后得到最新的帖子)。这就是我认为服务电话是更好的选择的原因。我同意延迟加载在这个用例中并不完全有用,但是还有很多其他用例。 - Predrag Maric


答案:


基本上,您需要在事务内部获取惰性数据。如果你的服务类是 @Transactional,当你在他们身边时,一切都应该没问题。一旦你离开了服务类,如果你试图 get 懒惰的集合,你会得到你的异常 main() 方法,线 System.out.println(random.getCreatedJobs());

现在,它归结为您的服务方法需要返回的内容。如果 userService.getRandomUser() 预计会返回一个初始化作业的用户,这样你就可以操作它们,然后这个方法负责获取它。使用Hibernate执行此操作的最简单方法是调用 Hibernate.initialize(user.getCreatedJobs())


6
2017-10-22 12:44



因此,与数据库的每次交互都必须在事务上下文中完成,事务上下文确保会话可用?是否可以将会话与当前线程相关联,以便会话始终可用? - stevecross
@feuerball,你是什么意思 to associate a session with the current thread?内部hibernate使用会话对象来获取数据库连接,一旦事务完成,会话也会与数据库连接一起关闭。因此,即使您尝试在事务上下文之外保持会话,也没有用处。 - Chaitanya
@feuerball你可以放 @Transactional 在其他级别,而不是服务类,但服务层是他们的自然场所。服务方法封装了业务操作,应该将其作为一个整体提交给数据库,或者作为一个整体回滚。任何其他“与当前线程关联的会话”,恕我直言,尽管可能,但是滥用这个框架并且可能导致应用层之间的界限突破。 - Predrag Maric
谢谢你澄清这一点。但想象一下,我有一个webapp用户可以登录写一些帖子。当用户成功登录时,有关他的一些信息与http会话相关联。现在假设有一个页面列出了他的所有帖子。他之前不需要这些帖子 posts 集合为空,访问它将再次导致异常。我是否必须调用类似的服务方法 getPostsByUser(User user)?懒惰加载不会完全没用吗? - stevecross
如果您很可能需要用户的帖子,那么当您获取用户时,急切地获取它们是有意义的。但是你可能会遇到陈旧数据的问题,同时创建的帖子将不可见(即使懒惰地提取它们也行不通,你仍然不会在最初的getter调用之后得到最新的帖子)。这就是我认为服务电话是更好的选择的原因。我同意延迟加载在这个用例中并不完全有用,但是还有很多其他用例。 - Predrag Maric


你有2个选择。

选项1 : 如BetaRide所述,使用 EAGER 抓取策略

选项2: 得到之后 user 从使用hibernate的数据库中,添加以下代码行来加载集合元素:

Hibernate.initialize(user.getCreatedJobs())

这告诉hibernate初始化集合元素


2
2017-10-22 12:41



但他没有直接使用Hibernate,他使用“repository.findAll();”。当该方法返回时,会话已经消失,并抛出了lazyinitializationexception。我现在遇到这个问题而且我不知道如何解决它。我不想使用eager,我想使用存储库方法进行延迟加载。 - Bruno Negrão Zica


考虑使用 JPA 2.1,与 实体图

延迟加载通常是JPA 2.0的一个问题。您必须在实体FetchType.LAZY或FetchType.EAGER中定义,并确保在事务中初始化关系。

这可以通过以下方式完成:

  • 使用读取实体的特定查询
  • 或者通过访问业务代码中的关系(每个关系的附加查询)。

这两种方法都远非完美, JPA 2.1实体图是更好的解决方案


2
2017-10-21 11:39





更改

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

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

或者在服务中使用Hibernate.initialize,它具有相同的效果。


0
2017-10-22 12:34



我认为这个答案的贬值是不公平的。它在某些情况下缺乏,但它完全正确。这是必要的,因为默认 FetchType 对于 @OneToMany 懒惰。改变了 FetchType 急切导致在会话关闭之前将子对象包含在对象图中,从而解决了问题(性能命中已确认)。 - 8bitjunkie
他想让懒惰的加载工作,所以他可以在必要时加载,他不想使用急切的提取。 - Bruno Negrão Zica


对于那些无法使用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);
    }
}

0
2017-10-26 10:39



readOnly 不是有效的属性 javax.transaction.Transactional。当我尝试使用它时,我的IDE在读取时强调它并告诉我这是无效的。 - 8bitjunkie