javaspring-bootspring-data-jpathymeleaf

Spring Boot: Participants Not Displayed in Schedule Despite Being Present in Database


I am working on a Spring Boot project where I have a Schedule entity that has a many-to-many relationship with a User entity. The participants are correctly added to the schedule in the database, but they are not being displayed when I fetch the schedule.

Some notable things:

When using the same query that spring is using, I can fetch the data perfectly inside of my database.

The only piece of data that is not displaying inside of my html is the participants. Every other bit of data is displaying correctly.

For some reason, my getAllSchedules method inside of my ScheduleService class, is not logging the participants, as it considers it empty? Much like inside of my @GetMapping.

Feel like I'm overlooking something too small to see.

Schedule.Java

@Entity
@Table(name="schedule")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Schedule {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long scheduleId;

    @Column(name = "date", nullable = false)
    private Date date;

    @Column(name = "start_time", nullable = false)
    private LocalTime startTime;

    @Column(name = "end_time", nullable = false)
    private LocalTime endTime;

    @Column(name = "trainer_id", nullable = false)
    private Long trainerId;

    @ManyToMany(fetch = FetchType.EAGER)
    private Set<User> participants;
}

User.java

@Entity
@Table(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
    @Column(name = "username", unique = true, nullable = false)
    private String username;

    @NotBlank(message = "Email is required")
    @Pattern(regexp = "(?i)^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,6}$", message = "Email must be valid")
    @Column(name = "email", unique = true, nullable = false)
    private String email;

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    @Pattern(regexp = "^[^\\s]*$", message = "Password must not contain spaces")
    @Column(name = "password", nullable = false)
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "roles", joinColumns = @JoinColumn(name = "userId"))
    @Column(name = "role")
    private Set<String> roles;

    @ManyToMany(mappedBy = "participants")
    private Set<Schedule> schedules;
}

ScheduleRepository.java

public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
}

ScheduleService.java

@Service
public class ScheduleService {

    private static final Logger logger = LoggerFactory.getLogger(ScheduleService.class);

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ScheduleRepository scheduleRepository;

    public void scheduleSession(Long trainerId, List<Long> userIds, Date date, LocalTime startTime, LocalTime endTime) {
        logger.info("Scheduling session with trainerId: {}, userIds: {}, date: {}, startTime: {}, endTime: {}",
                trainerId, userIds, date, startTime, endTime);
        
        Schedule schedule = new Schedule();
        schedule.setDate(date);
        schedule.setStartTime(startTime);
        schedule.setEndTime(endTime);
        schedule.setTrainerId(trainerId);

        logger.info("Attempting to add participants to the schedule");
        Set<User> participants = new HashSet<>();
        for (Long userId : userIds) {
            logger.info("Adding participant with ID: {}", userId);
            User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found"));
            participants.add(user);
        }
        schedule.setParticipants(participants);
        scheduleRepository.save(schedule);

        for (User Participant : participants) {
            logger.info ("Added participant with ID: {}", Participant.getUserId());
        }

        logger.info("List of participants: {}", participants);

        logger.info("Session scheduled successfully");
    }

    public List<Schedule> getAllSchedules() {
        List<Schedule> schedules = scheduleRepository.findAll();
        for (Schedule schedule : schedules) {
            logger.info("Schedule ID: {}", schedule.getScheduleId());
            for (User participant : schedule.getParticipants()) {
                logger.info("Participant ID: {}", participant.getUserId());
            }
        }
        return schedules;
    }
}

ScheduleController.java

@Controller
public class ScheduleController {
    private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class);

    @Autowired
    private ScheduleService scheduleService;

    @Autowired
    private UserService userService;

    @GetMapping("/schedule/view")
    public String viewSchedule(Model model) {
        List<Schedule> schedules = scheduleService.getAllSchedules();

        for (Schedule schedule : schedules) {
            logger.info("Schedule ID: {}", schedule.getScheduleId());

            if (schedule.getParticipants().isEmpty()) {
                logger.info("No participants in this schedule");
            } else {
                for (User participant : schedule.getParticipants()) {
                    logger.info("Participant ID: {}, Username: {}", participant.getUserId(), participant.getUsername());
                }
            }
        }
        model.addAttribute("schedules", schedules);
        return "viewschedule";
    }
}

viewschedule.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>View Schedule</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
<div class="container">
    <h1>View Schedule</h1>
    <table class="table table-bordered">
        <thead>
        <tr>
            <th>Date</th>
            <th>Start Time</th>
            <th>End Time</th>
            <th>Trainer</th>
            <th>Participants</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="schedule : ${schedules}">
            <td th:text="${schedule.date}"></td>
            <td th:text="${schedule.startTime}"></td>
            <td th:text="${schedule.endTime}"></td>
            <td th:text="${schedule.trainerId}"></td>
            <td>
                <ul>
                    <li th:each="participant : ${schedule.participants}" th:text="${participant.username}"></li>
                </ul>
            </td>
        </tr>
        </tbody>
    </table>
</div>
</body>
</html>

I have tried using custom queries, manually setting the joins, using queries directly inside of the database (it works in there), etc.


Solution

  • You have a bi-directional many-to-many relationship. When you are adding participants you must set both sides of the relation. Like this:

    for (Long userId : userIds) {
        logger.info("Adding participant with ID: {}", userId);
        User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found"));
        user.getSchedules().add(schedule);  // add this line
        participants.add(user);
    }
    

    For this to work smoothly you need also to initialize the User entity property with an empty set:

    @ManyToMany(mappedBy = "participants")
    private Set<Schedule> schedules = new HashSet<>();