1

I have a very basic Spring Boot/JPA stack app, with a controller, service layer, and repository that does not persist updates as I understand it should.

A trivial Entity:

@Entity
public class Customer {

  @Id
  @GeneratedValue(strategy=GenerationType.AUTO)
  private Long id;
  private String name;

  protected Customer() {}

  public Customer(String name) { this.name = name; }

  // standard getters,setters //
}

A trivial Repository:

@Repository
public interface CustomerRepository extends CrudRepository<Customer, Long> {}

A simple Service layer:

// If the service is @Transactional and the controller is not, the update does NOT occur
@Transactional
@Service
public class CustomerService {

  private static final Logger LOG = getLogger(CustomerService.class);

  @Autowired
  private CustomerRepository customerRepository;

  boolean updateCustomerName(Long id, String name) {
    Customer customer = customerRepository.findOne(id);

    if (customer == null) { return false; }

    // Modifies the entity
    customer.setName(name);

    // No explicit save()

    return true;
  }
}

And a REST controller that uses it all:

// If the controller is @Transactional and the service is not, the update occurs
@RestController
@RequestMapping("/mvc")
public class CustomerController {

  @Autowired
  private CustomerService customerService;

  @RequestMapping(path = "{id}", method = RequestMethod.PUT)
  public ResponseEntity updateCustomerName(@PathVariable Long id, @RequestParam("name") String name) {

    customerService.updateCustomerName(id,name);

    return ResponseEntity.noContent().build();
  }
}

These are wired together with a simple one-liner SpringBootApplication

I have SQL debug logs enabled and see the selects, update, etc.

With the code above: When the service method is invoked by the controller, the modified entity is not persisted. SQL logs show the select of the entity but no update.

There is also no update if nothing is marked @Transactional

However, simply by moving the @Transactional annotation from the service class to the controller class, the SQL update does occur.

If I add an explicit customerRepository.save(customer) to the service method, the update also occurs. But my understanding is that the ORM should automatically save modified persistent entities.

I'm sure the issue has something to do with the EntityManager lifecycle in the web request, but I'm puzzled. Do I need to do additional configuration?

Complete example at https://github.com/monztech/SO-41515160

EDIT: This was solved, see below. Per the Spring spec @Transactional does not work in package-private methods and mistakenly did not make the update service method public.

The update will occur if the method is public and the service class has the @Transactional annotation.

I do have another question, however. Why is the @Transactional annotation necessary? (the update does not occur without it) Shouldn't the entity manager still persist the object because of the open session in view mechanism that Spring uses, independent of any transaction?

Neil Stockton
  • 11,383
  • 3
  • 34
  • 29
Monz
  • 321
  • 2
  • 11
  • Try with @Transactional(readOnly=false), although the default value is false. It's a bit strange because whether the findOne method fetchs the values from the DB means that method is working correctly with @Transactional (working inside the transactional context, wrapped by the TransactionalManager decorator and so so...). And yes, Customer in that transactional context is managed in the jpa persistent context, so it should be updated when the method returns. – Dani Jan 06 '17 at 22:54
  • readOnly=false doesn't make a difference. Thanks. This is a strange one. I'm a Hibernate/Spring veteran but haven't used it in Spring Boot before. Seems I'm missing some step but I haven't had any luck finding it. – Monz Jan 06 '17 at 23:20
  • 1
    Interesting question. Can you try 2 things to see what happens. 1. Make the service method public and try adding the transactional annotation directly on the method. 2. Independently of 1, in your code as it is (other than making the method public) create an interface which defines the update method and have your service implement that interface. – Alan Hay Jan 07 '17 at 01:24
  • Regarding OSIV it will respect application defined transactions. http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.html this is normally what you want - for the session to be held open effectively in read only mode to render the response. – Alan Hay Jan 07 '17 at 20:37

1 Answers1

4

Make your updateCustomerName method public.

MarkOfHall
  • 3,334
  • 1
  • 26
  • 30
  • Thanks, but that didn't help. I wouldn't expect member access modifiers to modify Spring's behavior. The method is getting called. – Monz Jan 06 '17 at 23:26
  • But they do http://stackoverflow.com/questions/4396284/does-spring-transactional-attribute-work-on-a-private-method – Alan Hay Jan 07 '17 at 00:55
  • I tried a few things based on the response from @alanhay and it turns out this is the solution. I must have made a mistake when I tried it before but I've confirmed that simply changing the method in the Service `public` produces the required behavior. Thanks! – Monz Jan 07 '17 at 03:26
  • 1
    Ouhhh!, I'm sorry!!, I didn't notice that your method wasn't public, but I noticed you about the TransactionManager like decorator... It's very important to know how Spring works with JDK dynamic proxies or CGLIB proxies, and the behaviour with the Spring decorators. The proxies by default only can wrapped the interface public methods, such self-invocations cannot be intercepted by the proxy (only by the other target methods). So the decorators (in this case the TransactionManager) can't notice about the existence of not public methods. – Dani Jan 07 '17 at 08:16