JPA duplicate entries on child when update parent several times

Tags: , , ,



I am building the Rest API with Java, Spring Boot, Hibernate and JPA. I have 2 tables Users and Friends, where User will have an array of Friend. My client will send several update request on my User table. This the request the API will receive :

{
    "username": "bertrand",
    "email": "bertrand@gmail.com",
    "birthdate": "11/05/1984",
    "lang": "fr",
    "secretKey": "qefieqjnhfoipqnhefipqnfp",
    "updatedOn": "mise a jour date",
    "createdOn": "creation date",
    "friends": [
        {
            "username": "philippe",
            "userId": 2,
            "email": "philipppe@gmail.com"
        }
    ]
}

If my client send this request, my API returns :

 {
    "id": 1,
    "username": "bertrand",
    "password": "$2a$10$zGpK7/SOceA1xZnphrItpuYRkViBa7pNNgt9DsKDH1Q7HY50FE9hm",
    "email": "bertrandpetit10@gmail.com",
    "birthdate": "11 mai 1984",
    "lang": "fr",
    "secretKey": "8138124aff2b9226",
    "updatedOn": "mise a jour date",
    "createdOn": "creation date",
    "friends": [
        {
            "id": 1,
            "userId": 2,
            "username": "philippe",
            "email": "philipppe@gmail.com"
        }
    ]
}

This answer is good, but if my client send again the same request, the friend array is populated with a duplicated friend.

{
        "id": 1,
        "username": "bertrand",
        "password": "$2a$10$zGpK7/SOceA1xZnphrItpuYRkViBa7pNNgt9DsKDH1Q7HY50FE9hm",
        "email": "bertrandpetit10@gmail.com",
        "birthdate": "11 mai 1984",
        "lang": "fr",
        "secretKey": "8138124aff2b9226",
        "updatedOn": "mise a jour date",
        "createdOn": "creation date",
        "friends": [
            {
                "id": 1,
                "userId": 2,
                "username": "philippe",
                "email": "philipppe@gmail.com"
            },
            {
                "id": 2,
                "userId": 2,
                "username": "philippe",
                "email": "philipppe@gmail.com"
            }
        ]
    }

I would like that each friend to be unique and not to create a new entry in Friend table if already exists.

This is my User Entity :

Entity
@Table(name = "users")
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(name = "username", nullable = false, length = 20, unique = true)
private String username;

@Column(name = "password", nullable = false, length = 100)
private String password;

@Column(name = "email", nullable = false, length = 50, unique = true)
private String email;

@Column(name = "birthdate", nullable = false, length = 100)
private String birthdate;

@Column(name = "language", nullable = false, length = 2)
private String lang;

@Column(name = "secret_key", nullable = false, length = 200)
private String secretKey;

@Column(name = "updated_on", nullable = false, length = 100)
private String updatedOn;

@Column(name = "created_on", nullable = false, length = 100)
private String createdOn;

@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "owner_id", nullable = false)
private Set<Friend> friends = new HashSet<>();

public User() {
}

public User(String username, String email, String birthdate, String lang, String secretKey, String updatedOn,
        String createdOn, Set<Friend> friends, String password) {
    this.username = username;
    this.email = email;
    this.birthdate = birthdate;
    this.lang = lang;
    this.secretKey = secretKey;
    this.updatedOn = updatedOn;
    this.createdOn = createdOn;
    this.friends = friends;
    this.password = password;
}
+ getters ans setters...

and this is my Friend Entity :

    @Entity
    @Table(name = "friends")
    public class Friend {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "user_id", nullable = false, length = 20)
    private long userId;

    @Column(name = "username", nullable = false, length = 20)
    private String username;

    @Column(name = "email", nullable = false, length = 50)
    private String email;

    public Friend(long id, long userId, String username, String email) {
        this.id = id;
        this.userId = userId;
        this.username = username;
        this.email = email;
    }

What is the best way to do that please ?

Answer

That is because when you send a request for that child info

          { "username": "philippe",
            "userId": 2,
            "email": "philipppe@gmail.com"
           }

your child is considered a new entity for hibernate. Your key is null here. It has no way to know if it is duplicate or not.

1st Solution

Don’t send an info for update like that. First get the info from your Api, make changes and send that back for update. This way your child will have the id.

So client hits your api to retrieve user 1 and takes back

userRepository.findById(1L);

Client makes changes to user

Client calls API to update user which calls

userRepository.save(user)

but now the user will have as children

          { "id": 1         
            "username": "philippe",
            "userId": 2,
            "email": "philipppe@gmail.com"
           }

2nd solution

Change the key so that your children identity is not based on some database long value but instead is based on some business key. You could mark for example userId with username as a composite key

That way the key will always exist even before persisting something in your database. Therefore hibernate will know when something is duplicate or not even before it is persisted.

Here is a very popular article on that problem that you face

Don’t let hibernate steal your identity

3rd solution

@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "owner_id", nullable = false)
private Set<Friend> friends = new HashSet<>();

Remove cascade = CascadeType.ALL from User.

Then let the client from that API call to update only User fields and not something that has to do with it’s children. If client wants to update some Friend he must call the relevant API for that specific friend as it has done now for user.

Any Friend info contained in the call for User update will be ignored.



Source: stackoverflow