How to properly design RestController for spring REST API with JPA? - spring

I don't know how to properly design the architecture for a movie database web app backend.
I have a Movie Entity which has a list of Genre Entity and a Map of Actor Entitiys with their role in the movie:
// ...
#Data
#Entity
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String director;
private Date releaseDate;
private Long posterId;
#ManyToMany
#JoinTable(
name = "MOVIE_GENRES",
joinColumns = #JoinColumn(name = "MOVIE_ID"),
inverseJoinColumns = #JoinColumn(name = "GENRE_ID"))
private Set<Genre> genres = new HashSet<>();
// TODO how to rename value column (CAST -> ACTOR_ID)
#OneToMany
#MapKeyColumn(name = "ACTOR_ROLE")
private Map<String, Actor> cast = new HashMap<>();
// ...
}
I also have a REST Controller for Movies:
#RestController
public class MovieController {
private MovieRepository repository;
public MovieController(MovieRepository repository) {
this.repository = repository;
}
#GetMapping("/api/movies")
public List<Movie> all() {
return repository.findAll();
}
#PostMapping("/api/movies")
Movie newMovie(#RequestBody Movie newMovie) {
return repository.save(newMovie);
}
#GetMapping("/api/movies/{id}")
Movie one(#PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new MovieNotFoundException(id));
}
#PutMapping("/api/movies/{id}")
Movie replaceMovie(#RequestBody Movie newMovie, #PathVariable Long id) {
return repository.findById(id)
.map(movie -> {
movie.setTitle(newMovie.getTitle());
movie.setDirector(newMovie.getDirector());
movie.setReleaseDate(newMovie.getReleaseDate());
movie.setGenres(newMovie.getGenres());
movie.setCast(newMovie.getCast());
return repository.save(movie);
})
.orElseGet(() -> {
newMovie.setId(id);
return repository.save(newMovie);
});
}
#DeleteMapping("/api/movies/{id}")
void deleteMovie(#PathVariable Long id) {
repository.deleteById(id);
}
}
This is how it looks when I call /api/movies. I receive all the genre and cast information too, for every movie. Is this okay? I don't even need all this information when getting a list of all movies.
If I follow REST principles, shouldn't I get the cast via /api/movies/{id}/cast? I know how to add another #RestMapping that returns only the cast but it doesn't change the fact that the cast will still be included in every /api/movies call.
{
"id": 1,
"title": "The Matrix",
"director": null,
"releaseDate": null,
"posterId": 1,
"genres": [
{
"id": 4,
"name": "Science Fiction"
},
{
"id": 1,
"name": "Action"
}
],
"cast": {
"Agent Smith": {
"id": 3,
"name": "Hugo Weaving",
"gender": "MALE",
"dateOfBirth": "1960-04-04"
},
"Morpheus": {
"id": 2,
"name": "Laurence Fishburne",
"gender": "MALE",
"dateOfBirth": "1961-07-30"
},
"Thomas A. Anderson / Neo": {
"id": 1,
"name": "Keanu Reeves",
"gender": "MALE",
"dateOfBirth": "1964-09-02"
}
}
}

Good question: You should do a little more of a "deep dive" on this to understand what's going on. It's not a matter of REST design in this case. I don't know if you were surprised to see the cast included with movie information but you should be.
Your code return repository.findById(id) … seems clearly intended to retrieve only movie information but you notice you are also getting the cast. Did you do print the sql statements from the Spring Data Jpa system to see if it was acting the way you expected? I suspect not, because if you had you would probably have noticed that several sql statements are being generated. The first for the movie and then subsequent statements for the cast.
Once you traced down why you are getting multiple sql statements you will find that the statements are coming from the Entity->JSON conversion. This means that when your service returns the Movie entity the spring framework needs to convert it to JSON to send it over the wire and that code is walking the object graph. The object graph are the instances of the entities you have created in the JVM when you queried the database. Since you have mapped the Movie/Cast relationship the movie object includes possible references to the cast and when the JSON transform code does a get on the cast property JPA detects that an issues another request since spring framework is still holding the database transaction in scope. If the transaction was out of scope you would have gotten a LazyInitialization exception. All this you should research a little more so that you understand it.
So, how to you go about making a better design? You are at least two possibilities that come to mind. First, you could remove the cast mapping. Why do you think you need to have the cast as a collection within the movie? If you want to get the cast for a movie you could simply call castRepository.findByMovie(movie) and get the list. Second, you could use a DTO, or Data Transfer Object, which is a separate POJO that defines what you want your REST interface to actually return. In this case it could be a MovieDto class that is the same as the Movie entity class but without the cast property. Then you would change your movieRepository to have an method defined as Optional<MovieDto> findById(Long id) and spring-data-jpa will use the Projections feature to translate your Movie entity into a MovieDto object automagically.
Using the Projections feature would be my recommended approach. DTO's are meant for "views" for the business layer of your application. Different consumers of your service might want different views into the world of movies. Casting agents might want a list of all movies an actor appeared in while movie critics might want a list of cast as well as the movie and movie buffs might just want the details of a movie. All different DTOs, or views, for the same database. I would also carefully consider whether you actually need a cast mapping. I notice you have a cast = new HashMap<>(); code segment that creates a HashMap for the movie side of the relationship but you shouldn't need that and in the typical read use case it will be thrown away stressing the garbage collector. Finally I notice that you defined cast as a Map but why did you do that? What is going in the key of the map, the movie name? That's bad design. Actors can appear in more than one movie and movies can have more than one actor so you should have a many-to-many relationship.
Finally, this is why "is this good design" questions are frowned upon in SO. The answers are complicated and usually opinionated and not what SO is meant for.

Related

How can I get the "Many" side of an entity to show up in the API call result without running into Jackson recursive errors?

I have two entities, UserAccount and Post. There's a one to many relationship between them.
In my UserAccount class, I have:
//other UserAccount details
#OneToMany(mappedBy = "posterAccount")
private List<Post> posts;
And in my Post class, I have:
// Post details here
#ManyToOne
#JoinColumn(name = "accountId", referencedColumnName = "accountId")
private Account posterAccount;
If I try to make a call to any end point that uses Account or post, I get: java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
If I add #JsonIgnore to the ManyToOne mapping, the exception goes away and the program works.
Here's the issue:
When I do the api call to get a post, this is what I get back:
{
"postId": 1,
"title": "Title of Post 1",
"body": "Body of Post 1",
}
But I want something like this:
{
"postId": 1,
"title": "Title of Post 1",
"body": "Body of Post 1",
"posterAccount": {
// UserAccount info stuff here
}
}
But to get something like what's above, I need to remove #JsonIgnore. Removing #JsonIgnore will cause the recursive problem to happen again.
How can I get around this?
TL;DR : There is a pair of Jackson annotations designed to solve this specific case, namely #JsonManagedReference and #JsonBackReference. The field annotated with #JsonBackReference is not serialised in this case.
This issue is a nice warning bell that you might want to reconsider your architecture, or at least how you design your JPA entities. If requirements for your REST endpoints influence how you design your data access, then maybe it is time to introduce some sort of Resource / DTO / whatever-you-want-to-call-them classes so that you can do the mapping between the two layers more explicit?
At the very least I would think if I really need both the #OneToMany and #ManyToOne annotations. In most cases I had having just one of them sufficed. If needed I could always find the other side of a relationship using JPQL.

How to show data to user with DTO

This is the data that I am currently showing to the user:
{
"id": 3,
"name": "AB:11",
"description": "AB:11 is an Imperial Black Barley Wine brewed with ginger, black raspberries and chipotle peppers. A 12.8% rollercoaster of ginger zestiness and chipotle smokiness, all bound together with dark berry tartness and the decadent residual body of a Black Barley Wine.",
"method": {
"mash_temp": [
{
"temp": {
"value": 68,
"unit": "celsius"
}
}
]
}
And I don't need this "method" field. I tried to show data with DTO which looks like this:
public class Beer {
private Integer id;
private String name;
private String description;
private Method method;
private List<MashTemp> mashTemp;
private Temp temp;
// getters & setters
My DTO is giving me back a response like a:
"id": 1,
"name": "Bitch Please (w/ 3 Floyds)",
"description": "This limited edition American Barley Wine was brewed in collaboration with 3 Floyds Brewery. This beer had all the warm, boozy and smoky aspects of an Islay Scotch whisky with the sweet malt and devastatingly bitter attributes of a barley wine. Peat smoke features prominently, backed up with a complex fruity hop profile.",
"method": {
"mash_temp": [
{
"temp": {
"value": 65
}
}
]
},
"mashTemp": null,
"temp": null
Any Idea how to fix this?
You need the following classes:
public class BeerDto {
private Integer id;
private String name;
private String description;
private List<MashTempDto> mashTemp;
}
public class MashTempDto {
private TempDto temp;
}
public class TempDto {
private Integer value;
}
Now you need to map your original Beer object to BeerDto (the same is true for MashTempDto and TempDto) which you will then make available in your API.
I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Beer.class)
public interface BeerDto {
#IdMapping
Integer getId();
String getName();
String getDescription();
Set<MashTempDto> getMashTemp();
#EntityView(MashTemp.class)
interface MashTempDto {
TempDto getTemp();
}
#EntityView(Temp.class)
interface TempDto {
Integer getValue();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
BeerDto a = entityViewManager.find(entityManager, BeerDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<BeerDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!

Spring REST api - foreign key ID instead of entire object

I have a problem with Json returned by my REST api GET method.
That's how my entities looks like:
#Entity
public class Employee {
...
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "department_id", nullable = true)
private Department department;
}
#Entity
public class Department {
...
#OneToMany(mappedBy = "department", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonBackReference
private Set<Employee> employees;
}
And this is the answer I get, while I'm trying to GET Employee by its id:
{
"id": 1,
"surname": "smith",
"department": {
"id": 1,
"name": "HR",
"room": "13"
}
}
Now, Instead entire Department object, I would like to get just simple id: "department_id": 1, and I don't know how to do that.
Second question: what's the good practise in this situation in REST api? Should I leave it like it is; expose only id (what I'm asking you how to do); or use DTO and not showing it at all?
Moreover, anyway I'm going to add _links to this user's department, and in this case i thought that leaving only id should be ok (tell me if I'm wrong).
Looking forward for your answers!
A good practice is to define a DTO that represents the data that is exposed by your API.
This should be decoupled from your domain (Employee) as it will offer you more flexibility, just like what you want to achieve.
class EmployeeDTO extends RepresentationModel {
private long id;
private String surname;
private long departmentId;
// getters and setters
}
This should work. Of course you need to map your Employee entity to the EmployeeDTO. RepresentationModel contains the _links property that you want for the HATEOAS (for example, have a look at
https://www.baeldung.com/spring-hateoas-tutorial )
About exposing the id from your database, I think that a good reason for not doing it is that you are giving information about your database size for free and it's something that you might not want to. More information could even be derived from that.
Here you can find a good discussion on the topic:
Exposing database IDs - security risk?
I would suggest to have a look at UUID which is a universally unique alphanumeric identifier that doesn't expose this information about your data.
More about UUID: https://www.baeldung.com/java-uuid
#JsonIgnoreProperties
To just get department id without changing any implementation you may use #JsonIgnoreProperties({"name", "room"}) as following
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "department_id", nullable = true)
#JsonIgnoreProperties({"name", "room"})
private Department department;
which will respond with the following
[
{
"id": 1,
"surname": "smith",
"department": {
"id": 1
}
}
]
You may also like to explore other ways to achieve the same here
Best Practices
We should never expose and return our modal and entities as a response to APIs. We may create the DTOs/DAOs to receive and transfer the objects and data. You may also convert the entity to DTO and DTO to entity using mappers.
In the case of DTO, you may just include the department id and may fetch the object if required using the repository.

How to do not send #IdClass object in Spring JSON queries

I'm setting a server to get a CRUD api from a postgresql Database using JPA. Everytime I want to expose an object from the DB it duplicate the idObject.
When I get an object from the database using springframework and send it after that, it duplicate the idObject like this:
{
"siteId": 3,
"contractId": "1",
"name": "sitenumber1",
"siteIdObject": {
"siteId": 3,
"contractId": "1"
}
}
SiteId and contractId are repeating...
but I want something like that:
{
"siteId": 3,
"contractId": "1",
"name": "sitenumber1"
}
I want to avoid using DTO because I think there is a better way but I don't find it. Since I'm using springFramework for just one or two month I'm maybe forgeting something...
there is the code:
Site code:
#Entity
#IdClass(SiteId.class)
#Table(name = "site", schema="public")
public class Site {
#Id
#Column(name="siteid")
private Integer siteId;
#Id
#Column(name="clientid")
private Integer contractId;
private String name;
#JsonIgnore
#OneToMany(cascade=CascadeType.ALL, mappedBy = "site")
public Set<Device> devices;
//setter, getter, hash, equals, tostring, constructor empty one and full one
SiteId code:
public class SiteId implements Serializable {
private Integer siteId;
private Integer contractId;
// setter, getter, constructor empty and full, hash and equals
Thanks to help :)
Bessaix Daniel
If you are using Spring you might also be using Jackson so if you annotate your SiteIdclass with #JsonIgnoreType it shouldn't be serialized at all when the Site object is serialized.
I am however unsure if this will break your application logic now that the id object is not serialized anymore.

What is the best way to perform custom result sets using JPA or HQL with Spring?

I develop a little Web Service using Spring REST API. I just wanted to know what is the best way to build a custom data result set from a query using HQL or Criterias.
Let's assume we need to handle these 2 entities to perform the following HQL request:
SELECT m.idMission, m.driver, m.dateMission FROM MissionEntity m
The Mission entity (simplified form):
#Entity
public class Mission
{
Integer idMission; //id of the mission
String dateMission; //date of the mission
[...] //Other fields not needed for my request
#ManyToOne
#JoinColumn(name = "driver",
referencedColumnName = "id_user")
User driver; //the driver (user) associated to the mission
[...] //Accessors
};
And the User entity (the driver) (simplified form):
#Entity
public class User
{
Integer idUser; //id of the user
[...] //Others fields not needed for my request
#OneToMany
List<Mission> missionList; //the missions associated to the user
[...] //Accessors
};
JSON output (first result):
[
[ //Mission: depth = 0 (root)
1,
{ //Driver: depth = 1 (mission child -> User)
"idUser": 29,
"shortId": "Adr_Adr",
"lastname": "ADRIAN",
"firstname": null,
"status": "Driver",
"active": 1
},
"05/03/2015"
],
[...]
]
As you can see, I have a custom Mission entity result set (List) which the pattern for each Mission entity is the following:
+ Object
- missionId (Integer)
+ driver (User)
- idUser
- shortId
- lastname
- firstname
- status
- active
- dateMission (String)
But for the purpose of my request I only need for the User entity its firstname and its lastname.
So I need a result set like the following one:
+ Mission (Mission)
- missionId (Integer)
+ driver (User)
- lastname
- firstname
- dateMission (String)
As you can see, I want to keep the same JSON tree structure: a mission entity own a child User entity but this time with a partial set of attributes (only the firstname and the lastname is needed in the set).
For the moment, the only way to solve my problem is to use 2 additionnal POJO classes:
The UserProj class:
public class UserProj
{
private String firstname, lastname;
public UserProj(String firstname, String lastname)
{
this.firstname = firstname;
this.lastname = lastname;
}
[...] //Accessors
};
The MissionProj class:
public class MissionProj
{
private Integer missionId;
private UserProj driver;
private String dateMission;
public MissionProj(Integer missionId,
String driverFirstname, String driverLastname, String dateMission)
{
this.missionId = missionId;
{
this.driver = new UserProj(driverFirstname, driverLastname);
}
this.dateMission = dateMission;
}
[...] //Accessors
};
Here's now the request I use to get the wished JSON output result set:
[
{
"missionId": 1,
"driver": {
"firstname": null,
"lastname": "ADRIAN"
},
"dateMission": "05/03/2015"
},
[...]
]
As you can see, the result set is the one I was looking for! But my problem with that solution is that solution is not scalable. In fact, if I want to perform another custom result set for the User or the Mission entity with one additional field, I will have to create another POJO for this other custom result set. So for me this solution is not really a solution.
I think it should exist a way to do this properly using HQL or Criteria directly but I couldn't find it! Do you have an idea ?
Thanks a lot in advance for your help!
As you can see, the result set is the one I was looking for! But my problem with that solution is that solution is not scalable. In fact, if I want to perform another custom result set for the User or the Mission entity with one additional field, I will have to create another POJO for this other custom result set. So for me this solution is not really a solution.
You are honestly trying to push functionality too low in your architecture which obviously manifests the problem which you describe.
As a bit of background, the SELECT NEW functionality which is exposed by HQL/JPQL and the JPA Criteria was introduced as an easy way to take a query of selectables and inject them into a value object by properly selecting the right constructor method. What the caller does with the constructed value objects is an application concern, not one for the persistence provider.
I believe a more scalable solution would be not to rely on the persistence provider to deal with this but to instead push this back upstream on the Jackson API directly, which is already designed and built to handle this case quite easily.
You'd want Hibernate to return a List<Mission> objects which have the appropriate state from all dependent objects initialized based on your application needs. Then you would either have your service tier or controller transform this list using custom Jackson serializers.
public class MissionSerializer extends StdSerializer<Mission> {
private boolean serializeSpecialField;
public MissionSerializer() {
this( null );
}
public MissionSerializer(Class<Mission> clazz) {
super( clazz );
}
public void setSerializeSpecialField(boolean value) {
this.serializeSpecialField = value;
}
#Override
public void serialize(
Mission value,
JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException {
// use jgen to write the custom mappings of Mission
// using the serializeSpecialField to control whether
// you serialize that field.
}
}
Then at this point, it's a matter of getting the ObjectMapper and setting the serializer again either in your service tier or controller and toggling whether you serialize the extra field.
The benefit here is that the query remains constant, allowing the persistence provider to cache the entities from the query, improving performance, all while allowing the application tier to transform the results to the final destined output.
This solution does likely imply you may have a single query you have to manage and a single Jackson Serializer to handle this task, so from a technical debt it may be reasonable.
On the other hand, there are good arguments to avoid coupling two solutions for the sake of code reuse.
For example, what if you'd like to change one use case? Now because you reuse the same code, one use case change implies the other is impacted, must be tested, and verified it's still stable and unaffected. By not reusing code, you avoid this potential risk, especially if your test suite doesn't have full coverage.

Resources