I'm working on a Spring Data Rest based Spring Boot service whose data model is similar to the one in the tutorial:
https://www.baeldung.com/spring-data-rest-relationships
(In defining the entities, I'm using Lombok annotations):
#Data
#NoArgsConstructor
#Entity
#Table(name = "cale")
public class Book {
#Id
#GeneratedValue
private long id;
#Column(nullable=false)
private String title;
#ManyToOne
private Library library;
}
#Data
#NoArgsConstructor
#Entity
#Table(name = "library")
public class Library {
#Id
#GeneratedValue
private long id;
//...
}
invoking the endpoint /books I get the following json:
{
"_embedded": {
"books": [
{
"id": 22,
"title": "The title of the book",
"_links": {
"self": {
"href": "http://192.168.33.20:8080/books/22"
},
"book": {
"href": "http://192.168.33.20:8080/books/22"
},
"library": {
"href": "http://192.168.33.20:8080/books/22/library"
}
}
},
.
.
.
in HAL style, the only reference to the library linked to a given book is through an URL like http://192.168.33.20:8080/books/22/library
In order to get the id of the library associated to book 22, I have to perform a second GET call to the URL, which is inefficient in many cases.
Moreover, this makes it very hard to implement queries like "GET all books associated to the library whose id is 101".
Is there a way to let Spring Data Rest include also the id of the associated entity into the returned json? Something like:
{
"_embedded": {
"books": [
{
"id": 22,
"title": "The title of the book",
"library_id": 101,
"_links": {
.
.
.
}
},
.
.
You can create a projection and use it by default with an excerpt.
To define the projection :
#Projection(
name = "customBook",
types = { Book.class })
public interface CustomBook {
#Value("#{target.id}")
long getId();
String getTitle();
#Value("#{target.getLibrary().getId()}")
int getLibraryId();
}
And call :
http://192.168.33.20:8080/books/22?projection=customBook
To use this projection by default configure your repo :
#RepositoryRestResource(excerptProjection = CustomBook.class)
public interface BookRepository extends CrudRepository<Book, Long> {}
Related
I am requesting data using RestTemplate and get a JSON Object.
I want to insert this data into a relational database.
My thoughts after I have been reading was to convert this to java records.
It is a nested JSON Object where I want to insert the different levels into different tables.
I am building this project using Sping Boot,
I would like to insert the the following data:
id, owner, model, year
into a table namned cars
and then I want to insert the events inte an event table.
type, cost, date
into a table namned events
{
"meta": {
"total_count": 2,
"current_count": 2,
"per_page": 10,
"start": 1,
"end": 2,
"current_page": 1,
"page_count": 1
},
"data": [
{
"id": 1,
"owner": "Charles John",
"model": "Volvo,
"year": 2020
"services": {
"events": [
{
"type": "repair",
"cost": 1000
"date": "2022-01-12"
},
{
"type": "cleaning",
"cost": 200
"date": "2022-01-15"
},
{
"type": "washing",
"cost": 100
"date": "2022-03-05"
}
]
}
},
{
"id": 2,
"owner": "John Carlsson",
"model": "Mercedes,
"year": 2021
"services": {
"events": [
{
"type": "repair",
"cost": 4000
"date": "2022-02-12"
},
{
"type": "cleaning",
"cost": 200
"date": "2022-02-27"
}
]
}
}
]
}
My RestTemplate looks something similar to this:
public void getCarEvents() {
RestTemplate rt = new RestTemplate();
HttpEntity<?> request = createAuth();
String result = restTemplate.getForObject(buildUri("/cars").toString(), String.class);
ResponseEntity<CarsResponse> carsResponse = restTemplate.exchange(result, HttpMethod.GET, request, new ParameterizedTypeReference<CarsResponse> {});
List<CarsResponse> response = carsResponse.getBody();
...
...
...
}
Where CarsResponse looks something like this:
public record CarsResponse(
Meta meta;
ArrayList<Car> data
) {}
whre Car looks like this:
public record Car(
int id,
String name,
int age,
Services services
) {}
public record Services(
ArrayList<Event> events
) {}
public record Event (
String type,
int cost,
String date,
){}
Should I also make a class for Car and Event like the following?
#Entity
#Table(name = "event")
public class Event{
#Column(name = "type")
private String type;
#Column(name = "cost")
private int cost;
#Column(name = "date")
private String date;
//Getter, Setter, Constructor, toString()
}
I have only found example where they have more simple JSON Object and not found any with a nested version.
Where do I go from here. How do I only get the data from Event so I only insert this data into that table?
Also I would like to get the Id from Car inserted into the Event table to so I now for which car the event has been done for.
At first I would highly recommend into checking this question and particular it's first answer since you want to consume a REST API that produces a nested JSON. Basically, all answers are correct, but personally I found that one useful. You must create a POJO class just like this:
#Entity
#Table
#JsonIgnoreProperties(ignoreUnknown = true)
public class Car {
#Id
#SequenceGenerator(
name = "car_sequence",
sequenceName = "car_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "car_sequence"
)
private int id;
private String owner;
private String model;
private Long year;
// add getters and setters
}
The first two annotations have to do with the database stuff and transform the class as an entity and it's variables as columns for DB. The third annotation ignores any values from the JSON fields that are irrelevant from the variables you want and mention in your class. The annotations about id variable help to identify it as the primary key of your table and the rest are created for auto-increment in your db.
Of course, you must first add the JPA dependency in your project as well as the database's dependency. Also you need the Jackson Databind dependency as well.
Hope this helps!!!
Should be simple, but I find no way to solve this issue. I have a repository of Profile with a class #Id User with interface:
public interface ProfileRepository extends ElasticsearchRepository<Profile, User> {
The Profile is:
public class Profile {
#Id
private User user;
[... plus getters, setters, constructors]
The User is:
public class User {
private String id;
private String role;
[... plus getters, setters, constructors]
No problem saving the Profile (as you can see ElasticSearch puts a String representation of the user):
"_id": "User(id=TestID, role=TestRole)",
"_score": 1.0,
"_source": {
"_class": "com.example.model.Profile",
"user": {
"id": "TestID",
"role": "TestRole"
},
But after that I can't retrieve any of the profiles.
If I do any of the following:
profileRepository.findAll();
profileRepository.customFindByIdRole("TestID", "TestRole");
profileRepository.findById(new User("TestID", "TestRole"));
I always get:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [com.example.model.User]
Note: customFindByIdRole is implemented this way:
#Query("{\"query\": { \"bool\": { \"must\": [ { \"match\": { \"user.id\": \"?0\" } }, { \"match\": { \"user.role\": \"?1\" } } ] } }}")
Profile customFindByIdRole(String id, String role);
Also please note that if I add any of the Spring JPA dependencies a lot is being broken, so I'm not sure I can rely on #Embedded #IdClass etc.
I have simple scenario where there is relation between User and Skill,
means one user many skills, so I tried with:
User
#Data
#NoArgsConstructor
#Entity
#EqualsAndHashCode
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#OneToMany(mappedBy = "user")
private List<Skill> skills;
}
Skill
#Data
#NoArgsConstructor
#Entity
#EqualsAndHashCode
public class Skill {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String skillTitle;
#ManyToOne
#JoinColumn(name="user_id")
private User user;
}
UserRepository
#RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
List<User> findByName(#Param("name") String name);
}
SkillRepository
#RepositoryRestResource(collectionResourceRel = "skills", path = "skills")
public interface SkillRepository extends CrudRepository<Skill, Long>{
}
with all above I'm able to get response at for example url http://localhost:8085/users/1
{
"name": "Root",
"_links": {
"self": {
"href": "http://localhost:8085/users/1"
},
"user": {
"href": "http://localhost:8085/users/1"
},
"skills": {
"href": "http://localhost:8085/users/1/skills"
}
}
}
not the issue is I'm not figuring out why list of skills is not fetched, why only this is fetched
"skills": {
"href": "http://localhost:8085/users/1/skills"
}
not a full list of skills related to user/1.
UPDATE
Added projection as suggested:
UserProjection.java
#Projection(name = "inlineData", types=User.class)
public interface UserProjection {
String getName();
List<Skill> getSkills();
}
UserRepository.java is
#RepositoryRestResource(collectionResourceRel = "users", path = "users", excerptProjection = UserProjection.class)
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
List<User> findByName(#Param("name") String name);
}
response is:
{
"name": "Root",
"_links": {
"self": {
"href": "http://localhost:8085/users/1"
},
"user": {
"href": "http://localhost:8085/users/1{?projection}",
"templated": true
},
"skills": {
"href": "http://localhost:8085/users/1/skills"
}
}
}
The response is correct, it works as intended. #RepositoryRestResource follows HATEOAS principles. Spring documentation explains it as follows:
5.1.3. Resource Discoverability
A core principle of HATEOAS is that resources should be discoverable
through the publication of links that point to the available
resources ...
By issuing a request to the root URL ... the client can extract, from
the returned JSON object, a set of links that represent the next level
of resources that are available to the client ...
You get links that represent resources. To retrieve specific resources you should call corresponding URL. Your response for User 1 means that if you want to get Skills of User 1 you should call URL "http://localhost:8085/users/1/skills".
It is easier to understand it if you imagine that you have an HTML page that displays properties of User 1. This page does not directly display Skills, instead this page contains a link to Skills page. Only if a user clicks on this link the Skills page will be loaded.
It is important that you understand HATEOAS well.
Of course there can be cases when HATEOAS is not the best choice. But here we are not discussing HATEOAS, but explaining what is the idea behind this implementation of Spring. This approach can really be helpful in many cases. When you have 2 entities with 1-2 attributes, you may consider such approach as overkill. But if you have 30 - 50 entities, each with 3 - 5 relations, each relation containing 50 - 100 other entities, it can be quite hard to deal with such data model. And HATEOAS can make it much easier. With this approach you are just navigating these relations: load one entity, select needed relation, load entities on this relation, select needed entity, in this entity select needed relation, load this relation, or navigate back to its parent entity via parent relation, etc.
Lets assume I have the following JPA entities:
#MappedSuperclass
public abstract class BaseForumPersistable {
#Id
Long id;
String title;
Date creationDate;
#ManyToOne
User user;
//getters, setter
}
#Entity
public class ThematicArea() {
#OneToMany(mappedBy="thematicArea")
List<Topic> topics;
//getters, setters
}
public class Topic() {
String status;
boolean isSticky;
#ManyToOne
ThematicArea thematicArea;
#OneToMany
List<Post> posts
//getters, setters
}
I also use these entities for my REST Controllers that handle POST requests. So for instance for Topic I have an /api/topics endpoint. When I send something like this as a JSON object
{
"user": {
"id": 3,
"role": "Admin"
},
"thematicArea": {
"id": 1
},
"title": "asdf",
"status": "Active"
}
It fails to create the Thematic Area, although it perfectly creates the User entity. The controller signature is as follows:
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> create(#RequestBody final Topic entity)
So when I use the debugger with a breakpoint, the ThematicArea entity is not even de-serialized.
Moreover if I send an object like this:
{
"user": {
"id": 3,
"role": "Admin"
},
"thematicArea": {
"id": 1,
"title": "topic_title"
},
"title": "asdf",
"status": "Active"
}
Which now also includes a title field in the ThematicArea object the two title fields get mixed up. This leads me to believe that it's an issue of de-serializing. Any ideas how i can fix this.
I have two entities, Company and Job, with an OneToMany bidirectional relationship. My problem is that i can't lazy load the Company's List<Job> jobs.
For example when i do:
GET /api/companies/1 this is the JSON response:
{
"id": 1,
"name": "foo",
...
"_embedded": {
"jobs": [
{...},
...
{...}
],
"employees": [
{...},
{...}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/companies/1"
},
"jobs": {
"href": "http://localhost:8080/api/companies/1/jobs"
},
"employees": {
"href": "http://localhost:8080/api/companies/1/employees"
}
}
}
I don't want to have the _embedded since i didn't set the FetchType=EAGER.
Here are my models:
Company.java
#Entity
public class Company {
#Column(nullable = false, unique = true)
private String name;
#OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
private List<Job> jobs;
...
public Company() {
}
...
}
Job.java
#Entity
public class Job {
#Column(nullable = false)
public String title;
#Column(length = 10000)
public String description;
#ManyToOne(fetch=FetchType.LAZY)
private Company company;
...
public Job() {
}
...
}
As you can see the same thing happens for other OneToMany relationships (employees). Can i avoid returning the whole list of job openings or employees every time?
EDIT: from the Job side the lazy load works fine! I don't get in the response the company that is related with a Job. I have to explicitly do /api/jobs/123/company in order to get the company.
EDIT2: Projections only work for collections. In this case it's not what i need. Excerpts could work, but i want to avoid them. I don't want to explicilty do /api/companies/1?projection=MyProjection since i won't use more than one. I want to change the default behavior, just like the projections do in collections.
EDIT3: i tried this
#RestResource(exported = false)
#OneToMany(mappedBy = "company")
private List<Job> jobs;
and i get the error Detected multiple association links with same relation type! Disambiguate association.
it's really annoying. I just need to get rid of _embedded. Anything?
You can use Entity Graph.Entity graphs are used to override at runtime the fetch settings of attribute mappings.For example
#Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
#EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
From Spring Data Jpa Documentation "4.3.10. Configuring Fetch- and LoadGraphs"
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
In addition;