newbie to spring boot here. I've been following this tutorial: https://www.youtube.com/watch?v=jCYonZey5dY&t=1145s My implementation is about books and authors and I want the same functionality. An author can have multiple books and also a book can be co-written by more than one authors. I've come to a result where, POSTing an author works as intended, but POSTing a book does not update the join table, although both Users and Books tables are updated. My database is postgres. Implementation details:
Users Class:
@Entity
@Table
@Getter
@Setter
@NoArgsConstructor
public class Users {
@Id
@SequenceGenerator(
name = "user_sequence",
sequenceName = "user_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "user_sequence"
)
private Long userId;
private String password;
private LocalDate dateOfBirth;
private String fullName;
private String email;
@Enumerated(EnumType.STRING)
private Roles role;
@ManyToMany(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "USERS_BOOKS", joinColumns = {
@JoinColumn (name = "User_id", referencedColumnName = "userId")
},
inverseJoinColumns = {
@JoinColumn(name = "BookId", referencedColumnName = "bookId")
})
private Set<Book> books;
public Users(String password, LocalDate dateOfBirth, String fullName, String email, Roles role){
this.password = password;
this.dateOfBirth = dateOfBirth;
this.fullName = fullName;
this.email = email;
this.role = role;
}
public Users(String password, LocalDate dateOfBirth, String fullName, String email, Set<Book> books){
this.password = password;
this.dateOfBirth = dateOfBirth;
this.fullName = fullName;
this.email = email;
this.books = books;
}
public void addBooks(Set<Book> books){
if(this.role == Roles.AUTHOR){
for(Book book : books)
try{
this.books.add(book);
book.addAuthors(Set.of(this));
} catch(Exception e){
System.out.println("Wrong type of object for a List of Book");
continue;
}
} else {
throw new IllegalArgumentException("Non author users cannot hold books");
}
}
public void removeBooks(Set<Book> books){
for(Book book : books)
for(Book user : this.books){
if (user.equals(book)) {
this.books.remove(user);
}
else{
System.out.println("Book: "+book.getTitle()+ " is already not in set.");
}
}
}
}
Books class:
@Table(name = "Books")
@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Book {
@Id
@SequenceGenerator(
name = "book_sequence",
sequenceName = "book_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "book_sequence"
)
private Long bookId;
private String title;
private LocalDate releaseDate;
private String genresStr;
@Column(columnDefinition = "text")
private String content;
@ManyToMany(mappedBy="books", fetch=FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Users> authors;
public Book(String title, LocalDate releaseDate, String content, String genresStr){
this.genresStr = genresStr;
this.title= title;
this.releaseDate = releaseDate;
this.content = content;
}
public Book(String title, LocalDate releaseDate, String content, String genresStr, Set<Users> authors){
this.genresStr = genresStr;
this.title= title;
this.releaseDate = releaseDate;
this.content = content;
this.authors = authors;
}
public void addAuthors(Set<Users> authors){
System.out.println("addAuthorsCalled");
for(Users author : authors)
try{
this.authors.add(author);
author.addBooks(Set.of(this));
author.setRole(Roles.AUTHOR);
} catch(Exception e){
System.out.println("Wrong type of object for a List of Users");
}
}
public void removeAuthors(Set<Users> authors){
if(authors.size() == 1){
System.out.println("Can't delete all authors for certain book");
return;
}
for(Users author : authors)
for(Users user : this.authors){
if (user.equals(author)) {
this.authors.remove(user);
}
else{
System.out.println("User: "+author.getFullName()+ " is already not in set.");
}
}
}
}
Book controller:
@RestController
@RequestMapping(path = "api/books")
public class BookController {
private final BookService bookService;
private UserService userService;
public BookController(@Autowired BookService bookService, UserService userService){
this.bookService = bookService;
this.userService = userService;
}
@GetMapping
public List<Book> getBooks(){
return bookService.getBooks();
}
@PostMapping
public void addBook(@RequestBody Book book){
if(!book.getAuthors().isEmpty()){
for(Users author : book.getAuthors()){
author.setRole(Roles.AUTHOR);
userService.saveUser(author);
}
}
bookService.saveBook(book);
}
@DeleteMapping(path="{bookId}")
public void deleteBook(@PathVariable ("bookId") Long bookId){
bookService.deleteBook(bookId);
}
@PutMapping(path="{bookId}")
public void updateBook(
@PathVariable("bookId") Long bookId,
@RequestParam(required = false) String title,
@RequestParam(required = false) LocalDate releaseDate,
@RequestParam(required = false) String content,
@RequestParam(required = false) Set<Long> userIds
){
Set<Users> usersToAdd = null;
if(userIds!=null){
usersToAdd = userService.getUsersById(userIds);
}
bookService.updateBook(bookId, title, releaseDate, content, usersToAdd);
}
@PutMapping(path = "/{bookId}/delAuthor")
public void putMethodName(
@PathVariable Long bookId,
@RequestParam(required = true) Set<Long> userIds
){
Set<Users> usersToRemove = userService.getUsersById(userIds);
bookService.removeAuthors(bookId, usersToRemove);
}
}
User controller:
@RestController
@RequestMapping(path = "api/users")
public class UserController {
private final UserService userService;
public UserController(@Autowired UserService userService){
this.userService = userService;
}
@GetMapping
public List<Users> getAllUsers() {
return userService.getUsers();
}
@PostMapping
public void addNewUser(@RequestBody Users user) {
userService.saveUser(user);
}
@DeleteMapping(path="{userId}")
void deleteUser(@PathVariable ("userId") Long userId){
userService.deleteUser(userId);
}
@PutMapping(path="{userId}")
void updateUser(@PathVariable ("userId") Long userId,
@RequestParam(required = false) String fullName,
@RequestParam(required = false) String password,
@RequestParam(required = false) LocalDate dateOfBirth,
@RequestParam(required = false) String email,
@RequestParam(required = false) Set<Long> bookIds
){
userService.updateUser(userId, password, dateOfBirth, fullName, email, bookIds);
}
}
User service
@Service
public class UserService {
private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
private final UserRepository userRepository;
private BookRepository bookRepository;
public UserService(@Autowired UserRepository userRepository, BookRepository bookRepository){
this.userRepository = userRepository;
this.bookRepository = bookRepository;
}
public Users getUser(Long id){
return this.userRepository.getReferenceById(id);
}
public List<Users> getUsers(){
return this.userRepository.findAll();
}
public Set<Users> getUsersById(Set<Long> userIds){
Set<Users> users = new HashSet<>();
for(Long userId : userIds)
if(userId != null){
users.add(this.getUser(userId));
}
return users;
}
public void saveUser(Users user){
Optional<Users> userEmailOptional = userRepository.findUserByEmail(user.getEmail());
if(userEmailOptional.isPresent()){
throw new IllegalStateException("This email already exists in database");
}
if(user.getRole() == Roles.AUTHOR){
Optional<Users> authorByFullName = userRepository.findUserByFullName(user.getFullName());
if(authorByFullName.isPresent()){
throw new IllegalStateException("Authors cannot have the same name");
}
}
if(user.getBooks() != null && !user.getBooks().isEmpty())
user.setRole(Roles.AUTHOR);
user.setPassword(encoder.encode(user.getPassword()));
if(user.getBooks() != null && !user.getBooks().isEmpty())
for(Book book : user.getBooks())
bookRepository.save(book);
userRepository.save(user);
}
public void deleteUser(Long userId){
boolean exists = userRepository.existsById(userId);
if(!exists){
throw new IllegalStateException("No user with Id: " + userId+ " in database");
}
userRepository.deleteById(userId);
}
@Transactional
public void updateUser(Long userId,
String password,
LocalDate dateOfBirth,
String fullName,
String email,
Set<Long> bookIds){
Users user = userRepository.findById(userId).orElseThrow(() ->
new IllegalStateException("No user with id: "+userId+" in database"));
if(password != null && password.length() > 0){
user.setPassword(password);
}
if(dateOfBirth!=null){
user.setDateOfBirth(dateOfBirth);
}
if(fullName!=null && fullName.length() > 0){
user.setFullName(fullName);
}
if(email!=null && email.length() > 0){
Optional<Users> userOptional = userRepository.findUserByEmail(email);
if(userOptional.isPresent()){
throw new IllegalStateException("Email "+email+" already exists in database");
}
user.setEmail(email);
}
if(bookIds != null){
Set<Book> booksToAdd = new HashSet<Book>();
for(Long bookId : bookIds){
booksToAdd.add(bookRepository.getReferenceById(bookId));
}
user.addBooks(booksToAdd);
}
}
}
Book Service:
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(@Autowired BookRepository bookRepository){
this.bookRepository = bookRepository;
}
public List<Book> getBooks(){
return bookRepository.findAll();
//List.of(new Book("kostas", LocalDate.of(2000, Month.APRIL, 12)));
}
public void saveBook(Book book){
bookRepository.save(book);
}
public void deleteBook(Long bookId){
boolean exists = bookRepository.existsById(bookId);
if(!exists){
throw new IllegalStateException("Book with id: " + bookId + " does not exist");
}
bookRepository.deleteById(bookId);
}
@Transactional
public void removeAuthors(Long bookId, Set<Users> authors){
Book book = bookRepository.findById(bookId).
orElseThrow(() -> new IllegalStateException("No books with the id requested"));
if(!authors.isEmpty())
book.removeAuthors(authors);
}
@Transactional
public void updateBook(Long bookId, String title, LocalDate releasDate, String content, Set<Users> authors){
Book book = bookRepository.findById(bookId).
orElseThrow(() -> new IllegalStateException("No books with the id requested"));
if(title != null && title.length() > 0){
book.setTitle(title);
}
if(releasDate != null){
book.setReleaseDate(releasDate);
}
if(content != null && content.length() > 0){
book.setContent(content);
}
if(authors != null)
book.addAuthors(authors);
}
}
Roles is a simple enum:
public enum Roles {
AUTHOR, ADMIN, GUEST
}
Example of postman requests:
POST: localhost:8080/api/users
//Insert User with book
//Works fine
body:
{
"password":"itsDanBrown",
"dateOfBirth":"1964-07-22",
"fullName":"Dan Brown",
"email":"d.brown@mail.com",
"books":[
{
"title":"The Da Vinci Code",
"releaseDate":"2003-03-18",
"content":"",
"genres":["Mystery", "detective fiction", "conspiracy fiction", "thriller"]
},
{
"title":"Angels & Demons",
"releaseDate":"2000-05-30",
"content":"",
"genres":["Mystery", "thriller"]
}
]
}
POST: localhost:8080/api/users
//Insert Book with user
//Inserts value in users table and books table but not in users_books table
{
"title":"The Da Vinci Code",
"releaseDate":"2003-03-18",
"content":"",
"genres":["Mystery", "detective fiction", "conspiracy fiction", "thriller"],
"authors":[
{
"password":"password",
"dateOfBirth":"2000-02-02",
"fullName":"Dan Brown",
"email": "d.brown@email.com"
}
]
}
Thanks in advance! (Java version 18, Spring boot 3.4.2)
Hi there!
In a Spring JPA @ManyToMany relationship, one side is designated as the owner of the relationship. The owner side is responsible for managing the link between the entities in the join table.
What's Happening in Your Case
In your example, User is the owning side of the relationship. When you call the /users endpoint: The User service saves each book (from the request) individually. Then it saves the user - having all the books associated with it. This sequence allows Hibernate to create the link in the users_books join table.
Issue with /books Endpoint
When you call the /books endpoint: You retrieve the authors (users) from the request. You set their role and save them in the database. However, the link is not created because the owning side (User) has no reference to the book. Without this reference, Hibernate doesn't know it should create an entry in the users_books join table.
How to fix
if (!book.getAuthors().isEmpty()) {
for (Users author : book.getAuthors()) {
author.setRole(Roles.AUTHOR);
author.addBooks(Set.of(book)); // Establish the bidirectional link
userService.saveUser(author);
}
}
bookService.saveBook(book);