diff --git a/jpa/jpa21/README.md b/jpa/jpa21/README.md index 5b7ea2db..5d588044 100644 --- a/jpa/jpa21/README.md +++ b/jpa/jpa21/README.md @@ -51,4 +51,57 @@ public interface UserRepository extends CrudRepository { Calling `UserRepository.plus1BackedByOtherNamedStoredProcedure(…)` will execute the stored procedure `plus1inout` using the meta-data declared on the `User` domain class. -`UserRepository.plus1inout(…)` will derive stored procedure metadata from the repository and default to positional parameter binding and expect a single output parameter of the backing stored procedure. \ No newline at end of file +`UserRepository.plus1inout(…)` will derive stored procedure metadata from the repository and default to positional parameter binding and expect a single output parameter of the backing stored procedure. + +## Support for custom SqlResultSetMapping with ConstructorResult + +Sometimes, e.g. for analytics, it is handy to be able to return a different entity result type from a Repository query method than the base Repository entity type or an interface based projection. + +In those cases one can leverage JPAs `SqlResultSetMapping` feature to map the columns of the result of a query to different fields. + +JPA 2.1 introduced the new `SqlResultSetMapping` type `ConstructorResult` which allows to map columns of a result set row to a constructor invocation +which can be nicely used in combination with Value Objects. + +This example shows how to define a custom `SqlResultSetMapping` for the result of an analytical native query that reports the usage summary for a set of Subscriptions. + +`SqlResultSetMapping` definition on the Subscription entity class: +```java +@Entity +@NoArgsConstructor +@SqlResultSetMapping( + name="subscriptionSummary", + classes = @ConstructorResult( + targetClass = SubscriptionSummary.class, + columns={ + @ColumnResult(name="productName", type=String.class), + @ColumnResult(name="subscriptions", type=long.class) +})) +@NamedNativeQuery( + name="Subscription.findAllSubscriptionSummaries", + query="select product_name as productName, count(user_id) as subscriptions from subscription group by product_name order by productName", + resultSetMapping = "subscriptionSummary" +) +@Data +public class Subscription { +... +} +``` + +`SubscriptionSummary` is modelled as a value object: +```java +@Value +public class SubscriptionSummary { + + private final String product; + + private final Long usageCount; +} +``` + +The `SubscriptionRepository` declares the custom query method `findAllSubscriptionSummaries` which is backed by the named native query declared on the `Subscription` entity. +```java +interface SubscriptionRepository extends CrudRepository { + + List findAllSubscriptionSummaries(); +} +``` \ No newline at end of file diff --git a/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/CustomResultSetMappingsConfiguration.java b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/CustomResultSetMappingsConfiguration.java new file mode 100644 index 00000000..dbc7363a --- /dev/null +++ b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/CustomResultSetMappingsConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 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 + * + * http://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 example.springdata.jpa.resultsetmappings; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Thomas Darimont + */ +@SpringBootApplication +class CustomResultSetMappingsConfiguration { +} diff --git a/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/Subscription.java b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/Subscription.java new file mode 100644 index 00000000..d635b5cb --- /dev/null +++ b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/Subscription.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 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 + * + * http://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 example.springdata.jpa.resultsetmappings; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.ColumnResult; +import javax.persistence.ConstructorResult; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.NamedNativeQuery; +import javax.persistence.SqlResultSetMapping; + +/** + * @author Thomas Darimont + */ +@Entity +@NoArgsConstructor +@SqlResultSetMapping( // + name="subscriptionSummary", // + classes = @ConstructorResult( + targetClass = SubscriptionSummary.class, // + columns={ + @ColumnResult(name="productName", type=String.class), // + @ColumnResult(name="subscriptions", type=long.class) + })) +@NamedNativeQuery( + name="Subscription.findAllSubscriptionSummaries", // + query="select product_name as productName, count(user_id) as subscriptions from subscription group by product_name order by productName", // + resultSetMapping = "subscriptionSummary") +@Data +public class Subscription { + + @Id + @GeneratedValue + Long id; + + String productName; + + long userId; + + public Subscription(String productName, long userId) { + this.productName = productName; + this.userId = userId; + } +} diff --git a/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/SubscriptionRepository.java b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/SubscriptionRepository.java new file mode 100644 index 00000000..2cb86ce3 --- /dev/null +++ b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/SubscriptionRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 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 + * + * http://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 example.springdata.jpa.resultsetmappings; + +import org.springframework.data.repository.CrudRepository; + +import java.util.List; + +/** + * @author Thomas Darimont + */ +public interface SubscriptionRepository extends CrudRepository { + + /** + * Returns an aggregated {@link SubscriptionSummary} by Product. + *

+ * Note that this example uses a JPA 2.1 Constructor based {@link javax.persistence.SqlResultSetMapping} + * in combination with native query defined in {@link Subscription}. + * + * @return + */ + List findAllSubscriptionSummaries(); +} diff --git a/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/SubscriptionSummary.java b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/SubscriptionSummary.java new file mode 100644 index 00000000..ef4880a1 --- /dev/null +++ b/jpa/jpa21/src/main/java/example/springdata/jpa/resultsetmappings/SubscriptionSummary.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 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 + * + * http://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 example.springdata.jpa.resultsetmappings; + +import lombok.Value; + +/** + * @author Thomas Darimont + */ +@Value +public class SubscriptionSummary { + + private final String product; + + private final Long usageCount; +} diff --git a/jpa/jpa21/src/main/resources/schema.sql b/jpa/jpa21/src/main/resources/schema.sql index a2f3db41..1c3a6509 100644 --- a/jpa/jpa21/src/main/resources/schema.sql +++ b/jpa/jpa21/src/main/resources/schema.sql @@ -5,3 +5,10 @@ BEGIN ATOMIC set res = arg + 1; END /; + +DROP table subscription IF EXISTS +/; +CREATE TABLE subscription(id IDENTITY, product_name VARCHAR(255), user_id INT) +/; + + diff --git a/jpa/jpa21/src/test/java/example/springdata/jpa/resultsetmappings/SubscriptionRepositoryIntegrationTests.java b/jpa/jpa21/src/test/java/example/springdata/jpa/resultsetmappings/SubscriptionRepositoryIntegrationTests.java new file mode 100644 index 00000000..33d6db48 --- /dev/null +++ b/jpa/jpa21/src/test/java/example/springdata/jpa/resultsetmappings/SubscriptionRepositoryIntegrationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 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 + * + * http://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 example.springdata.jpa.resultsetmappings; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Thomas Darimont + */ +@RunWith(SpringRunner.class) +@DataJpaTest +@Transactional +public class SubscriptionRepositoryIntegrationTests { + + private static final String SERVICE_1 = "Service 1"; + private static final String SERVICE_2 = "Service 2"; + + @Autowired + SubscriptionRepository repository; + + @Test + public void shouldReturnCorrectSubscriptionSummary() { + + repository.save(new Subscription(SERVICE_1, 1)); + repository.save(new Subscription(SERVICE_1, 2)); + repository.save(new Subscription(SERVICE_1, 3)); + repository.save(new Subscription(SERVICE_2, 3)); + repository.save(new Subscription(SERVICE_2, 4)); + + List subscriptionSummaries = repository.findAllSubscriptionSummaries(); + + assertThat(subscriptionSummaries) // + .flatExtracting(s -> asList(s.getProduct(), s.getUsageCount())) + .contains(SERVICE_1, 3L, SERVICE_2, 2L); + } +} +