diff --git a/persistence-modules/spring-boot-persistence-h2/pom.xml b/persistence-modules/spring-boot-persistence-h2/pom.xml index 5b5e255211..777bc6cb2d 100644 --- a/persistence-modules/spring-boot-persistence-h2/pom.xml +++ b/persistence-modules/spring-boot-persistence-h2/pom.xml @@ -29,12 +29,30 @@ com.h2database h2 + + org.projectlombok + lombok + ${lombok.version} + compile + + + org.hibernate + hibernate-core + ${hibernate.version} + + + com.vladmihalcea + db-util + ${db-util.version} + com.baeldung.h2db.demo.server.SpringBootApp 2.0.4.RELEASE + 5.3.11.Final + 1.0.4 diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/LazyLoadNoTransSpringBootApplication.java b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/LazyLoadNoTransSpringBootApplication.java new file mode 100644 index 0000000000..a52d9fc2f9 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/LazyLoadNoTransSpringBootApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.h2db.lazy_load_no_trans; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@EnableTransactionManagement +public class LazyLoadNoTransSpringBootApplication { + public static void main(String[] args) { + SpringApplication.run(LazyLoadNoTransSpringBootApplication.class, args); + } +} diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/config/DatasourceProxyBeanPostProcessor.java b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/config/DatasourceProxyBeanPostProcessor.java new file mode 100644 index 0000000000..c087427b65 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/config/DatasourceProxyBeanPostProcessor.java @@ -0,0 +1,67 @@ +package com.baeldung.h2db.lazy_load_no_trans.config; + +import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; +import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator; +import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +import javax.sql.DataSource; +import java.lang.reflect.Method; + +@Component +public class DatasourceProxyBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof DataSource && !(bean instanceof ProxyDataSource)) { + // Instead of directly returning a less specific datasource bean + // (e.g.: HikariDataSource -> DataSource), return a proxy object. + // See following links for why: + // https://stackoverflow.com/questions/44237787/how-to-use-user-defined-database-proxy-in-datajpatest + // https://gitter.im/spring-projects/spring-boot?at=5983602d2723db8d5e70a904 + // http://blog.arnoldgalovics.com/2017/06/26/configuring-a-datasource-proxy-in-spring-boot/ + final ProxyFactory factory = new ProxyFactory(bean); + factory.setProxyTargetClass(true); + factory.addAdvice(new ProxyDataSourceInterceptor((DataSource) bean)); + return factory.getProxy(); + } + return bean; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + private static class ProxyDataSourceInterceptor implements MethodInterceptor { + private final DataSource dataSource; + + public ProxyDataSourceInterceptor(final DataSource dataSource) { + this.dataSource = ProxyDataSourceBuilder.create(dataSource) + .name("MyDS") + .multiline() + .logQueryBySlf4j(SLF4JLogLevel.INFO) + .listener(new DataSourceQueryCountListener()) + .build(); + } + + @Override + public Object invoke(final MethodInvocation invocation) throws Throwable { + final Method proxyMethod = ReflectionUtils.findMethod(this.dataSource.getClass(), + invocation.getMethod().getName()); + if (proxyMethod != null) { + return proxyMethod.invoke(this.dataSource, invocation.getArguments()); + } + return invocation.proceed(); + } + } +} diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/entity/Document.java b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/entity/Document.java new file mode 100644 index 0000000000..9d69e7eb58 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/entity/Document.java @@ -0,0 +1,26 @@ +package com.baeldung.h2db.lazy_load_no_trans.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.Immutable; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Immutable +public class Document { + + @Id + private Long id; + + private String title; + + private Long userId; +} diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/entity/User.java b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/entity/User.java new file mode 100644 index 0000000000..ae9cb9e4e8 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/entity/User.java @@ -0,0 +1,37 @@ +package com.baeldung.h2db.lazy_load_no_trans.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.Immutable; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Immutable +public class User { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private String comment; + + @OneToMany(mappedBy = "userId") + @Fetch(FetchMode.SUBSELECT) + private List docs = new ArrayList<>(); +} diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/repository/UserRepository.java b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/repository/UserRepository.java new file mode 100644 index 0000000000..bafe484163 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.baeldung.h2db.lazy_load_no_trans.repository; + +import com.baeldung.h2db.lazy_load_no_trans.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/service/ServiceLayer.java b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/service/ServiceLayer.java new file mode 100644 index 0000000000..ff3783fd9d --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/java/com/baeldung/h2db/lazy_load_no_trans/service/ServiceLayer.java @@ -0,0 +1,34 @@ +package com.baeldung.h2db.lazy_load_no_trans.service; + +import com.baeldung.h2db.lazy_load_no_trans.entity.User; +import com.baeldung.h2db.lazy_load_no_trans.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; + +@Service +public class ServiceLayer { + + @Autowired + private UserRepository userRepository; + + @Transactional(readOnly = true) + public long countAllDocsTransactional() { + return countAllDocs(); + } + + public long countAllDocsNonTransactional() { + return countAllDocs(); + } + + private long countAllDocs() { + return userRepository.findAll() + .stream() + .map(User::getDocs) + .mapToLong(Collection::size) + .sum(); + } + +} diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/resources/application-lazy-load-no-trans-off.properties b/persistence-modules/spring-boot-persistence-h2/src/main/resources/application-lazy-load-no-trans-off.properties new file mode 100644 index 0000000000..b5fb841685 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/resources/application-lazy-load-no-trans-off.properties @@ -0,0 +1,13 @@ +spring.datasource.url=jdbc:h2:mem:mydb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.hibernate.ddl-auto=create-drop +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +logging.level.org.hibernate.SQL=INFO +logging.level.org.hibernate.type=TRACE +spring.jpa.properties.hibernate.validator.apply_to_ddl=false +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=false +spring.jpa.open-in-view=false \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/resources/application-lazy-load-no-trans-on.properties b/persistence-modules/spring-boot-persistence-h2/src/main/resources/application-lazy-load-no-trans-on.properties new file mode 100644 index 0000000000..04579e1dae --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/main/resources/application-lazy-load-no-trans-on.properties @@ -0,0 +1,13 @@ +spring.datasource.url=jdbc:h2:mem:mydb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.hibernate.ddl-auto=create-drop +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +logging.level.org.hibernate.SQL=INFO +logging.level.org.hibernate.type=TRACE +spring.jpa.properties.hibernate.validator.apply_to_ddl=false +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true +spring.jpa.open-in-view=false \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/resources/data.sql b/persistence-modules/spring-boot-persistence-h2/src/main/resources/data.sql index 2d7b446005..b8835e70cb 100644 --- a/persistence-modules/spring-boot-persistence-h2/src/main/resources/data.sql +++ b/persistence-modules/spring-boot-persistence-h2/src/main/resources/data.sql @@ -10,4 +10,17 @@ CREATE TABLE billionaires ( INSERT INTO billionaires (first_name, last_name, career) VALUES ('Aliko', 'Dangote', 'Billionaire Industrialist'), ('Bill', 'Gates', 'Billionaire Tech Entrepreneur'), -('Folrunsho', 'Alakija', 'Billionaire Oil Magnate'); \ No newline at end of file +('Folrunsho', 'Alakija', 'Billionaire Oil Magnate'); + +insert into USER values (101, 'user1', 'comment1'); +insert into USER values (102, 'user2', 'comment2'); +insert into USER values (103, 'user3', 'comment3'); +insert into USER values (104, 'user4', 'comment4'); +insert into USER values (105, 'user5', 'comment5'); + +insert into DOCUMENT values (1, 'doc1', 101); +insert into DOCUMENT values (2, 'doc2', 101); +insert into DOCUMENT values (3, 'doc3', 101); +insert into DOCUMENT values (4, 'doc4', 101); +insert into DOCUMENT values (5, 'doc5', 102); +insert into DOCUMENT values (6, 'doc6', 102); \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-h2/src/test/java/com/baeldung/lazy_load_no_trans/LazyLoadNoTransPropertyOffIntegrationTest.java b/persistence-modules/spring-boot-persistence-h2/src/test/java/com/baeldung/lazy_load_no_trans/LazyLoadNoTransPropertyOffIntegrationTest.java new file mode 100644 index 0000000000..cb5063d8a4 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/test/java/com/baeldung/lazy_load_no_trans/LazyLoadNoTransPropertyOffIntegrationTest.java @@ -0,0 +1,41 @@ +package com.baeldung.lazy_load_no_trans; + +import com.baeldung.h2db.lazy_load_no_trans.LazyLoadNoTransSpringBootApplication; +import com.baeldung.h2db.lazy_load_no_trans.service.ServiceLayer; +import com.vladmihalcea.sql.SQLStatementCountValidator; +import org.hibernate.LazyInitializationException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LazyLoadNoTransSpringBootApplication.class) +@ActiveProfiles("lazy-load-no-trans-off") +public class LazyLoadNoTransPropertyOffIntegrationTest { + + @Autowired + private ServiceLayer serviceLayer; + + private static final long EXPECTED_DOCS_COLLECTION_SIZE = 6; + + @Test(expected = LazyInitializationException.class) + public void whenCallNonTransactionalMethodWithPropertyOff_thenThrowException() { + serviceLayer.countAllDocsNonTransactional(); + } + + @Test + public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() { + SQLStatementCountValidator.reset(); + + long docsCount = serviceLayer.countAllDocsTransactional(); + + assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); + + SQLStatementCountValidator.assertSelectCount(2); + } +} diff --git a/persistence-modules/spring-boot-persistence-h2/src/test/java/com/baeldung/lazy_load_no_trans/LazyLoadNoTransPropertyOnIntegrationTest.java b/persistence-modules/spring-boot-persistence-h2/src/test/java/com/baeldung/lazy_load_no_trans/LazyLoadNoTransPropertyOnIntegrationTest.java new file mode 100644 index 0000000000..5968fde7b7 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-h2/src/test/java/com/baeldung/lazy_load_no_trans/LazyLoadNoTransPropertyOnIntegrationTest.java @@ -0,0 +1,47 @@ +package com.baeldung.lazy_load_no_trans; + +import com.baeldung.h2db.lazy_load_no_trans.LazyLoadNoTransSpringBootApplication; +import com.baeldung.h2db.lazy_load_no_trans.service.ServiceLayer; +import com.vladmihalcea.sql.SQLStatementCountValidator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = LazyLoadNoTransSpringBootApplication.class) +@ActiveProfiles("lazy-load-no-trans-on") +public class LazyLoadNoTransPropertyOnIntegrationTest { + + @Autowired + private ServiceLayer serviceLayer; + + private static final long EXPECTED_DOCS_COLLECTION_SIZE = 6; + private static final long EXPECTED_USERS_COUNT = 5; + + @Test + public void whenCallNonTransactionalMethodWithPropertyOn_thenGetNplusOne() { + SQLStatementCountValidator.reset(); + + long docsCount = serviceLayer.countAllDocsNonTransactional(); + + assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); + + SQLStatementCountValidator.assertSelectCount(EXPECTED_USERS_COUNT + 1); + } + + @Test + public void whenCallTransactionalMethodWithPropertyOn_thenTestPass() { + SQLStatementCountValidator.reset(); + + long docsCount = serviceLayer.countAllDocsTransactional(); + + assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); + + SQLStatementCountValidator.assertSelectCount(2); + } +}