Pass #Transactional method result from Service to Controller Layer Spring Boot - java

I'm trying to lazily fetch a ManyToMany relationship (Courses - Students) from the Service and pass the result to the Controller. While i'm in the Service, no LazyInitializationException is thrown, thanks to the #Transactional annotation. However, while i'm in the Controller the LazyInitializationException is thrown (while getting Course.students), because the Session was closed. How can i resolve this issue, without eagerly fetch the Collection? That's my code: Couse Model
#Entity
#Getter
#Setter
public class Course {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String name;
#ManyToMany
#JoinTable(name = "COURSES_STUDENTS",
joinColumns = {#JoinColumn(name = "COURSE_ID")},
inverseJoinColumns = {#JoinColumn(name = "STUDENT_ID")})
private Set<Student> students;
public Course() {
this.students = new HashSet<>();
}
Student Model
#Entity
#Getter
#Setter
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String name;
#ManyToMany(mappedBy = "students")
private Set<Course> courses;
public Student() {
this.courses = new HashSet<>();
}
}
Course Repository
#Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
}
Course Service
#Service
public class CourseService {
private final CourseRepository courseRepository;
#Autowired
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
#Transactional
public ResponseEntity<List<Course>> findAll() {
return this.courseRepository.findAll().isEmpty() ? ResponseEntity.noContent().build()
: ResponseEntity.ok(this.courseRepository.findAll());
}
}
Course Controller
#Controller
#RequestMapping("/")
public class CourseController {
private final CourseService courseService;
#Autowired
public CourseController(CourseService courseService) {
this.courseService = courseService;
}
#GetMapping
public ResponseEntity<List<Course>> index() {
return this.courseService.findAll();
}
}
application.properties
spring.datasource.url=jdbc:h2:~/database;AUTO_SERVER=TRUE
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.jpa.open-in-view=false
spring.mvc.hiddenmethod.filter.enabled=true
logging.level.org.springframework.web=DEBUG
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Thanks in advance.

So there are 2 approaches :
What is this spring.jpa.open-in-view=true property in Spring Boot?
This is bad for performance and must be avoided at all costs.
use jpql queries to join fetch lazy collections needed in DAO layer so they are available in the controller when you need them to be.
All in all, do not use transactional to keep the db session open to fetch lazy collections. Just join fetch lazy collections in db / dao layer to have the data needed for each endpoint available.
If you want have a look here for how to use join fetch How to fetch FetchType.LAZY associations with JPA and Hibernate in a Spring Controller

Related

JPA is not saving data

Im trying to save data to the database, but instead of it JPA is saving null to the database, I am usually doing it with dto, but since it s a very small project, I want to do it without it
Entity
#Setter
#Getter
#Entity
#AllArgsConstructor
#NoArgsConstructor
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String surname;
private String department;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "STUDENT_COURSE_TABLE",
joinColumns = {
#JoinColumn(name="student_id",referencedColumnName = "id")
}, inverseJoinColumns = {
#JoinColumn(name = "couse_id",referencedColumnName = "id")
})
#JsonManagedReference
private Set<Course> courses;
}
DAO
#Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
}
Controller
#RestController
#RequestMapping("/api")
public class StudentCourseController {
private final StudentRepository studentRepository;
private final CourseRepository courseRepository;
public StudentCourseController(StudentRepository studentRepository,
CourseRepository courseRepository) {
this.studentRepository = studentRepository;
this.courseRepository = courseRepository;
}
#PostMapping
public Student addStudent (Student student){
return studentRepository.save(student);
}
}
and in my application.properties
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto = update
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialec
t
Make sure is deserialization correct in controller method "addStudent" - If You want to pass Student entity in request body, add annotation #RequestBody to method parameter like:
#PostMapping
public Student addStudent (#RequestBody Student student){
return studentRepository.save(student);
}
If You do not do that - there is possibility to null/empty parameter, what can lead to saving nulls into db.
By the way:
Consider using DTO or Request classes to pass entity in/out your REST application - it will help you avoid circular reference in future and problems with de/serialization your entity.
Consider using ResponseEntity instead of returning object to output - method with ResponseEntity should be like:
#PostMapping
public ResponseEntity<Student> addStudent (#RequestBody Student
student){
return ResponseEntity.ok(studentRepository.save(student));
}

lazy loaded list of object in spring jpa transactional service is empty

I am using Spring data jpa in the backend codes. I have included entities, service and controller codes.
The sample codes are simple. Topic and Comment are OneToMany relationship. Comments are configured to be lazy loaded. Now if I calls updateTopic to update topic. In the console, I will see a query to fetch the comments list from the comment table, and then it will update the topic table. But the comments list is empty even though it does have some comments linked with the topic.
#Entity
#Table(name = "TOPIC")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Topic implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name = "NAME", length = 45)
#NotNull
private String name;
#OneToMany(
fetch = FetchType.LAZY,
cascade = CascadeType.REMOVE,
mappedBy = "topic"
)
private List<Comment> comments= new ArrayList<>();
}
#Entity
#Table(name = "COMMENT")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Comment implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Topic_ID")
private Topic topic;
}
#Service
#Transactional
public class TopicService {
private final TopicRepository topicRepository ;
public TopicService(TopicRepository topicRepository ) {
this.topicRepository = topicRepository ;
}
public Topic updateTopic(Topic topic) {
Topic newTopic = topicRepository.save(topic);
System.out.println("from service: " + newTopic.getComments().size());
return newTopic;
}
}
Then if I tried to get the comments from the controller instead, then the comment list is not empty. In the console, it will show the update query, then the select query to get comments list. My question is how I can lazy load the comments list from the service class? It seems to be related to the transactional session.
#RestController
#RequestMapping("/api")
public class TopicRestController {
#PutMapping("/release/{id}")
public Topic updateTopic(#PathVariable long id, #RequestBody Topic newTopic ) {
return topicService.getTopicById(id)
.map(topic-> {
Topic t = topicService.updateTopic(newTopic);
System.out.println("controller: " + topic.getComments().size());
})
.orElseThrow(() -> new CustomException("The topic with id: " + id + " cannot be found", HttpStatus.NOT_FOUND));
}
}
You have to map them to DTOs somehow.
You may see my article in the profile for details.
Please, don’t use FetchType.EAGER, it will affect the performance.

org.hibernate.hql.internal.ast.QuerySyntaxException: Apartment is not mapped [from Apartment]

I have springboot rest appplication with Hibernate and MySQL. I have this error:
nested exception is
org.springframework.dao.InvalidDataAccessApiUsageException:
org.hibernate.hql.internal.ast.QuerySyntaxException: Apartment is not
mapped [from Apartment]
But I dont know where I have a mistake. I have two tables: Apartments and Residents in DB. But now I try only getAllApartments() method. I use Intellij and I even checked my DB in her. And I have little picture near my Entity class, where I have correct data source. And I think that I checked names my class and fields.
This is my Entity:
#Entity
#Table(name = "Apartments")
public class Apartment {
#Column(name = "apartment_id")
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer apartmentId;
#Column(name = "apartment_number"
private Integer apartmentNumber;
#Column(name = "apartment_class")
private String apartmentClass;
#OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH},
mappedBy = "apartment")
List<Resident> residentList;
My DAO method:
#Repository
public class ApartmentDAOImpl implements ApartmentDAO {
#Autowired
private EntityManager entityManager;
#Override
public List<Apartment> getAllApartment() {
Session session = entityManager.unwrap(Session.class);
Query query = session.createQuery("from Apartment");
List<Apartment> apartmentList = query.getResultList();
return apartmentList;
}
My Controller:
#RestController
#RequestMapping("/api")
public class ApartmentController {
#Autowired
ApartmentService apartmentService;
#GetMapping("/apartments")
public List<Apartment> getAllApartments() {
List<Apartment> apartmentList = apartmentService.getAllApartment();
return apartmentList;
}
I also have service layer without any logic.
My property.file
spring.datasource.url=jdbc:mysql://localhost:3306/correct_db?useSSL=false&serverTimezone=UTC
spring.datasource.username=projuser
spring.datasource.password=projuser
Give me advice, please.
Maybe, As I used multi module application, Hibernate or Spring didn't see my Entity.
And I clearly indicated my Entity class with #EntityScan(basePackages = {"com.punko.entity"}) under my SpringBootApplication class:
#SpringBootApplication(scanBasePackages = "com.punko")
#EntityScan(basePackages = {"com.punko.entity"})
public class SpringBootApplicationConfig {
public static void main(String[] args) {
SpringApplication.run(SpringBootApplicationConfig.class, args);
}

Getting 404 Error when calling a Spring Boot API

I wrote a few request mappings and connected my Spring Boot application to a Postgresql DB using JPA. However, when I try to call the API, I get the following message:
{"timestamp":"2021-01-30T21:58:34.028+00:00","status":404,"error":"Not Found","message":"","path":"/api/v1/sessions"}. I tried printing a message when the API is called and it works so I think it might have something to do with the JPA connection? (I also tested if the DB and credentials are good using SQL Shell, and they are ok)
My model:
#Entity(name = "sessions")
public class Session {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long session_id;
private String session_name;
private String session_description;
private Integer session_length;
#ManyToMany
#JoinTable(
name = "session_speakers",
joinColumns = #JoinColumn(name = "session_id"),
inverseJoinColumns = #JoinColumn(name = "speaker_id"))
private List<Speaker> speakers;
My controller:
#Controller
#RequestMapping("/api/v1/sessions")
public class SessionsController {
#Autowired
private SessionRepository sessionRepository;
#GetMapping
public List<Session> list() {
System.out.println("Get method called");
return sessionRepository.findAll();
}
#GetMapping
#RequestMapping("{id}")
public Session get(#PathVariable Long id) {
return sessionRepository.getOne(id);
}
My repository:
public interface SessionRepository extends JpaRepository<Session, Long> {
}
And lastly, my application properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/databaseName
spring.datasource.username=postgres
spring.datasource.password=mypasswordhere
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.show-sql=true
#GetMapping(value = "/{id}")<---remove Request mapping and also add the slash here
public Session get(#PathVariable Long id) {
return sessionRepository.getOne(id);
}

#Transactional not working in Spring Boot with CrudRepository

I was trying to implement bi-directional relationships bettwen my entities.
Student
#Table(name = "students")
#Entity
public class Student {
#Id
// #GeneratedValue(strategy = GenerationType.AUTO)
private long album;
#NotNull
private String name;
#NotNull
private String surname;
#OneToMany(mappedBy = "student", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
private List<StudentSection> studentSections;
#Transactional(propagation=Propagation.REQUIRED, readOnly=true, noRollbackFor=Exception.class)
public void addSection(Section section){
if(this.studentSections == null){
this.studentSections = new ArrayList<>();
}
StudentSection studentSectionToAdd = new StudentSection();
studentSectionToAdd.setStudent(this);
studentSectionToAdd.setSection(section);
this.studentSections.add(studentSectionToAdd); //here
section.addStudentSection(studentSectionToAdd);
}
}
the connecting entity in a ManyToMany relationship
#Table(name = "student_section")
#Entity
public class StudentSection {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Integer grade;
private Date date;
#NotNull
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
#JoinColumn(name = "student_id")
private Student student;
#NotNull
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
#JoinColumn(name = "section_id")
private Section section;
}
and Section
#Table(name = "sections")
#Entity
public class Section {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
private String name;
#NotNull
private Integer sizeOfSection;
#NotNull
private Boolean isActive;
#OneToMany(mappedBy = "section", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
private List<StudentSection> studentSections;
void addStudentSection(StudentSection studentSection){
if(this.studentSections == null){
this.studentSections = new ArrayList<>();
}
this.studentSections.add(studentSection);
}
}
I ran into a problem with the Student.addSection() method. When trying to execute it I got an error on the this.studentSections.add(studentSectionToAdd); line, saying
failed to lazily initialize a collection of role: Student.studentSections, could not initialize proxy - no Session
I read about it and found out that the best way to fix this is to add the #Transactional annotation to the method, however it didnt change anything and I cant get it to work.
I also tried moving the Student.addSection() method to
StudentServiceImpl
#Service
#Primary
public class StudentServiceImpl implements StudentService {
protected StudentRepository studentRepository;
#Autowired
public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
#Override
#Transactional(propagation = Propagation.REQUIRED, readOnly = true, noRollbackFor = Exception.class)
public void addSection(Student student, Section section) {
if (student.getStudentSections() == null) {
student.setStudentSections(new ArrayList<>());
}
StudentSection studentSectionToAdd = new StudentSection();
studentSectionToAdd.setStudent(student);
studentSectionToAdd.setSection(section);
student.getStudentSections().add(studentSectionToAdd);
//section.addStudentSection(studentSectionToAdd);
}
}
but I still got the error.
I am also using CrudRepository to retrive entities from the database.
#Repository
public interface StudentRepository extends CrudRepository<Student, Long> {
Student findByName(String name);
}
This is where I call the method
#Component
public class DatabaseLoader implements CommandLineRunner {
private final StudentRepository studentRepository;
private final SectionRepository sectionRepository;
private final StudentSectionRepository studentSectionRepository;
private final StudentService studentService;
#Autowired
public DatabaseLoader(StudentRepository studentRepository, SectionRepository sectionRepository, StudentSectionRepository studentSectionRepository,
StudentService studentService) {
this.studentRepository = studentRepository;
this.sectionRepository = sectionRepository;
this.studentSectionRepository = studentSectionRepository;
this.studentService = studentService;
}
#Override
public void run(String... strings) throws Exception {
//Testing entities
Student student = new Student();
student.setAlbum(1L);
student.setName("student");
student.setSurname("test");
this.studentRepository.save(student);
Section section = new Section();
section.setName("section");
section.setSizeOfSection(10);
section.setIsActive(true);
this.sectionRepository.save(section);
//end
//Adding Student to a Section test
Student student1 = studentRepository.findByName("student");
//student1.setStudentSections(this.studentSectionRepository.findAllByStudent(student1));
Section section1 = sectionRepository.findByName("section");
//section1.setStudentSections(this.studentSectionRepository.findAllByStudent(student1));
studentService.addSection(student1, section1);
this.studentRepository.save(student1);
//end test
}
}
Also when I retrive StudentSection lists from the database here and set them im both objects before adding a new relationship it works fine, but this is not really the solution I am going for.
The problem is that every call from the run() method to studentRepository and studentService are separate sessions/transactions.
It's virtually as-if you did this:
...
beginTransaction();
this.studentRepository.save(student);
commit();
...
beginTransaction();
this.sectionRepository.save(section);
commit();
beginTransaction();
Student student1 = studentRepository.findByName("student");
commit();
beginTransaction();
Section section1 = sectionRepository.findByName("section");
commit();
// This does it's own transaction because of #Transactional
studentService.addSection(student1, section1);
beginTransaction();
this.studentRepository.save(student1);
commit();
Since transaction = session here, it means that student1 is detached, and that the lazy-loaded studentSections collection cannot be loaded on-demand outside the session, and hence the code fails.
Inserting a new student and a new section and associating them should really be one transaction, so if a later step fails, it's all rolled back.
Which basically means that you want the entire run() method to be one transaction, so in your case, it is the run() method that should be #Transactional, not the addSection() method.
Generally, in a 3-tiered approach, you would put transaction boundaries on service layer:
Presentation tier. This is #Controller classes, or the run() method for a simple command-line program.
Logic tier. This is #Service classes. This is where you put #Transactional, so each service call is an atomic transaction, i.e. it either succeeds or it fails, as far as the database updates are concerned, no half updates.
Data tier. This is #Repository and #Entity classes.
As such, you should keep the instantiation and initialization of the Student and Section objects in the run() method, but the rest of the code, incl. save(), should be moved to a single method in a #Service class.
About this
#Transactional(propagation=Propagation.REQUIRED, readOnly=true, noRollbackFor=Exception.class)
public void addSection(Section section){
#Transactional works only for spring-managed beans and Entities are not managed by spring.
You get this exception because you try load a lazy relations outside a session (because your entity is actually in detached-state).
To re-attach --> entityManager.merge(student);
But the best thing to do is to load the relation at query-time. By using EntityGraph for example -->
#EntityGraph(attributePaths="studentSections")
Student findByName(String name);

Categories

Resources