#Transactional not working in Spring Boot with CrudRepository - java

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);

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));
}

Spring MVC Transactional in dao service and controller layers

I'm using Spring MVC with Spring data.
Simple example of my problem:
My dao Service class:
#Service
#AllArgsConstructor
#Transactional
public class FooService{
private FooRepository fooRepo;
public Foo save(Foo foo){
return fooRepo.save(foo);
}
}
and controller:
#Controller
#AllArgsConstructor
#Transactional //if I remove this, method add does not save a foo.
//But I don't understand why, because FooService already has #Transactional annotation
public class FooController{
private FooService fooService;
#PostMapping("/add")
public String add(#RequestParam("programName") String programName, #RequestParam("id") long id){
Foo foo = fooService.findById(id).get();
foo.setProgramName(programName);
fooService.save(foo);
return "somePage";
}
}
If I remove #Transaction annotation from controller class, method save will not update foo object.
And I don't understand why I should mark controller by #Transactional annotation if I already mark service class by this annotation?
############ UPDATE ####################
Simple detailed description:
I have Program and Education entities. One Program has many Education, Education entity has foreign key program_id.
There is a page with Program form, there are fields: program id, program theme,..., and field with a list of education id separated by commas.
I'm trying to update the education list at the program, so I add a new education id at the page form and click save. Through debugger I see, that new education has appeared in the program, but changes do not appear in the database.
#Controller
#RequestMapping("/admin/program")
#AllArgsConstructor //this is lombok, all services autowired by lombok with through constructor parameters
#Transactional//if I remove this, method add does not save a foo.
//But I don't understand why, because FooService already has #Transactional annotation
public class AdminProgramController {
private final ProgramService programService;
private final EducationService educationService;
#PostMapping("/add")
public String add(#RequestParam("themeName") String themeName, #RequestParam("orderIndex") int orderIndex,
#RequestParam(value = "educationList", defaultValue = "") String educationList,
#RequestParam(value = "practicalTestId") long practicalTestId){
saveProgram(themeName, orderIndex, educationList, practicalTestId);
return "adminProgramAdd";
private Program saveProgram(long programId, String themeName, int orderIndex, String educationList, long practicalTestId){
List<Long> longEducationList = Util.longParseEducationList(parsedEducationList); //this is list of Education id separeted by commas that I load from page form
//creating new program and set data from page form
Program program = new Program();
program.setId(programId);
program.setThemeName(themeName);
program.setOrderIndex(orderIndex);
//starting loop by education id list
longEducationList.stream()
.map(educationRepo::findById)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(edu->{
//linking program and education
program.getEducationList().add(edu);
edu.setProgram(program);
});
//saving new program or updating by service if there is one already
Program savedProgram = programService.save(program);
//saving education with updated program
for(Education edu : savedProgram.getEducationList())
{
educationService.save(edu);
}
return savedProgram;
}
}
ProgramService:
#Service
#AllArgsConstructor //this is lombok, all services autowired by lombok with throught constructor parameters
#Transactional
public class ProgramService {
private ProgramRepo programRepo;
//other code here.....
public Program save(Program program) {
Optional<Program> programOpt = programRepo.findById(program.getId());
//checking if the program is already exist, then update it paramateres
if(programOpt.isPresent()){
Program prgm = programOpt.get();
prgm.setThemeName(program.getThemeName());
prgm.setOrderIndex(program.getOrderIndex());
prgm.setPracticalTest(program.getPracticalTest());
prgm.setEducationList(program.getEducationList());
return programRepo.save(prgm);
}
//if not exist then just save new program
else{
return programRepo.save(program);
}
}
}
Education service
#Service
#AllArgsConstructor //this is lombok, all services autowired by lombok with throught constructor parameters
#Transactional
public class EducationService {
private EducationRepo educationRepo;
//other code here....
public Education save(Education education){
return educationRepo.save(education);
}
}
Program entity:
#Entity
#ToString(exclude = {"myUserList", "educationList", "practicalTest"})
#Getter
#Setter
#NoArgsConstructor
public class Program implements Comparable<Program>{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "theme_name")
private String themeName;
#Column(name = "order_index")
private int orderIndex; //from 1 to infinity
#OneToMany(mappedBy = "program", fetch = FetchType.LAZY)
#OrderBy("orderIndex asc")
private List<Education> educationList = new ArrayList<>();
#OneToMany(mappedBy = "program", fetch = FetchType.LAZY)
private List<MyUser> myUserList = new ArrayList<>();
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "test_id")
private PracticalTest practicalTest;
public Program(int orderIndex, String themeName) {
this.orderIndex = orderIndex;
this.themeName = themeName;
}
public Program(long id) {
this.id = id;
}
//other code here....
}
Education entity:
#Entity
#ToString(exclude = {"program", "myUserList"})
#Getter
#Setter
#NoArgsConstructor
public class Education implements Comparable<Education>{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String link;
#Column(name = "order_index")
private int orderIndex;
private String type;
private String task;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "program_id")
private Program program;
#OneToMany(mappedBy = "education", fetch = FetchType.LAZY)
private List<MyUser> myUserList = new ArrayList<>();
public Education(String link, int orderIndex, String task, Program program) {
this.link = link;
this.orderIndex = orderIndex;
this.task = task;
this.program = program;
}
//other code here....
}
Program repo:
#Repository
public interface ProgramRepo extends CrudRepository<Program, Long> {
Optional<Program> findByPracticalTest(PracticalTest practicalTest);
Optional<Program> findByOrderIndex(int orderIndex);
List<Program> findByIdBetween(long start, long end);
}
Education repo:
#Repository
public interface EducationRepo extends CrudRepository<Education, Long> {
Optional<Education> findByProgramAndOrderIndex(Program program, int orderIndex);
#Query("select MAX(e.orderIndex) from Education e where e.program.id = ?1")
int findLastEducationIndexByProgramId(long programId);
}
I think the problem is program object created in one transaction and saved in another. That's why if I put Transactional on controller it works. There are two ways to solve the problem:
Without transactional on the controller: then I must save education object at first, because it has program id field and then save the program object.
With transactional on controller: then saving order has no matter, because saving object occurs in one transaction

LazyInitializationException after return updated entity

I have some model with two relations:
#Entity
#Table(name = "data_model")
public class DataModel {
#Id
#GeneratedValue
#Column(name = "model_id")
private Integer id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "dataModel", cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE})
private List<OutputField> outputFields;
#OneToMany(mappedBy = "dataModel", cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE})
private List<Query> queries;
//some another fields
}
I use Spring Data JPA and I want to update entity. I write simple service:
#Service
public class DataModelService {
#Autowired
private DataModelRepository dataModelRepository;
#Transactional
public DataModel createOrUpdate(DataModel dataModel) {
return dataModelRepository.save(dataModel);
}
//another methods
}
I write simple test:
public class DataModelServiceTest {
#Autowired
private DataModelService dataModelService;
#Test
void shouldUpdateDataModel() {
DataModel dataModelBeforeUpdate = dataModelService.getById(1);
dataModelBeforeUpdate.getQueries().get(0).setSqlQuery("SELECT 1");
DataModel updatedModel = dataModelService.createOrUpdate(dataModelBeforeUpdate);
assertThat(updatedModel.getQueries(), notNullValue());
}
}
But, I get error, when I try to call method getQieries():
Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' exception.
In debug I see:
Questions:
Why does this error occur and how can I fix it? How do I make hibernate return all links after an update?
Why is the outputFields field filled in correctly, but the queries field is not?
It happens because you are trying to initialize collection outside a transaction. To fix this add #DataJpaTest and #RunWith(SpringRunner.class) annotations to your test class. By default, data JPA tests are transactional.
Refer here for more details.

Java Spring Hibernate - #Transactional between different transactions

I'm creating a test and basically doing different transactions inside a #Transactional method.
I add a Project, then add a Task to it, and last will fetch the project again from DB to test it has the task saved.
Please note the case I'm showing is a unit test but I'm interested in fixing the transactional methods and not the test itself as I already had this in the past in "production code".
Model Classes:
#Entity
#Table(name = "Task")
public class Task {
#Id
#SequenceGenerator(name = "TaskSeq", sequenceName = "TaskSeq", initialValue = 100)
#GeneratedValue(generator = "TaskSeq")
private Long id;
#Column(nullable = false)
private String name;
private String description;
private LocalDateTime inZ;
private LocalDateTime outZ;
private boolean completed;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
#JoinColumn(name = "projectId")
private Project project;
}
#Entity
#Table(name = "Project")
public class Project {
#Id
#SequenceGenerator(name = "ProjectSeq", sequenceName = "ProjectSeq", initialValue = 100)
#GeneratedValue(generator = "ProjectSeq")
private Long id;
#Column(nullable = false)
private String name;
#OneToMany(mappedBy = "project", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
private List<Task> tasks;
}
Service Classes:
#Service
public class ProjectServiceImpl implements ProjectService {
private final ProjectRepository projectRepository;
#Autowired
public ProjectServiceImpl(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
#Override
public Project save(Project project) {
return projectRepository.save(project);
}
#Override
public List<Project> findAll() {
return projectRepository.findAll();
}
}
#Service
public class TaskServiceImpl implements TaskService {
private TaskRepository taskRepository;
private ProjectRepository projectRepository;
#Autowired
public TaskServiceImpl(TaskRepository taskRepository, ProjectRepository projectRepository) {
this.taskRepository = taskRepository;
this.projectRepository = projectRepository;
}
#Override
#Transactional
public Task addTaskToProject(Long id, Task task) {
Project project = projectRepository.findById(id).orElseThrow(() -> new RuntimeException());
task.setProject(project);
return taskRepository.save(task);
}
}
The class I'm trying to use the transactional method:
public class TaskServiceTest extends JavaExampleApplicationTests {
#Autowired
private ProjectService projectService;
#Autowired
private TaskService taskService;
#Test
// #Transactional
public void canInsertTaskToProject() {
Project project = new Project();
project.setName("create company");
project = projectService.save(project);
Task task = new Task();
task.setName("Check how many people we need to hire");
task = taskService.addTaskToProject(project.getId(), task);
assertTrue(task.getId() > 0);
List<Project> projects = projectService.findAll();
assertEquals(1, projects.size());
assertEquals(1, projects.get(0).getTasks().size());
assertEquals(task.getId(), projects.get(0).getTasks().get(0).getId());
}
}
If I add a #Transactional(REQUIRES_NEW) to the methods in the service it will work, but I don't want it as if this method is called inside a real transaction I want it to be rolled back accordingly. Also I'd like to avoid using too many REQUIRES_NEW to avoid future problems
If I remove the #Transactional from the test method, it won't work when I test the size of the task list on last two lines as they are lazy.
If I add the #Transactional on the test method, it will throw me NullPointerException on the two last lines as when I do a projectService.findAll() the tasks are not commited yet
What is the best way to make it work ? I thought that inside a #Transactional when I used another command from db it would get the latest updates that were not committed yet..
Thanks in advance!
Update: added the reason I removed the #Transactional from test
In its roots this is a design issue. In the test code you're making changes and then verifying that those changes are made. This brings us to the problem of #Transactional or not.
With #Transactional, you end up in the situation where the changes are made, but they're not flushed or committed yet, so you can't see the changes in the same transaction. In this case you would either need to flush the results, and/or reconsider your transaction boundaries.
Without #Transactional, the individual transactions work fine, but since you're not inside a transaction, you can't initialize the lazy-loaded entities. For this your option is to either load the values in a way that eagerly initializes those, or load the values in a way that doesn't require initialization. Both of those would probably involve custom queries in your repository.
Without seeing the actual codebase, it's impossible to say what would be the optimal way to go. Using saveAndFlush() will probably solve the problem in this case, but it's not much of a general solution.

How to resolve LazyInitializationException in Spring Data JPA?

I have two classes that have a one-to-many relation. When I try to access the lazily loaded collection I get the LazyInitializationException.
I have been searching the web for a while and now I know that I get the exception because the session that was used to load the class which holds the collection is closed.
However, I did not find a solution (or at least I did not understand them). Basically I have these classes:
User
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue
#Column(name = "id")
private long id;
#OneToMany(mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public Set<Job> getCreatedJobs() {
return createdJobs;
}
public void setCreatedJobs(final Set<Job> createdJobs) {
this.createdJobs = createdJobs;
}
}
UserRepository
public interface UserRepository extends JpaRepository<User, Long> {}
UserService
#Service
#Transactional
public class UserService {
#Autowired
private UserRepository repository;
boolean usersAvailable = false;
public void addSomeUsers() {
for (int i = 1; i < 101; i++) {
final User user = new User();
repository.save(user);
}
usersAvailable = true;
}
public User getRandomUser() {
final Random rand = new Random();
if (!usersAvailable) {
addSomeUsers();
}
return repository.findOne(rand.nextInt(100) + 1L);
}
public List<User> getAllUsers() {
return repository.findAll();
}
}
Job
#Entity
#Table(name = "job")
#Inheritance
#DiscriminatorColumn(name = "job_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Job {
#Id
#GeneratedValue
#Column(name = "id")
private long id;
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private User creator;
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public User getCreator() {
return creator;
}
public void setCreator(final User creator) {
this.creator = creator;
}
}
JobRepository
public interface JobRepository extends JpaRepository<Job, Long> {}
JobService
#Service
#Transactional
public class JobService {
#Autowired
private JobRepository repository;
public void addJob(final Job job) {
repository.save(job);
}
public List<Job> getJobs() {
return repository.findAll();
}
public void addJobsForUsers(final List<User> users) {
final Random rand = new Random();
for (final User user : users) {
for (int i = 0; i < 20; i++) {
switch (rand.nextInt(2)) {
case 0:
addJob(new HelloWorldJob(user));
break;
default:
addJob(new GoodbyeWorldJob(user));
break;
}
}
}
}
}
App
#Configuration
#EnableAutoConfiguration
#ComponentScan
public class App {
public static void main(final String[] args) {
final ConfigurableApplicationContext context = SpringApplication.run(App.class);
final UserService userService = context.getBean(UserService.class);
final JobService jobService = context.getBean(JobService.class);
userService.addSomeUsers(); // Generates some users and stores them in the db
jobService.addJobsForUsers(userService.getAllUsers()); // Generates some jobs for the users
final User random = userService.getRandomUser(); // Picks a random user
System.out.println(random.getCreatedJobs());
}
}
I have often read that the session has to be bound to the current thread, but I don't know how to do this with Spring's annotation based configurations.
Can someone point me out how to do that?
P.S. I want to use lazy loading, thus eager loading is no option.
Basically, you need to fetch the lazy data while you are inside of a transaction. If your service classes are #Transactional, then everything should be ok while you are in them. Once you get out of the service class, if you try to get the lazy collection, you will get that exception, which is in your main() method, line System.out.println(random.getCreatedJobs());.
Now, it comes down to what your service methods need to return. If userService.getRandomUser() is expected to return a user with jobs initialized so you can manipulate them, then it's that method's responsibility to fetch it. The simplest way to do it with Hibernate is by calling Hibernate.initialize(user.getCreatedJobs()).
Consider using JPA 2.1, with Entity graphs:
Lazy loading was often an issue with JPA 2.0. You had to define at the entity FetchType.LAZY or FetchType.EAGER and make sure the relation gets initialized within the transaction.
This could be done by:
using a specific query that reads the entity
or by accessing the relation within business code (additional query for each relation).
Both approaches are far from perfect, JPA 2.1 entity graphs are a better solution for it:
http://www.thoughts-on-java.org/jpa-21-entity-graph-part-1-named-entity/
http://www.thoughts-on-java.org/jpa-21-entity-graph-part-2-define/
You have 2 options.
Option 1 : As mentioned by BetaRide, use the EAGER fetching strategy
Option 2 : After getting the user from database using hibernate, add the below line in of code to load the collection elements:
Hibernate.initialize(user.getCreatedJobs())
This tells hibernate to initialize the collection elements
Change
#OneToMany(mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();
to
#OneToMany(fetch = FetchType.EAGER, mappedBy = "creator")
private Set<Job> createdJobs = new HashSet<>();
Or use Hibernate.initialize inside your service, which has the same effect.
For those who have not the possibility to use JPA 2.1 but want to keep the possibility to return a entity in their controller (and not a String/JsonNode/byte[]/void with write in response):
there is still the possibility to build a DTO in the transaction, that will be returned by the controller.
#RestController
#RequestMapping(value = FooController.API, produces = MediaType.APPLICATION_JSON_VALUE)
class FooController{
static final String API = "/api/foo";
private final FooService fooService;
#Autowired
FooController(FooService fooService) {
this.fooService= fooService;
}
#RequestMapping(method = GET)
#Transactional(readOnly = true)
public FooResponseDto getFoo() {
Foo foo = fooService.get();
return new FooResponseDto(foo);
}
}
You should enable Spring transaction manager by adding #EnableTransactionManagement annotation to your context configuration class.
Since both services have #Transactional annotation and default value property of it is TxType.Required, current transaction will be shared among the services, provided that transaction manager is on. Thus a session should be available, and you won't be getting LazyInitializationException.

Categories

Resources