This seems like a trivial problem to me, but I cannot fathom a solution.
I have two Entities User
and Bank
, as well as a "join table" banks_users
:
@Entity
@Table(name = "users")
@Getter
@EqualsAndHashCode
@NoArgsConstructor
@RequiredArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private long id;
@Getter
@Setter
@JoinTable(
name = "banks_users",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "bank_id"))
@ManyToMany(
targetEntity = Bank.class,
fetch = FetchType.EAGER,
cascade = {MERGE, PERSIST})
private Set<Bank> banks;
// Further details omitted for brevity //
}
@Getter
@Entity
@Table(name = "banks")
@EqualsAndHashCode
@NoArgsConstructor
@RequiredArgsConstructor
public class Bank {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private long id;
@Setter
@ManyToMany(
targetEntity = User.class,
fetch = FetchType.EAGER,
cascade = {MERGE, PERSIST},
mappedBy = "banks")
private Set<User> users;
// Further details omitted for brevity //
}
I'm now trying to implement an enpoint that takes 1 user_id
and n bank_id
s, and puts (override existing relations) the User
into all specified Bank
s:
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE)
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
private final UserService userService;
/** Set the relation of one user to any number of banks. */
@PutMapping("/user/{id}/banks")
public ResponseEntity<Void> setUserBelongsToBanks(
@PathVariable long id,
@RequestBody Set<Long> bankIds
) {
logger.info("Setting user with id {} to banks with ids: {}", id, bankIds);
userService.setUserToBanks(id, bankIds);
return ResponseEntity.noContent().build();
}
// Further details omitted for brevity //
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BankRepository bankRepository;
/**
* Finds a user with the given id.
*
* @throws ResponseStatusException with {@link HttpStatus#NOT_FOUND} if no user with the given id exists
*/
public User getUserById(long userId) {
return userRepository
.findById(userId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "{user.not-found}"));
}
/**
* Set the relation of one user to any number of banks.
*
* @param userId The id of the user.
* @param bankIds The ids of the banks.
*/
public void setUserToBanks(long userId, Set<Long> bankIds) {
var user = getUserById(userId);
var userBanks = user.getBanks();
var banks = bankRepository.findAllById(bankIds);
if (banks.size() != bankIds.size())
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "{bank.not-found}");
userBanks.clear();
userBanks.addAll(banks);
userRepository.save(user);
}
// Further details omitted for brevity //
}
When I now create an authorized user and a bank, link the two, and then call the endpoint with an empty array as the body:
### Detach a user from a bank
PUT {{baseUrl}}/user/{{userId}}/banks
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}
[]
... I get a code 204
response, but the (postgres) database doesn't update:
/* Setting user with id 30 to banks with ids: [] */
Hibernate: select u1_0.id,b1_0.user_id,b1_1.id,b1_1.name,u1_0.email,u1_0.password_hash from users u1_0 left join banks_users b1_0 on u1_0.id=b1_0.user_id left join banks b1_1 on b1_1.id=b1_0.bank_id where u1_0.id=?
Hibernate: select u1_0.id,b1_0.user_id,b1_1.id,b1_1.name,u1_0.email,u1_0.password_hash from users u1_0 left join banks_users b1_0 on u1_0.id=b1_0.user_id left join banks b1_1 on b1_1.id=b1_0.bank_id where u1_0.id=?
What is the correct way to manage "join tables" in this regard?
Is it really necessary to query user.getBanks()
as a Set<Bank>
in a way that deserializes (the content of) n POJOS? In my imagination this should only affect ids.
Does spring-boot have something like Laravels synch
and detach
?
Well, I found the answer myself:
The problem was using the @EqualsAndHashCode
which causes JPA to get confused while synching the entities.
This Blogpost offers a detailed explanation, as to why the annotation should be avoided.