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.
Related
I'm struggling to find what feels like a good design for a Spring Boot CRUD REST API app that involves several OneToMany relationships w/ join tables. For example, consider this DB structure in MySQL which allows one "Recipe" to be associated with several "Recipe Categories":
create table recipes
(
id int auto_increment primary key,
name varchar(255)
);
create table recipe_categories
(
id int auto_increment primary key,
name varchar(64) not null
);
create table recipe_category_associations
(
id int auto_increment primary key,
recipe_category_id int not null,
recipe_id int not null,
constraint recipe_category_associations_recipe_categories_id_fk
foreign key (recipe_category_id) references recipe_categories (id)
on update cascade on delete cascade,
constraint recipe_category_associations_recipes_id_fk
foreign key (recipe_id) references recipes (id)
on update cascade on delete cascade
);
On the Java side, I'm representing the structures as JPA entities:
#Entity
#Table(name = "recipes")
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
#OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
#JsonManagedReference
private Set<RecipeCategoryAssociation> recipeCategoryAssociations;
// ... setter/getters ...
}
#Entity
#Table(name = "recipe_categories")
public class RecipeCategory {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
#Column(name = "name", nullable = false)
private String name;
// ... setter/getters ...
}
#Entity
#Table(name = "recipe_category_associations")
public class RecipeCategoryAssociation {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
#ManyToOne(optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "recipe_category_id", nullable = false)
private RecipeCategory recipeCategory;
#ManyToOne(optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "recipe_id", nullable = false)
#JsonBackReference
private Recipe recipe;
// ... setter/getters ...
}
This works OK, but my hang-up is that to persist/save a new Recipe via REST JSON API, the caller needs to know about the join table recipe_category_associations. For example a PUT request w/ this payload could add a new Recipe to the DB associating it with the "category foo" recipe category:
{
"name": "Chicken soup",
"recipeCategoryAssociations": [{
"recipeCategory": {
"id": 123,
"name": "category foo"
}
}]
}
Using this in the controller:
#PutMapping(path = PATH, produces = "application/json")
#Transactional
public #ResponseBody Recipe addNewRecipe(#RequestBody Recipe recipe) {
return recipeRepository.save(recipe);
}
To me, the inclusion of "recipeCategoryAssocations" key in the JSON payload feels weird. From the client POV, it doesn't really need to know the mechanism creating this association is a join table. Really, it just wants to set a list of recipe category ids like:
{
"name": "Chicken soup",
"recipeCategories": [123, 456, ...]
}
Any tips how best to accomplish this in nice way? It'd be nice if I can keep the REST implementation super clean (e.g., like I have now with one recipeRepository.save(recipe); call). Thanks in advance.
When writing software we expect requirement to change. Therefore we want to make sure our code will be flexible and easy to evolve.
Coupling our server response with our DB structure makes our code very rigid. If a client needs a new field or if we want to arrange the DB schema differently everything will change.
There are several different approaches to designing your software correctly. A common approach is called "Clean Architecture" and is outlined in a book by this title by the great Uncle Bob. The Book itself outlines the approach in high level but there are many example projects online to see what it means in action.
For example this article by my favourite Java blog:
[baeldung.com/spring-boot-clean-architecture][1]
If you are looking for something simpler, you can follow the ["3-Tier Architecture"][2] (not really an architecture in my mind). Separate your code in to 3 layer:
Controller/Resource/Client
Service/BusinessLogic
Repository/DataAccess
Each layer will use a different data object. the business logic layer will have the object in it's purest form without constraints regarding who will want to read it and where it is stored and will be mapped/converted to the objects in the other layers as needed.
So in your case you might have 3 (or more) different objects:
RecipeDTO
Recipe
model.Recipe (and model.RecipeCategoryAssociation etc.)
Make sure that the Business level object only have fields that makes sense from a business logic. The code in each layer will use the objects that are relevant to that layer. When a rest controller class for example calls the business logic server it will need to convert the DTO object to the Business level object for example. Very important to maintain this separation between layers
I have 3 Entity A, B, C where C will created though JSON with #OneToOne relation Entity A and #ManyToOne Entity B,
How to send the data using JSON, do I only need to send the ID or complete data of Entity A,B. also I made the relation insertable and updateable falsed, because they dont need to update or create, only need to select among list of value.
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(...)
private Set<B> objB = new HashSet<B>();
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(...)
private A objA;
JSON I am posting,
{
"name": "Test Store",
"description": "Test Description",
"objB": [
{
"id": 19
}
],
"objA": {
"id": 1,
}
}
If I send like this, then Object is null for objB and JSON parse error for objA. How I can Handle this. what can be the best approche.
Did you try use like that instead?
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(...)
private List<B> objB;
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.
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.
I have the following two resources, and their association;
#Table(name = "Item")
#Data
#Entity
public class Item {
#ManyToOne
#JoinColumn(name = "fk_wrapper")
private Wrapper wrapper;
#Id
#GeneratedValue
private String id;
private Integer someValue;
}
and;
#Table(name = "Wrapper")
#Data
#Entity
public class Wrapper {
#Id
#GeneratedValue
private String id;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "fk_wrapper")
private List<Item> items;
private String someField;
}
Then, first, I create a Wrapper;
POST http://localhost:8080/wrappers/
{
"someField": "asd"
}
http://localhost:8080/wrappers/1 created, then I create two Item's, linked to this Wrapper;
POST http://localhost:8080/items/
{
"someValue": "5",
"wrapper": "http://localhost:8080/wrappers/1"
}
&
POST http://localhost:8080/items/
{
"someValue": "7",
"wrapper": "http://localhost:8080/wrappers/1"
}
After all this, when I call the endpoint http://localhost:8080/wrappers/1/items, I get the list of these two items, as expected, but what the trouble is that, I cannot seem to have a sorting feature on this endpoint. I seem to be able to sort in http://localhost:8080/items endpoint, but while fetching with association, there doesn't seem to be a sorting feature. Is this lack of sorting is intended, or am I lacking some configuration?
P.S. when I create a custom search method, for example;
#RepositoryRestResource
public interface ItemRepository extends JpaRepository<Item, String> {
List<Item> findByWrapper_Id(#Param("id") String id, Sort sort);
}
Then I can use the sorting with http://localhost:8080/items/search/findByWrapper_Id endpoint, but too ugly imo, considering there is already an auto-generated endpoint.
Spring Data Rest doesn't support sorting on the associations.
You seem to have already found the best way to do what you need, according to the Spring Data Rest team - create a query for fetching the data you need. That will indeed support both pagination and sorting.
The reason why it's not supported has to do with the time when the queries are made to fetch the main resource (before the association endpoints are built) and the facts that the association endpoint makes use of the the loaded entity associations directly and that for supporting sort, a new query would need to be made anyway.
More detailed information can be found here:
https://jira.spring.io/browse/DATAREST-725?focusedCommentId=122244&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-122244
Cheers!