I have two cases where child entity save works differently when invoked from different context.
When I update child with no cascading parent, modified data is also updated, but this happens only from controller, if I invoke from CommandLineRunner
that parent update doesn't happen.
This is my service
@Service
public class BookService {
private final BookAuthorRepository bookAuthorRepository;
private final BookRepository bookRepository;
@Autowired
public BookService(BookAuthorRepository bookAuthorRepository, BookRepository bookRepository) {
this.bookAuthorRepository = bookAuthorRepository;
this.bookRepository = bookRepository;
}
public void updateBookAuth() {
Book book = bookRepository.findById(3).get();
book.setName("should not update");
book.getBookAuthor().setName("new name");
bookAuthorRepository.save(book.getBookAuthor());
}
}
When I invoke this from class below, only BookAuthor
name is updated, which is the right behaviour.
@Component
public class MyRunner implements CommandLineRunner {
@Autowired
private BookService bookService;
@Override
public void run(String... args) throws Exception {
bookService.updateBookAuth();
}
}
But if I invoke this service method from controller :
@RestController
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/")
public void test() {
bookService.updateBookAuth();
}
}
then both Book
name and BookAuthor
name are updated, but I didn't understand why Book
name is updated.
Here are the entities :
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToOne
private BookAuthor bookAuthor;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public BookAuthor getBookAuthor() {
return bookAuthor;
}
public void setBookAuthor(BookAuthor bookAuthor) {
this.bookAuthor = bookAuthor;
}
}
@Entity
public class BookAuthor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToOne(mappedBy = "bookAuthor")
private Book book;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
}
I thought about transactional also, if I write transaction on method then all context is opened and both parent-child will be updated but in this case I don't use it so why is it happening ?
You can check if the book entity is still managed when you get it from the repository. For this we can inject the entity manager directly in the service and add the log statement like this:
@Service
@RequiredArgsConstructor
public class BookService {
private final BookBookAuthorRepository bookAuthorRepository;
private final BookRepository bookRepository;
private final EntityManager entityManager;
public void updateBookAuth() {
Book book = bookRepository.findById(1L).get();
System.out.println("----------> is managed: " + entityManager.contains(book));
book.setName("should not update");
book.getBookAuthor().setName("new name");
bookAuthorRepository.save(book.getBookAuthor());
}
}
Now when you call the updateBookAuth function from the MyRunner it will print:
----------> is managed: false
And when you call the updateBookAuth function from the BookController it will print:
----------> is managed: true
This means that in the case of the rest controller the database operations were executed in the single persistence session and because of that both book and author entities were updated.
And in the case of the command line runner the book entity query was executed in the separate persistence session and was detached after that, and the author entity update was executed in the separate persistence session, because of that book entity update was lost.
This happens because the Spring web framework by default ties the persistence session and the request life-cycles together. This is implemented like this to prevent the problems with the lazy initialize associations in the view layer.
To prevent this behavior you can disable it with the following setting in the application.properties file:
spring.jpa.open-in-view=false
Please read more about this feature in this article