Spring boot/Spring data jpa - how to update related entity? - java

I have following entities:
#Entity
#Table(name = "profile")
public class Profile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#OneToOne(cascade = CascadeType.ALL)
private ProfileContacts profileContacts;
...
}
and
#Entity
#Table(name = "profile_contacts")
public class ProfileContacts {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "description")
private String description;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
}
I am trying to update it by sending this JSON with update to REST controller:
{
"id": 1,
"description": "an update",
"profileContacts": {
"firstName": "John",
"lastName": "Doe"
}
}
so in the end it calls
profileRepository.save(profile);
where profileRepository is instance of ProfileRepository class:
public interface ProfileRepository extends JpaRepository<Profile, Long> {
}
which is spring-data-jpa interface.
But each time after such update it updates profile table but adds new row to profile_contacts table (table which corresponds to ProfileContactsentity) instead of updating existing ones.
How can I achieve updating?

As per your JSON structure. Yes it will create new profileContacts entry for every time.
The problem every time while saving profile entity you are passing "id": 1 that means Hibernate can identify the entity by this id value (primary key) but for profileContacts mapping you are not sending the id that's why Hibernate considering it has a new entity every time.
To update your profileContacts entity make sure to pass the id of it.
Example:
{
"id": 1,
"description": "an update",
"profileContacts": {
"id" : yourEntityId
"firstName": "John",
"lastName": "Doe"
}
}

Well, that's the expected behavior.
You're not telling hibernate to update the profileContacts.
For the framework to be able to update it, you need to send the profileContact's primary key - which in your case is the ProfileContacts#id.
Something like this:
{
"id": 1,
"description": "an update",
"profileContacts": {
"id": 1
"firstName": "John",
"lastName": "Doe"
}
}

Need to specify the join column in the parent Entity.
#Entity
#Table(name = "profile")
public class Profile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#OneToOne(cascade = CascadeType.ALL)
**#JoinColumn(name = "id")** //
private ProfileContacts profileContacts;
...
}
Now when you try to save Profile entity it will save the child entity also.
And also needs to include Id in jason request for child entity also
{
"id": 1,
"description": "an update",
"profileContacts": {
"id": 1,
"firstName": "John",
"lastName": "Doe"
}
}

Ok, I see the problem. As #Matheus Cirillo pointed out, you need to tell the hibernate to update the row.
Now, how do you tell the hibernate to update a row - By providing the primary key of the existing row.
But, creating an object with the primary key set is not enough. You need that entity class to be attached to the entity manager and the persistence context.
You can have something like,
//This attaches the entity to the entity manager
ProfileContacts existingProfileContacts = profileContactRepository.getOne(2);
Profile profile = new Profile();
....
....
profile.setProfileContacts(existingProfileContacts);
profileRepository.save(profile);
I hope this helps.

Related

Spring boot JPA - bidirectional relationsship: returns foreign key instead of the related object

I have two entities: Customer and Address.
It is a bidirectional relationsship - one address can have many customers (oneToMany) and one customer can only have one address (manyToOne).
Executing GET request for customers returns:
[
{
"id": 1,
"name": "Foo",
"contact": "5512312",
"email": "Foo#gmail.com",
"address": {
"id": 1,
"street": "X",
"postalCode": 123,
"houseNo": "10",
"city": "New York"
}
}
]
When a new customer, with the exact same address properties as the one exists in DB - is being added with POST request, the json response returns the foreign key related to the existing object in DB instead of the object itself:
[
{
"id": 1,
"name": "Foo",
"contact": "5512312",
"email": "Foo#gmail.com",
"address": {
"id": 1,
"street": "X",
"postalCode": 123,
"houseNo": "10",
"city": "New York"
}
},
{
"id": 2,
"name": "Bar",
"contact": "5512312",
"email": "Bar#gmail.com",
"address": 1 <----------- it returns the foreign key instead of the object
}
]
So what I expect is that whenever a new customer, that has an address that already exists in the DB, is being added - it should return the address object instead of the foreign key from the json response.
Code:
Customer.java
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
#Entity
#Table
public class Customer {
#Id
#SequenceGenerator(
name = "customer_sequence",
sequenceName = "customer_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "customer_sequence"
)
private Long id;
private String name;
private String contact;
private String email;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "address_id", nullable = false)
private Address address;
[...]
Address.java
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
#Entity
#Table
public class Address {
#Id
#SequenceGenerator(
name = "address_sequence",
sequenceName = "address_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "address_sequence"
)
private Long id;
private String street;
private int postalCode;
private String houseNo;
private String city;
#JsonIgnore
#OneToMany(mappedBy = "address")
private Set<Customer> customers;
[...]
CustomerController.java
//...
#PostMapping
public void createCustomer(#RequestBody Customer customer) {
customerService.createCustomer(customer);
}
[...]
And the service that saves the customer to the DB which also makes sure that no address is being created if already exists in database (it checks on every property to be equal from the param):
//...
public void createCustomer(Customer customer) {
Optional<Customer> customerWithExistingAddress = customerRepository.findAll()
.stream()
.filter(x -> x.getAddress().equals(customer.getAddress()))
.findFirst();
customerWithExistingAddress.ifPresent(c -> customer.setAddress(c.getAddress()));
customerRepository.save(customer);
}
[...]
You're probably getting this behavior because of JsonIdentityInfo, so it's a serialization problem you have not a persistence one. I'm assuming you're using a relational database (Hibernate for NoSql has Jpa like annotations but then that would make this a different problem) and that data is being persisted correctly.
See the javadocs:
In practice this is done by serializing the first instance as full object and object identity, and other references to the object as reference values

many-to-many mapping with extra column - how to set mapping and how to serialise?

I am very new to Hibernate and I am trying to solve an issue similar to this question, specifically the answer.
I have something very similar set up (in my case it's Author, Book, and the mapping table AuthorBook). I am using this in Spring Data JPA, so I have the following components:
Repositories: AuthorRepository, BookRepository, AuthorBookRepository
Services: AuthorService, BookService, AuthorBookRepository
Controllers: AuthorController, BookController, AuthorBookController
My entities are:
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "author")
private Set<AuthorBook> authorBooks;
// getters and setters
}
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "book")
private Set<AuthorBook> authorBooks;
// getters and setters
}
#Entity
public class AuthorBook {
#Id
#GeneratedValue
private Integer id;
#ManyToOne
#JoinColumn(name = "user_id")
private Author author;
#ManyToOne
#JoinColumn(name = "book_id")
private Book book;
#Column(name = "isMainAuthor")
private boolean isMainAuthor;
// getters and setter
}
My understanding is that I should make the following POST requests:
Create an author:
{
"name": "Test Author"
}
Create a book:
{
"name": "Test Book"
}
Create the mapping:
{
"author": {
"id": 1
},
"book": {
"id": 2
},
"isMainAuthor": true
}
First of all: is this the correct way to use this? If no, how should I use it instead? If yes, how is serialisation supposed to work? Because if I do it like this, and then fetch the list of books, the response will be infinitely long like so:
[
{
"id": 2,
"name": "Test Book",
"authorBooks": [
{
"id": 3,
"author": {
"id": 1,
"name": "Test Author",
"authorBooks": [
{
"id": 3,
"author": {
"id": 1,
"name": "Test Author",
"authorBooks": [
{
"id": 3,
"author": {
"id": 1,
"name": "Test Author",
"authorBooks": [
...
I know that I could use #JsonIgnore on the Author and Book getters in AuthorBook, and #JsonProperty on the setters so that deserialisation still works. However, as someone who is unexperienced with Hibernate, this seems like a hack to me. What is the cleanest and best way to solve this?
Also, is there a way to update the mapping via the author and book endpoints directly? I might already have an author in the database and want to just add a new book he wrote by adding a new Book entity and providing the relation as part of it, but I seem not to be able to do that.
This is absolutely not a hacker trick. With the #JsonIgnore annotation, this problem is solved. This problem is called infinite recursion with the release of Jackson JSON and nothing can be done with it. For this, they came up with an annotation.
You probably need to set an embedded ID within the mapping table. Keeping the Author and Book table as such, create a new class
#Embeddable
public class AuthorBookId{
#Column(name = "user_id")
private String authorId;
#Column(name = "book_id")
private String bookId;
// Constructors
// Getters and Setters
}
And change the AuthorBook table as
#Entity
public class AuthorBook {
#EmbeddedId private AuthorBookId id = new AuthorBookId();
#ManyToOne
#MapsId("user_id")
#JsonIgnore
private Author author;
#ManyToOne
#MapsId("book_id")
#JsonIgnore
private Book book;
#Column(name = "isMainAuthor")
private boolean isMainAuthor;
// getters and setter
}

How to update object hibernate

I have a problem with update object from java code to postgresql.
When I'm updating object, I'm having a problem with id, because id is null. Thus in database is creating a new row instead of overwriting the same row.
It seems like, the row in database:
id = 1
name = "name"
then I'm sending a json, like:
{
name: "newName"
}
then I'm getting the new row and it seems:
id = 1, name = "name"
id = 2, name = "newName"
It's because when I'm sending a json there id is null, so postgresql is creating a new row, but I want to overwrite the row with id = 1. One of solution to add in #OneToOne(orphanRemoval = true), but I don't like it, because it deleting the old id then creating the new id. How do I can overwrite the initial row if I don't have id?
Thanks!
class A{
#Id
#GeneratedValue
UUID id;
#OneToOne(mappedBy = "A", cascade = CascadeType.ALL)
B b;
}
class B{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#OneToOne(optional = false)
#JoinColumn(name = "A_id")
A a;
}
updatedA.setInfo(jsonSend.getInfo());
Send the id to the client and when it call put/post, send it back. You wouldnt know which record should be updated without that (this is the regular solution).
Other solution, if the name is unique/natural id (i dont think so), you can post the old name and the new one too. Then select the record with old name, update the object and save.
I'm using CrudRepository.
Well some part of code, entities:
public class Human {
#Id
#GeneratedValue
Long id;
#Column(name = "first_name")
String firstName;
#Column(name = "last_name")
String lastName;
#JoinColumn(name = "data_document_id")
#Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
#OneToOne(cascade = CascadeType.PERSIST)
DataDocument dataDocument;
#OneToMany(mappedBy = "human", cascade = CascadeType.ALL)
#Singular
List<Book> books;
}
public class DataDocument {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
}
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
Boolean isReaded;
Integer type;
Integer cost;
Instant date;
}
Here I make POST to create a human:
{
"firstName": "firstName",
"lastName": "lastName",
"dataDocument": {
"name": "randomDocument"
},
"books": [
{
"isReaded": true,
"type": 0,
"cost": 100,
"date": "2019-06-11"
}
]
}
humanRepository.save(human);
So it's ok, I'm having to get the row in database:
TABLE HUMAN
human_id = 1, firstName = firstName, lastName = lastName, data_document_id = 1
TABLE DOCUMENT_DATA
data_document_id = 1, name = randomDocument
TABLE BOOK
book_id = 1, human_id = 1, type = 0, cost = 100, date = "2019-06-11", isReaded = true
Then I want to update my the human. For example json:
{
"id": 1,
"firstName": "firstName",
"lastName": "lastName",
"dataDocument": {
"name": "NEW_DOCUMENT"
},
"books": [
{
"isReaded": true,
"type": 0,
"cost": 100,
"date": "2019-06-11"
}
]
}
My method:
public Human updateHuman(Long humanId, updateHuman){
Human human = humanRepository.findById(humanId);
human.setDocumentData(updateHuman.getDocumentData());
humanRepository.save(human);
}
Then in the table, I will get:
TABLE HUMAN
human_id = 1, firstName = firstName, lastName = lastName, data_document_id = 2
TABLE DOCUMENT_DATA
data_document_id = 1, name = randomDocument
data_document_id = 2, name = NEW_DOCUMENT
That's the problem. I want to overwrite data_document_id = 1 to new name. I know some solutions like #OneToOne(orphanRemoval = true) but it would gives the next result:
TABLE DOCUMENT_DATA
data_document_id = 2, name = NEW_DOCUMENT
Also I'd can to write code like this:
public Human updateHuman(Long humanId, updateHuman){
Human human = humanRepository.findById(humanId);
Long id = human.getDocumentData().getId();
human.setDocumentData(updateHuman.getDocumentData());
human.getDocumentData().setId(id);
humanRepository.save(human);
}
Then it's would work fine. But I have like 40 fields and 10 of them Lists and 10 of them another entities with too many fields.
I just want to find the easiest way how I can update the object. I thought about query, but I'm not sure if it will work.
Just I have a HUMAN to UPDATED_SOME_FIELDS = HUMAN_WITH_UPDATED_SOME_FIELDS.
between, I solved my problem with 'spring beanutils copyproperties' maybe it helps to somebody.

Not save an enum value in the database, why?

Does not save an enum value in the database, I think that's it, or I do not understand, I try to save since the console shows me a data, but when I ask from the database I get another value
#Entity
#Table(name = "User")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name = "name")
private String name;
#Column(name = "password")
private String password;
#Column(unique = true, name = "email")
private String email;
#Column(name="rol")
Rol rol;
#Column(name = "life")
Life life;
public User() {
}
}
i have this in the .info, show this message "OWNER"
Set<User> users =new HashSet<>();
log.info("CONSOLE"+ user.getRol());
users.add(user);
meet.setUsers(users);
return meetRepository.save(meet);
but in swagger i get other value
ROL: PARTICIPANT
[
{
"id": 1,
"name": "string2",
"state": "pause",
"bet": {
"profit": 0,
"inversion": 0
},
"users": [
{
"id": 1,
"name": "string",
"password": "string",
"email": "ema",
"rol": "PARTICIPANT",
"life": "suspend"
}
]
}
]
If your fields rol and life are Enums you have to declare them as Enums with #Enumerated. There are two options. Default will store the ordinal number. Or you can choose to use the string name to store in the DB. That's the better option in terms of maintainability:
#Enumerated(EnumType.STRING)
#Column(name="rol")
Rol rol;
#Enumerated(EnumType.STRING)
#Column(name = "life")
Life life;
Two remarks:
When the database field has the same name as the attribute you can omit #Column. And if the table has the same name as the entity this is also true for the #Table annotatoin.
Read more about Enums and JPA and if you really should use it in one of my articles: https://72.services/de/should-you-use-enums-with-jpa/
When you save a Enum to the database as stated above you have the option of saving it as String or as Numerical Ordinal. The numerical option is very painfull because every time you need to update your enum values or change the order of the values you will mess with your database options for the field. Also another thing that is very painfull even when you store the strings on the database is that when you are using some ORM such as Hibernate and you have NULL values. So keep your enums as strings and avoid NULL value.

Why Hibernate inserts a new record if i passed an empty id in json to child entity during update

I have the ff. pojos: i make it short for simplicity
public class CaseOutlineHeader{
#Id
#GeneratedValue(generator = "COH_SequenceStyleGenerator")
#Column(name = "outline_id")
private Long id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "outline_id", nullable = true, referencedColumnName = "outline_id",
foreignKey = #ForeignKey(name = "FK_COH_REF_COD_01"))
private List<CaseOutlineDetails> caseOutlineDetails;
}
public class CaseOutlineDetails{
#Id
#GeneratedValue(generator = "COD_SequenceStyleGenerator")
#Column(name = "ID")
private Long id;
#ManyToOne(targetEntity = MaintCaseEvent.class)
#JoinColumn(name = "EVENT_ID", nullable = false,
foreignKey = #ForeignKey(name = "FK_COD_REF_CASE_EVENT"),
referencedColumnName = "CASE_EVENT_ID")
private MaintCaseEvent eventId;
#Column(name = "outline_id")
private Long outlineId;
}
And then my service: It's pretty straightforward if the passed json of COD contains id that is not null it will update COH along with the COD . else if i passed a null id of COD i will save as a new entry in COD.
#Override
#Transactional
public CaseOutlineHeader update(CaseOutlineHeader caseoutline) {
LOG.info("SERVICE: Updating CaseOutlineHeader...{}", caseoutline);
if (!CollectionUtils.isEmpty(caseoutline.getCaseOutlineDetails())) {
caseoutline.getCaseOutlineDetails().forEach(cod -> {
if (cod.getId() != null) {
cod.setUpdatedBy(cod.getUpdatedBy());
cod.setUpdatedDate(new Date());
caseOutlineHeaderRepository.update(caseoutline);
} else {
cod.setOutlineId(caseoutline.getId());
cod.setCreatedBy("dwade");
caseOutlineDetailsRepository.save(cod);
}
});
}
return caseoutline;
}
Passed json data in swagger:
{
"id": 1,
"caseOutlineDetails": [
{
"eventId": { "id": 1 },
"updatedBy": "kobe",
"id" : 1,
"outlineId" : 1
},
{
"eventId": { "id": 2},
"outlineId" : 1
}
],
"caseType": "string",
"code": "updateC",
"description": "updateD",
"updatedBy": "kobe",
"updatedDate": "2018-07-23T00:55:16.767Z"
}
As you can see i passed a second data in COD which dont have an id property it will be saved. while the first one will be updated which contains an id.
Here is my repo:
#Override
public CaseOutlineHeader update(CaseOutlineHeader caseOutlineHeader) {
em.merge(caseOutlineHeader);
em.flush();
return caseOutlineHeader;
}
#Override
public CaseOutlineDetails save(CaseOutlineDetails caseOutlineDetails) {
LOG.info("REPOSITORY : SAVE = " + caseOutlineDetails);
em.persist(caseOutlineDetails);
return caseOutlineDetails;
}
But in my logger i dont know why it inserts a new record first with id and foreign key only as the value. But on the last line of insert statement is successful
Hibernate: insert into case_outline_details (access_level...
Hibernate: update case_outline_header set access_level=?...
Hibernate: update case_outline_details set access_level=?...
Hibernate: select seq_cod.nextval from dual
Hibernate: insert into case_outline_details (access_level...
It should just be update,update,then insert.
You declared caseOutlineDetails with cascade = CascadeType.ALL. So when you're updating a CaseOutlineHeader, hibernate will automatically update the list, inserting rows if needed. Then you're doing your stuff and insert it again.
when deserializing the serialized entity instance, need to get a persistent
entity instance from a persistence context and update its fields with
new values from this detached instance
Hibernate merge finds an entity instance by id taken from the passed object (either
an existing entity instance from the
persistence context is retrieved, or a new instance loaded from
the database); copies fields from the passed object to this
instance; returns newly updated instance.
so if id is null that does not finds instance of the object so merge inserts the new
record and returns the object.

Categories

Resources