0

I'm wondering where the actual behaviour of Spring Data JPA is documented, as it happens to be different from what can be expected in regular JPA with an entity manager. I would be interested to find a documentation on the life-cycle of entities in Spring JPA.

For instance, suppose we have three entities, Message, Author and Label. A message can have multiple labels, but only one author.

So Message is basically :

@Entity
public class Message implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    @ManyToMany
    private Set<Label> labels = new HashSet<>();
    @ManyToOne
    private Author author;
    ...
}

Links are unidirectional.

Consider the following code in a service :

  Author a = new Author("auteur A");
  a = authorRepository.save(a);
  Message msg = new Message("un message", "texte du message", a);
  msg = messageRepository.save(msg);            
  for (int i=0; i < 10; i++) {
    Label lab = new Label("label"+i);
    lab = labelRepository.save(lab);
    labelRepository.flush();                
    msg.addLabel(lab);
  }            
  messageRepository.save(msg);

If I omit the last line (messageRepository.save(msg)), the labels are created, but they are not really added to a message. I find this unexpected, considering the underlying technology is JPA:

The equivalent code for standard JPA with Entity manager would be:

 EntityManagerFactory emf = Persistence.createEntityManagerFactory("demo1PU");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction = em.getTransaction();
 transaction.begin();
 Author a = new Author("auteur A");
 em.persist(a);
 Message msg = new Message("un message", "texte du message", a);
 em.persist(msg);            
 for (int i=0; i < 10; i++) {
   Label lab = new Label("label"+i);
   em.persist(lab);              
   msg.addLabel(lab);
 }            
 transaction.commit();        
 em.close();
 emf.close();

In the entitymanager-based code, you don't need to save the message twice : as you are still in the same transaction, the message is a managed entity, and all changes to this object made while the transaction is active are also made to the database entries.

Apparently, the entities managed by Spring are a bit different from those manipulated by regular EntityManagers. But is there some explicit documentation somewhere ? The spring-data-jpa-reference.pdf file doesn't help much.

khaemuaset
  • 217
  • 1
  • 9
  • 1
    Do you execute your service code within a `@Transactional` method? – Lesiak Dec 01 '20 at 13:11
  • Yes, of course. – khaemuaset Dec 01 '20 at 14:31
  • 2
    Turn on the SQL `spring.jpa.hibnerate.show-sql=true` as a start to understand what's going on. But otherwise, who owns the Message-Label relationship and why are you flushing? Show your Label entity code. – K.Nicholas Dec 01 '20 at 15:13
  • As a side note, if you want to know how repository methods work, check out `SimpleJpaRepository` – crizzis Dec 01 '20 at 21:32
  • @K.Nicholas : the label code has just id/text field. Everything is is in Message. `spring.jpa.hibnerate.show-sql=true ` is active, and no insertion is done in the `message_label` table. Flushing was done just in case. – khaemuaset Dec 02 '20 at 08:06
  • @crizzis thanks... so jpa performs a simple em.persist ... – khaemuaset Dec 02 '20 at 08:07
  • You have not declared an owner of the manytomany relationship. Either add a 'mappedBy' property or use '@JoinTable' annotations. – K.Nicholas Dec 02 '20 at 08:08
  • You don't need to declare an owner if the manytomany is not bidirectional. – khaemuaset Dec 02 '20 at 08:10
  • It's a problem of transactions. The code is currently executed in a `@PostConstruct method`. If I call it from a controller and not from `@PostConstruct,` everything works correctly. The explicit entity manager code didn't have the problem because it started the transactions. See https://stackoverflow.com/questions/17346679/transactional-on-postconstruct-method – khaemuaset Dec 02 '20 at 08:16
  • 1
    @Lesiak so, it was the problem. methods annotated with PostConstruct can't be transactional. – khaemuaset Dec 02 '20 at 08:18

1 Answers1

1

Ok, just to avoid people having hard time figuring what the problem is:

The offending code was in fact the following :

 @Transactional
 @PostConstruct
 public void initDatabase() {        
        if (messageRepository.count() == 0) {
            Auteur a = new Auteur("auteur A");
            a = auteurRepository.save(a);
            Message msg = new Message("un message", "texte du message", a);
            msg = messageRepository.save(msg); // (pas nécessaire, vu qu'on le fait à la fin de la méthode...)
            for (int i = 0; i < 10; i++) {
                Label lab = new Label("label" + i);
                lab = labelRepository.save(lab);
                msg.addLabel(lab);
            }          
        }
    }

And the problem is that when @PostConstruct annotated methods are called, dependency injection has been done, which is good, but the whole application context is not ready, so the code was not executed in a transaction.

Simply using

 @Transactional
 @EventListener(ContextRefreshedEvent.class)
 public void initDatabase() {        
  ...
 }

Instead solves the problem.

khaemuaset
  • 217
  • 1
  • 9