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: