Search code examples
javapostgresqlspring-boothibernatespring-data-jpa

Synching JPA ManyToMany Relationship doesn't work as expected


This seems like a trivial problem to me, but I cannot fathom a solution.

Problem

I have two Entities User and Bank, as well as a "join table" banks_users:

User Entity

@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 //
}

Bank Entity

@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_ids, and puts (override existing relations) the User into all specified Banks:

UserController

@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 //

}

UserService

@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:

Logs

/* 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=?

Question 1

What is the correct way to manage "join tables" in this regard?

Question 2

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.

Question 2.1

Does spring-boot have something like Laravels synch and detach?


Previously visited Sources:


Solution

  • 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.