Spring: Create object with endpoint by id - java

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;

Related

how to Use #JsonIgnore according to the rest request

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.

How to paginate entities with OneToMany relationship with Fetch without warning firstResult/maxResults specified with collection fetch?

I would like to be able to create pagination for pulling all customers from the database (MYSQL), but I encountered a hibernate n+1 problem, which I then solved, but I encountered another problem: 2023-02-09 16:57:04.933 WARN 11660 --- [io-8080-exec-10] o.h.h.internal.ast.QueryTranslatorImpl : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
This problem I tried to solve with EntityGraph, but still nothing. Then I tried to use two Query, which collected the id and then used the IN clause, but this caused a huge sql query, which led to the generation of many "IN" which, with a huge dataset, can be problematic.
I am currently in a quandary and do not know how to solve this problem. I would like the figures to be fetched along with the customers, but I have no idea how to do it in such a way that the pagination works properly
I want to return CustomerDTO who have numberOfCreatedFigures attribute which is mapping from method in customer entity. This method is returning a size of customer figures.
I am using lombok for args/getters/setters. I've been trying to do everything, but nothing seems to fix the issue.
Config class with a mapper
#Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.createTypeMap(Customer.class, CustomerDTO.class)
.addMappings(mapper -> mapper
.map(Customer::numberOfCreatedFigures, CustomerDTO::setNumberOfFigures));
return modelMapper;
}
Customer class
public class Customer implements UserDetails, Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank(message = "Your name cannot be blank")
private String name;
#NotBlank(message = "Your name cannot be blank")
private String surname;
#NotBlank(message = "Your login cannot be blank")
private String login;
#NotBlank(message = "Your password cannot be blank")
private String password;
#Enumerated(EnumType.STRING)
private Role role;
private Boolean locked = false;
private Boolean enabled = true;
#OneToMany(mappedBy = "createdBy",
cascade = {CascadeType.MERGE, CascadeType.PERSIST},
fetch = FetchType.LAZY,
orphanRemoval = true)
#ToString.Exclude
private Set<Figure> figures = new HashSet<>() ...;
Figure class
public abstract class Figure implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(updatable = false, insertable = false)
private String figureType;
#Version
private Integer version;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "created_by_id")
#CreatedBy
#ToString.Exclude
private Customer createdBy;
#CreatedDate
private LocalDate createdAt;
#LastModifiedDate
private LocalDate lastModifiedAt;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "last_modified_by_id")
#LastModifiedBy
#ToString.Exclude
private Customer lastModifiedBy;
private Integer numberOfModification = 0 ...;
CustomerDTO class
public class CustomerDTO {
private Long id;
private String name;
private String surname;
private String login;
private Integer numberOfFigures;
private Role role;}
Method from Customer Controller
#GetMapping
public ResponseEntity<Page<CustomerDTO>> listAll(#PageableDefault Pageable pageable) {
return new ResponseEntity<>(customerService.listAll(pageable)
.map(customer -> modelMapper
.map(customer, CustomerDTO.class)), HttpStatus.OK);
}
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(Customer.class)
public interface CustomerDTO {
#IdMapping
Long getId();
String getName();
String getSurname();
String getLogin();
#Mapping("SIZE(figures)")
Integer getNumberOfFigures();
Role getRole();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
CustomerDTO a = entityViewManager.find(entityManager, CustomerDTO.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<CustomerDTO> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
You could load the customers with the figures relationship eagerly initialized.
For this case, an entity graph would be suitable. You'd need to create a new repository method like this:
#Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
#EntityGraph(attributePaths = "figures")
List<Customer> findWithFiguresBy(Pageable pageable);
}
Then, you'd need call this repository method when searching instead of the one you are using now. With this approach, your figures relationship can remain lazily fetched (which is generally important as eager fetching is a code smell), but whenever you need to fetch customers with the figures eagerly loaded, you can use this method.
If you want to lear more about entity graphs, I recommend these articles:
JPA Entity Graph by Hibernate maintainer Vlad Mihalcea
JPA Entity Graph by Baeldung
Side note: if you had more than one association which needs to be loaded eagerly, you couldn't use an entity graph for that as it would result in a MultipleBagFetchException. Instead, you would load your parent entities as usual and then collect all ids into a list (say customerIds). Then, you'd need to load all child associations (say figures and otherFigures) by the customer id (JPQL example: select f from Figure f where f.customer.id in :customerIds) and place the figures in a Map<Long, List<Figure> (where the Long parameter is the customer id). Your mapper logic would then need to use the entities from the Maps for the DTOs instead of directly from the parent entity.

Problems with the REST API

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.

In spring boot JPA, how to properly POST an object whose entity representation has a foreign key association to a different entity?

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

How to implement lazy loading using Spring data JPA (JPARepository)?

I am using Spring Data JPA and Hibernate as a provider. I've created several Repository classes which extends to JPARepository<Entity,Serializable> class. I am failing at the moment when I am fetching one entity it brings attached / connected entities along with it ! which are either connected via #OneToOne #OneToMany etc. How can I avoid fetching those connected entities ?
I have tried with #OneToMany(fetch=FetchType.LAZY) etc but still no luck. Following are my java code:
Repository
public interface TicketRepository extends JpaRepository<Ticket, Integer>{
}
Ticket Entity
#Entity
#Table(name = "tbl_tickets")
public class Ticket {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Integer id;
#Column(name = "customer", nullable = false, length = 256)
private String customer;
#OneToOne(cascade=CascadeType.ALL,fetch=FetchType.LAZY)
#JoinColumn
private User creator;
// ... other properties
}
Service
#Service
public class TicketService {
public Ticket save(Ticket obj,String id) {
User user = userService.findById(Integer.valueOf(id));
obj.setCreator(user);
Ticket savedTicket = ticketRepository.save(obj);
}
}
savedTicket always fetches User entity as well which I do not want to. How could I achieve this ?
Thanks
Get Lazy loading working on nullable one-to-one mapping you need to let hibernate do Compile time instrumentation and add a #LazyToOne(value = LazyToOneOption.NO_PROXY) to the one-to-one relation.
#OneToOne(cascade=CascadeType.ALL,fetch=FetchType.LAZY)
#JoinColumn
#LazyToOne(value = LazyToOneOption.NO_PROXY)
private User creator;
Hope this will work.

Categories