starter kit, @Transactional does not work as expect ?

Hi there,
I am new to Vaadin (coming from SmartGWT) and before deciding to port an existing large business application to Vaadin,
I am trying to unlock a few technical points, name ley to get comfortable with data persistence.

I have played a bit the Bakery starter kit.

One thing I need to be able to manage are hierachical structures.

So I am populating a dummy tree structure and then from the UI, I make sure I can :

  • browse the tree (works)
  • search the tree (works)
  • delete nodes (does not work as expected)
  • add nodes (does not work as expected)

The issue that is happening is that, apparently, hibernate session does not get closed between service calls triggered by user interaction on the UI.
As a result I get exceptions like:

[font=Courier New]
[i]
org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.semiosys.qse.backend.data.entity.Noeud]
with identifier [311]
: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.semiosys.qse.backend.data.entity.Noeud#311]

[/i]
[/font]

a) Strangely enough, if I select a node and ask to delete it, only its children nodes gets deleted (but the UI is not refreshed, only the database gets updated).
b) If I delete a node, and then if I delete one of his brother, I will get the exception saying their common parent was not save. But my code explicitly call save and flush.

Does it means the transaction never gets committed ???
! Would it be a Spring related issue ?

I will now expose the source code.
Hopefully somebody can help me on this, since I already spent quite a lot of time on this!

Thank you very much,
Elie

[font=Courier New] /* The node tree element for implementing hierarchical structure ('noeud'=node in french)*/ @SuppressWarnings("serial") @Entity @Indexed @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Noeud extends AbstractEntity {
public Noeud(TypeNoeud type, String label, Noeud parent) {
    this.type=type;
    this.label=label;
    this.parent=parent;
}

@Enumerated
private TypeNoeud type;

@Field(index=Index.YES) @Basic
private String label;    

@OneToMany( cascade= {CascadeType.ALL}, fetch=FetchType.EAGER , orphanRemoval=true)
@JoinColumn(name = "parent_id") 
@OrderColumn(name = "position")
private List<Noeud> children = new LinkedList<Noeud>(); 


@ManyToOne( fetch=FetchType.EAGER , optional = true)
@JoinColumn(name = "parent_id", referencedColumnName = "id",insertable=false,updatable=false)
private Noeud parent;

public String getAbsoluteId() {
    return String.valueOf(type) + ":" + String.valueOf(getId());
}

public void addChild(Noeud n) { children.add(n); }
public void removeChild(Noeud n) { children.remove(n); }

@Override
public String toString() { return label + "("+getId()+")" ; }

}


@Transactional
public interface NoeudRepository extends JpaRepository<Noeud, Long> {
List findByType(TypeNoeud type);
List findByLabelLike(String substring);
@Query(“FROM Noeud n WHERE n.parent = ?1”)
List findChildrenOf(Noeud parent);
}
[/font]

[font=Courier New]

@Service
public class NoeudService {
@Cacheable(“noeuds”)
public List findByType(TypeNoeud type) {
return getRepository().findByType(type);
}

 public List<Noeud> findByLabelLike(String substring) {
     return getRepository().findByLabelLike("%"+substring+"%");
 }
 
@Cacheable(value="noeuds")
 public List<Noeud> findChildrenOf(Noeud parent) {
     return getRepository().findChildrenOf(parent);
 }
 
@Cacheable(value = "noeuds")
@Transactional
 public Noeud save(Noeud n) {
     return getRepository().save(n);
 }

@CachePut(value = "noeuds")
@Transactional
 public Noeud update(Noeud n) {
     return getRepository().save(n);
 }
 
@Cacheable(value = "noeuds")
@Transactional
 public Noeud saveAndFlush(Noeud n) {
     return getRepository().saveAndFlush(n);
 }
 
 @CacheEvict(value = "noeuds")
 @Transactional
 public void delete(Noeud noeud) {
      getRepository().delete(noeud);
 }
 
 @Transactional
 public void flush() {
     getRepository().flush();
 }
 
 public boolean hasChildren(Noeud item) {
        if (item.getType() == TypeNoeud.PARAGRAPHE) // leaf node for sure
            return false;
        else {
            return item.getChildren().size()>0;
        }
    }
 
 @CacheEvict(value= "noeuds",  allEntries = true)
 // version for cascading ALL (including REMOVE)
  public void [u]


removeItem

[/u](Noeud node) {
System.out.println("removeItem: node to be deleted = " + node.getType() + " " + node.getLabel());
if(node.getParent()==null) { // Must be a top level node
delete(node);
flush();
return;
}
// Cascade REMOVE
node.getParent().removeChild(node);
System.out.println(“SAVING PARENT NODE #” + node.getParent().getId());
saveAndFlush(node.getParent()); // never get flushed, but still children are cascaded removed in DB !
}

 @Transactional
 public Noeud createNode(Noeud parent, String label, boolean asAChild) {
        Noeud newnode = null; 
        
        if(asAChild && parent!=null)  {
            // Création comme un fils du parent
            newnode = new Noeud(parent.getType().getChildrenType(), label, parent);
            //save(newnode);
            parent.getChildren().add(newnode);
            save(parent); // this one only if CASCADE PERSIST
            // insertion au bon endroit
        } else  {
            // Création comme un frère du 'parent' (parent alors considéré comme brother)
            Noeud actualParent = parent.getParent();
            newnode = new Noeud(parent.getType(), label, actualParent);
            // save(newnode);
            
            int position = actualParent.getChildren().indexOf(parent);
            actualParent.getChildren().add(position+1, newnode);
            save(actualParent); // this one only if CASCADE PERSIST
        }
        flush();
        return newnode;
    }
    
 
 
 public List<Noeud> collectSubnodes(Noeud node, boolean withLeaves) {
        List<Noeud> queue = new ArrayList<Noeud>();
        collectRecursive(node, queue, withLeaves);
        return queue;
    }

 }

[/font]

[font=Courier New]

@SpringComponent
@SuppressWarnings(“serial”)
public class
SimpleTaxonomieDataProvider
extends
AbstractBackEndHierarchicalDataProvider
<Noeud, String> {

private final NoeudService noeudService;

@Autowired
public SimpleTaxonomieDataProvider(NoeudService noeudService) {
this.noeudService = noeudService;
}

@Override
public int getChildCount(HierarchicalQuery<Noeud, String> query) {
return (int) fetchChildren(query).count();
}

@Override
public boolean hasChildren(Noeud item) {
return noeudService.hasChildren(item);
}

public void
removeItem
(Noeud node) {
Noeud parent = node.getParent();
noeudService.removeItem(node);
if(parent!=null)
refreshItem(parent);
else
refreshAll();
}

public List collectSubnodes(Noeud node, boolean withLeaves) {
return noeudService.collectSubnodes(node, withLeaves);
}
/**
*

  • @param parent Noeud de référence. noeud parent ou noeud frère. Est null si c’est au niveau le plus haut (TYPEVEILLE)
  • @param label le libellé du nom
  • @param asAChild indique si on crée le noeud comme un fils du parent ou un frère du parent
  • @return
    */
    public Noeud createNode(Noeud parent, String label, boolean asAChild) {
    Noeud n = noeudService.createNode(parent, label, asAChild);
    refreshItem(parent);
    return n;
    }

public List search(String substring) {
return noeudService.findByLabelLike(substring);
}

public void toutRafraichir() {
refreshAll();
}
@Override
protected Stream fetchChildrenFromBackEnd(HierarchicalQuery<Noeud, String> query) {
Optional op = query.getParentOptional();

if (!op.isPresent()) // fetching all top level nodes (all these nodes have no parent by definition).
return noeudService.findByType(TypeNoeud.TYPEDEVEILLE).stream();
else
return noeudService.findChildrenOf(op.get()).stream();

}
public List getTopLevelNodes() {
return noeudService.findByType(TypeNoeud.TYPEDEVEILLE);
}
}
[/font]

[font=Courier New]

@SpringBootApplication(scanBasePackageClasses = { QSEUI.class, Application.class, UserService.class, NoeudService.class, SecurityConfig.class })
@EnableJpaRepositories(basePackageClasses = { OrderRepository.class, NoeudRepository.class })
@EntityScan(basePackageClasses = { User.class, Order.class, Noeud.class, LocalDateJpaConverter.class })
@EnableEventBus
@EnableScheduling
@EnableAsync
@EnableCaching
@EnableTransactionManagement // https://docs.spring.io/spring/docs/5.0.1.RELEASE/spring-framework-reference/data-access.html#spring-data-tier
public class Application extends SpringBootServletInitializer {

public static final String APP_URL = "/";
public static final String LOGIN_URL = "/login.html";
public static final String LOGOUT_URL = "/login.html?logout";
public static final String LOGIN_FAILURE_URL = "/login.html?error";
public static final String LOGIN_PROCESSING_URL = "/login";

public static void main(String args) {
    SpringApplication.run(Application.class, args);
}

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(Application.class);
}

}
[/font]

![37813.png|1164x558](upload://g0dH84gZ4dG4ABGSzjmPn0IhQzn.png)

Hello,

There are a lot of things going on here… Doing this kind of use case with JPA/Hibernate can be really tricky.

One thing that I noticed is when you’re performing a node delete

// Cascade REMOVE
          node.getParent().removeChild(node);
          System.out.println("SAVING PARENT NODE #" + node.getParent().getId()); 
          saveAndFlush(node.getParent());  // never get flushed, but still children are cascaded removed in DB 

the node.setParent(null) is missing. Maybe you could to it inside the
removeChild
method. Make sure you’re setting the node’s parent when you add a node too.

Probably your entityManager is transaction scoped, so you’ll have to deal with detached entities as well.

I bet all your problems are related to JPA/Hibernate. Consider using the cascade just for deleting and JPQL for the other transactions.

Hope it helps!