Expose Association in Entity via Rest in Spring Boot - spring-boot

I'm new in Spring Boot and a would like to add an association to the HTTP-Request.
Example:
// Transaction.java
#Entity
public class Transaction extends RepresentationModel<Transaction> {
// id;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Tag> tags;
}
// Tag.java
#Entity
public class Tag extends RepresentationModel<Transaction> {
// id;
// name..., and so on
}
Now I have two Repositories:
public interface TagRepository extends JpaRepository<Tag, Long> {
}
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
}
When calling GET http://localhost:8080/transactions
I get:
{
"_embedded": {
"transactions": [
{
"date": "2021-01-01T01:01:11.000+00:00",
"and so on": "more fields" // but no "tags": [...]
"_links": {
"self": {"href": "http://localhost:8080/transactions/1"},
"tags": {"href": "http://localhost:8080/transactions/1/tags"}
}
}
]
},
"_links": {...},
"page": {...}
}
Is there any way to Modify the default Spring Endpoints to return an array of tags?
I would like to bypass create a custom Controller for this, because i need some parameters and filter methods.
The reason I need this function is, that I have a page which will load about 1k transactions and I don't want do make n+1 requests on GET /transactions/n/tags
Thanks for Help.

You need to add the join table annotation like this:
// Transaction.java
#Entity
public class Transaction extends RepresentationModel<Transaction> {
// id;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(
name = "tag_transaction",
joinColumns = #JoinColumn(name = "transaction_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "tag_id", referencedColumnName = "id"))
private List<Tag> tags;
}
// Tag.java
#Entity
public class Tag extends RepresentationModel<Transaction> {
// id;
// name..., and so on
}

Related

spring-data-jpa - Limit how deep objects populate

I have the following entities:
#Entity
#Table(name = "business", schema = "public")
public class Business {
// some properties
}
#Entity
#Table(name = "appuser", schema = "public")
public class AppUser implements UserDetails {
// some properties
#JsonManagedReference
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
private List<UserBusinessRole> userBusinessRoles = new ArrayList<>();
}
#Entity
#Table(name = "appuser_business_role", schema = "public")
public class UserBusinessRole {
// some properties
#ManyToOne
#JoinColumn(name = "business_id")
private Business business;
}
These work without issue when calling individually, however, I also have an entity that has business AND app user:
#Entity
#Table(name = "import_session", schema = "public")
public class ImportSession {
// some properties
#JsonIgnore
#ManyToOne()
#JoinColumn(
name = "requester_user_id",
referencedColumnName = "id"
)
private AppUser requester;
#ManyToOne
#JoinColumn(name = "business_id")
private Business business;
}
But it returns duplicate values for business like below (listed under roles and in the root object):
{
"id": 14,
...
"requesterDto": {
"id": 123,
"emailAddress": "bar#bar.com",
"userBusinessRolesDto": [
{
"id": 6,
"type": "ADMIN",
"businessDto": {
"name": "Foo Inc"
...
}
}
]
},
"businessDto": {
"name": "Foo Inc"
}
}
Is there a way to make it ONLY return certain fields, or control how 'deep' it populates, without a lot of manual fiddeling / creating separate DTOs all over? So it would look something like this for example:
{
"id": 14,
...
"requesterDto": {
"id": 123,
"emailAddress": "bar#bar.com"
},
"businessDto": {
"name": "Foo Inc"
...
}
}

JPA Repository many to one

I have Student entity and Course entity. This is #ManyToOne relationship i.e. Student may attend only one course at a time, but courses may have multiple students.
#Entity
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String studentId;
private String firstName;
private String secondName;
#ManyToOne
#JoinColumn(name = "course_id")
//#JsonIgnore
private Course course;
#Entity
public class Course {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String courseName;
#OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "course", orphanRemoval = true, targetEntity = Student.class)
private List<Student> students = new ArrayList<>();
I post my data with the following json:
{
"id": 1,
"courseName": "course134",
"students" : [
{
"id" : 1,
"studentId": "123",
"firstName": "John1",
"secondName": "Name1"
},
{
"id" : 2,
"studentId": "1234567",
"firstName": "John2",
"secondName": "Name2"
}
then, as I get courses I receive:
{
"id": 1,
"courseName": "course134",
"students": []
}
How to list Students attending specific course?
I made up a Query in StudentRepository
#Query("SELECT s from Student s where s.id = :courseName")
Optional<Student> getStudentByCourseName(String courseName);
Still not working.
this is my Repository code:
#Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
Optional<Course> findCourseByCourseName(String courseName);
#Query("SELECT c.students FROM Course c WHERE c.courseName = :courseName")
Optional<Student> getStudentsByCourseName(String courseName);
}
this is my Service method
public Optional<Student> findStudentByCourse(String courseName){
return courseRepository.getStudentsByCourseName(courseName);
}
and finally my Controller:
#GetMapping("/student/course/{courseName}")
public ResponseEntity<Student> findCoursesWithStudentId(#PathVariable String courseName) {
Optional<Student> byCourseName = studentService.findStudentByCourse(courseName);
if (byCourseName.isPresent()) {
return ResponseEntity.ok(byCourseName.get());
} else {
return ResponseEntity.notFound().build();
}
}
You should query the Course table, not the Student table. Also, the query will return the list, not just one entity, so change your method's return type also...
#Query("SELECT c.students FROM Course c WHERE c.courseName = :courseName")
List<Student> getStudentsByCourseName(String courseName) {}
edit
You can always do it like so:
Excute the simple method:
Course findByCourseName(String courseName) {}
and then just get its Students by a simple:
course.getStudents();

SnippetException: Cannot document response fields as the response body is empty

Here is my Controller:
#PostMapping("post")
#PreAuthorize("hasAuthority('WRITE')")
public ResponseEntity<?> createPost(#RequestBody PostEntity postEntity) {
return new ResponseEntity<>(postService.createPost(postEntity), HttpStatus.CREATED);
}
Service :
#Override
public Post createPost(PostEntity postEntity) {
return postFactory.buildPost(postEntityRepository.save(postEntity));
}
//Post is Immutable class here
public Post buildPost(PostEntity entity) {
return new Post.Builder()
.groupId(entity.getGroupEntity().getGroupId())
.postedBy(entity.getPostedBy().getUsername())
.postType(entity.getType())
.postMedia(entity.getPostMedia())
.postId(entity.getPostId())
.build();
}
Here is my mockMvc:
#BeforeEach
public void setUp(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.alwaysDo(document("{method-name}",
preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
.build();
}
Here is my Test:
this.mockMvc.perform(post("/api/post")
.contentType(MediaTypes.HAL_JSON)
.contextPath("/api")
.content(this.objectMapper.writeValueAsString(postEntity)))
.andExpect(status().isCreated())
.andDo(
document("{method-name}", preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestFields(describeCreatePostRequest()),
responseFields(describePostEntityResult())
));
Here is Post call:
#Value.Immutable
#JsonSerialize(as = ImmutablePost.class)
#JsonDeserialize(as = ImmutablePost.class)
#JsonInclude(JsonInclude.Include.NON_NULL)
#Relation(value = "post", collectionRelation = "posts")
public interface Post {
Long getPostId();
String getPostType();
String postedBy();
#Nullable
PostMedia postMedia();
Long groupId();
class Builder extends ImmutablePost.Builder {}
}
PostEntity #Entity class here is json format:
{
"type" : "text",
"postedBy" : {
"username": "sandeep"
},
"postMedia" : {
"mediaType": "text",
"mediaUrl": "null",
"content": "Hi this is testing media content",
"createdAt": 1234,
"updatedAt": 689
},
"groupEntity": {
"groupId": 4
}
}
And Post entity class:
#Entity
#Table(name = "POST")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class PostEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "POST_ID")
private Long postId;
#Column(name = "POST_TYPE")
private String type;
#ManyToOne
#JoinColumn(name = "username", referencedColumnName = "username")
private User postedBy;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "post_media_id", referencedColumnName = "id")
private PostMedia postMedia;
#ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
#JoinColumn(name = "GROUP_ID", referencedColumnName = "GROUP_ID")
private GroupEntity groupEntity;
public PostEntity() {
}
}
I have tried
objectMapper.readValue(objectMapper.writeValueAsString(postEntity), ImmutablePost.class);
As well. but still its not working, I am facing same exception:
org.springframework.restdocs.snippet.SnippetException: Cannot document response fields as the response body is empty
at org.springframework.restdocs.payload.AbstractFieldsSnippet.verifyContent(AbstractFieldsSnippet.java:191)
at org.springframework.restdocs.payload.AbstractFieldsSnippet.createModel(AbstractFieldsSnippet.java:147)
at org.springframework.restdocs.snippet.TemplatedSnippet.document(TemplatedSnippet.java:78)
at org.springframework.restdocs.generate.RestDocumentationGenerator.handle(RestDocumentationGenerator.java:191)
The problem is with the "PostMedia" setters "PostMedia" and "GroupEntity". You need to accept strings in the setters parameter and convert them to the corresponding entity. Expecting the setter's arguments as the custom entity type is making the problem.
For example:
public class MyModel {
private CustomEnum myEnum;
public CustomEnum getMyEnum() {
return myEnum;
}
public void setMyEnum(String enumName) {
this.myEnum = CustomEnum.valueOf(enumName);
}
}

Spring Data Rest POST response different from GET response

On my Spring Data Rest project I have a Competition entity that references an GeoLocation entity:
public class Competition {
#Id
private String uname;
[...]
#NotNull
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private GeoLocation geoLocation;
}
public class GeoLocation {
#Id private Long id;
private Double latitude;
private Double longitude;
}
Since every Competition must have a GeoLocation defined, the Competition entity handles the creation via cascade. When creating a new Competition entity via POST, I get the following response:
{
"uname": "Some Competition",
"geoLocation": {
[content of geoLocation]
},
"_links": {
[...]
}
}
But when I call the newly created competition, the content of the GeoLocation will be wrapped in a content field.
{
"uname": "Some Competition",
"geoLocation": {
"content": {
[content of geoLocation]
}
},
"_links": {
[...]
}
}
I would expect that both requests would deliver the same response?
#JsonUnwrapped solved this issue for me:
public class Competition {
#Id
private String uname;
[...]
#NotNull
#JsonUnwrapped
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private GeoLocation geoLocation;
}

Spring Boot getting empty _embedded array for related entity

Using Spring Boot I'm Having the following abbreviated structure of entities:
#Entity
#Table(name = "item")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id", updatable = false)
protected Long id;
...
}
#Entity
#Table(name = "book")
public class Book extends Item implements Serializable {
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "item_author", joinColumns = #JoinColumn(name = "item_id", referencedColumnName = "id"), inverseJoinColumns = #JoinColumn(name = "author_id", referencedColumnName = "id"))
private Set<Author> authors;
}
#Entity
#Table(name = "author")
public class Author implements Serializable {
#Id
#GeneratedValue
private Long id;
#ManyToMany(mappedBy="authors")
private List<Book> books = new ArrayList<Book>();
private String name;
}
My DAOs are just plain simple RestResource interfaces for all entities, like:
#RestResource(path="items", rel="items")
public interface ItemDao extends CrudRepository<Item, Long> {
}
When I query an entity by id, it is all good
GET > http://localhost:8080/shelfventory/authors/1
{
"name" : "Jhonny Cash",
"used" : true,
"_links" : {
"self" : {
"href" : "http://localhost:8080/shelfventory/authors/1"
},
"author" : {
"href" : "http://localhost:8080/shelfventory/authors/1"
},
"books" : {
"href" : "http://localhost:8080/shelfventory/authors/1/books"
}
}
}
But when I try to follow the links for a related object I just get an empty embedded:
GET > http://localhost:8080/shelfventory/authors/1/books
{
"_embedded" : {
"books" : [ ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/shelfventory/authors/1/books"
}
}
}
What am I doing wrong, how to solve it?
Consider adding these two properties to your application.properties to keep your #Entity and schema in sync:
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=true

Resources