【问题标题】:How to resolve LazyInitializationException in Spring Data JPA?如何解决 Spring Data JPA 中的 LazyInitializationException?
【发布时间】:2014-12-17 21:39:42
【问题描述】:

我有两个具有一对多关系的类。当我尝试访问延迟加载的集合时,我得到了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;
    }

}

用户存储库

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

用户服务

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

工作服务

@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 的基于注释的配置来做到这一点。 有人能指出我该怎么做吗?

附:我想使用延迟加载,因此无法使用急切加载。

【问题讨论】:

    标签: java spring hibernate


    【解决方案1】:

    改变

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

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

    或者在你的服务中使用 Hibernate.initialize,效果是一样的。

    【讨论】:

    • 我认为这个答案的反对票是不公平的。它在某些情况下是缺乏的,但在其他方面是完全正确的。这是必要的,因为 @OneToMany 的默认 FetchType 是 Lazy。将FetchType 更改为 eager 会导致子对象在会话关闭之前包含在对象图中,从而解决问题(确认性能命中)。
    • 他想让延迟加载工作,以便在必要时加载,他不想使用急切获取。
    【解决方案2】:

    你有两个选择。

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

    选项 2: 使用 hibernate 从数据库中获取 user 后,添加以下代码行以加载集合元素:

    Hibernate.initialize(user.getCreatedJobs())
    

    这告诉hibernate初始化集合元素

    【讨论】:

    • 但是他没有直接使用Hibernate,他使用“repository.findAll();”。当该方法返回时,会话已经消失并抛出了延迟初始化异常。我现在遇到这个问题,我不知道如何解决它。我不想使用 Eager,我想让延迟加载与存储库方法一起工作。
    • @BrunoNegrãoZica 你有没有想过如何解决这个问题?我现在正在经历同样的问题。
    【解决方案3】:

    基本上,您需要在事务中获取惰性数据。如果您的服务类是@Transactional,那么当您在其中时,一切都应该没问题。一旦你退出服务类,如果你尝试get惰性集合,你会得到那个异常,它在你的main()方法中,行System.out.println(random.getCreatedJobs());

    现在,归结为您的服务方法需要返回什么。如果 userService.getRandomUser() 预计会返回一个用户初始化作业,以便您可以操作它们,那么获取它是该方法的责任。使用 Hibernate 最简单的方法是调用 Hibernate.initialize(user.getCreatedJobs())

    【讨论】:

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

    考虑使用 JPA 2.1实体图

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

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

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

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

    【讨论】:

      【解决方案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);
          }
      }
      

      【讨论】:

      • readOnly 不是javax.transaction.Transactional 的有效属性。当我尝试使用它时,我的 IDE 在 read 中加了下划线并告诉我这是无效的。
      • @8bitjunkie,我相信这个例子中使用的是 org.springframework.transaction.annotation.Transactional。
      【解决方案6】:

      您应该通过在上下文配置类中添加@EnableTransactionManagement 注释来启用 Spring 事务管理器。

      由于两个服务都有@Transactional 注释并且默认value 属性是TxType.Required,如果事务管理器打开,当前事务将在服务之间共享。因此会话应该可用,而您不会收到LazyInitializationException

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-05-22
        • 2016-03-26
        • 2020-08-21
        • 2010-10-09
        • 2018-08-11
        • 2019-09-30
        • 2012-09-28
        相关资源
        最近更新 更多