Add performance module.

Add new module using JMH to benchmark certain aspects of query parsing & rendering.

See: #3309
This commit is contained in:
Christoph Strobl
2024-05-17 13:18:18 +02:00
parent d60efe6cf9
commit b8319a07b9
9 changed files with 543 additions and 0 deletions

View File

@@ -58,6 +58,12 @@
<profiles>
<profile>
<id>benchmark</id>
<modules>
<module>spring-data-jpa-performance</module>
</modules>
</profile>
<profile>
<id>hibernate-62</id>
<properties>

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>3.4.x-LABS-QUERY-SNAPSHOT</version>
</parent>
<name>Spring Data JPA - Performance</name>
<artifactId>spring-data-jpa-performance</artifactId>
<properties>
<jmh.version>1.37</jmh.version>
</properties>
<dependencies>
<dependency>
<groupId>${groupId}</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation-api}</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta-persistence-api}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${hibernate.groupId}.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate}</version>
<exclusions>
<exclusion>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</exclusion>
</exclusions>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${hibernate.groupId}.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.mp911de.microbenchmark-runner</groupId>
<artifactId>microbenchmark-runner-junit5</artifactId>
<version>0.4.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>org.springframework.data.jpa.performance.tests</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
</project>

View File

@@ -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();
}

View File

@@ -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<Profile> 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<Profile> getProfiles() {
return profiles;
}
public void setProfiles(Set<Profile> profiles) {
this.profiles = profiles;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Person, Integer> {
List<Person> findAllByFirstname(String firstname);
List<IPersonProjection> findAllAndProjectToInterfaceByFirstname(String firstname);
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname);
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
List<Person> findAllWithNativeQueryByFirstname(String firstname);
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="benchmark">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>org.springframework.data.jpa.domain.AbstractPersistable</class>
<class>org.springframework.data.jpa.domain.AbstractAuditable</class>
<class>org.springframework.data.jpa.model.Person</class>
<class>org.springframework.data.jpa.model.Profile</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
</persistence>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.data" level="error"/>
<!-- <logger name="org.hibernate.SQL" level="debug" />-->
<root level="error">
<appender-ref ref="console"/>
</root>
</configuration>

View File

@@ -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<String, String> 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<Person> baselineEntityManagerCriteriaQuery(BenchmarkParameters parameters) {
CriteriaBuilder criteriaBuilder = parameters.entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = criteriaBuilder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
TypedQuery<Person> typedQuery = parameters.entityManager
.createQuery(query.where(criteriaBuilder.equal(root.get("firstname"), "first")));
return typedQuery.getResultList();
}
@Benchmark
public List<Person> 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<Person> derivedFinderMethod(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllByFirstname("first");
}
@Benchmark
public List<IPersonProjection> derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname("first");
}
@Benchmark
public List<Person> stringBasedQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first");
}
@Benchmark
public List<Person> stringBasedNativeQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname("first");
}
}