Why do I need #Transactional for saving an OneToOne mapped Entity - spring

I have a simple straight forward demo application with spring-boot, spring-data-jpa and a h2-DB.
I have build two entities which are mapped by an OneToOne relationship.
Post.java
#Entity
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
#OneToOne(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private PostDetail postDetail;
}
PostDetail.java
#Entity
public class PostDetail {
#Id
#GeneratedValue
private Long id;
private String message;
#OneToOne(fetch = FetchType.LAZY)
#MapsId
#JoinColumn(name = "id")
private Post post;
}
I try to create and save a new Post. Then I try to create a new PostDetail, set the previous generated Post to it and save it. In the one controller sample I dont have a #Transactional annotation and in the seconde sample I do annotate the method with #Transactional
#RestController
public class TestController {
#Autowired
PostRepository postRepository;
#Autowired
PostDetailRepository postDetailRepository;
#GetMapping("/test1")
public String test1() {
Post post = new Post();
post.setId(2L);
post.setTitle("Post 1");
postRepository.save(post);
PostDetail detail = new PostDetail();
detail.setMessage("Detail 1");
detail.setPost(post);
postDetailRepository.save(detail);
return "";
}
#Transactional
#GetMapping("/test2")
public String test2() {
Post post = new Post();
post.setId(2L);
post.setTitle("Post 1");
postRepository.save(post);
PostDetail detail = new PostDetail();
detail.setMessage("Detail 1");
detail.setPost(post);
postDetailRepository.save(detail);
return "";
}
}
Why do I get in the first sample a org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.demo.jpa.model.Post exception and in the other sample not?
Can anyone explain why this happens?

You use bidirectional #OneToOne association. As hibernate documentation states:
Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
So, you should rewrite your test method in this way:
#GetMapping("/test1")
public String test1() {
Post post = new Post();
post.setId(2L);
post.setTitle("Post 1");
PostDetail detail = new PostDetail();
detail.setMessage("Detail 1");
// synchronization of both sides of #OneToOne association
detail.setPost(post);
post.setDetail(detail);
// thanks to CascadeType.ALL on Post.postDetail
// postDetail will be saved too
postRepository.save(post);
return "";
}

You shouldn’t be saving those 2 entities separately — you should set PostDetail inside of post object and save only the Post object. Hibernate will take care of saving the aggregated PostDetail.
That is why you are getting PersistentObjectException which you are able to workaround by keeping it inside of the same transaction.

we do not always need a bidirectional mapping when we are mapping two entities
you can simple have a unidirection most of the time
Post post = new Post();
post.setId(2L);
post.setTitle("Post 1");
PostDetail detail = new PostDetail();
detail.setMessage("Detail 1");
detail.setPost(post);
postRepository.save(post);
as you have cascade.all ,so hibernate saves Post first and then it saves PostDetail, now as per the rule of Transaction behavior ,either it is completely done or not done,Hence we can not have the situation that Post is saved but PostDetail did not,Hence to avoid such ambiguity it is important to have #Transaction annotation ,at method level or may be class level as per your requirement

Related

Spring boot hibernate #ManyToMany doesn't commit or returns incomplete data when I execute any method on the junction table from non-owner entity

I'm currently working on a Spring Boot project for an online shop. It's my first project with Spring Boot (and my first post here), so my coding is not the best.
Context for the questions:
My shop (for now) has a lists of products and whishlists of different users (shopping lists), which have a bidirectional #ManyToMany relation (i left here the relevant details for my question(s)):
Product.java entity:
#Entity
public class Product extends RepresentationModel\<Product\>{
#Id
#GeneratedValue
#JsonView(ProductView.DescriptionExcluded.class)
private Integer id;
#ManyToMany()
#JoinTable(
name = "Shopping_Product",
joinColumns = { #JoinColumn(name = "id", referencedColumnName = "id") },
inverseJoinColumns = { #JoinColumn(name = "list_id", referencedColumnName = "list_id") })
#JsonIgnore
private Set<ShoppingList> shoppinglists = new HashSet<>();
// Constructor, getters, setters ....
ShoppingList.java entity:
#Entity
public class ShoppingList {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(ShoppingListView.ProductsExcluded.class)
private Integer list_id;
#JsonView(ShoppingListView.ProductsIncluded.class)
#ManyToMany(mappedBy = "shoppinglists")
private Set<Product> products = new HashSet<>();
// Constructor, getters, setters ...
I chose Product as the owner because i wanted to delete (tho it would be more fit to show something like "offer expired", but I'll stick to delete for now) the product from all existing lists when the admin takes it down from the shop, which works as expected:
ProductResource.java (controller):
#DeleteMapping("/categs/*/sub/*/products/{id}")
public ResponseEntity<String> deleteProduct(#PathVariable int id) {
Optional<Product> optional = productRepository.findById(id);
if(!optional.isPresent()) throw new NotFoundException("Product id - " + id);
Product prod = optional.get();
productRepository.delete(prod);
return ResponseEntity.ok().body("Product deleted");
}
My problems now are related to the ShoppingList entity, which is not the owner.
Any call I make to the Product resource (controller) works as expected, but anything from the other side either fails or returns incomplete results, like the following:
1.
I call retrieve all products from a list and it returns only the first object (the list has at least 2):
ShoppingListResource.java (controller):
#RestController
public class ShoppingListResource {
#Autowired
private ProductRepository productRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private ShoppingListRepository shoppinglistRepository;
#GetMapping("/user/lists/{id}")
public Set<Product> getShoppinglistProducts(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only check your list(s)!");
// All lists are shown for a product
// Product p = productRepository.findById(10111).get();
// Set<ShoppingList> set = p.getShoppinglists();
// set.stream().forEach(e -> log.info(e.toString()));
// Only first product is shown for a list
return shoppingList.getProducts();
This is what hibernate does on the last row (only returns 1/2 products)
Hibernate: select products0_.list_id as list_id2_3_0_,
products0_.id as id1_3_0_,
product1_.id as id1_1_1_,
product1_.description as descript2_1_1_,
product1_.name as name3_1_1_,
product1_.price as price4_1_1_,
product1_.subcat_id as subcat_i5_1_1_ from shopping_product products0_ inner join product product1_ on products0_.id=product1_.id where products0_.list_id=?
As i said above, I can delete a product and it gets removed automatically from all existing lists, but when i try the same from ShoppingList entity does nothing:
Same controller
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only delete your list(s)!");
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
Also, when i try to add/delete product from an existing list, does nothing.
This is my repo with full code, if you'd like to test directly (dev branch is up to date):
https://github.com/dragostreltov/online-store/tree/dev
You can just use admin admin as authentication (on the H2 console too). More details on the readme.
All DB data at app start is inserted from a .sql file.
I checked other similar questions and tried different methods on my ShoppingList entity (on the delete issue), like:
#PreRemove
public void removeListsFromProducts() {
for(Product p : products) {
p.getShoppinglists().remove(this);
}
}
Spring/Hibernate: associating from the non-owner side
And still doesn't work.
UPDATE:
I found out what issues I was having, I'll post an answer with the solution.
For anyone who's got the same/similar problems as I did, this is how I resolved them:
For point 1
(Hibernate only retrieves the first product from a shoppingList (Set))
I made multiple tests on my retrieve method and found out my Set was only containing 1 object, despite calling .add(product) twice.
As you can see, I'm using HashSet for both entities:
In Product (owner):
private Set<ShoppingList> shoppinglists = new HashSet<>();
In ShoppingList (mappedBy):
private Set<Product> products = new HashSet<>();
Thanks to this answer: https://stackoverflow.com/a/16344031/18646899
I learnt:
HashSet (entirely reasonably) assumes reflexivity, and doesn't check for equality when it finds that the exact same object is already in the set, as an optimization. Therefore it will not even call your equals method - it considers that the object is already in the set, so doesn't add a second copy.
In particular, if x.equals(x) is false, then any containment check would also be useless.
Taking this into account, I overwrote the hashCode() and equals() methods in Product.class and now
shoppingList.getProducts()
works as expected.
For point 2
(not being able to delete associations of non-owner entity before deleting the row from it's table)
Added lazy fetch and cascade to Product #ManyToMany:
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH})
And added the following methods:
In Product class:
public void addShoppinglist(ShoppingList list) {
this.shoppinglists.add(list);
list.getProducts().add(this);
}
public void removeShoppinglist(ShoppingList list) {
this.shoppinglists.remove(list);
list.getProducts().remove(this);
}
In ShoppingList class:
public void addProduct(Product product) {
this.products.add(product);
product.getShoppinglists().add(this);
}
public void removeProduct(Product product) {
this.products.remove(product);
product.getShoppinglists().remove(this);
}
Added #Transactional and modified the method inside the controller (ShoppingListResource) for deleteShoppingList:
#RestController
public class ShoppingListResource {
...
#Transactional
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
...
shoppingList.getProducts().stream().forEach(e -> {
e.removeShoppinglist(shoppingList);
});
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
}
And now this is working as expected, the shoppingList's associations are deleted first then the shoppingList itself.

Sync a #ManyToOne relationship after saveAndFlush with spring data jpa

I have this entity in a spring boot project. Having the insertable/updatable false is useful in many cases but causes problems when saving the entity. Here's the code.
#Entity
#Table(name = "BOOK", schema = "DIST")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID", nullable = false)
private Integer id;
#ManyToOne
#JoinColumn(name = "AUTHOR_ID", insertable = false, updatable = false)
private Author author;
#Column(name = "AUTHOR_ID")
private Integer authorId;
and this method to save a new book
#Transational
public void test() {
Book book = new Book();
book.setAuthorId(1);
book = bookRepository.saveAndFlush(book);
book.getAuthor().getName(); //author is null
}
The getAuthor() relationship is always null after saving, I'm wondering if there's a way to fetch it. In other words I'd like to be able to force the ManyToOne relationship to be populated but I cannot find a clean way to do it.
This approach doesn't work.
#Transational
public void test() {
Book book = new Book();
book.setAuthorId(1);
book = bookRepository.saveAndFlush(book);
book = bookRepository.findById(book.getId());
book.getAuthor().getName(); //author is still null
}
This one seems to be an options but I don't like it.
#Transational
public void test() {
Book book = new Book();
book.setAuthorId(1);
book = bookRepository.saveAndFlush(book);
entityManager.clear()
book = bookRepository.findById(book.getId());
book.getAuthor().getName(); //author is NOT null
}
Any hints?
I happen to be having and have been investigating how to solve this same issue.
Our data model/use case also requires us to have the foreign key field explicitly defined as a number as well as including the referenced object for being able to easily access its properties.
It seems JPA is expecting us to physically populate the nested object before calling save. Most of the examples on the web only have the many to one field and not a separate numeric ID column field
Adding cache isolation annotation to Book:
#Entity
#Table(name = "BOOK", schema = "DIST")
#Cache(isolation = CacheIsolationType.ISOLATED)
public class Book {
appears to be a solution, in my opinion this is less clunky than explicitly calling EntityManager.clear(). But still requires an explicit getById call after the save. In my scenario I already had the isolation defined for solving other issues so adding the extra get is not a big issue. This code example you gave should work after adding the cache isolation annotation:
#Transational
public void test() {
Book book = new Book();
book.setAuthorId(1);
book = bookRepository.saveAndFlush(book);
book = bookRepository.findById(book.getId());
book.getAuthor().getName();
}
I haven't yet been able to find a better solution. I'll keep an eye on this question to see if anyone provides one.
You can try to refresh the object from the Session of the EntityManager
example:
import javax.persistence.EntityManager;
#Transational
public void test() {
Book book = new Book();
book.setAuthorId(1);
book = bookRepository.saveAndFlush(book);
// this will re-read the state of give object
// from underlying database
entityManager.refresh(book);
book.getAuthor().getName(); //author is NOT null
}

Spring Boot many to many post method not updating data

My User class looks like this :
#Data
#Entity
public class User {
#Id
Long userID;
#ManyToMany(mappedBy = "admins")
private List<ClassRoom> classRooms = new ArrayList<>();
}
And my ClassRoom class like this :
#Data
#Entity
public class ClassRoom {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long classRoomID;
#ManyToMany
#JoinTable(name ="classroom_user",
joinColumns = #JoinColumn(name = "classroom_id"),
inverseJoinColumns = #JoinColumn(name = "user_id"))
private List<User> admins = new ArrayList<>();
}
And in my UserController class, I have :
#PostMapping("user/{id}/c")
User addClassRoom(#PathVariable Long id,#RequestBody ClassRoom newClassRoom)
{
logger.debug(repository.findById(id));
return repository.findById(id)
.map(user -> {
user.getClassRooms().add(newClassRoom);
user.setClassRooms(user.getClassRooms());
return repository.save(user);
})
.orElseGet(() -> {
return null;
});
}
And I POST and empty JSON ({}) and I see no change in my users. The Classroom or an empty Classroom doesn't get added in the User.
What is the problem here? How can I resolve this ?
user.getClassRooms().add(newClassRoom); is suffice, user.setClassRooms(user.getClassRooms()); not required.
You will have to perform cascade save operation.List all cascade types explicitly and don't use mappedBy, instead use joincolumns annotation.
Can you paste the logs, please? Is Hibernate doing any insert into your table? Has the database schema been created in the DB correctly? One thing I recommend you to do is to add a custom table name on the top of your User class, using annotations like so: #Table(name = "users"). In most SQL dialects user is a reserved keyword, hence it is recommended to always annotate User class a bit differently, so that Hibernate won't have any problems to create a table for that entity.
IMO you must find classRoom by its id from repository, if it's new, you must create a new entity and save it first. Then assign it to user and save it.
The object you receive from the post method was not created by the entity manager.
After using user.getClassRooms().add(newClassRoom);
We must use userRepository.save(user);

Spring JPA nested Exception: detached entity passed to persist

I tried almost all what I could find here in SO, and another sites tutorials about creating an One to One Relationship with Hibernate.
So, I have two models, here are the last modifications, like for example the #MapsId annotation I also removed in previous test.
Usuario:
#Entity
#Table(name="usuarios")
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Usuario {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="usuarios_id_seq")
#SequenceGenerator(name="usuarios_id_seq", sequenceName="usuarios_id_seq", allocationSize=1)
#Column(name="id")
private Long id;
#ManyToOne
#JoinTable(name="roles_usuarios", joinColumns={#JoinColumn(name="usuarios_id", referencedColumnName="id")}, inverseJoinColumns={#JoinColumn(name="roles_id", referencedColumnName="id")})
private Rol rol;
#OneToOne(cascade = CascadeType.ALL, mappedBy="usuario")
private Cliente cliente;
Cliente:
#Entity
#Table(name="clientes")
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Cliente {
#Id
//#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="clientes_id_seq")
//#SequenceGenerator(name="clientes_id_seq", sequenceName="clientes_id_seq", allocationSize=1)
//#Column(name="id")
private Long id;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="usuario_id", referencedColumnName="id")
#MapsId
private Usuario usuario;
Cliente Controller:
#PostMapping("/")
public ResponseEntity<Void> postCliente(#RequestBody Cliente cliente, UriComponentsBuilder ucBuilder) {
if( clienteService.isClienteExist(cliente) ){
return new ResponseEntity<Void>(HttpStatus.CONFLICT);
}
clienteService.save(cliente);
HttpHeaders headers = new HttpHeaders();
headers.setLocation( ucBuilder.path("/{id}").buildAndExpand(cliente.getId()).toUri() );
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
Cliente Service:
#Override
public Cliente save(Cliente cliente) {
Cliente clt = new Cliente();
clt.setUsuario(cliente.getUsuario());
clt.setRazonSocial(cliente.getRazonSocial());
clt.setRfc(cliente.getRfc());
clt.setDireccion(cliente.getDireccion());
clt.setEmail(cliente.getEmail());
clt.setTelefono(cliente.getTelefono());
clt.setContacto(cliente.getContacto());
clt.setTelContacto(cliente.getTelContacto());
clt.setEmailContacto(cliente.getEmailContacto());
return clienteRepository.save(clt);
}
If you notice I also have a many to one relationship with a Rol table which works fine, but when I pass information in the OneToOne which I pass it as a JSON it produces: detached entity passed to persist: com.swargos.entities.Usuario
IDK if I'm missing some annotations, or is that the database is created when running the spring application.
I'm providing a somewhat qualified guess, since you didn't include code that shows how you call persist.
The error means that the Usuario instance you are passing to persist() already has a primary key, but it is not a managed entity of that persistence context, see here for Entity Object lifecycle
My guess is that the Usuario instance was loaded by another EntityManager, then json-serialized to the front-end, and then posted back to the backend, and you need to set it on a Cliente (Since you have cascade in both directions it may also be the Cliente being set on the Usuario). Every time an entity has been loaded in one Persistence Context, and you want to save it in another you must either call em.merge() or you must call em.find() to load it into it (and then set the changes).
JPA is not magic, the life-cycle of the Entities and the Persistence Context which manage them is well defined, and unless the developer understands how these mechanisms work, a lot of time will be wasted trying to work against the framework.
Also #MapsId should only be used if Cliente used an #EmbeddedId for it primary key, which does not seem to be the case.

Spring Repository issue

I seem to be baffled on how JPA Repositories are suppose to work.
In a nut-shell
#Entity
public class User extends AbstractEntity {
protected final static String FK_NAME = "USER_ID";
#Column(nullable = false)
private String firstName;
#OneToMany(cascade = ALL, fetch = FetchType.LAZY, orphanRemoval = true)
#JoinColumn(name = "userId")
private List<Detail> details = new ArrayList<Detail>();
}
#Entity
public class Detail extends AbstractEntity {
Long userId;
String hello;
}
#Repository
public interface UserRepository extends CrudRepository<User, Long> {
User findByFirstName(#Param("firstName") String firstName);
}
And here is the only controller in the app:
#RestController
public class Home {
#Autowired
UserRepository userRepository;
#Autowired
DetailsRepository loanRepository;
#RequestMapping(value = "")
public HttpEntity home() {
User user = userRepository.findByFirstName("John");
if (user == null) {
user = new User();
user.setFirstName("John");
}
Detail detail = new Detail();
detail.setHello("Hello Msh");
user.getDetails().add(detail);
userRepository.save(user);
return new ResponseEntity("hi", HttpStatus.OK);
}
}
Below a screenshot from debugging session where the app just started and the get request to home() method creates new user, new detail, adds detail to user.
Below example - when the user is saved, the detail entity gets updated
Now on the next request, the old user John is found and has been added a new instance of detail.
The old user has been saved but now the newly created detail does not get updated outside.
How come this only works first time ?
Basically theres so much fail going on so that I would advise you to go a step backwards. If youre wana go the short path of getting a solution for exactly this problem continue reading ;)
First part related to the answer of Jaiwo99:
As I can see in the gradle view of intellij, your using Spring Boot. So it is necessary to place #EnableTransactionManagement on top of your configuration class. Otherwise the #Transacion annotation does not have any effect.
Second part your JPA/Hibernate model mapping. Theres so much bad practise on the net that it is no wonder that most beginners have troubles starting with it.
A correct version could look like (not tested)
#Entity
public class User extends AbstractEntity {
#Column(nullable = false)
private String firstName;
#OneToMany(cascade = ALL, fetch = FetchType.LAZY, orphanRemoval = true, mappedBy="user")
private List<Detail> details = new ArrayList<Detail>();
public void addDetail(Detail detail) {
details.add(detail);
detail.setUser(user);
}
}
#Entity
public class Detail extends AbstractEntity {
#ManyToOne
private User user;
private String hello;
public void setUser(User user){
this.user = user;
}
}
Some general advice related to creating a model mapping:
avoid bi-directional mappings whenever possible
cascade is a decision made on the service level and not at the model level and can have huge drawbacks. So for beginners avoid it.
I have no idea why people like to put JoinColumn, JoinTable and whatever join annotation on top of fields. The only reason to do this is when you have a legacy db (my opinion). When you do not like the names created by your jpa provider, provide a different naming strategy.
I would provide a custom name for the user class, because this is in some databases a reserved word.
Very simple, the first time you saved a new entity outside of hibernate session, the second time, the user object you got is a detached object, by default hibernate will not consider it is changed in this case.
*solution *
Move this logic to another service class, which annotated with #transactional
Or
Annotate your controller with transactional
Or
Override equals and hashCode method on user class may also help

Resources