Max Sakharov
Max Sakharov

Reputation: 13

spring data hibernate lazy loading

I'm in the middle of learning Spring Data/Hibernate stuff and faced with known issue of lazy loading in Hibernate. I tried different approaches described in stackoverflow and other resources, but it looks like they don't work. I'm using hibernate in coupe with spring data annotation configuration. My approaches were:

  1. Plain loading of @OneToMany dependency with fetch = FetchType.LAZY. As expected I receive LazyInitializationException with message about

could not initialize proxy - no Session

  1. Using transactional repository. Same result as for the first point;
  2. Using Spring transaction template. Same result;
  3. Using @NamedEntityGraph of JPA 2.1. Here situation is more interesting. My test pass, however in SQL debug I can see data are not loaded lazy, but at the first time I querying repository. Another words child table joined to parent table first time I'm querying repository.

I pushed my test project to github, so it's possible to review it there https://github.com/megamaxskx/hibernate_lazy_fetch

UPDATED

Spring transaction template approach: Repository:

@Repository
public interface PlainParentRepository extends CrudRepository<PlainParent, Long> {

}

Configuration:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = REPOSITORY_LOCATION)
public class DBConfig {

    public static final String REPOSITORY_LOCATION = "com.lazyloadingtest";
    private static final String ENTITIES_LOCATION = "com.lazyloadingtest";

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
    }

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        jpaVendorAdapter.setDatabase(Database.H2);
        jpaVendorAdapter.setGenerateDdl(true);
        jpaVendorAdapter.setShowSql(true);
        return jpaVendorAdapter;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean lemfb = new LocalContainerEntityManagerFactoryBean();
        lemfb.setDataSource(dataSource());
        lemfb.setJpaVendorAdapter(jpaVendorAdapter());
        lemfb.setPackagesToScan(ENTITIES_LOCATION);
        lemfb.setJpaProperties(hibernateProperties());
        return lemfb;
    }

    protected Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.format_sql", true);
        return properties;
    }

}

Child class:

@Entity
public class EntityGraphChild implements Serializable {

    public static long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private long id;

    private String name;

    @ManyToOne
    private PlainParent parent;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public PlainParent getParent() {
        return parent;
    }

    public void setParent(PlainParent parent) {
        this.parent = parent;
    }
}

Parent class:

@Entity
public class PlainParent implements Serializable {

    public static long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private long id;

    private String name;

    @OneToMany(targetEntity = PlainChild.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<PlainChild> children;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<PlainChild> getChildren() {
        return children;
    }

    public void setChildren(Set<PlainChild> children) {
        this.children = children;
    }
}

Spring transaction template test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {
        DBConfig.class,
})
public class SpringTransactionTemplateTest {

    @Autowired
    private PlainParentRepository repository;

    @Autowired
    private JpaTransactionManager transactionManager;

    @Test
    public void testRepository() {
        PlainChild child1 = new PlainChild();
        child1.setName("first child");

        PlainChild child2 = new PlainChild();
        child2.setName("second child");

        PlainParent parent = new PlainParent();
        parent.setId(1l);
        HashSet<PlainChild> children = new HashSet<PlainChild>(Arrays.asList(child1, child2));
        parent.setChildren(children);
        repository.save(parent);

        TransactionTemplate txTemplate = new TransactionTemplate();
        txTemplate.setTransactionManager(transactionManager);

        Set<PlainChild> fromDB = txTemplate.execute(new TransactionCallback<Set<PlainChild>>() {
            public Set<PlainChild> doInTransaction(TransactionStatus transactionStatus) {
                PlainParent fromDB = repository.findOne(1L);
                return fromDB.getChildren();
            }
        });

        assertEquals(2, fromDB.size());
    }

 }

NamedEntityGraph approach:

Child:

@Entity
public class EntityGraphChild implements Serializable {

    public static long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private long id;

    private String name;

    @ManyToOne
    private EntityGraphParent parent;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public EntityGraphParent getParent() {
        return parent;
    }

    public void setParent(EntityGraphParent parent) {
        this.parent = parent;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EntityGraphChild child = (EntityGraphChild) o;

        if (id != child.id) return false;
        return name != null ? name.equals(child.name) : child.name == null;

    }
}

Parent:

@Entity
@NamedEntityGraph(
        name = "graph.Parent.children",
        attributeNodes = @NamedAttributeNode(value = "children")
)
public class EntityGraphParent implements Serializable {

    public static long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private long id;

    private String name;

    @OneToMany(targetEntity = EntityGraphChild.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<EntityGraphChild> children;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<EntityGraphChild> getChildren() {
        return children;
    }

    public void setChildren(Set<EntityGraphChild> children) {
        this.children = children;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EntityGraphParent parent = (EntityGraphParent) o;

        if (id != parent.id) return false;
        if (name != null ? !name.equals(parent.name) : parent.name != null) return false;
        return children != null ? children.equals(parent.children) : parent.children == null;

    }

    @Override
    public int hashCode() {
        int result = (int) (id ^ (id >>> 32));
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (children != null ? children.hashCode() : 0);
        return result;
    }
}

Test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {
        DBConfig.class,
})
public class EntityGraphParentRepositoryTest {

    @Autowired
    private EntityGraphParentRepository repository;

    @Test
    public void testRepository() {
        EntityGraphChild child1 = new EntityGraphChild();
        child1.setName("first child");

        EntityGraphChild child2 = new EntityGraphChild();
        child2.setName("second child");

        EntityGraphParent parent = new EntityGraphParent();
        parent.setId(1l);
        parent.setName("ParentGraph");
        HashSet<EntityGraphChild> children = new HashSet<EntityGraphChild>(Arrays.asList(child1, child2));
        parent.setChildren(children);

        repository.save(parent);

        System.out.println("--- Before querying repo");
        EntityGraphParent fromDB = repository.findByName("ParentGraph");
        System.out.println("--- After querying repo");
        assertEquals(2, fromDB.getChildren().size());
        System.out.println("--- Test finished");
    }

}

Repository:

@Repository
public interface EntityGraphParentRepository extends CrudRepository<EntityGraphParent, Long> {

    @EntityGraph(value = "graph.Parent.children", type = EntityGraph.EntityGraphType.LOAD)
    public EntityGraphParent findByName(String name);
}

Upvotes: 1

Views: 7591

Answers (1)

Roman Nazarenko
Roman Nazarenko

Reputation: 608

You misunderstand relationships laziness in JPA. When you mark your OneToMany relationship as a lazy one, it's your Set that will actually become lazy rather than getChildren. In other words, you need to access Set's contents to trigger lazy relationship to be fetched, and you need a transaction for this. Following your questions:

  1. Since your outer code is not transactional, the transaction itself gets committed on repository method call's end. The Session object is no longer accessible as it's bound to a transaction and you get the error.
  2. In your transaction template you access getChildren, not children set's content. This is what I'm talking about in the brief part.
  3. EntityGraphs are intended to override relationship fetching strategies defined in mappings. Since you've marked Parent.children field to be fetched in your graph, this field is now required to be fetched eagerly on every database interaction this graph is applied to.

Upvotes: 3

Related Questions