使用Spring Data JpaRepositories时发生懒加载异常

7

I have following entities:

User:

@Entity
public class User {

    @Id
    @Column(nullable = false)
    private String email = "";

    @Column(nullable = false)
    private String nickname = "";

    @Column(nullable = false)
    private String password = "";

    @ManyToMany(cascade = CascadeType.ALL)
    private List<NewsSource> newsSources;

    // getters and setters
}

新闻来源:

@Entity
public class NewsSource {

    @Id
    @Column(nullable = false)
    private URL url;

    private LocalDateTime updateTime;

    @OneToMany(cascade = CascadeType.ALL)
    private List<News> newses;

    @ManyToMany(cascade = CascadeType.ALL)
    private List<User> users;
}

UsersRepository和NewsSourcesRepository是Spring Data JPA中简单的JpaRepositories。它们的配置如下:

@Configuration
@EnableTransactionManagement
@PropertySource("classpath:database_config.properties")
@EnableJpaRepositories(basePackages = {"news.repositories" })
public class RepositoriesConfiguration {

    @Bean(destroyMethod = "close")
    DataSource dataSource(Environment env) {
        HikariConfig dataSourceConfig = new HikariConfig();
        dataSourceConfig.setDriverClassName(env.getRequiredProperty("db.driver"));
        dataSourceConfig.setJdbcUrl(env.getRequiredProperty("db.url"));
        dataSourceConfig.setUsername(env.getRequiredProperty("db.username"));
        dataSourceConfig.setPassword(env.getRequiredProperty("db.password"));

        return new HikariDataSource(dataSourceConfig);
    }

    @Bean
    LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource);
        entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManagerFactoryBean.setPackagesToScan("pl.mielecmichal.news.entities");

        Properties jpaProperties = new Properties();
        jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
        jpaProperties.put("hibernate.hbm2ddl.auto", env.getRequiredProperty("hibernate.hbm2ddl.auto"));
        jpaProperties.put("hibernate.ejb.naming_strategy", env.getRequiredProperty("hibernate.ejb.naming_strategy"));
        jpaProperties.put("hibernate.show_sql", env.getRequiredProperty("hibernate.show_sql"));
        jpaProperties.put("hibernate.format_sql", env.getRequiredProperty("hibernate.format_sql"));
        entityManagerFactoryBean.setJpaProperties(jpaProperties);

        return entityManagerFactoryBean;
    }

    @Bean
    JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

}

我的测试在第15行抛出了一个LazyInitializationException异常。错误信息如下:

无法延迟初始化角色集合:news.entities.users.User.newsSources,无法初始化代理对象 - 未找到会话

    @Test
    public void cascadeRelationsShouldBeRetrieved() throws MalformedURLException {
        NewsSource source = new NewsSource();
        source.setUrl(new URL(SOME_URL));
        newsSourcesRepository.save(source);
        newsSourcesRepository.flush();

        User user = new User();
        user.setEmail(EMAIL);
        List<NewsSource> sources = new ArrayList<>();
        sources.add(source);
        user.setNewsSources(sources);
        usersRepository.save(user);
        usersRepository.flush();

        User savedUser = usersRepository.findOne(EMAIL);
        NewsSource newsSource = savedUser.getNewsSources().get(0);
        assertThat("News source should be saved", newsSource.getUrl(), is(SOME_URL));

        NewsSource savedSource = newsSourcesRepository.findOne(newsSource.getUrl());
        assertThat("New user should be saved in M2M relation", savedSource.getUsers(), Matchers.contains(user));
    }

如果我用 @Transactional 注释我的测试,异常将不会被抛出,但我不确定这是否是解决问题的适当方式。


你尝试在查询中使用join fetch了吗? - Pulkit
我不写任何查询,Spring Data会完成这项工作。 - Michał Mielec
可以使用存储库接口中的@Query注释编写自己的查询。 - Pulkit
1个回答

15

默认情况下,ManyToMany注释的获取类型是lazy

FetchType fetch() default LAZY;

在您的情况下,User类中的newsSources将会被延迟获取。

为了简单起见,假设您直接没有使用Transactional注解。任何数据库操作都需要进行事务处理。由于您使用的是Spring Data JPA存储库,故所有JPA存储库方法都会应用Transactional注解。这些方法调用的事务在调用方法时开始,并在方法执行结束时结束。除非存在同一个数据库的外部事务。

考虑以下几行:

User savedUser = usersRepository.findOne(EMAIL);
NewsSource newsSource = savedUser.getNewsSources().get(0);

事务在usersRepository.findOne(EMAIL)中开始和结束。现在"User savedUser"对象有newsSources,这将被懒加载。因此,当您调用savedUser.getNewsSources()时,它会尝试使用持久性会话进行懒加载。由于事务上下文已经关闭,因此没有活动的关联会话。

现在,如果您将Transactional注释添加到使用Test注释注释的方法中,则事务将从这里开始,并且当调用savedUser.getNewsSources()时,将使用相同的事务。现在,当您执行savedUser.getNewsSources()操作时,它将与一个关联的会话一起工作,因此可以正常工作。

在测试方法上放置transactional注释没有任何问题。由于映射是懒惰的,所以必须在某个地方放置transactional注释。在这里,由于您直接在测试方法中调用jpa存储库方法并在延迟引用对象上执行操作,因此必须在使用注释的测试方法上使用transactional注释。

类似问题: LazyInitializationException: failed to lazily initialize a collection of roles, could not initialize proxy - no Session


你的回答帮了我很多。如果我定义了接口:public interface UserRepository extends JpaRepository<User, Long> { User findByUserName(String userName); ....} 并使用userRepository.findByUserName("aa").getUserName()在测试中可以正常工作,你能解释一下为什么吗? - yuxh
用户名字段不需要懒加载,因为它在同一张表中。而一对多或多对多可以懒加载,因为涉及其他表和连接。这是你要求的吗? - prem kumar
请检查我的问题:https://dev59.com/hlUM5IYBdhLWcg3wQeiJ - yuxh
看看这个是否有帮助.. https://dev59.com/MZDea4cB1Zd3GeqPYz6P
我没有使用过getOne... 其中一个原因可能是为了检查记录的存在而不是获取详细信息。通常,id是索引的,但要获取详细信息,它必须从存储表信息的位置读取.. 尝试沿着这些线路思考.. 还可以查看此链接 https://dev59.com/kHI-5IYBdhLWcg3w8NOB?noredirect=1&lq=1
- prem kumar
由于您正在使用Spring Data JPA存储库,因此所有JPA存储库方法都应用了Transactional注释。这些方法调用的事务在调用方法时开始,并在方法执行完成时结束。因此,您是否意味着,如果我有一个带有@Transactional注释的Service类方法,并且该方法调用Spring JPA存储库,那么JPA存储库事务将扩展到Service类方法?因为我使用了@Transactional,所以我是否不会Service方法类中遇到LazyIniitalizationException - 8bitjunkie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接