简体   繁体   中英

Spring boot JPA OneToMany relationship - create related object if not exist

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.

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:

在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM