web-dev-qa-db-fra.com

Comment enregistrer le parent et l'enfant en une seule fois (JPA et Hibernate)

Je commence à vous montrer mon scénario.

Ceci est mon objet parent:

@Entity
@Table(name="cart")
public class Cart implements Serializable{  

    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Id
    @Column(name="id")
    private Integer id; 

    @OneToMany(mappedBy="cart", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<CartItem> cartItems; 

    ...
}

Voici mon objet enfant:

@Entity
@Table(name="cart_item")
public class CartItem implements Serializable{  

    @GeneratedValue(strategy=GenerationType.IDENTITY)   
    @Id
    @Column(name="id")
    private Integer id;     

    @ManyToOne
    @JoinColumn(name="cart_id", nullable=false)
    private Cart cart;

    ...
}

Comme vous pouvez le voir en regardant la base de données, dans le tableau cart_item (objet enfant) le champ cart_id a une clé étrangère dans le champ id de la table cart (objet parent).

enter image description here

Voici comment je sauvegarde l'objet:

1) il y a un restController qui lit un objet JSON:

@RestController
@RequestMapping(value = "rest/cart")
public class CartRestController {

    @Autowired
    private CartService cartService;    

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(value = HttpStatus.CREATED)
    public void create(@RequestBody CartDto cartDto) {
        cartService.create(cartDto);
    }
}

2) Ceci est le CartService , c'est juste une interface :

public interface CartService {  
    void create(CartDto cartDto); 
}

Voici l'implémentation de CartService:

import org.springframework.transaction.annotation.Transactional;

    @Service
    @Transactional
    public class CartServiceImpl implements CartService {   
        @Autowired
        private CartDao cartDao;

        @Override
        public void create(CartDto cartDto) {
            cartDao.create(cartDto);
        }
    }

CartDao est juste une autre interface, je vous montre seulement son implémentation:

@Repository
public class CartDaoImpl implements CartDao {

    @Autowired 
    private SessionFactory sessionFactory;

    // in this method I save the parent and its children
    @Override
    public void create(CartDto cartDto) {       

        Cart cart = new Cart(); 

        List<CartItem> cartItems = new ArrayList<>();                   

        cartDto.getCartItems().stream().forEach(cartItemDto ->{     
            //here I fill the CartItem objects;     
            CartItem cartItem = new CartItem();         
            ... 
            cartItem.setCart(cart);
            cartItems.add(cartItem);                
        });
        cart.setCartItems(cartItems);

        sessionFactory.getCurrentSession().save(cart);                  
    }
}

Lorsque j'essaie d'enregistrer un nouveau panier et ses cart_item s j'obtiens cette erreur:

SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/webstore] threw 
exception [Request processing failed; nested exception is 
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Object of 
class     
[com.depasmatte.webstore.domain.CartItem] with identifier [7]: optimistic locking failed; 
nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by 
another transaction (or unsaved-value mapping was incorrect) : 
[com.depasmatte.webstore.domain.CartItem#7]] with root cause
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction
 (or unsaved-value mapping was incorrect) : [com.depasmatte.webstore.domain.CartItem#7]

Je suppose que l'erreur dépend du fait que lorsque Hibernate essayez d'enregistrer le a cart_item , l'ID du panier n'existe pas encore !

Quelle est la bonne façon d'enregistrer un objet parent et son générateur sur le coup? Je vous remercie

5
MDP

Voici la liste des règles que vous devez suivre, afin de pouvoir stocker une entité parent avec ses enfants en une seule fois:

  • le type de cascade PERSIST doit être activé (CascadeType.ALL convient également)
  • une relation bidirectionnelle doit être définie correctement sur des deux côtés . Par exemple. parent contient tous les enfants dans son champ de collection et chaque enfant a une référence à son parent.
  • la manipulation des données est effectuée dans le cadre d'une transaction. AUCUN MODE AUTOCOMMIT IS ADMIS.
  • seule l'entité parent doit être enregistrée manuellement (les enfants seront enregistrés automatiquement en raison du mode cascade)

Problèmes de mappage:

  • supprimer @Column(name="id") des deux entités
  • rendre le setter pour cartItems privé . Depuis Hibernate utilise sa propre implémentation de List, et vous ne devriez jamais le changer directement via setter
  • initialisez votre liste private List<CartItem> cartItems = new ArrayList<>();
  • utilisez @ManyToOne(optional = false) au lieu de nullable = false dans le @JoinColumn
  • préférez fetch = FetchType.LAZY pour les collections
  • il est préférable d'utiliser la méthode d'assistance pour établir des relations. Par exemple. la classe Cart doit avoir une méthode:

    public void addCartItem(CartItem item){
        cartItems.add(item);
        item.setCart(this);
    }
    

Problèmes de conception:

  • il n'est pas bon de passer des DTO à la couche DAO. Il est préférable de faire la conversion entre les DTO et les entités même au-dessus de la couche de service.
  • il vaut mieux éviter ce genre de passe-partout comme la méthode save avec référentiels Spring Data JPA
2
Taras

Une chose importante est clairement absente de la discussion qui est très importante pour cette question qui est à qui appartient cette relation. vous mettez mappedBy dans l'entité parent, ce qui signifie que le propriétaire de cette relation va à l'entité enfant, il doit remplir cette relation en définissant explicitement la propriété sinon cette relation ne sera pas construite. Mettez l'annotation JoinColumn au-dessus de Parent, cela garantira que le propriétaire de la relation est parent, il établira cette relation lorsque l'entité parent sera enregistrée automatiquement

1
sqzaman

Avez-vous vérifié ce post? La ligne a été mise à jour ou supprimée par une autre transaction (ou le mappage des valeurs non enregistrées était incorrect)

Vous pouvez trouver une réponse appropriée dans celle-ci, je pense que votre problème vient de votre getCurrentSession, même si vous utilisez des sessions car hibernate n'est pas thread-safe, une session est toujours un objet léger et non threadsafe. Vous devriez creuser quelque chose d'ici.

En fait, lorsqu'un thread/session enregistre un objet dans la base de données, si un autre essaie la même opération, il déclenchera ce type d'erreur car les identifiants existent déjà, donc les opérations sont impossibles.

À votre santé!

1
Palencar

Assurez-vous que votre méthode est transactionnelle. vous pouvez rendre la méthode transactionnelle en utilisant @Transactional annotation au-dessus de la signature de la méthode.

1
Alien

Je sais que cela ne concerne pas directement la question, car les services sont utilisés, mais Google m'a amené ici quand j'ai eu un problème similaire. Dans mon cas, j'utilisais les référentiels Spring JPA. Assurez-vous d'annoter l'interface du référentiel avec @ org.springframework.transaction.annotation.Transactional

0
gmode