web-dev-qa-db-fra.com

Spring JpaRepositroy.save () ne semble pas lever d'exception sur les enregistrements en double

Je joue actuellement sur Spring boot 1.4.2 dans lequel j'ai intégré Spring-boot-starter-web et Spring-boot-starter-jpa.

Mon principal problème est que lorsque je sauvegarde une nouvelle entité, cela fonctionne bien (tout est cool).

Cependant, si j'enregistre une nouvelle entité produit avec le même identifiant (par exemple une entrée en double), elle ne déclenche pas d'exception. Je m'attendais à ConstrintViolationException ou quelque chose de similaire.

Étant donné la configuration suivante:

Application.Java

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

ProductRepository.Java

@Repository
public interface ProductRepository extends JpaRepository<Product, String> {}

JpaConfig.Java

@Configuration
@EnableJpaRepositories(basePackages = "com.verric.jpa.repository" )
@EntityScan(basePackageClasses ="com.verric.jpa")
@EnableTransactionManagement
public class JpaConfig {

    @Bean
    JpaTransactionManager transactionManager() {
        return new JpaTransactionManager();
    }
}

Remarque JpaConfig.Java et Application.Java sont dans le même package.

ProductController.Java

@RestController
@RequestMapping(path = "/product")
public class ProductController {

    @Autowired
    ProductRepository productRepository;

    @PostMapping("createProduct")
    public void handle(@RequestBody @Valid CreateProductRequest request) {
        Product product = new Product(request.getId(), request.getName(), request.getPrice(), request.isTaxable());
        try {
            productRepository.save(product);
        } catch (DataAccessException ex) {
            System.out.println(ex.getCause().getMessage());
        }
    }
}

et enfin Product.Java

@Entity(name = "product")
@Getter
@Setter
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Product {

    protected Product() { /* jpa constructor*/ }

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private Long price;

    @Column
    private Boolean taxable;
}

Les getter, setter et equalsHashcode .. sont lombok annotations.

Divers:

Chaussure de printemps: 1.4.2

ORM en veille prolongée: 5.2.2.FINAL

Ce problème se produit indépendamment de l'annotation du contrôleur avec ou sans @Transactional

La base de données sous-jacente montre clairement l'exception

2016-11-15 18:03:49 AEDT [40794-1] verric@stuff ERROR:  duplicate key value violates unique constraint "product_pkey"
2016-11-15 18:03:49 AEDT [40794-2] verric@stuff DETAIL:  Key (id)=(test001) already exists

Je sais que c'est mieux (plus courant) de casser les données d'accès aux données dans sa propre couche de service au lieu de les jeter dans le contrôleur

La sémantique du contrôleur n'est pas ReST

Ce que j'ai essayé:

Spring CrudRepository exceptions

J'ai essayé d'implémenter la réponse de cette question, malheureusement mon code ne frappe jamais l'exception DataAccesException

Spring JPA génère-t-il une erreur si la fonction de sauvegarde échoue?

Encore une réponse similaire à la question ci-dessus.

http://www.baeldung.com/spring-dataIntegrityviolationexception

J'ai essayé d'ajouter le bean à ma classe JPAconfig.Java qui est:

   @Bean
   public PersistenceExceptionTranslationPostProcessor exceptionTranslation(){
      return new PersistenceExceptionTranslationPostProcessor();
   }

Mais rien ne semblait arriver.

Désolé pour le long post, ty à l'avance

13
Verric

Je pense que vous connaissez CrudRepository.save() est utilisé à la fois pour l'insertion et la mise à jour. Si un identifiant est inexistant, il sera considéré comme un insert. S'il existe, il sera considéré comme une mise à jour. Vous pouvez obtenir une exception si vous envoyez l'identifiant comme nul.

Comme vous n'avez aucune autre annotation que @Id sur votre variable id, la génération de l'identifiant unique doit être gérée par votre code. Sinon, vous devez utiliser @GeneratedValue annotation.

5
shazin

Ma solution est beaucoup plus propre. Spring Data nous fournit déjà un bon moyen de définir comment une entité est considérée comme nouvelle. Cela peut facilement être fait en implémentant Persistable sur nos entités, comme documenté dans la référence .

Dans mon cas, tout comme les PO, les identifiants proviennent d'une source externe et ne peuvent pas être générés automatiquement. Ainsi, la logique par défaut utilisée par Spring Data pour considérer une entité comme nouvelle si l'ID est nul n'aurait pas fonctionné.

@Entity
public class MyEntity implements Persistable<UUID> {

    @Id
    private UUID id;

    @Transient
    private boolean update;

    @Override
    public UUID getId() {
        return this.id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public boolean isUpdate() {
        return this.update;
    }

    public void setUpdate(boolean update) {
        this.update = update;
    }

    @Override
    public boolean isNew() {
        return !this.update;
    }
}

Ici, j'ai fourni un mécanisme permettant à l'entité d'exprimer si elle se considère comme nouvelle ou non au moyen d'une autre propriété booléenne transitoire appelée update. Comme la valeur par défaut de update sera false, toutes les entités de ce type sont considérées comme nouvelles et entraîneront la levée d'un DataIntegrityViolationException lorsque vous tenterez d'appeler repository.save(entity) avec le même ID.

Si vous souhaitez effectuer une fusion, vous pouvez toujours définir la propriété update sur true avant de tenter une sauvegarde. Bien sûr, si votre cas d'utilisation ne vous oblige jamais à mettre à jour des entités, vous pouvez toujours renvoyer true à partir de la méthode isNew et vous débarrasser du champ update.

Les avantages de cette approche par rapport à la vérification si une entité avec le même ID existe déjà dans la base de données avant l'enregistrement sont nombreux:

  1. Évite un aller-retour supplémentaire dans la base de données
  2. Nous ne pouvons pas garantir qu'au moment où un thread a déterminé que cette entité n'existe pas et est sur le point de persister, un autre thread ne tente pas de faire de même et entraîne des données incohérentes.
  3. Meilleures performances grâce à 1 et évitant les mécanismes de verrouillage coûteux.
  4. Atomique
  5. Simple
15
adarshr

Pour s'appuyer sur la réponse de Shazins et clarifier. le CrudRepositroy.save () ou JpaRespository.saveAndFlush () les deux délégués à la méthode suivante

SimpleJpaRepository.Java

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Par conséquent, si un utilisateur essaie de créer une nouvelle entité qui se trouve avoir le même identifiant qu'une entité existante, les données Spring mettront simplement à jour cette entité.

Pour atteindre ce que je voulais à l'origine, la seule chose que j'ai pu trouver était de retomber uniquement sur JPA, c'est-à-dire

@Transactional
@PostMapping("/createProduct")
public Product createProduct(@RequestBody @Valid Product product) {
    try {
        entityManager.persist(product);
        entityManager.flush();
    }catch (RuntimeException ex) {
        System.err.println(ex.getCause().getMessage());
    }

    return product;
}

Ici, si nous essayons de persister et que l'entité new avec un id déjà existant dans la base de données, elle lèvera l'exception de violation de contrainte comme nous le voulions à l'origine.

0
Verric