-1

There are three classes (Course, Lesson, User).

@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "usr")
@Data
public class User extends RepresentationModel<User> implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstname;
    private String lastname;
    private String username;
    private String password;
    @ElementCollection(targetClass = ERole.class, fetch = FetchType.EAGER)
    @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
    @Enumerated(EnumType.STRING)
    private Set<ERole> roles;
}
@Data
@Entity
@NoArgsConstructor
public class Lesson extends RepresentationModel<Lesson> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String startTime;
    private String endTime;
    private String dayOfWeek;
    @ManyToOne
    private User teacher;
}
@EqualsAndHashCode(callSuper = true)
@Data
@Entity
public class Course extends RepresentationModel<Course> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Date startDate;
    private Date endDate;
    private String name;
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Set<User> teachers;
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Set<User> students;
    private String description;
    @ManyToMany(cascade = CascadeType.ALL)
    private Set<Lesson> lessons;
}

And also RestController (CoursesController). When accessing the server at /courses, I get the correct server response with all fields

enter image description here.

@RestController
@RequestMapping("/courses")
public class CoursesController {
    private final CourseService courseService;
    private final UserService userService;
    private final LessonService lessonService;

    @Autowired
    public CoursesController(CourseService courseService, UserService userService, LessonService lessonService) {
        this.courseService = courseService;
        this.userService = userService;
        this.lessonService = lessonService;
    }

    @GetMapping
    @Operation(
          summary = "getAllCourses",
            description = "Returns all available courses"
    )
    public ResponseEntity<Page<Course>> getAllCourses(@PageableDefault(sort = "id", size = 5) Pageable pageable) {
        try {
            Page<Course> coursePage = courseService.findAll(pageable);
            for (Course course : coursePage.getContent())
                course.add(linkTo(methodOn(CoursesController.class).getCourse(course.getId().toString())).withSelfRel());
            return ResponseEntity.ok(courseService.findAll(pageable));
        }
        catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
    }
    @GetMapping("/{course-id}")
    @Operation(
            summary = "getCourse",
            description = "Returns course by ID"
    )
    public ResponseEntity<Course> getCourse(@PathVariable ("course-id") String courseId) {
        try {
            Course course = courseService.getCourseById(courseId);
            course.add(linkTo(methodOn(CoursesController.class).getCourse(courseId)).withSelfRel());
            return ResponseEntity.ok(course);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
}

Why, when requesting a course by ID (GET /courses/{id}), does Spring return an incomplete object (despite the fact that I manually added several teachers, students and lessons)?

enter image description here

I need to get all the fields of my object. My CourseRepository below.

@Repository
@Transactional
public interface CourseRepository extends JpaRepository<Course, Long> {
}

My CourseService below.

@Service
public class CourseService {
    private final CourseRepository courseRepository;
    private final LessonRepository lessonRepository;
    private final UserRepository userRepository;

    @Autowired
    public CourseService(CourseRepository courseRepository, LessonRepository lessonRepository, UserRepository userRepository) {
        this.courseRepository = courseRepository;
        this.lessonRepository = lessonRepository;
        this.userRepository = userRepository;
    }

    public Page<Course> findAll(Pageable pageable) {
        return courseRepository.findAll(pageable);
    }

    public Course createCourse(CourseDto courseDto) {
        Course course = new Course(courseDto.getStartDate(), courseDto.getEndDate(), courseDto.getName(), courseDto.getDescription());
        return courseRepository.saveAndFlush(course);
    }

    public Optional<Course> getCourseById(String id) {
        return courseRepository.findById(Long.parseLong(id));
    }

    public Course updateCourse(CourseDto courseDto, String id) {
        Course course = courseRepository.findById(Long.parseLong(id)).get();
        course.setStartDate(courseDto.getStartDate());
        course.setEndDate(courseDto.getEndDate());
        course.setName(courseDto.getName());
        course.setDescription(courseDto.getDescription());
        return courseRepository.saveAndFlush(course);
    }

    public Page<Lesson> getLessonsByCourse(String courseId, Pageable pageable) {
        Course course = courseRepository.findById(Long.parseLong(courseId)).get();
        return new PageImpl<>(new ArrayList<>(course.getLessons()), pageable, course.getLessons().size());
    }

    public Course addLesson(String courseId, LessonDto lessonDto) {
        Course course = courseRepository.findById(Long.parseLong(courseId)).get();
        Lesson lesson = new Lesson();
        lesson.setStartTime(lessonDto.getStartTime());
        lesson.setEndTime(lessonDto.getFinishTime());
        lesson.setDayOfWeek(lessonDto.getDayOfWeek());
        lesson.setTeacher(userRepository.getUserById(lessonDto.getTeacherId()));
        lessonRepository.saveAndFlush(lesson);
        System.out.println(lesson);
        course.getLessons().add(lesson);
        return courseRepository.saveAndFlush(course);
    }

    public void deleteCourse(String id) {
        courseRepository.deleteById(Long.parseLong(id));
    }
}
Evgeniy
  • 1
  • 1
  • Don't let your JPA model extend `RepresentationModel` you have now tied your JPA layer to Spring HATEOAS which you shouldn't do. What does your `getCourseByCourseId` method look like, I assume it is using `getById` instead of `findById`. – M. Deinum Feb 01 '22 at 09:52
  • Please don't add additional code as comments that is totally unreadable. Also those methods seem to be useless as those are already provided by the regular `JpaRepository` (`findAll` and `findById`). But I was also asking for the service **not** the repository. – M. Deinum Feb 01 '22 at 10:04
  • 1
    The problem is your `findByCourseId` method. You should use `findById`. The first will generate a query and ignore the JPA metadata for retrieving the entity, whilst the `findById` will use the `entityManager.find` method which will use the JPA metadata to retrieve the whole graph. So as I stated ditch those added methods which aren't needed in the first place (`findAll`, `deleteById` and `findById` are already there). – M. Deinum Feb 01 '22 at 10:21
  • Corrected, but the problem is not solved. The object is not returned with all fields. – Evgeniy Feb 01 '22 at 10:31
  • Which I would (or might) expect as well. I would links to be generated for those additional relationshps (at least normally with Spring Data RESt handling this is what would happen). I wonder what happens if you ditch the `RepresentationModel` from your JPA model and just expose `Course` then. As stated you don't really want your JPA and HATEOAS stuff to be intertwined. You want to have a specialized projection/dto to expose. WHy does it work for your `findAll`. well you aren't adding links to it (although you think it does but your `findAll` executes twice!). – M. Deinum Feb 01 '22 at 10:41
  • @M.Deinum Many thanks. You solved my problem.) – Evgeniy Feb 01 '22 at 10:45

1 Answers1

0

Which I would (or might) expect as well. I would links to be generated for those additional relationshps (at least normally with Spring Data RESt handling this is what would happen). I wonder what happens if you ditch the RepresentationModel from your JPA model and just expose Course then. As stated you don't really want your JPA and HATEOAS stuff to be intertwined. You want to have a specialized projection/dto to expose. WHy does it work for your findAll. well you aren't adding links to it (although you think it does but your findAll executes twice!).

Removed RepresentationModel from User class. Thx to @M.Deinum

Evgeniy
  • 1
  • 1