5

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
}

News Source:

@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 and NewsSourcesRepository are simple JpaRepositories from Spring Data JPA. Their configuration is as follow:

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

}

My test throws an LazyInitializationException on line 15. The message is:

failed to lazily initialize a collection of role: news.entities.users.User.newsSources, could not initialize proxy - no Session

    @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));
    }

If I annotate my test as @Transactional exception is not thrown, but I'm not sure is this a proper way to solve this.

Michał Mielec
  • 1,199
  • 12
  • 32

1 Answers1

12

ManyToMany annotation by default it's fetch type is lazy

FetchType fetch() default LAZY;

In your case, newsSources in User class will be lazily fetched.

Let's assume that you are directly not using Transactional Annotation for the sake of simplicity. For any database operation a transaction is required. As you are using spring data jpa repositories, Transactional annotation is applied to all jpa repository methods. The transactions for these method calls begin when the method is called and ends when the method execution is finished. The last statement holds unless there is an outer transaction of the same database.

Consider following lines,

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

Transaction starts and ends in the usersRepository.findOne(EMAIL) itself. Now "User savedUser" object has newsSources which will be loaded lazily. So when you call savedUser.getNewsSources(), it tries to load lazily using the persistence session. As the transaction context was already closed there is no active associated session.

Now if you add Transactional annotation to the method annotated with Test annotation, transaction begins here itself and now when savedUser.getNewsSources() is called, this same transaction will be used. And now when you do savedUser.getNewsSources(), there is an associated session with it and therefore will work properly.

There is nothing wrong in putting transactional annotation on test method. As the mapping is lazy, somewhere you have to put transactional annotation. Here as you are directly calling jpa repository method within test method and performing operation on lazy reference object, you definitely have to use transactional annotation on the annotated test method.

Similar question: LazyInitializationException: failed to lazily initialize a collection of roles, could not initialize proxy - no Session

Community
  • 1
  • 1
prem kumar
  • 4,631
  • 2
  • 20
  • 32
  • you answer helps a lot . If I define interface:public interface UserRepository extends JpaRepository { User findByUserName(String userName); ....} and use userRepository.findByUserName("aa").getUserName() will ok in test, can you explain why? – yuxh Mar 03 '18 at 03:23
  • username field does not needed to be loaded lazily as it is in the same table. whereas as a one to many or many to many can be loaded lazily as it involves other tables and joins. is that what you asked for? – prem kumar Mar 03 '18 at 06:06
  • please check my question:https://stackoverflow.com/questions/49080175/spring-data-jpa-getone-throw-lazyinitializationexception-and-findby-not – yuxh Mar 03 '18 at 07:53
  • see if this helps.. https://stackoverflow.com/questions/33218762/difference-between-crudrepository-findone-and-jparepository-getone i have not used getOne... one reason for this could be for checking the existence of a record rather than fetch details. Usually ids are indexed but to fetch details it has to read from where table information is stored.. try to think along those lines.. checkout this link as well https://stackoverflow.com/questions/1607532/when-to-use-entitymanager-find-vs-entitymanager-getreference?noredirect=1&lq=1 – prem kumar Mar 03 '18 at 11:48
  • _"As you are using spring data jpa repositories, **Transactional annotation is applied to all jpa repository methods**. The transactions for these method calls begin when the method is called and ends when the method execution is finished"_ Are you therefore saying that, if I have a `Service` class method annotated with `@Transactional` and that method calls a Spring JPA repository, then the JPA repository transaction will be extended to the `Service` class method? Should I therefore **not** get a `LazyIniitalizationException` in the `Service` method class because I have used `@Transactional`? – 8bitjunkie Mar 08 '18 at 18:46