ManyToOne Field Loads Eagerly When Lazy Initialization Enabled

Hi,
I want to use lazy initialization for a string field. To do this i added bytecode enhancer to my project.
It worked for basic field but there is a problem. When I enable lazy initialization, basic field loading lazily that’s ok this is what i want but @ManyToOne annotated field loading eagerly even if the fetch type lazy.

When lazy initialization is disabled
select post0_.id as id1_0_0_, post0_.user_id as user_id4_0_0_, post0_.content as content2_0_0_, post0_.title as title3_0_0_ from tbl_post post0_ where post0_.id=?

When lazy initialization is enabled

select post0_.id as id1_0_0_, post0_.user_id as user_id4_0_0_, post0_.title as title3_0_0_ from tbl_post post0_ where post0_.id=?
select user0_.id as id1_1_0_, user0_.password as password2_1_0_, user0_.username as username3_1_0_ from tbl_user user0_ where user0_.id=?

Why @ManyToOne(fetch = FetchType.LAZY) annotated field fetched eagerly when I enable the lazy initialization? How can I fix this?

User Entity

@Data
@Entity
@Table(name = "tbl_user")
public class User implements Serializable {

    @Id
    @Column(name = "id")
    @SequenceGenerator(name = "seq_tbl_user_id", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_tbl_user_id")
    private Long id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Post> posts = new ArrayList<>();
}

Post Entity

@Data
@Entity
@Table(name = "tbl_post")
public class Post implements Serializable {

    @Id
    @Column(name = "id")
    @SequenceGenerator(name = "seq_tbl_post_id", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_tbl_post_id")
    private Long id;

    @Column(name = "title")
    private String title;

    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @Column(name = "content", nullable = false)
    @Basic(fetch = FetchType.LAZY)
    private String content;

    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User author;

}

Environment

  • SpringBoot 2.4.3
  • Hibernate 5.4.28.Final

When you say you enable or disable “lazy initialization”, what exactly do you mean?

I assume you mean configuring the enhancer for lazy initialization, but need to make sure.

Yes, that’s exactly what I mean.
Here is my configuration

<plugin>
	<groupId>org.hibernate.orm.tooling</groupId>
	<artifactId>hibernate-enhance-maven-plugin</artifactId>
	<version>${hibernate.version}</version>
	<executions>
		<execution>
			<configuration>
				<failOnError>true</failOnError>
				<enableDirtyTracking>true</enableDirtyTracking>
				<enableLazyInitialization>true</enableLazyInitialization>
			</configuration>
			<goals>
				<goal>enhance</goal>
			</goals>
		</execution>
	</executions>
</plugin>

Also, have you enabled the use of “enhanced proxies” (hibernate.bytecode.allow_enhancement_as_proxy)?

Also, can you show pseudo-code that shows how you access / load this graph?

1 Like

Without seeing your code, I’m 99% sure that is the problem you are seeing.

Play around with the following test and HEAD of 5.4. Specifically the setting of hibernate.bytecode.allow_enhancement_as_proxy. False (the default on 5.4) leads to what you see. True (the default on 5.5/master) leads to what you think you want.

@RunWith( BytecodeEnhancerRunner.class )
@EnhancementOptions( lazyLoading = true )
public class TheTests extends BaseNonConfigCoreFunctionalTestCase {
	private SQLStatementInterceptor statementInterceptor;

	@Test
	public void testIt() {
		statementInterceptor.clear();

		inTransaction(
				(session) -> {
					final Post loaded = session.byId( Post.class ).load( 10 );

					assertTrue( Hibernate.isInitialized( loaded ) );

					// The post's author should be an enhanced-proxy...
					assertTrue( Hibernate.isPropertyInitialized( loaded, "author" ) );
					assertFalse( Hibernate.isInitialized( loaded.getAuthor() ) );

					// there should have been just a single SQL statement
					// and it should have included the `tbl_post.user_id` column
					assertEquals( 1, statementInterceptor.getSqlQueries().size() );
					final String sql = statementInterceptor.getSqlQueries().get( 0 );
					assertTrue( sql.toLowerCase( Locale.ROOT ).contains( ".user_id" ) );


					// now let's access the lazy `Post#content`
					statementInterceptor.clear();
					final String content = loaded.getContent();
					assertEquals( 1, statementInterceptor.getSqlQueries().size() );

					// and now, let's access the author
					statementInterceptor.clear();
					final User author = loaded.getAuthor();
					assertEquals( 0, statementInterceptor.getSqlQueries().size() );
					final Integer id = author.getId();
					assertEquals( 0, statementInterceptor.getSqlQueries().size() );
					final String username = author.getUsername();
					assertEquals( 1, statementInterceptor.getSqlQueries().size() );
				}
		);
	}

	@Before
	public void prepareTestData() {
		inTransaction(
				(session) -> {
					final User user = new User( 1, "johnny", "appleseed" );

					new Post( 10, "My first post", "Testing 1, 2, 3", user );
					new Post( 11, "Follow-up post", "Hello?", user );

					session.persist( user );
				}
		);
	}

	@After
	public void cleanUpTestData() {
		inTransaction(
				(session) -> {
					session.createQuery( "delete Post" ).executeUpdate();
					session.createQuery( "delete User" ).executeUpdate();
				}
		);
	}

	@Override
	protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
		super.configureStandardServiceRegistryBuilder( ssrb );
		ssrb.applySetting( AvailableSettings.ALLOW_ENHANCEMENT_AS_PROXY, "true" );

		statementInterceptor = new SQLStatementInterceptor( ssrb.getSettings() );
	}

	@Override
	protected void applyMetadataSources(MetadataSources sources) {
		super.applyMetadataSources( sources );
		sources.addAnnotatedClass( User.class )
				.addAnnotatedClass( Post.class );
	}

	@Entity( name = "User" )
	@Table(name = "tbl_user")
	public static class User {

		@Id
		@Column(name = "id")
		private Integer id;

		@Column(name = "username")
		private String username;

		@Column(name = "password")
		private String password;

		@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
		private List<Post> posts = new ArrayList<>();

		private User() {
		}

		public User(Integer id, String username, String password) {
			this.id = id;
			this.username = username;
			this.password = password;
		}

		public Integer getId() {
			return id;
		}

		private void setId(Integer id) {
			this.id = id;
		}

		public String getUsername() {
			return username;
		}

		public void setUsername(String username) {
			this.username = username;
		}

		public String getPassword() {
			return password;
		}

		public void setPassword(String password) {
			this.password = password;
		}

		public List<Post> getPosts() {
			return posts;
		}

		public void setPosts(List<Post> posts) {
			this.posts = posts;
		}
	}

	@Entity( name = "Post" )
	@Table(name = "tbl_post")
	public static class Post {

		@Id
		@Column(name = "id")
		private Integer id;

		@Column(name = "title")
		private String title;

		@Basic(fetch = FetchType.LAZY)
		@Column(name = "content", nullable = false)
		private String content;

		@ManyToOne(fetch = FetchType.LAZY)
		@JoinColumn(name = "user_id")
		private User author;

		private Post() {
		}

		public Post(Integer id, String title, String content, User author) {
			this.id = id;
			this.title = title;
			this.content = content;
			this.author = author;

			author.posts.add( this );
		}

		public Integer getId() {
			return id;
		}

		private void setId(Integer id) {
			this.id = id;
		}

		public String getTitle() {
			return title;
		}

		public void setTitle(String title) {
			this.title = title;
		}

		public String getContent() {
			return content;
		}

		public void setContent(String content) {
			this.content = content;
		}

		public User getAuthor() {
			return author;
		}

		public void setAuthor(User author) {
			this.author = author;
		}
	}
}

 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.test.bytecode.enhancement.lazy.proxy;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.Hibernate;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;

import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.EnhancementOptions;
import org.hibernate.testing.jdbc.SQLStatementInterceptor;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

@RunWith( BytecodeEnhancerRunner.class )
@EnhancementOptions( lazyLoading = true )
public class TheTests extends BaseNonConfigCoreFunctionalTestCase {
	private SQLStatementInterceptor statementInterceptor;

	@Test
	public void testIt() {
		statementInterceptor.clear();

		inTransaction(
				(session) -> {
					final Post loaded = session.byId( Post.class ).load( 10 );

					assertTrue( Hibernate.isInitialized( loaded ) );

					// The post's author should be an enhanced-proxy...
					assertTrue( Hibernate.isPropertyInitialized( loaded, "author" ) );
					assertFalse( Hibernate.isInitialized( loaded.getAuthor() ) );

					// there should have been just a single SQL statement
					// and it should have included the `tbl_post.user_id` column
					assertEquals( 1, statementInterceptor.getSqlQueries().size() );
					final String sql = statementInterceptor.getSqlQueries().get( 0 );
					assertTrue( sql.toLowerCase( Locale.ROOT ).contains( ".user_id" ) );


					// now let's access the lazy `Post#content`
					statementInterceptor.clear();
					final String content = loaded.getContent();
					assertEquals( 1, statementInterceptor.getSqlQueries().size() );

					// and now, let's access the author
					statementInterceptor.clear();
					final User author = loaded.getAuthor();
					assertEquals( 0, statementInterceptor.getSqlQueries().size() );
					final Integer id = author.getId();
					assertEquals( 0, statementInterceptor.getSqlQueries().size() );
					final String username = author.getUsername();
					assertEquals( 1, statementInterceptor.getSqlQueries().size() );
				}
		);
	}

	@Before
	public void prepareTestData() {
		inTransaction(
				(session) -> {
					final User user = new User( 1, "johnny", "appleseed" );

					new Post( 10, "My first post", "Testing 1, 2, 3", user );
					new Post( 11, "Follow-up post", "Hello?", user );

					session.persist( user );
				}
		);
	}

	@After
	public void cleanUpTestData() {
		inTransaction(
				(session) -> {
					session.createQuery( "delete Post" ).executeUpdate();
					session.createQuery( "delete User" ).executeUpdate();
				}
		);
	}

	@Override
	protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
		super.configureStandardServiceRegistryBuilder( ssrb );
		ssrb.applySetting( AvailableSettings.ALLOW_ENHANCEMENT_AS_PROXY, "true" );

		statementInterceptor = new SQLStatementInterceptor( ssrb.getSettings() );
	}

	@Override
	protected void applyMetadataSources(MetadataSources sources) {
		super.applyMetadataSources( sources );
		sources.addAnnotatedClass( User.class )
				.addAnnotatedClass( Post.class );
	}

	@Entity( name = "User" )
	@Table(name = "tbl_user")
	public static class User {

		@Id
		@Column(name = "id")
		private Integer id;

		@Column(name = "username")
		private String username;

		@Column(name = "password")
		private String password;

		@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
		private List<Post> posts = new ArrayList<>();

		private User() {
		}

		public User(Integer id, String username, String password) {
			this.id = id;
			this.username = username;
			this.password = password;
		}

		public Integer getId() {
			return id;
		}

		private void setId(Integer id) {
			this.id = id;
		}

		public String getUsername() {
			return username;
		}

		public void setUsername(String username) {
			this.username = username;
		}

		public String getPassword() {
			return password;
		}

		public void setPassword(String password) {
			this.password = password;
		}

		public List<Post> getPosts() {
			return posts;
		}

		public void setPosts(List<Post> posts) {
			this.posts = posts;
		}
	}

	@Entity( name = "Post" )
	@Table(name = "tbl_post")
	public static class Post {

		@Id
		@Column(name = "id")
		private Integer id;

		@Column(name = "title")
		private String title;

		@Basic(fetch = FetchType.LAZY)
		@Column(name = "content", nullable = false)
		private String content;

		@ManyToOne(fetch = FetchType.LAZY)
		@JoinColumn(name = "user_id")
		private User author;

		private Post() {
		}

		public Post(Integer id, String title, String content, User author) {
			this.id = id;
			this.title = title;
			this.content = content;
			this.author = author;

			author.posts.add( this );
		}

		public Integer getId() {
			return id;
		}

		private void setId(Integer id) {
			this.id = id;
		}

		public String getTitle() {
			return title;
		}

		public void setTitle(String title) {
			this.title = title;
		}

		public String getContent() {
			return content;
		}

		public void setContent(String content) {
			this.content = content;
		}

		public User getAuthor() {
			return author;
		}

		public void setAuthor(User author) {
			this.author = author;
		}
	}
}

2 Likes

Thank you for your awesome answer. It works like a charm. I appreciate that.

1 Like