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.
* `associations` - Example project to show how to create an entity and its association with another entity in a single HTTP call.

== Spring Data web support

Expand Down
119 changes: 119 additions & 0 deletions rest/associations/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
= Spring Data REST - Associations example

This example shows how to create an entity and its association with another entity in a single HTTP call.

For example, given parent entity "Parent" and child entity "Child" that is associated with a given Parent, you can create new parent and associated child records with a single HTTP call.

== Details

To add a parent and a child record in a single API call using Spring Data REST, you must configure a cascading relationship (cascade = CascadeType.ALL) on your JPA entity and send a nested JSON payload to the parent’s repository endpoint.

By default, Spring Data REST exposes repositories as individual HATEOAS endpoints and expects associations to be linked via URIs. To force it to accept and save a child nested inside a parent object in a single POST request, implement the configuration below:

== Configure the JPA Entities

You must use a bidirectional relationship or an explicitly managed unidirectional relationship with CascadeType.ALL or CascadeType.PERSIST:

.JPA Entities
====
[source,java]
----
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

// The cascade attribute ensures the child is saved when the parent is saved
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();

// Helper method to keep both sides of the relationship in sync
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}

// Getters and setters
}

@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;

// Getters and setters
}
----
====

== Expose Only the Parent Repository

For Spring Data REST to seamlessly deserialize the nested collection instead of treating it as a resource link, the cleanest approach is to not export the child repository. If you do not export the child repository, Spring Data REST automatically falls back to standard Jackson serialization, embedding the child elements directly:

.Exposing only the parent repository
====
[source,java]
----
@RepositoryRestResource(collectionResourceRel = "parents", path = "parents")
public interface ParentRepository extends CrudRepository<Parent, Long> {
}

// Keep exported = false so Spring Data REST processes children inline
@RepositoryRestResource(exported = false)
public interface ChildRepository extends CrudRepository<Child, Long> {
}
----
====

== Send the HTTP API Request

Issue a POST request to the parent collection endpoint with the nested child records inside the JSON body:

[source,bash]
----
curl -X POST http://localhost:8080/api/parents \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"children": [
{ "name": "Jane Doe" },
{ "name": "Jimmy Doe" }
]
}'
----

Response
====
[source,bash]
----
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/parents/2"
},
"parent" : {
"href" : "http://localhost:8080/parents/2"
}
},
"name" : "John Doe",
"children" : [ {
"name" : "Jane Doe"
}, {
"name" : "Jimmy Doe"
} ]
}
----
====


This example uses Spring JPA to manage associations between entities.
49 changes: 49 additions & 0 deletions rest/associations/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,45 @@
/*
* 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.associations;

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 a parent and child record with a single HTTP POST call using
* Spring Data REST and JPA cascade.
*
* @author Steve Rutherford
*/
@SpringBootApplication
public class Application {

public static void main(String... args) {
SpringApplication.run(Application.class, args);
}

@Autowired ParentRepository parents;

@PostConstruct
public void init() {
var parent = new Parent("Jane Doe");
parent.addChild(new Child("Jimmy Doe"));
parents.save(parent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2014-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.associations;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;

import com.fasterxml.jackson.annotation.JsonIgnore;

/**
* Child entity associated with a given {@link Parent}.
*
* @author Steve Rutherford
*/
@Entity
public class Child {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

private String name;

@ManyToOne
@JoinColumn(name = "parent_id")
@JsonIgnore private Parent parent;

Child() {}

public Child(String name) {
this.name = name;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Parent getParent() {
return parent;
}

public void setParent(Parent parent) {
this.parent = parent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.associations;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;

import java.util.ArrayList;
import java.util.List;

/**
* Aggregate root representing a parent with a one-to-many relationship to {@link Child} entities.
*
* @author Steve Rutherford
*/
@Entity
public class Parent {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

private String name;

// The cascade attribute ensures children are saved when the parent is saved
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = jakarta.persistence.FetchType.EAGER) private List<Child> children = new ArrayList<>();

Parent() {}

public Parent(String name) {
this.name = name;
}

/**
* Helper method to keep both sides of the relationship in sync.
*/
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Child> getChildren() {
return children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.associations;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

/**
* Spring Data repository interface to manage {@link Parent} instances. Exposed as a REST resource so that Spring Data
* REST handles the parent endpoint. The child repository is intentionally not exported so that Spring Data REST falls
* back to standard Jackson serialization and accepts nested children inline.
*
* @author Steve Rutherford
*/
@RepositoryRestResource(collectionResourceRel = "parents", path = "parents")
public interface ParentRepository extends CrudRepository<Parent, Long> {}
Loading
Loading