I have two entities, Book and BookEvent. They are linked by a ManyToOne-Relationship, i.e. one Book can have many BookEvents.
I view Book as the aggregate root and have one Repository for the Book entity, but not for the BookEvent.
#Entity
#Table(name = "book")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BookEvent> events = new ArrayList<>();
public List<BookEvent> getEvents() {
return events;
}
public void setEvents(List<BookEvent> events) {
for (BookEvent event: events) {
event.setBook(this);
}
this.events = events;
}
public void addEvent(BookEvent event) {
events.add(event);
event.setBook(this);
}
public void removeEvent(BookEvent event) {
events.remove(event);
event.setBook(null);
}
}
(Other getters/setters are omitted here).
#Entity
public class BookEvent {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne
#JoinColumn(name="book_id")
private Book book;
private LocalDate date;
#PreRemove
private void removeEventFromBook(){
book.removeEvent(this);
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
if (!this.book.getEvents().contains(this)) {
this.book.getEvents().add(this);
}
}
}
I now want to add a new event to the book after the book has been created. I use Spring Data Rest.
Creating the book with a POST and one event works fine:
{
"title": "My example book",
"events": [
{
"type": "BOUGHT",
"date": "2017-05-09"
}
]
}
Gives the answer:
{
"title": "My example book",
"events": [
{
"id": 3,
"date": "2017-05-09",
"_links": {
"book": {
"href": "http://localhost:8080/api/books/2"
}
}
}
]
}
But if I then do a JSON Patch to append one new event, the event is included in the response to the PATCH request, but it is actually not saved in the database (a GET on the book afterwards does not return the event and when the database the column book_id is null).
[
{
"op": "add",
"path": "/events/-",
"value":
{
"date": "2017-05-09"
}
}
]
When using the debugger, the setEvents() method is called on the initial POST request, but during the PATCH request, only the getEvents() method is called - no setBook() or addEvent() method. I think the problem is there.
Do I have a problem with my entity setup?
The problem was my setup as a bidirectional OneToMany setup without a join table. The problem can be fixed in two ways:
Create a join table. This is done by adding a #JoinTable annotation to the events attribute the Book class. This needs one additional table in the database, therefore I did not chose this way.
Use a unidirectional OneToMany setup (see Java Persistence/OneToMany). This is only supported by JPA 2.x, but this was no problem in a Spring Boot 2.0 setup. The implementation looks really clean this way.
My code now looks as following:
#Entity
#Table(name = "book")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(cascade = ALL) //The cascade is important as otherwise a new event won't be saved.
#JoinColumn(name="book_id", referencedColumnName = "id")
private List<BookEvent> events = new ArrayList<>();
//Standard getter and setter for getEvents() and setEvents()
}
#Entity
public class BookEvent {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name="book_id")
private Long bookId;
//No getter/setter for bookId was necessary
}
No special getters/setters which update the reciprocal link were necessary. This will get a clean JSON response with SDR without a _links attribute on each event. Adding and deleting new entries work as well.
Related
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 a springboot application with JPA. The ORM setup is a ManyToOne and I have roughly followed the excellent post here from Vlad so that I have only setup the #ManyToOne on the child.
My Entities are HealthCheck (Many) which must have a Patient (One). The issue is that when I retrieve a HealthCheck via my Rest controller instead of getting just the id of the patient I get the whole entity.
For my project I probably will get the whole patient with a HealthCheck, but I would like to know how i could get just the HealthCheck with the patient_id instead of the whole patient entity if I so needed to do so.
HEALTH CHECK
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Data
public class HealthCheck {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Patient patient;
//Getters and Setters
PATIENT
#Entity
#Data
public class Patient {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(unique = true)
#NotEmpty(message = "Name must not be null or empty")
private String name;
// Getters and Setters
The HealthCheckServiceImpl uses the derived queries to get one by id, and its this call thats used to get a HealthCheck by the REST controller:
#Override
public HealthCheck get(Long id) {
log.info("Getting a single Health Check");
return healthCheckRepository.getById(id);
}
The result of a call to the REST controller results in something like:
{
"id": 2,
"patient": {
"id": 2,
"name": "Jono",
"hibernateLazyInitializer": {}
},
"other fields": "some comment",
"hibernateLazyInitializer": {}
}
Note1 the whole patient entity is returned
Note2 that because I have only used the ManyToOne annottaion on the child side I dont get the Jackson recursion issues some others do
QUESTION: How can I control the returned HealthCheck so that it includes the patient_id not the whole object?
UPDATE
The line that calls to the service is :
#GetMapping("get/{id}")
public ResponseEntity<HealthCheck> getHealthCheck(#PathVariable("id") Long id) {
return ResponseEntity.ok()
.header("Custom-Header", "foo")
.body(healthCheckService.get(id));
I breakpoint on healthCheckService.get(id)) but noting on the debugger looks like it contains an entity reference:
UPDATE2
Well, it seems you're returning your entities objects directly from your controller.
The issue is that when I retrieve a HealthCheck via my Rest controller instead of getting just the id of the patient I get the whole entity.
You can use the DTO Pattern explained here and here as response in your controller.
That, also decouples your domain from your controller layer.
This is a simplified example:
#Data
#NoArgsConstructor
public class HealthCheckDto {
private Long id;
private PatientDto patient;
private String otherField;
public HealthCheckDto(HealthCheck healthCheck) {
this.id = healthCheck.getId();
this.patient = new PatientDto(healthCheck.getPatient());
this.otherField = healthCheck.getOtherField();
}
}
#Data
#NoArgsConstructor
public class PatientDto {
private Long id;
public PatientDto(Patient patient) {
this.id = patient.getId();
}
}
// In your controller
public ResponseEntity<HealthCheckDto> getHealthCheck(Long id) {
HealthCheck healthCheck = healthCheckService.getById(id);
return ResponseEntity.ok(new HealthCheckDto(healthCheck));
}
I have an Account object containing a OneToMany relation with Beneficiary object and this relationship is bi-directional so I have a ManyToOne relation in the Beneficiary Object with Account Object
public class Account {
#Id
#GeneratedValue
private Long id;
private String name;
private String number;
//Other fields
#OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
#JsonManagedReference
private List<Beneficiary> beneficiaries = new ArrayList<>();
}
public class Beneficiary {
#Id
#GeneratedValue
private Long id;
//Other fields
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id")
#OnDelete(action = OnDeleteAction.CASCADE)
private Account account;
}
In the JSON response, I need the Account information containing the list of Beneficiaries and for each Beneficiary I just need the Account name and Account number. Is it possible to serialize it somehow so that I get response in this fashion? Or do I need to modify my entity structures?
Sample Account Response -
{
"id": 123,
"name": "Name1",
"number": "111111",
"beneficiaries": [
{
"id": 1,
"account": {
"name": "Name2",
"number": "222222"
}
},
{
"id": 2,
"account": {
"name": "Name3",
"number": "333333"
}
}
]
}
You are not supposed to serialize your JPA objects. Instead, you need to define domain objects. These are objects are the ones to be serialize and exposed to the business. This abstraction decouples your REST or SOAP or whatever interface with your JPA layer.
I would create a domain class for your account class. Call it AccountDTO or something like that. Any object being returned from your JPA repositories need to be mapped to this DTO objects and bubbled up to the services layer. Then your DTO is the class which models your business needs. In there you can just put the accounts and the beneficiaries names.
DTO stands for Data Transfer Objects. These are the ones supposed to be serialized and sent between systems.
One idea would be to use a custom serializer.
You would have to write a custom serializer, similar to this:
public class NestedAccountSerializer extends StdSerializer<Account> {
public NestedAccountSerializer() {
this(null);
}
public NestedAccountSerializer(Class<Account> t) {
super(t);
}
#Override
public void serialize(Account account, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeObject(new AccountView(account.getName(), account.getNumber()));
}
private static class AccountView {
#JsonProperty
private final String name;
#JsonProperty
private final String number;
AccountView(String name, String number) {
this.name = name;
this.number = number;
}
}
}
And then use it like this in your Beneficiary class:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id")
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonSerialize(using = NestedAccountSerializer.class)
private Account account;
Please, let me know if it helped.
maybe another many-to-many relationship issue with spring data-jpa and how to update an existing entity with another existing entities.
I'll put a short version of my Entities just for clarify only when the error occurs.
I have a Peticion entity:
#Entity
#Table(name = "peticiones")
#JsonIdentityInfo(generator =
ObjectIdGenerators.PropertyGenerator.class,property = "id", scope =
Peticion.class)
#Validated
public class Peticion
{
private int id;
private Usuario usuario;
private Categoria categoria;
private Set<Tag> tags;
#ManyToMany( fetch = FetchType.LAZY, cascade = {
CascadeType.MERGE
} )
#JoinTable( name="peticion_tag", joinColumns= #JoinColumn(name = "peticion_id", referencedColumnName="id"), inverseJoinColumns = #JoinColumn(name="tag_id", referencedColumnName="id") )
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
And Tag entity:
#Entity
#Table(name = "tags")
#JsonIdentityInfo(generator =
ObjectIdGenerators.PropertyGenerator.class,property = "id", scope =
Tag.class)
public class Tag
{
private int id;
#Size( min = 4 )
private String nombre;
private Set<Peticion> peticiones;
private Set<Categoria> categorias;
private Set<Usuario> usuarios;
#ManyToMany( mappedBy="tags" )
public Set<Peticion> getPeticiones() {
return peticiones;
}
public void setPeticiones(Set<Peticion> peticiones) {
this.peticiones = peticiones;
}
Ok, so when I try to put or patch one Peticion in the format of:
{
"id": 123,
"usuario":{
"id": 5
},
"categoria":{
"id": 7
},
"tags":[
{
"id":3
},
{
"id":10
}
]
}
When I send this information, I got an error that says that I have a constraint violation saying that name I suppose the one property for Tag is null... So I figure it out that this is trying to create another entity, but that's not the case I wanna do, I wanna update the relationships between Peticion and Tag, and for example if I do this:
{
"id": 123,
"usuario":{
"id": 5
},
"categoria":{
"id": 7
},
"tags":[]
}
It works perfectly, I mean it deletes the relationship tags that were before. So I don't know if I'm sending the json correctly or do I have to put another configuration annotation or something in my Entities.
Note: I'm using JpaRepository for saving/updating and my controller only calls the method save.
Thank you
You might be missing fetches on the relation entity on your database layer, I think you should also share the repositories and queries that you use to fetch the linked data.
Having a bit of bother trying to figure out how to get my #ManyToMany mapping working in Hibernate in Dropwizard (using dropwizard-hibernate 6.2). I've tried several of the online examples. I'm trying to persist a twitter stream with user_mentions saved in a Targets table which is m2m with the Tweets table. So far all my attempts have been with an existing Target and a new Tweet (and due to my business rules, that will always be the case). I'll show code momentarily, but the consistent problem I'm having is that that the tweets_targets table winds up in all cases with the target_id set to the correct value, but the tweet_id set to 0.
Code is based on an article here: http://viralpatel.net/blogs/hibernate-many-to-many-annotation-mapping-tutorial/
// Target class
#Entity
#Table(name="targets")
public class Target {
private long id;
private List<Tweet> tweets = new ArrayList<Tweet>();
#Id
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
#ManyToMany(mappedBy="targets",targetEntity=Tweet.class)
public List<Tweet> getTweets() {
return tweets;
}
public void setTweets(List<Tweet> tweets) {
this.tweets = tweets;
}
}
// Tweet class
#Entity
#Table(name="tweets")
public class Tweet {
private long id;
private List<Target> targets = new ArrayList<Target>();
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "targets_tweets", joinColumns = {
#JoinColumn(name = "tweet_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "target_id", nullable = false, updatable = false)
})
public List<Target> getTargets() {
return this.targets;
}
public void setTargets(List<Target> targets) {
this.targets = targets;
for(Target t: targets){
t.getTweets().add(this);
}
}
}
The actual saving of a new Tweet is done in the DAO class which inherits from AbstractDAO in DropWizard. Relevant code is:
public long create(Tweet tweet) {
tweet.setTargets(getTargets(tweet));
return persist(tweet).getId();
}
#SuppressWarnings("unchecked")
private List<Target> getTargets(Tweet tweet) {
String[] mentions = tweet.getUserMentions().split(",");
return namedQuery(Target.FIND_BY_HANDLE)
.setParameterList("handles", mentions).list();
}
My named query just returns a list of all my targets based on their twitter handle as reported by the streams API.
Found the answer, hopefully this will help someone else.
The Id's in my DB are autoincrementing (I know, there's all kinds of debate on that, but it's what I have to work with), so once I added the annotation #GeneratedValue(strategy=GenerationType.IDENTITY) to the Tweet's Id property, everything started working.