From 23ef6ed815458b0432174fca31184a4dc383ba45 Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Wed, 2 Sep 2026 15:34:54 -0400 Subject: [PATCH 1/4] DATAREST-1343-ruthes00. Created example that demonstrates how to use a custom entity lookup. Signed-off-by: ruthes00 --- .../SpringDataRestCustomization.java | 20 +++++++++++++++++++ .../springdata/rest/entitylookup/User.java | 19 ++++++++++++++++++ .../rest/entitylookup/UserRepo.java | 12 +++++++++++ 3 files changed, 51 insertions(+) create mode 100644 rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java create mode 100644 rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java create mode 100644 rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java new file mode 100644 index 000000000..8c48cb131 --- /dev/null +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java @@ -0,0 +1,20 @@ +package example.springdata.rest.associations; + +// Source - https://stackoverflow.com/q/54651741 +// Posted by undef, modified by community. See post 'Timeline' for change history +// Retrieved 2026-09-02, License - CC BY-SA 4.0 + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; +import org.springframework.web.servlet.config.annotation.CorsRegistry; + +@Configuration +public class SpringDataRestCustomization implements RepositoryRestConfigurer { + @Override + public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { + config.withEntityLookup() + .forRepository(UserRepo.class, User::getUsername, UserRepo::findByUsername); + } +} + diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java new file mode 100644 index 000000000..5a6270346 --- /dev/null +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java @@ -0,0 +1,19 @@ +package example.springdata.rest.associations; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "app_user") +public class User { + @Id + private Long id; + private String username; + private String fullName; + +} diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java new file mode 100644 index 000000000..898d251d8 --- /dev/null +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java @@ -0,0 +1,12 @@ +package example.springdata.rest.associations; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.util.Optional; + +@RepositoryRestResource(exported = true) +public interface UserRepo extends JpaRepository { + Optional findByUsername(String username); +} + From 5f0a92e7f7c123210a557e64162b3dd7173fe1f1 Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Wed, 2 Sep 2026 15:58:09 -0400 Subject: [PATCH 2/4] DATAREST-1343-ruthes00. Created example that demonstrates how to use a custom entity lookup. Signed-off-by: ruthes00 --- rest/entitylookup/README.adoc | 189 ++++++++++++++++++ rest/entitylookup/pom.xml | 49 +++++ .../rest/entitylookup/Application.java | 43 ++++ .../SpringDataRestCustomization.java | 9 +- .../springdata/rest/entitylookup/User.java | 6 +- .../rest/entitylookup/UserRepo.java | 5 +- 6 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 rest/entitylookup/README.adoc create mode 100644 rest/entitylookup/pom.xml create mode 100644 rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java diff --git a/rest/entitylookup/README.adoc b/rest/entitylookup/README.adoc new file mode 100644 index 000000000..ce6dd0597 --- /dev/null +++ b/rest/entitylookup/README.adoc @@ -0,0 +1,189 @@ += Spring Data REST - Entity Lookup Example + +Sample application that demonstrates how to configure a custom entity lookup in Spring Data REST, allowing resources to be addressed by a meaningful business identifier (e.g. `username`) instead of the default database-generated primary key (`id`). + +== Overview + +By default, Spring Data REST exposes entities at URIs that include the entity's primary key, for example: + +---- +GET /users/1 +---- + +This example shows how to override that behaviour so that a `User` can be looked up by its `username` field instead: + +---- +GET /users/jdoe +---- + +== Project Structure + +[source] +---- +src/main/java/example/springdata/rest/entitylookup/ +├── Application.java # Spring Boot entry point; seeds an initial User on startup +├── User.java # JPA entity mapped to the "app_user" table +├── UserRepo.java # Spring Data JPA repository exposed via Spring Data REST +└── SpringDataRestCustomization.java # RepositoryRestConfigurer that registers the custom lookup +---- + +== Key Components + +=== `User` Entity + +The `User` entity is a standard JPA entity mapped to the `app_user` table. It carries three fields: + +* `id` – auto-assigned primary key (`Long`) +* `username` – the business identifier used for REST lookups +* `fullName` – display name of the user + +[source,java] +---- +@Entity +@Table(name = "app_user") +public class User { + @Id + private Long id; + private String username; + private String fullName; +} +---- + +=== `UserRepo` Repository + +`UserRepo` extends `JpaRepository` and is exported as a REST resource via `@RepositoryRestResource`. It also declares a `findByUsername` query method that the custom lookup delegates to. + +[source,java] +---- +@RepositoryRestResource(exported = true) +public interface UserRepo extends JpaRepository { + Optional findByUsername(String username); +} +---- + +=== `SpringDataRestCustomization` Configuration + +This is the heart of the example. By implementing `RepositoryRestConfigurer`, the application registers a custom entity lookup that: + +1. Extracts the `username` from a `User` instance (used to build the URI). +2. Resolves a `username` path segment back to a `User` entity (used when handling incoming requests). + +[source,java] +---- +@Configuration +public class SpringDataRestCustomization implements RepositoryRestConfigurer { + + @Override + public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { + config.withEntityLookup() + .forRepository(UserRepo.class, User::getUsername, UserRepo::findByUsername); + } +} +---- + +The two method references passed to `forRepository` are: + +[cols="1,3"] +|=== +| Method reference | Purpose + +| `User::getUsername` +| Extracts the lookup value from an entity instance (used when generating `_links`). + +| `UserRepo::findByUsername` +| Resolves the lookup value from the URL path back to an entity (used when handling `GET /users/{username}`). +|=== + +== Running the Application + +The application uses an in-memory H2 database, so no external infrastructure is required. + +[source,bash] +---- +./mvnw spring-boot:run -pl rest/entitylookup +---- + +On startup, `Application.init()` saves a single `User` to the database so there is data to query immediately. + +== Example Requests + +Once the application is running, you can interact with the REST API. + +=== 1. POST a new user + +Create a new `User` by posting a JSON body to the collection resource. +The `id` field must be supplied because the entity does not use auto-generation in this example. + +[source,bash] +---- +curl -X POST http://localhost:8080/users \ + -H "Content-Type: application/json" \ + -d '{"id": 42, "username": "jdoe", "fullName": "John Doe"}' +---- + +=== 2. GET the user by ID + +Retrieve the newly created user using its numeric primary key. + +[source,bash] +---- +curl http://localhost:8080/users/42 +---- + +NOTE: Without the custom entity lookup configured in `SpringDataRestCustomization`, this would be the *only* way to address the resource. + +=== 3. PUT an update to the user by ID + +Replace the user's data using the numeric primary key as the path segment. + +[source,bash] +---- +curl -X PUT http://localhost:8080/users/42 \ + -H "Content-Type: application/json" \ + -d '{"id": 42, "username": "jdoe", "fullName": "Jonathan Doe"}' +---- + +=== 4. PUT an update to the user by username + +Thanks to the custom entity lookup, the same update can be performed using the `username` as the path segment instead of the numeric ID. + +[source,bash] +---- +curl -X PUT http://localhost:8080/users/jdoe \ + -H "Content-Type: application/json" \ + -d '{"id": 42, "username": "jdoe", "fullName": "Jonathan Doe"}' +---- + +=== 5. PUT an update by username — omitting the `id` field + +Because the custom entity lookup resolves the record from the `username` in the URL, the `id` field does not need to be included in the request body. +Spring Data REST identifies the target entity from the path segment and merges the supplied fields, so the following request is equivalent to example 4: + +[source,bash] +---- +curl -X PUT http://localhost:8080/users/jdoe \ + -H "Content-Type: application/json" \ + -d '{"username": "jdoe", "fullName": "Jonathan Doe"}' +---- + +NOTE: This is one of the practical advantages of a custom entity lookup — clients can work with natural business identifiers and are not required to track or transmit internal database IDs. + +=== 6. GET the user by username + +Retrieve the user using the human-readable `username` identifier — the primary benefit of the custom entity lookup. + +[source,bash] +---- +curl http://localhost:8080/users/jdoe +---- + +Without the custom lookup, this request would return a `404 Not Found` because Spring Data REST would try to parse `jdoe` as a `Long` primary key. With the customization in place, Spring Data REST uses `username` as the resource identifier in both directions — when building hypermedia `_links` and when resolving incoming requests. + +== Technologies Used + +* https://spring.io/projects/spring-boot[Spring Boot] +* https://spring.io/projects/spring-data-jpa[Spring Data JPA] +* https://spring.io/projects/spring-data-rest[Spring Data REST] +* https://jakarta.ee/specifications/persistence/[Jakarta Persistence (JPA)] +* https://www.h2database.com[H2 In-Memory Database] +* https://projectlombok.org[Lombok] diff --git a/rest/entitylookup/pom.xml b/rest/entitylookup/pom.xml new file mode 100644 index 000000000..fc2000353 --- /dev/null +++ b/rest/entitylookup/pom.xml @@ -0,0 +1,49 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-rest-examples + 4.0.0-SNAPSHOT + + + spring-data-rest-associations + Spring Data REST - Associations Example + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + jakarta.persistence + jakarta.persistence-api + + + + org.hsqldb + hsqldb + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + com.h2database + h2 + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + + + diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java new file mode 100644 index 000000000..94b08e9f1 --- /dev/null +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-present 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 example.springdata.rest.entitylookup; + +import jakarta.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sample application that demonstrates how to create custom entity lookup instead of "Id". + * + * @author Steve Rutherford + */ +@SpringBootApplication +public class Application { + + public static void main(String... args) { + SpringApplication.run(Application.class, args); + } + + @Autowired UserRepo users; + + @PostConstruct + public void init() { + var user = new User(); + users.save(user); + } +} diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java index 8c48cb131..d06efd8b1 100644 --- a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/SpringDataRestCustomization.java @@ -1,14 +1,13 @@ -package example.springdata.rest.associations; - -// Source - https://stackoverflow.com/q/54651741 -// Posted by undef, modified by community. See post 'Timeline' for change history -// Retrieved 2026-09-02, License - CC BY-SA 4.0 +package example.springdata.rest.entitylookup; import org.springframework.context.annotation.Configuration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.web.servlet.config.annotation.CorsRegistry; +/** + * @author Steve Rutherford + */ @Configuration public class SpringDataRestCustomization implements RepositoryRestConfigurer { @Override diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java index 5a6270346..6b2d112f7 100644 --- a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/User.java @@ -1,4 +1,4 @@ -package example.springdata.rest.associations; +package example.springdata.rest.entitylookup; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -6,6 +6,9 @@ import lombok.Getter; import lombok.Setter; +/** + * @author Steve Rutherford + */ @Getter @Setter @Entity @@ -15,5 +18,4 @@ public class User { private Long id; private String username; private String fullName; - } diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java index 898d251d8..818e71f5e 100644 --- a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/UserRepo.java @@ -1,10 +1,13 @@ -package example.springdata.rest.associations; +package example.springdata.rest.entitylookup; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import java.util.Optional; +/** + * @author Steve Rutherford + */ @RepositoryRestResource(exported = true) public interface UserRepo extends JpaRepository { Optional findByUsername(String username); From f56515fa70a4ba6f49a88713126491ad7093c852 Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Wed, 2 Sep 2026 16:52:09 -0400 Subject: [PATCH 3/4] DATAREST-1343-ruthes00. Added unit tests. Signed-off-by: ruthes00 --- README.adoc | 1 + .../java/example/springdata/rest/entitylookup/Application.java | 3 +++ rest/entitylookup/src/test/resources/documentation.properties | 1 + rest/pom.xml | 1 + 4 files changed, 6 insertions(+) create mode 100644 rest/entitylookup/src/test/resources/documentation.properties diff --git a/README.adoc b/README.adoc index 4bb6e1094..a7807c003 100644 --- a/README.adoc +++ b/README.adoc @@ -111,6 +111,7 @@ WARNING: If you're done using it, don't forget to shut it down! * `security` - A sample REST web-service secured using Spring Security. * `starbucks` - A sample REST web-service built with Spring Data REST and MongoDB. * `uri-customizations` - Example project to show URI customization capabilities. +* `entitylookup` - Example project to show how to. == Spring Data web support diff --git a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java index 94b08e9f1..05ab3af0d 100644 --- a/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java +++ b/rest/entitylookup/src/main/java/example/springdata/rest/entitylookup/Application.java @@ -38,6 +38,9 @@ public static void main(String... args) { @PostConstruct public void init() { var user = new User(); + user.setId(1L); + user.setUsername("admin"); + user.setFullName("Admin User"); users.save(user); } } diff --git a/rest/entitylookup/src/test/resources/documentation.properties b/rest/entitylookup/src/test/resources/documentation.properties new file mode 100644 index 000000000..bdb0fef8b --- /dev/null +++ b/rest/entitylookup/src/test/resources/documentation.properties @@ -0,0 +1 @@ +org.springframework.restdocs.outputDir=target/generated-snippets \ No newline at end of file diff --git a/rest/pom.xml b/rest/pom.xml index b824e7b92..146c67def 100644 --- a/rest/pom.xml +++ b/rest/pom.xml @@ -21,6 +21,7 @@ security headers uri-customization + entitylookup From b20023e6aa1682cef20a04a61312b418cebacf2a Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Wed, 2 Sep 2026 17:02:08 -0400 Subject: [PATCH 4/4] DATAREST-1343-ruthes00. Fixed readme entry. Signed-off-by: ruthes00 --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index a7807c003..bc22b33aa 100644 --- a/README.adoc +++ b/README.adoc @@ -111,7 +111,7 @@ WARNING: If you're done using it, don't forget to shut it down! * `security` - A sample REST web-service secured using Spring Security. * `starbucks` - A sample REST web-service built with Spring Data REST and MongoDB. * `uri-customizations` - Example project to show URI customization capabilities. -* `entitylookup` - Example project to show how to. +* `entitylookup` - Example project to show how to use a custom entity lookup. == Spring Data web support