How do I update a new reference in a OneToOne bidirectional relationship?

Viewed 284

I have two simple entities

//Address @Entity     
@OneToOne(fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "user_id", nullable = false)
private User user;

and

//User @Entity
@OneToOne(mappedBy = "user", orphanRemoval = true)
Address address;

But I am unable to update the user with a new address.

First approach:

    Address address = new Address();
    User user = userRepository.findByUsername("someemail");
    address.setUser(user);
    addressRepository.save(address);

the code runs fine but an insert statement is done and now I have two address records in the database which causes future exceptions.

Second approach

Address address = new Address();
    User user = userRepository.findByUsername("someemail");
    address.setUser(user);
    user.setAddress(address);   //added this line
    addressRepository.save(address);

Now it throws exception

org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1

Third approach

Address address = new Address();
User user = userRepository.findByUsername("someemail");
address.setUser(user);
user.setAddress(address); 
userRepository.save(user);  //added this
addressRepository.save(address);

New exception:

org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing 

Temp solution

This solution works, but it's the most ugly code I've written and also I have no idea why it works. If someone has a better answer I will mark as solution.

First add cascade to the entity

@OneToOne(mappedBy = "user", orphanRemoval = true, cascade = CascadeType.ALL)
Address address;

And then update using this:

User user = userRepository.findByUsername("someemail");
    if (user.getAddress() != null) {
        addressRepository.delete(user.getAddress());
        user.setAddress(address);
        address.setUser(user);
        userRepository.save(user);
    } else {
        address.setUser(user);
        addressRepository.save(address);
    }
4 Answers

//Address @Entity

@OneToOne(fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "user_id", nullable = false)
private User user;

and

//User @Entity
@OneToOne(mappedBy = "user", orphanRemoval = true)
Address address;

According to the above code, you have declared that the entity Address will be the one that has the mapping for the relevant User. So your Address table in database has an extra field user_id that maps this address with the relevant user.

Check your 1st approach

Address address = new Address();                         <--- new address entity created
User user = userRepository.findByUsername("someemail");  <--- user retrieved from DB
address.setUser(user);                                   <----New address record in address table is set up to have the relevant user
addressRepository.save(address);

You should do it the other way around

Also add @Transactional to the method that executed the following code

Address address = new Address();                        
User user = userRepository.findByUsername("someemail");  
if (user.getAddress() != null) {
    Address oldAddress = user.getAddress();
    oldAddress.setUser(null);
}
user.setAddress(address);                                 
userRepository.save(user);

For this to work you have to modify in your User entity the

 @OneToOne(mappedBy = "user", orphanRemoval = true, cascade = CascadeType.ALL)
 Address address;

Now when you execute the above code, the previous address entity will be updated to have a null reference for a user and this way the orphanRemoval will do it's job and remove it from database while the new address will be persisted and linked with the relative user.

This is the final working answer

//address entity, no cascade non sense
    @JoinColumn(name = "user_id", nullable = false, unique=true)
    private User user;

and

 //User @Entity
@OneToOne(mappedBy = "user", orphanRemoval = true)
Address address;

And then update reference by

  if (user.getAddress() != null) {
        user.setAddress(null);
        userRepository.save(user);
    }
    address.setUser(user);
    addressRepository.save(address);

Let's suppose we have this:

@Entity
public class User {
 @OneToOne(orphanRemoval = true)
 Address address;
}

When you put the orphanRemoval on the @OneToOne ,the User table in the database will have a field created as a FK with PK of the adress table,so that hibernate can keep track of changes. When you try to save a User ,even without changing the adress value ,a select query will be triggered to CHECK if there's any difference between the selected adress fields and the adress object contained in the user object that you passed to the save method.If there's any change ,a select will be triggered on the adress table to see if that record is still there ,and if so ,after updating the user table , a delete query will be issued to delete that orphan adress. hope this helps.

You and Thomas Edison have a lot in common ...

    User user = userRepository.findById(1L).get();
    user.getAddress().setSomething("YYY");
    addressRepository.save(user.getAddress());

Really though, you should not make problems for yourself. If you do, print the SQL to see what's going on.

    User user = userRepository.findById(1L).get();
    addressRepository.delete(user.getAddress());
    user = userRepository.save(user);
    addressRepository.save(Address.builder().something("YYY").user(user).build());

I would do this differently.

// Address
@OneToOne(mappedBy = "address")
private User user;

// User
@OneToOne
private Address address;

and

    User user = userRepository.findById(2L).get();
    user.setAddress(addressRepository.save(Address.builder().something("YYY").user(user).build()));
    userRepository.save(user);
Related