0

I have read a lot of @Transactional annotation, I saw stackoverflow answers but it does not help me. So I am creating my question.

My case is to save user with unique email. In DB I have user with email xxx@xxx.com, and I am saving user with the same email address. For saving I have to use entityManager.merge() because of this post thymeleaf binding collections it is not important.

First example:

@Controller
public class EmployeeController extends AbstractCrudController {

    // rest of code (...)

    @RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
    public String processNewEmployee(Model model, @ModelAttribute("employee") User employee, BindingResult result, HttpServletRequest request) {
        prepareUserForm(model);
        if (!result.hasErrors()) {
            try {
                saveEmployee(employee);
                model.addAttribute("success", true);
            } catch (Exception e) {
                model.addAttribute("error", true);
            }
        }

        return "crud/employee/create";
    }

    @Transactional
    public void saveEmployee(User employee) {
        entityManager.merge(employee);
    }

    private void prepareUserForm(Model model) {
        HashSet<Position> positions = new HashSet<Position>(positionRepository.findByEnabledTrueOrderByNameAsc());
        HashSet<Role> roles = new HashSet<Role>(roleRepository.findAll());
        User employee = new User();

        model.addAttribute("employee", employee);
        model.addAttribute("allPositions", positions);
        model.addAttribute("allRoles", roles);
    }
}

This code is throwing TransactionRequiredException, I do not know why? It looks like @Transactional annotation did not work, so I moved annotation to processNewEmployee()

Second example:

@Controller
public class EmployeeController extends AbstractCrudController {

    // rest of code (...)

    @Transactional
    @RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
    public String processNewEmployee(Model model, @ModelAttribute("employee") User employee, BindingResult result, HttpServletRequest request) {
        prepareUserForm(model);
        if (!result.hasErrors()) {

            try {
                entityManager.merge(employee);
                model.addAttribute("success", true);
            } catch (Exception e) {
                model.addAttribute("error", true);
            }
        }

        return "crud/employee/create";
    }

    private void prepareUserForm(Model model) { /*(.....)*/ }
}

And this code is throwing PersistenceException (because of ConstraintViolationException) and of course I got "Transaction marked as rollbackOnly" exeption.

When I try to save email which not exists this code works fine, so I thing that @Transactional annotation is configured well.

If this is important I am putting my TransationManagersConfig:

@Configuration
@EnableTransactionManagement
public class TransactionManagersConfig implements TransactionManagementConfigurer {

    @Autowired
    private EntityManagerFactory emf;

    @Autowired
    private DataSource dataSource;

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager tm =
                new JpaTransactionManager();
        tm.setEntityManagerFactory(emf);
        tm.setDataSource(dataSource);
        return tm;
    }

    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return transactionManager();
    }
}

Could you explain my what I am doing wrong and suggest possible solution of this problem?

Solution:

Thanks to R4J I have created UserService and in my EmployeeController I am using it instead of entityManager.merge() now it works fine

@Service
public class UserService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void merge(User user) {
        entityManager.merge(user);
    }
}

And EmployeeController:

@Controller
public class EmployeeController extends AbstractCrudController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
    public String processNewEmployee(Model model, @ModelAttribute("employee") User employee, BindingResult result, HttpServletRequest request) {
         // (.....)
         userService.merge(employee);
         // (.....)
    }

}
Community
  • 1
  • 1
Purzynski
  • 1,107
  • 2
  • 13
  • 35

1 Answers1

3

Your transactions don't work because you call directly 'this.saveEmployee(...)' from your 'public String processNewEmployee' method.

How come?

When you add @Transactional, Spring creates a Proxy for your Component and proxies all public methods. So when Spring itself calls your method as a HTTP Rest Request it is considered an external call that goes properly through a Proxy and new Transaction is started as required and code works.

But when you have a Proxied Component and you call 'this.saveEmployee' (that has @Transactional annotation) inside your class code you are actually bypassing the Proxy Spring has created and new Transaction is not started.

Solution: Extract entire database logic to some sort of a Service or DAO and just Autowire it to your Rest Controller. Then everything should work like a charm.

You should avoid direct database access from Controllers anyway as it is not a very good practice. Controller should be as thin as possible and contain no business logic because it is just a 'way to access' your system. If your entire logic is in the 'domain' then you can add other ways to run business functionalities (like new user creation) in a matter of just few lines of code.

Rafal G.
  • 4,252
  • 1
  • 25
  • 41
  • Thx for your response and advice about DDD. I understand what you have written and I will try to re-factor my code. Now I updated my question, please look at my solution and give me some comment if its ok or not so I could mark your answer as correct. – Purzynski Sep 01 '15 at 19:59
  • It looks much better now. Proxies in Spring can be tricky at times ;) – Rafal G. Sep 01 '15 at 20:15