diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index c9ba7c275..c9dacfcc8 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -351,6 +351,16 @@ hibernate-jpamodelgen ${hibernate} + + org.hibernate.orm + hibernate-core + ${hibernate} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh} + jakarta.persistence jakarta.persistence-api diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java new file mode 100644 index 000000000..05f66f120 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java @@ -0,0 +1,205 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.benchmark; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.benchmark.model.IPersonProjection; +import org.springframework.data.jpa.benchmark.model.Person; +import org.springframework.data.jpa.benchmark.model.Profile; +import org.springframework.data.jpa.benchmark.repository.PersonRepository; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +@Testable +@Fork(1) +@Warmup(time = 2, iterations = 3) +@Measurement(time = 2) +@Timeout(time = 2) +public class RepositoryFinderBenchmarks { + + private static final String PERSON_FIRSTNAME = "first"; + private static final String COLUMN_PERSON_FIRSTNAME = "firstname"; + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + EntityManager entityManager; + PersonRepository repositoryProxy; + + @Setup(Level.Iteration) + public void doSetup() { + + createEntityManager(); + + if (!entityManager.getTransaction().isActive()) { + + if (ObjectUtils.nullSafeEquals( + entityManager.createNativeQuery("SELECT COUNT(*) FROM person", Integer.class).getSingleResult(), + Integer.valueOf(0))) { + + entityManager.getTransaction().begin(); + + Profile generalProfile = new Profile("general"); + Profile sdUserProfile = new Profile("sd-user"); + + entityManager.persist(generalProfile); + entityManager.persist(sdUserProfile); + + Person person = new Person(PERSON_FIRSTNAME, "last"); + person.setProfiles(Set.of(generalProfile, sdUserProfile)); + entityManager.persist(person); + entityManager.getTransaction().commit(); + } + } + + this.repositoryProxy = createRepository(); + } + + @TearDown(Level.Iteration) + public void doTearDown() { + entityManager.close(); + } + + private void createEntityManager() { + + LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); + factoryBean.setPersistenceUnitName("benchmark"); + factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + factoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); + factoryBean.setPersistenceXmlLocation("classpath*:META-INF/persistence-jmh.xml"); + factoryBean.setMappingResources("classpath*:META-INF/orm-jmh.xml"); + + Properties properties = new Properties(); + properties.put("jakarta.persistence.jdbc.url", "jdbc:h2:mem:test"); + properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + properties.put("hibernate.hbm2ddl.auto", "update"); + properties.put("hibernate.xml_mapping_enabled", "false"); + + factoryBean.setJpaProperties(properties); + factoryBean.afterPropertiesSet(); + + EntityManagerFactory entityManagerFactory = factoryBean.getObject(); + entityManager = entityManagerFactory.createEntityManager(); + } + + PersonRepository createRepository() { + JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager); + return repositoryFactory.getRepository(PersonRepository.class); + } + } + + @Benchmark + public PersonRepository repositoryBootstrap(BenchmarkParameters parameters) { + return parameters.createRepository(); + } + + @Benchmark + public List baselineEntityManagerCriteriaQuery(BenchmarkParameters parameters) { + + CriteriaBuilder criteriaBuilder = parameters.entityManager.getCriteriaBuilder(); + CriteriaQuery query = criteriaBuilder.createQuery(Person.class); + Root root = query.from(Person.class); + TypedQuery typedQuery = parameters.entityManager + .createQuery(query.where(criteriaBuilder.equal(root.get(COLUMN_PERSON_FIRSTNAME), PERSON_FIRSTNAME))); + + return typedQuery.getResultList(); + } + + @Benchmark + public List baselineEntityManagerHQLQuery(BenchmarkParameters parameters) { + + Query query = parameters.entityManager + .createQuery("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1"); + query.setParameter(1, PERSON_FIRSTNAME); + + return query.getResultList(); + } + + @Benchmark + public Long baselineEntityManagerCount(BenchmarkParameters parameters) { + + Query query = parameters.entityManager.createQuery( + "SELECT COUNT(*) FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1"); + query.setParameter(1, PERSON_FIRSTNAME); + + return (Long) query.getSingleResult(); + } + + @Benchmark + public List derivedFinderMethod(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public List derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public List stringBasedQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public List stringBasedQueryDynamicSort(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, Sort.by(COLUMN_PERSON_FIRSTNAME)); + } + + @Benchmark + public List stringBasedNativeQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithNativeQueryByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public Long derivedCount(BenchmarkParameters parameters) { + return parameters.repositoryProxy.countByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public Long stringBasedCount(BenchmarkParameters parameters) { + return parameters.repositoryProxy.countWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME); + } +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/IPersonProjection.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/IPersonProjection.java new file mode 100644 index 000000000..6934c0ae6 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/IPersonProjection.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.benchmark.model; + +/** + * @author Christoph Strobl + */ +public interface IPersonProjection { + + String getFirstname(); + String getLastname(); +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/Person.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/Person.java new file mode 100644 index 000000000..3996c0b61 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/Person.java @@ -0,0 +1,119 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.benchmark.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; + +import java.util.Set; + +/** + * @author Christoph Strobl + */ +@Entity +@Table(name = "person") +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) // + private Integer id; + + private String firstname; + private String lastname; + + private int age; + private boolean decased; + + @Column(nullable = false, unique = true) // + private String emailAddress; + + @ManyToMany // + private Set profiles; + + public Person() {} + + public Person(String firstname, String lastname) { + this(firstname, lastname, "%s.%s@benchmark.com"); + } + + public Person(String firstname, String lastname, String emailAddress) { + + this.firstname = firstname; + this.lastname = lastname; + this.emailAddress = emailAddress; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public boolean isDecased() { + return decased; + } + + public void setDecased(boolean decased) { + this.decased = decased; + } + + public Set getProfiles() { + return profiles; + } + + public void setProfiles(Set profiles) { + this.profiles = profiles; + } +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/Profile.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/Profile.java new file mode 100644 index 000000000..816554553 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/Profile.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.benchmark.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * @author Christoph Strobl + */ +@Entity +public class Profile { + + @Id + @GeneratedValue private Integer id; + private String name; + + public Profile() {} + + public Profile(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java new file mode 100644 index 000000000..3ba5fc00c --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.benchmark.repository; + +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.benchmark.model.IPersonProjection; +import org.springframework.data.jpa.benchmark.model.Person; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.ListCrudRepository; + +/** + * @author Christoph Strobl + */ +public interface PersonRepository extends ListCrudRepository { + + List findAllByFirstname(String firstname); + + List findAllAndProjectToInterfaceByFirstname(String firstname); + + @Query("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1") + List findAllWithAnnotatedQueryByFirstname(String firstname); + + @Query("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1") + List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort); + + @Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true) + List findAllWithNativeQueryByFirstname(String firstname); + + Long countByFirstname(String firstname); + + @Query("SELECT COUNT(*) FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1") + Long countWithAnnotatedQueryByFirstname(String firstname); +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java new file mode 100644 index 000000000..482c4454f --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; + +import org.springframework.data.domain.Sort; + +/** + * @author Mark Paluch + */ +@Testable +@Fork(1) +@Warmup(time = 2, iterations = 3) +@Measurement(time = 2) +@Timeout(time = 2) +public class HqlParserBenchmarks { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + DeclaredQuery query; + Sort sort = Sort.by("foo"); + QueryEnhancer enhancer; + + @Setup(Level.Iteration) + public void doSetup() { + + String s = """ + SELECT e FROM Employee e JOIN e.projects p + WHERE TREAT(p AS LargeProject).budget > 1000 + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE "cost overrun" + """; + + query = DeclaredQuery.of(s, false); + enhancer = QueryEnhancerFactory.forQuery(query); + } + } + + @Benchmark + public Object measure(BenchmarkParameters parameters) { + return parameters.enhancer.applySorting(parameters.sort); + } + +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java new file mode 100644 index 000000000..724a41735 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import java.io.IOException; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; + +import org.springframework.data.domain.Sort; + +/** + * @author Mark Paluch + */ +@Testable +@Fork(1) +@Warmup(time = 2, iterations = 3) +@Measurement(time = 2) +@Timeout(time = 2) +public class JSqlParserQueryEnhancerBenchmarks { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + JSqlParserQueryEnhancer enhancer; + Sort sort = Sort.by("foo"); + private byte[] serialized; + + @Setup(Level.Iteration) + public void doSetup() throws IOException { + + String s = """ + select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE + except + select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE + union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; + + enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.of(s, true)); + } + } + + @Benchmark + public Object applySortWithParsing(BenchmarkParameters p) { + return p.enhancer.applySorting(p.sort); + } + +} diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml b/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml new file mode 100644 index 000000000..60c6b5c97 --- /dev/null +++ b/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml @@ -0,0 +1,28 @@ + + + + + org.hibernate.jpa.HibernatePersistenceProvider + org.springframework.data.jpa.domain.AbstractPersistable + org.springframework.data.jpa.domain.AbstractAuditable + org.springframework.data.jpa.benchmark.model.Person + org.springframework.data.jpa.benchmark.model.Profile + true + +