diff --git a/pom.xml b/pom.xml index 22fdf5a97..617cc011d 100755 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,12 @@ + + benchmark + + spring-data-jpa-performance + + hibernate-62 diff --git a/spring-data-jpa-performance/pom.xml b/spring-data-jpa-performance/pom.xml new file mode 100644 index 000000000..2c965be00 --- /dev/null +++ b/spring-data-jpa-performance/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + org.springframework.data + spring-data-jpa-parent + 3.4.x-LABS-QUERY-SNAPSHOT + + + Spring Data JPA - Performance + spring-data-jpa-performance + + + 1.37 + + + + + + ${groupId} + spring-data-jpa + ${version} + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation-api} + + + + jakarta.persistence + jakarta.persistence-api + ${jakarta-persistence-api} + compile + + + + ${hibernate.groupId}.orm + hibernate-core + ${hibernate} + + + net.bytebuddy + byte-buddy + + + compile + + + + ${hibernate.groupId}.orm + hibernate-jpamodelgen + ${hibernate} + provided + + + + jakarta.xml.bind + jakarta.xml.bind-api + ${jaxb} + provided + + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + 0.4.0.RELEASE + test + + + + com.h2database + h2 + ${h2} + test + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.springframework.data.jpa.performance.tests + + + + + + + + + + jitpack.io + https://jitpack.io + + + + diff --git a/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/model/IPersonProjection.java b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/model/IPersonProjection.java new file mode 100644 index 000000000..224a90287 --- /dev/null +++ b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/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.model; + +/** + * @author Christoph Strobl + */ +public interface IPersonProjection { + + String getFirstname(); + String getLastname(); +} diff --git a/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/model/Person.java b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/model/Person.java new file mode 100644 index 000000000..140e36998 --- /dev/null +++ b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/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.model; + +import java.util.Set; + +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; + +/** + * @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-performance/src/main/java/org/springframework/data/jpa/model/Profile.java b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/model/Profile.java new file mode 100644 index 000000000..cab53c2bf --- /dev/null +++ b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/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.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-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java new file mode 100644 index 000000000..560379c0d --- /dev/null +++ b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java @@ -0,0 +1,38 @@ +/* + * 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; + +import java.util.List; + +import org.springframework.data.jpa.model.IPersonProjection; +import org.springframework.data.jpa.model.Person; +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.model.Person p WHERE p.firstname = ?1") + List findAllWithAnnotatedQueryByFirstname(String firstname); + + @Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true) + List findAllWithNativeQueryByFirstname(String firstname); +} diff --git a/spring-data-jpa-performance/src/main/resources/META-INF/persistence.xml b/spring-data-jpa-performance/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000..4727d4263 --- /dev/null +++ b/spring-data-jpa-performance/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,11 @@ + + + + org.hibernate.jpa.HibernatePersistenceProvider + org.springframework.data.jpa.domain.AbstractPersistable + org.springframework.data.jpa.domain.AbstractAuditable + org.springframework.data.jpa.model.Person + org.springframework.data.jpa.model.Profile + true + + diff --git a/spring-data-jpa-performance/src/main/resources/logback.xml b/spring-data-jpa-performance/src/main/resources/logback.xml new file mode 100644 index 000000000..d026572cc --- /dev/null +++ b/spring-data-jpa-performance/src/main/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + + + diff --git a/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/RepositoryFinderTests.java b/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/RepositoryFinderTests.java new file mode 100644 index 000000000..86d4579fd --- /dev/null +++ b/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/RepositoryFinderTests.java @@ -0,0 +1,160 @@ +/* + * 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; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import jmh.mbr.junit5.Microbenchmark; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.openjdk.jmh.annotations.Benchmark; +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.Warmup; +import org.springframework.data.jpa.model.IPersonProjection; +import org.springframework.data.jpa.model.Person; +import org.springframework.data.jpa.model.Profile; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +@Microbenchmark +@Warmup(time = 5, iterations = 3) +@Measurement(time = 5) +public class RepositoryFinderTests { + + @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("first", "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() { + + Map properties = new HashMap<>(); + properties.put("jakarta.persistence.jdbc.url", "jdbc:h2:mem:test"); + properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + properties.put("hibernate.hbm2ddl.auto", "update"); + EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("benchmark", properties); + + 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("firstname"), "first"))); + + return typedQuery.getResultList(); + } + + @Benchmark + public List baselineEntityManagerHQLQuery(BenchmarkParameters parameters) { + + Query query = parameters.entityManager + .createQuery("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1"); + query.setParameter(1, "first"); + + return query.getResultList(); + } + + @Benchmark + public List derivedFinderMethod(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllByFirstname("first"); + } + + @Benchmark + public List derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname("first"); + } + + @Benchmark + public List stringBasedQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first"); + } + + @Benchmark + public List stringBasedNativeQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithNativeQueryByFirstname("first"); + } + +}