I have two entities like below - one with an auto generated ID and one more with Assigned Id.
Id of assignee is sent from request it self.
And i have ManyToOne relationship between them. But when i Insert it shows gives below error
"org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing: com.example.entity.Task.assignee->com.example.entity.Assignee
Below Are my classes and annotations
package com.example.entity;
#Data
#Entity
public class Task {
#GeneratedValue(generator = "system-uuid")
#Id
private UUID id;
private String name;
private String description;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "assignee_id")
private Assignee assignee;
}
package com.example.entity;
#Data
#Entity
public class Assignee implements Persistable<UUID> {
#Id
private UUID id;
private String name;
#OneToMany(mappedBy = "assignee",fetch = FetchType.LAZY)
private List<Task> tasks = new ArrayList<>();
#Transient
private boolean isNew = true;
#Override
public boolean isNew() {
return isNew;
}
#PrePersist
#PostLoad
void markNotNew() {
this.isNew = false;
}
}
Below are my repository and service class which gets called from controller
#Repository
public interface TaskRepository extends JpaRepository<Task, UUID> {
}
#Service
#Slf4j
public class TaskServiceImpl implements TaskService {
#Autowired
TaskRepository taskRepository;
#Override
public Task create(Task request) {
return taskRepository.save(task);
}
#Override
public Task update(Task request) {
return taskRepository.save(request);
}
}
So when I send a request from postmen - like below
{
"name": "test task",
"description": "Do this task man",
"assignee": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Lucia"
}
}
So as i told task - has auto generated ID , where as Assignee ID is sent from caller of this API. But i get below exception - when i call save method of repository. It is complaining to save assignee, but i have given Cascade.ALL
in the mapping of entity.
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing: com.example.entity.Task.assignee->com.example.entity.Assignee
Related
I have two classes one for Customers and one for Transaction. In the transaction, I have a field custID(int) which is also present in Customer. I have all the getter and setter, repos, services, and controller as well. But in one of my methods in the service layer, I am getting NoSuchElementException.
I understand that while the code runs and checks for a record in the database with passed custID, it cannot find the record. But I have mentioned what to do in such case. But my code doesn't move to the code block at all.
Customer Class
#Entity
#Table(name = "customers")
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int custID;
private String custName;
private String email;
private String phone;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
private LocalDate registrationDate;
Transaction Class
#Entity
#Table(name = "transactions")
public class Transaction {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int transID;
private int custID;
private int transAmount;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
private LocalDate transDate;
Service layer from where the issue arises:
TransactionServiceImpl
#Override
public Transaction addTransaction(CustomerTransaction customerTransaction) {
Customer customer = customerTransaction.getCustomer();
Transaction transaction = customerTransaction.getTransaction();
if(customerService.findCustomerByID(transaction.getCustID()) == null) {
customerService.addCustomer(customer);
return transactionRepository.save(transaction);
} else{
return transactionRepository.save(transaction);
}
}
What I am doing is, pass a wrapper object CustomerTransaction that has the info for the customer and transaction. And check if the customer is already registered with custID. If its there, it only records the transaction(which works fine as in 'else' block). But if it is not there I want to record the customer and the transaction both as in 'if' block. But it throws the NoSuchElementException: No value present.
But if I am to pass only the customer details via customer's service layer it adds the customer.
CustomerServiceImpl
#Override
public Customer addCustomer(Customer customer) {
return customerRepository.save(customer);
}
Postman Requests:
For customer only:
"custName": "Bibek Bhattarai",
"email": "spongebob#gmail.com",
"phone": 9803064423,
"registrationDate": "03-01-2023"
}
For customerTransaction:
{
"customer":{
"custName": "Sponge Bob",
"email": "spongebob#gmail.com",
"phone": 9803064423,
"registrationDate": "03-01-2023"
},
"transaction":{
"custID":9,
"transAmount": 5000,
"transDate": "04-01-2023"
}
}
You may get this exception because the value returned by findCustomerByID() is an Optional of Customer (Optional<Customer>) and not null.
Instead of customerService.findCustomerByID(transaction.getCustID()) == null you should have customerService.findCustomerByID(transaction.getCustID()).isEmpty().
Say I have a oneToMany relationship between Person and Job. Each person has only one job, while a job has many persons.
I have a controller which calls the service which calls the repository that will execute the queries.
here they are:
#RestController
#CrossOrigin()
#RequestMapping(path = "api/person")
public class PersonController {
private final PersonService personService;
#Autowired
public PersonController(PersonService personService) {
this.personService = personService;
}
#PostMapping
public Person storePerson(#RequestBody Person person) {
return this.personService.storePerson(person);
}
//...more code is also here
}
#Service
public class PersonService {
private final PersonRepository personRepository;
#Autowired
public PersonService(PersonRepository personRepository, CountryRepository countryRepository,
JobRepository jobRepository, RoleRepository roleRepository, HairColorRepository hairColorRepository) {
this.personRepository = personRepository;
}
public Person storePerson(Person person) {
return this.personRepository.save(person);
}
//...more code is also here
}
#Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
}
Now the models and the way I define the relationship between them. I can code this in two ways.
Senario 1:
#Entity
#Table(name = "people")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = Job.class)
#JoinColumn(name = "job_id")
private Job job;
// ...getters and setters, constructors, toString(), etc are here
}
#Entity
#Table(name = "jobs")
public class Job {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String name;
#OneToMany(mappedBy = "job", orphanRemoval = true, cascade = CascadeType.ALL)
private Set<Person> persons;
// ...getters and setters, constructors, toString(), etc are here
}
I use postman to insert records into this database.
I send a POST request and this is the body:
First Json
{
"name": "James",
"job": {
"id": null,
"name": "Doctor"
}
}
This works perfectly, because it creates the person, it also creates a new job that DID NOT EXIST in the database, and also creates the relationship between the two.
But on second request, I want to reuse the Job. So I make this request:
Second Json
{
"name": "David",
"job": {
"id": 1,
"name": "Doctor"
}
}
Here I get an Exception:
{
"timestamp": "2022-08-05T11:20:41.037+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "detached entity passed to persist: ir.arm.archiver.job.Job; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: ir.arm.archiver.job.Job",
"path": "/api/person"
}
Senario2
If I change the Cascade values in the relationship annotations a bit, I get the exact opposite results. If in Person.java I change the annotations for the private Job job field to use Cascade.MERGE like this:
#ManyToOne(cascade = CascadeType.MERGE, fetch = FetchType.EAGER, targetEntity = Job.class)
#JoinColumn(name = "job_id")
private Job job;
Then, when I pass the First Json, this time, I get an exception:
{
"timestamp": "2022-08-05T11:36:17.854+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : ir.arm.archiver.person.Person.job -> ir.arm.archiver.job.Job; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : ir.arm.archiver.person.Person.job -> ir.arm.archiver.job.Job",
"path": "/api/person"
}
BUT, if I create the job record myself in the database, and then I execute the request with the Second Json, it will work, and create the person with the relationship to the existing job record.
Now my question is:
How Can I combine the two? I want the JPA to do both.
Is there any way, to be able to pass both jsons and the jpa automatically creates the job, if the Id is null, and fetch and reuse it if it has Id?
I found the fix. Remove cascade attribute for Job member variable of Person Entity. JPA is combining both and also reusing the existing job from database.
Update Person Entity :
#Entity
#Table(name = "people")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne(fetch = FetchType.EAGER, targetEntity = Job.class)
#JoinColumn(name = "job_id")
private Job job;
// ...getters and setters, constructors, toString(), etc are here
}
Output (In DB Person Table) :
Job Table :
I have an entity class State which extends abstract class Location and implemented it's abstract methods. When I made a PostMapping request on postman with the content type set to Json, I got an error unsupported Media Type as shown below:
"error:" Unsupported Media Type,
"trace": org.springframework.web.HttpMediaTpeNotSupportedException:Content type:'application/json;...
Abstract class:
#MappedSuperclass
public abstract class Location {
#Id
#Column(name="id")
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Integer id;
#Column(name = "long")
private String longitude;
#Column(name = " lat")
private String latitude;
public Location (){}
//assessors & mutators ommited with abstract methods
}
Concrete State entity class:
#Table(name="State")
public class State extends Location {
#Pattern(regexp = "[0-9]{5}")
private String sateCode;
#JsonManagedReference
#OneToOne(mappedBy="state",
cascade=CascadeType.All, fetch=FetchType.Lazy)
private City city = new City();
//constructors & other implementations omitted
#Override
public void setCity(String city){
this.city=city
}
public void setStateCode(String code){
this.stateCode = stateCode;
}
///getter omitted
}
Concrete City entity class:
#Table(name = "City");
public class City extends Location {
#JsonBackReference
#OneToOne(cascade={
CascadeType.DeTACH, CascadeType. MERGE})
#JoinColumn(name = "state_city_id")
private State state;
//constructors & other implementations omitted
#Override
public void setState(State state) {
this.state=state;
}
}
}
Controller:
#Controller
#RequestMapping(path = "location/destination")
public class LocationController{
#Autowired
#Qualifier("stateImpl")
private LocationService service;
#PostMapping(value = "/state", consumes = org.springframework.http.MediaType.APPLICATION_JSON_VALUE, produces = org.springframework.http.MediaType.APPLICATION_JSON_VALUE)
public void save( #Valid #RequestBody State s ,BindingResult theBindingResult,
HttpServletResponse r) throws IOException{
if(theBindingResult.hasErrors()) {
throw new InputException("Wrong data input!");
}
service.save(s);
r.sendRedirect("");
}
#GetMapping()
public ResponseEntity<Object>findAll(){
return new ResponseEntity<Object>(service.findAll(),HttpStatus.OK);
}
}
Error message on postman:
"status": 415,
"error": "Unsupported Media Type",
"trace": "org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8 not supported\r\n\tat...'
The code worked when I changed the city field in the State entity class like so:
#Entity
#Table(name="State")
public State extends Location {
#JsonBackReference
#OneToOne(mappedBy="state",
cascade=CascadeType.All, fetch=FetchType.Lazy)
private City city = new City();
}
And the state field in the City entity like so:
#Entity
#Table(name="City")
public class City extends Location {
#JsonManagedReference
#OneToOne(cascade={
CascadeType.DeTACH, CascadeType. MERGE})
#JoinColumn(name = "state_city_id")
private State state;
}
```.
Though I'm yet to understand what really happened behind the scenes.
I have two entities (Project, OtherData) with one abstract entity. I'm using MySQL and Quarkus framework.
Problem: When I try to save Project entity field project_id remains null.
Table schemas:
On next picture there is shown, fk constraint in "project_other_data" table:
Abstract Entity:
#MappedSuperclass
public class AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
// getters and setters
}
Project Entity
#Entity
#Table(name = "projects")
public class Project extends AbstractEntity {
#NotNull
#Column(name = "name")
private String name;
#NotNull
#Column(name = "surname")
private String surname;
#Column(name = "date_create")
#JsonbDateFormat(value = "yyyy-MM-dd")
private LocalDate dateCreate;
#Column(name = "date_update")
#JsonbDateFormat(value = "yyyy-MM-dd")
private LocalDate dateUpdate;
#OneToOne(mappedBy = "project", cascade = CascadeType.ALL)
private OtherData otherData;
// getters and setters
}
OtherData Entity
#Entity
#Table(name = "project_other_data")
public class OtherData extends AbstractEntity {
#OneToOne
#JoinColumn(name = "project_id")
private Project project;
#Column(name = "days_in_year")
private Integer daysInYear;
#Column(name = "holidays_in_year")
private Integer holidaysInYear;
#Column(name = "weeks_in_year")
private Integer weeksInYear;
#Column(name = "free_saturdays")
private Integer freeSaturdays;
#Column(name = "downtime_coefficient")
private BigDecimal downtimeCoefficient;
#Column(name = "changes")
private Integer changes;
// getters and setters
}
Saving entities with code:
#Path("projects")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class ProjectRest {
#Inject
ProjectService projectService;
#POST
public Response saveProject(Project project) {
return Response.ok(projectService.saveProject(project)).build();
}
}
#RequestScoped
#Transactional
public class ProjectService {
#Inject
EntityManager entityManager;
public Project saveProject(Project project) {
if (project.getId() == null) {
entityManager.persist(project);
} else {
entityManager.merge(project);
}
return project;
}
}
I was able to reproduce the problem by POSTing a new Project with an embedded OtherData. The body I used for the POST:
{
"name": "John",
"surname": "Doe",
"otherData": {}
}
Point is: the database entity is also used as DTO. Thus, the field project in otherData for the request body is set to null (since no Project is passed along this would be a recursive infinite definition).
During processing the entity from the rest controller to the service to the repository, the project of otherData is never set. A quick fix is to modify ProjectService::saveProject as follows:
public Project saveProject(Project project) {
project.getOtherData().setProject(project); // This line was added
if (project.getId() == null) {
entityManager.persist(project);
} else {
entityManager.merge(project);
}
return project;
}
This will fix the database issue (the project_id will be set), but leads to the next issue. The response body cannot be serialized due to an
org.jboss.resteasy.spi.UnhandledException: javax.ws.rs.ProcessingException: RESTEASY008205: JSON Binding serialization error javax.json.bind.JsonbException: Unable to serialize property 'otherData' from com.nikitap.org_prod.entities.Project
...
Caused by: javax.json.bind.JsonbException: Recursive reference has been found in class class com.nikitap.org_prod.entities.Project.
The object structure is cyclic (project references otherData, which return references project, ...) and Jackson is unable to resolve this cycle.
To fix this issue, I would suggest to separate DTOs and database entity and explicitly map between them. In essence:
Structure the Dto-object to represent the JSON-Request and -Response you expect to receive, in a non-cyclic order
Transfer JSON-related annotations from the database entity classes to the DTO classes
In the service- or repository-layer (your choice), map the DTO to the database entites, setting all fields (including the references from project to otherData and vice-versa)
In the same layer, map database-entites back to non-cyclic DTOs
Return the DTOs from the REST endpoint
I use Micronaut Data with JPA and have two entities. The first one is Recipe:
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
#ManyToOne
private Category category;
#OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<Step> steps;
// + other fields, getters and setters
}
The second one is ParseError which refers to Recipe:
#Entity
#Table(name = "parse_error")
public class ParseError implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY)
private Recipe recipe;
#Id
#Enumerated(EnumType.ORDINAL)
#Column(name = "problem_area")
private ProblemArea problemArea;
private String message;
// + other fields, getters and setters
}
Now I would like to provide DTO in API with ParseError properties but not with whole Recipe entity because it contains ManyToOne and OneToMany relations which are not needed in this case. So I created projection DTO for that:
#Introspected
public class ParseErrorDto {
private Integer recipeId;
private String recipeName;
private ParseError.ProblemArea problemArea;
private String message;
// + getters and setters
}
And added listAll() method into ParseErrorRepository:
#Repository
public interface ParseErrorRepository extends CrudRepository<ParseError, Integer> {
List<ParseErrorDto> listAll();
}
But it seems that Micronaut Data is not able to project properties from nested entities or I missed something in the DTO or the repository method:
ParseErrorRepository.java:22: error: Unable to implement Repository
method: ParseErrorRepository.listAll(). Property recipeId is not
present in entity: ParseError
I also tried to create RecipeDto:
#Introspected
public class RecipeDto {
private Integer id;
private String name;
// + getters and setters
}
And updated ParseErrorDto accordingly:
#Introspected
public class ParseErrorDto {
private RecipeDto recipe;
private ParseError.ProblemArea problemArea;
private String message;
// + getters and setters
}
Again no success:
ParseErrorRepository.java:22: error: Unable to implement Repository
method: ParseErrorRepository.listAll(). Property [recipe] of type
[RecipeDto] is not compatible with equivalent property declared in
entity: ParseError
Is Micronaut Data able to handle this use case by DTO projection? If not then is there another way how can I solve it in Micronaut Data?
Now (in latest version 1.0.0.M1) it is not possible. So I created feature request issue for that: https://github.com/micronaut-projects/micronaut-data/issues/184
Current workaround is to map entity bean into DTO bean in Java stream or reactive stream for example and do the properties mapping manually or by Mapstruct.
Update: Here is an answer to question from comments with an example how to do the workaround using Mapstruct:
Add Mapstruct dependency into build.gradle:
implementation "org.mapstruct:mapstruct:$mapstructVersion"
annotationProcessor "org.mapstruct:mapstruct-processor:$mapstructVersion"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:$mapstructVersion"
Define mapper:
import org.mapstruct.Mapper;
#Mapper(
componentModel = "jsr330"
)
public interface ParseErrorMapper {
ParseErrorDto entityToDto(#NotNull ParseError parseError);
EntityReference recipeToDto(#NotNull Recipe recipe);
}
And here is a usage of that mapper in the controller:
#Controller("/parse-error")
public class ParseErrorController {
private final ParseErrorRepository repository;
private final ParseErrorMapper mapper;
public ParseErrorController(ParseErrorRepository repository, ParseErrorMapper mapper) {
this.repository = repository;
this.mapper = mapper;
}
#Get("all")
#Transactional
public Page<ParseErrorDto> getAll(final Pageable pageable) {
return repository.findAll(pageable).map(mapper::entityToDto);
}
}