When creating my rest application, I had a big problem. It lies in the nesting of elements and my misunderstanding of how to properly give them through the rest controller. I have something like the following structure:
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
#JsonManagedReference
private List<TaskCard> taskCards;
}
public class TaskCard {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "taskCard", fetch = FetchType.LAZY)
#JsonManagedReference
private List<Task> tasks;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
#JsonBackReference
private User user;
}
public class Task {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "task_card_id")
#JsonBackReference
private TaskCard taskCard;
}
All these classes are entities. As you might have guessed, I have a rest controller for each of these classes. This is where the problems begin. In my head, the structure of my rest api should look something like this: /users/1/taskkards/1/tasks/1/. It looks complicated, right? Exactly! When I try to just get the user, then there are no problems in the principe, I can just send a /users request that my user rest controller will process. But if we go down to taskcard, then there are already problems. It turns out that my TaskCardController should look like this:
#RestController
#RequestMapping("users/{userId}/taskcards")
public class TaskCardController {
// get, post, update, delete methods
}
This already means that I must also transfer the user info here to make sure that the task card belongs to him. But at the same time, because my user has a task cards field, where can I get this value from? Do I need to go to the repository and look for it in the database, or just do something like that?
#GetMapping
public List<TaskCard> getAllTaskCards(#PathVariable("userId") User user) {
return user.getTaskCards();
}
Is it normal at all that such nesting arises? Why then do I need to create controllers for taskCard and task, if I can just get them from user? This is also one of the problems.If we go down even further to the task, then here everything is very bad. My task controller should look like this?
#RestController
#RequestMapping("users/{userId}/taskcards/{cardId}/tasks")
public class TaskController {
// get, post, update, delete methods
}
Something tells me no. This will be terrible if I have to get a user and a task card to request a task. What should I do in this situation? How do I design my rest API correctly?
For the Task and TaskCard controllers just send the JSON from the client and then fetch the corresponding User from the database. For example,
#RestController
#RequestMapping("/taskcards")
public class TaskCardController {
#Autowired
private UserRepo userRepo; // supposing you are using Spring Data repositories
#PostMapping("/addTaskCard")
public ResponseEntity addTaskCard(#RequestBody TaskCard newTaskCard){
User user = userRepo.getUserById(newTaskCard.getUser().getId());
user.getTaskCards().add(newTaskCard);
userRepo.save(user);
return ResponseEntity.ok().build();
}
}
You can continue using this method. Send just enough data from the client and then in the backend do your queries and get the other related entities from the databse.
Related
NOTE : Different concepts are not included in order to focus on the problem. It is normally incorrect to use Entity in API requests. In this example, Entities were used in the API architecture to focus only on the problem.
I was making some examples with JPA.
I had to use #JsonIgnore when I was establishing a relationship. When I didn't use JsonIgnore, it went into an infinite loop and gave a serialization error. Then I solved my problem by adding #JsonIgnore annotation to the relevant field.
However, for example, when I want to bring all the users using branch number 1 and branch number 1, I cannot return the list because it marks it with #JsonIgnore. Can I filter #JsonIgnore markup according to rest requests?
If a POST request comes, #JsonIgnore should work, but if a GET request comes, #JsonIgnore should be inactive.
#Entity
#Getter
#Setter
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private double balance;
#ManyToOne
#JoinColumn(name = "branchCode")
private Branch branch;
}
#Entity
#Getter
#Setter
public class Branch {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String address;
#Column(unique = true)
private Integer branchCode;
#OneToMany(mappedBy = "branch")
#JsonIgnore
private List<Account> accounts;
}
Add brach ( POST METHOD )
Get Account ( POST METHOD )
Get Account info ( GET METHOD )
Get Branch ( GET METHOD )
It is possible to add and remove annotations at runtime. Check out this article on the subject. The idea here is that you would set the annotation during a POST call, and remove the annotation during a GET call.
Whether or not this will work with Jackson, or your use case, I do not know, but your question related specifically to "use the #JsonIgnore object conditionally". Whether this approach works or not is something you can determine through experimentation, and hopefully report back with your results.
I've a spring boot application which uses Hibernate as an ORM and DGS framework as the graphql engine. I've been struggling with finding ways to initialize a lazy loaded collection, the proper way. I've the following scenario:
application.properties
# The below has been set to false to get rid of the anti-pattern stuff it introduces
spring.jpa.open-in-view=false
...
#Entity
public class User {
#Id
#GeneratedValue
private UUID id;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Article> articles;
...
}
#Entity
public class Article {
#Id
#GeneratedValue
private UUID id;
#ManyToOne(optional = false, fetch = FetchType.LAZY)
private User user;
...
}
My User data fetcher looks something like this:
#DgsComponent
public class UserDataFetcher {
#Autowired
private UserService userService;
#DgsQuery
public User getUserById(#InputArgument UUID id) {
return userService.findById(id);
}
...
}
My UserService looks something like this:
#Service
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Override
public User findById(UUID id) {
return userRepository.findById(id).orElseThrow(DgsEntityNotFoundException::new);
}
...
}
Now, I only want to initialize/load my articles collections from the DB when the user asks for it in the graphql query. For that purpose I created a child resolver for my articles which only executes when a user asks for the article in the query. My UserDataFetcher started looking like this:
#DgsComponent
public class UserDataFetcher {
#Autowired
private UserService userService;
#DgsQuery
public User getUserById(#InputArgument UUID id) {
return userService.findById(id);
}
#DgsData(parentType = "User", field = "articles")
public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
User user = dfe.getSource();
Hibernate.initialize(user.getArticles());
return user.getArticles();
}
...
}
But, the above started throwing exceptions telling me that Hibernate couldn't find an open session for the above request. Which made sense because there wasn't any so I put a #Transactional on top of my child resolver and it started looking like this:
#DgsComponent
public class UserDataFetcher {
#Autowired
private UserService userService;
#DgsQuery
public User getUserById(#InputArgument UUID id) {
return userService.findById(id);
}
#DgsData(parentType = "User", field = "articles")
#Transactional
public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
User user = dfe.getSource();
Hibernate.initialize(user.getArticles());
return user.getArticles();
}
...
}
However, the above didn't work either. I tried moving this #Transactional into my service layer as well but even then it didn't work and it throwed the same exception. After much deliberation, I founded out that (maybe) Hibernate.initialize(...) only works if I call it in the initial transaction, the one which fetched me my user in the first place. Meaning, it's of no use to me since my use-case is very user-driven. I ONLY want to get this when my user asks for it, and this is always going to be in some other part of my application outside of the parent transaction.
I am looking for solutions other than the following:
Changing the child resolver to something like this:
#DgsData(parentType = "User", field = "articles")
#Transactional
public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
User user = dfe.getSource();
List<Article> articles = articlesRepository.getArticlesByUserId(user.getUserId);
return articles;
}
I am not in the favor of the above solution since I feel this is under-utilizing the ORM itself by trying to resolve the relation yourself rather than letting hibernate itself do it. (Correct me if I wrong thinking this way)
Changing my User entity to use FetchMode.JOIN.
#Entity
public class User {
#Id
#GeneratedValue
private UUID id;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
private List<Article> articles;
...
}
This is the same as telling hibernate to eagerly load the below collection no matter what. I don't want this either.
Setting spring.jpa.open-in-view=false to spring.jpa.open-in-view=true. Not in the favor of this either since this is just a band aid for LazyInitializationExceptions.
Any other solutions that just makes your forget about LazyInitializationException by keeping the session open throughout the lifecycle of the request.
Please note this answers assumes that Spring Data JPA can be used.
Helpful can be full dynamic usage of EntityGraphs
Entity Graphs give us a possibility to define fetch plans and declare which
relations (attributes) have to be queried from the database.
According to the documentation
You can do something similar to this
productRepository.findById(1L, EntityGraphUtils.fromAttributePaths(“article, “comments”));
And pass all necessary params (relations) based on user selection to the EntityGraphUtils.fromAttributePaths method.
This give us possibility to fetch only necessary data.
Additional resources:
Sample project
Spring Blog mentioned this extension
JPA EntityGraph
EntityGraph
Another workaround I've used is to skip any child resolver and just load additional entities conditionally in the base resolver.
#DgsQuery
public User getUserById(#InputArgument UUID id) {
var user = userService.findById(id);
if (dfe.getSelectionSet().contains("articles") {
Hibernate.initialize(user.getArticles());
}
return user;
}
I'm currently creating a microservice with spring boot and mysql to manage information about auctions. I have created a Bid-object and an Offer-object. Next to some properties of bid and offers, the most important thing here is the OneToMany-Relationship between Offer and Bid, since obviously every offer can have multiple related Bids.
I use the default JpaRepository-Interface for my database interactions, and tested my database structure by entering data and testing if I would get the correct output. This all worked fine, but when I tried to test the endpoints of my service that entered the data, I got some curious behaviour. First of all, here's my structure, so you can keep up with what I'm talking about. These are (shortened) versions of my Bid and Offer-Objects:
#Entity
#Data
public class Bid {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#NotNull
private String bidderUUID;
#NotNull
#JsonBackReference
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "offerId")
private Offer offer;
#NotNull
private Integer amount;
private Boolean hasWon;
}
#Entity
#Data
public class Offer {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
private String creatorUUID;
#JsonManagedReference
#OneToMany(mappedBy = "offer")
List<Bid> bids;
}
This is my very simple repsitory and controller:
public interface BidRepository extends JpaRepository<Bid, Long> {
}
#RestController
#RequestMapping("/bid")
public class BidController {
#Autowired
private BidRepository bidRepository;
#GetMapping("/bids")
public List<Bid> getAllBids() {
return bidRepository.findAll();
}
#PostMapping("/add")
public void createBid(#RequestBody Bid request) {
bidRepository.saveAndFlush(request);
}
}
With and offer with the id 27 in the database I proceeded to send a bid to the service.
I'm using postman to test my requests, and this is what I put in my request body, when adressing the endpoint localhost:8080/bid/add:
{
"amount": 2,
"bidderUUID": "eine uuid",
"offerId": 27
}
I received a 200 OK response, and thought, seems fine, but the data in the database is wrong, since it looks like this:
The offer is missing, even though the ID 27 definitely exists. Also, when I'm entering 27 manually and pushing it to the database, the data is correctly recognized.
I think this problem has something to do with the fact, that I expect an offer-object when posting the new bid, but only give him the ID, but when I enter the entire object, I get an org.hibernate.PersistentObjectException: detached entity passed to persist.
How can I make spring accept the id of the offer, when transmitting a new bid object?
This happed to me also, so as the solution I added the mapping column as Entity variable. In your case if I say, your Bid entity is missing the offer_id column mapping, although it mentioned the relationship. Add below entry in your Bid table as below:
#Column(name = "offer_id")
private Long offerId;
// generate setter-getter
add referencedColumn on the #JoinColumn annotation.
#JoinColumn(name = "offerId", referencedColumn = "id")
private Offer offer;
I'm building a Spring Data REST / Spring HATEOAS based application and I'm attempting to following the principles of DDD outlined here (and elsewhere):
BRIDGING THE WORLDS OF DDD & REST - Oliver Gierke
In particular the concept of aggregates and complex state changes via dedicated resources.
Also avoid using HTTP PATCH or PUT for (complex) state transitions of your business domain because you are missing out on a lot of information regarding the real business domain event that triggered this update. For example, changing a customer’s mailing address is a POST to a new "ChangeOfAddress" resource, not a PATCH or PUT of a “Customer” resource with a different mailing address field value.
What I'm struggling with is a means of enforcing this while allowing cosmetic changes to the aggregate root.
Using this simplified example:
#Entity
public class Customer
{
private #Id #GeneratedValue(strategy = GenerationType.AUTO) Long id;
private String name;
private String comment;
#Access(AccessType.PROPERTY)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses = new HashSet<>();
... getters and setters
public void addAddress(Address address)
{
addresses.add(address);
... custom code to raise events etc
}
}
public interface Customer extends CrudRepository<Customer, Long>
{
}
What is the best/correct way to allow a cosmetic change (e.g. update comment) but but prevent changes that directly update the child collection?
The only thing I can think of doing if having the setter throw an exception if there is an attempt to modify the child collection.
#Entity
public class Customer
{
private #Id #GeneratedValue(strategy = GenerationType.AUTO) Long id;
private String name;
private String comment;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
#Access(AccessType.PROPERTY)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses = new HashSet<>();
}
If I have a entity that contains an object of an another class, for example a Book entity that has within it a Publisher entity that is associated as follows:
#ManyToOne
#JoinColumn(name="PUB_CODE", referencedColumnName = "PUB_CODE")
private Publisher pub;
Is this a secure/correct (I saw the correct data in the DB in this example, but not 100% sure if it would work in all cases) approach to post an object that has foreign key association in the database? I don't know if this is safe to do in terms of transaction atomicity or in terms of threading, or if it is efficient. Relevant code below:
Book.java
package app.domain;
/*imports*/
#Entity
public class Book implements Serializable{
/**
*
*/
private static final long serialVersionUID = -6902184723423514234L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(nullable = false, unique=true)
private String bookName;
#Column(nullable = false)
private int pageCount;
#ManyToOne
#JoinColumn(name="PUB_CODE", referencedColumnName="PUB_CODE")
private Publisher pub;
/*public getters and setters*/
}
Publisher.java
package app.domain;
/*imports*/
#Entity
public class Publisher implements Serializable {
private static final long serialVersionUID = 4750079787174869458L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="PUB_CODE",nullable = false, unique = true)
private String publisherCode;
#Column(nullable = false)
private String publisherName;
/*public getters and setters*/
}
BookRepo.java
package app.service;
/*imports*/
public interface BookRepo extends JpaRepository<Book, Long>{
#Query("SELECT pb FROM Publisher pb WHERE pb.publisherCode = TRIM(UPPER(:pubCode))")
public Publisher findPublisherByPubCode(#Param("pubCode")String pubCode);
}
BookController.java
package app.controller;
/*imports*/
#RestController
#RequestMapping(value = "/books")
public class BookController {
private BookRepo bookRepo;
#Autowired
public BookController(BookRepo bookRepo) {
this.bookRepo = bookRepo;
}
//The ApiPathParam is for JSONDOC purposes
#RequestMapping(value = "/create", method = RequestMethod.POST)
public List<Book> create(#ApiPathParam(name = "book") #RequestBody Book book, #ApiPathParam(name = "pubCode") #RequestParam("pubCode") String pubCode) {
// Assume exception handling
Publisher pbToAttachToThisBook = bookRepo.findPublisherByPubCode(pubCode);
book.setPub(pbToAttachToThisBook);
bookRepo.save(book);
return bookRepo.findAll();
}
}
Post object body (input into a POST tool):
{
"bookName": "goosebumps",
"id": 0,
"pageCount": 332,
"pub": {
"id": 0,
"publisherCode": "",
"publisherName": "",
"serialVersionUID": 0
},
"serialVersionUID": 0
}
pubCode parameter input provided, also into the POST tool, in the same call as above: 'SC'
After the above code was executed, in the Book table, there was an entry for the book above, with its PUB_CODE foreign key column filled in with 'SC', and the returned List<Book> of the POST controller method that was called showed that the newly added book included the Publisher entity information (such as the full name "Scholastic") for publisher with PUB_CODE='SC' that was already existing in the database.
Thank you.
The technique you posted originally (passing the FK ID, retrieving it manually in your controller, and setting it on the entity explicitly) is valid and secure.
I don't know of a cleaner approach unless you move to HATEOAS principals, which allows for resource link handling: http://projects.spring.io/spring-hateoas/
Sounds like you need to separate/decouple your data layer's domain model from your Rest Resources/ API specs, as they could evolve at a different pace. Also your choice of JPA should not influence the API specs.
Should this feel like something you want to pursue there lots of resources out there including Best Practices for Better RESTful API