Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 use a custom entity lookup.

== Spring Data web support

Expand Down
189 changes: 189 additions & 0 deletions rest/entitylookup/README.adoc
Original file line number Diff line number Diff line change
@@ -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<User, Long> {
Optional<User> 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]
49 changes: 49 additions & 0 deletions rest/entitylookup/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-rest-examples</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>

<artifactId>spring-data-rest-associations</artifactId>
<name>Spring Data REST - Associations Example</name>

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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();
user.setId(1L);
user.setUsername("admin");
user.setFullName("Admin User");
users.save(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
config.withEntityLookup()
.forRepository(UserRepo.class, User::getUsername, UserRepo::findByUsername);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package example.springdata.rest.entitylookup;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

/**
* @author Steve Rutherford
*/
@Getter
@Setter
@Entity
@Table(name = "app_user")
public class User {
@Id
private Long id;
private String username;
private String fullName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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<User, Long> {
Optional<User> findByUsername(String username);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.springframework.restdocs.outputDir=target/generated-snippets
1 change: 1 addition & 0 deletions rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<module>security</module>
<module>headers</module>
<module>uri-customization</module>
<module>entitylookup</module>
</modules>

<dependencies>
Expand Down