web-dev-qa-db-fra.com

Supprimer ne pas travailler avec JpaRepository

J'ai une application Spring 4 où j'essaie de supprimer une instance d'une entité de ma base de données. J'ai l'entité suivante:

@Entity
public class Token implements Serializable {

    @Id
    @SequenceGenerator(name = "seqToken", sequenceName = "SEQ_TOKEN", initialValue = 500, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqToken")
    @Column(name = "TOKEN_ID", nullable = false, precision = 19, scale = 0)
    private Long id;

    @NotNull
    @Column(name = "VALUE", unique = true)
    private String value;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "USER_ACCOUNT_ID", nullable = false)
    private UserAccount userAccount;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "EXPIRES", length = 11)
    private Date expires;

    ...
    // getters and setters omitted to keep it simple
}

J'ai une interface JpaRepository définie:

public interface TokenRepository extends JpaRepository<Token, Long> {

    Token findByValue(@Param("value") String value);

}

J'ai une configuration de test unitaire qui fonctionne avec une base de données en mémoire (H2) et je pré-remplit la base de données avec deux jetons:

@Test
public void testDeleteToken() {
    assertThat(tokenRepository.findAll().size(), is(2));
    Token deleted = tokenRepository.findOne(1L);
    tokenRepository.delete(deleted);
    tokenRepository.flush();
    assertThat(tokenRepository.findAll().size(), is(1));
}

La première assertion passe, la seconde échoue. J'ai essayé un autre test qui modifie la valeur du jeton et l'enregistre dans la base de données. Il fonctionne effectivement. Je ne sais donc pas pourquoi la suppression ne fonctionne pas. Il ne jette pas non plus d'exception, mais ne le conserve pas dans la base de données. Cela ne fonctionne pas non plus avec ma base de données Oracle.


Modifier

Toujours avoir ce problème. J'ai réussi à faire en sorte que la suppression persiste dans la base de données en l'ajoutant à mon interface TokenRepository:

@Modifying
@Query("delete from Token t where t.id = ?1")
void delete(Long entityId);

Cependant, ce n'est pas une solution idéale. Des idées sur ce que je dois faire pour que cela fonctionne sans cette méthode supplémentaire?

27
Twisty McGee

J'ai eu le même problème

Peut-être que votre entité UserAccount a un @OneToMany avec Cascade sur un attribut.

Je viens de retirer la cascade, ce qui pourrait persister lors de la suppression ...

19
Davi Arimateia

Vous devez ajouter une fonction PreRemove dans la classe dans laquelle vous avez plusieurs objets comme attributs, par exemple dans Education Class, qui ont une relation avec UserProfileEducation.Java

private Set<UserProfile> userProfiles = new HashSet<UserProfile>(0);

@ManyToMany(fetch = FetchType.EAGER, mappedBy = "educations")
public Set<UserProfile> getUserProfiles() {
    return this.userProfiles;
}

@PreRemove
private void removeEducationFromUsersProfile() {
    for (UsersProfile u : usersProfiles) {
        u.getEducationses().remove(this);
    }
}
6
Taimur

Il est fort probable qu'un tel comportement se produise lorsque vous avez une relation bidirectionnelle et que vous ne synchronisez pas les deux côtés alors que les deux parents sont persistants (attachés à la session en cours). 

C'est délicat et je vais expliquer cela avec l'exemple suivant.

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Long id;

    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "parent")
    private Set<Child> children = new HashSet<>(0);

    public void setChildren(Set<Child> children) {
        this.children = children;
        this.children.forEach(child -> child.setParent(this));
    }
}
@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    public void setParent(Parent parent) {
        this.parent = parent;
    }
}

Écrivons un test (un test transactionnel)

public class ParentTest extends IntegrationTestSpec {

@Autowired
private ParentRepository parentRepository;

@Autowired
private ChildRepository childRepository;

@Autowired
private ParentFixture parentFixture;

@Test
public void test() {
    Parent parent = new Parent();
    Child child = new Child();

    parent.setChildren(Set.of(child));
    parentRepository.save(parent);

    Child fetchedChild = childRepository.findAll().get(0);
    childRepository.delete(fetchedChild);

    assertEquals(1, parentRepository.count());
    assertEquals(0, childRepository.count()); // FAILS!!! childRepostitory.counts() returns 1
}
}

Test assez simple non? Nous créons un parent et un enfant, sauvegardons-le dans la base de données, puis récupérons un enfant de la base de données, le supprimons et nous nous assurons enfin que tout fonctionne comme prévu. Et ce n'est pas.

La suppression ici n'a pas fonctionné car nous n'avons pas synchronisé l'autre partie de la relation qui est PERSISTED IN CURRENT SESSION. Si le parent n’était pas associé à la session en cours, notre test réussirait, c’est-à-dire.

@Component
public class ParentFixture {
...
 @Transactional(propagation = Propagation.REQUIRES_NEW)
 public void thereIsParentWithChildren() {
     Parent parent = new Parent();
     Child child = new Child();
     parent.setChildren(Set.of(child));

     parentRepository.save(parent);
 }
} 

et

@Test
public void test() {
    parentFixture.thereIsParentWithChildren(); // we're saving Child and Parent in seperate transaction

    Child fetchedChild = childRepository.findAll().get(0);
    childRepository.delete(fetchedChild);

    assertEquals(1, parentRepository.count());
    assertEquals(0, childRepository.count()); // WORKS!
}

Bien sûr, cela ne fait que prouver mon point de vue et explique le comportement que l’OP a dû affronter. La bonne façon de procéder est évidemment de garder en phase les deux parties de la relation, ce qui signifie:

class Parent {
    ...
     public void dismissChild(Child child) {
         this.children.remove(child);
     }

     public void dismissChildren() {
        this.children.forEach(child -> child.dismissParent()); // SYNCHRONIZING THE OTHER SIDE OF RELATIONSHIP 
        this.children.clear();
     }

}

class Child {
 ...
     public void dismissParent() {
         this.parent.dismissChild(this); //SYNCHRONIZING THE OTHER SIDE OF RELATIONSHIP
         this.parent = null;
     }
 }

Il est évident que @PreRemove pourrait être utilisé ici.

4
pzeszko

Je viens de traverser cela aussi. Dans mon cas, je devais faire en sorte que la table enfant ait un champ de clé étrangère nullable, puis supprimer le parent de la relation en définissant null, puis en appelant save, delete et flush. 

Je n'ai vu aucune suppression dans le journal ni aucune exception avant de le faire. 

3
Lucas Holt

Si vous utilisez une version plus récente de Spring Data, vous pouvez utiliser la syntaxe deleteBy ... afin de pouvoir supprimer l'une de vos annotations: P

ensuite, le comportement est déjà traité par un ticket Jira: https://jira.spring.io/browse/DATAJPA-727

2
Eruvanos

Votre valeur initiale pour id est 500. Cela signifie que votre identifiant commence par 500

@SequenceGenerator(name = "seqToken", sequenceName = "SEQ_TOKEN",
initialValue = 500, allocationSize = 1)

Et vous sélectionnez un article avec l'identifiant 1 ici

 Token deleted = tokenRepository.findOne(1L);

Alors vérifiez votre base de données pour préciser que

1
Bitman

Une solution consiste à utiliser cascade = CascadeType.ALL comme ceci dans votre service userAccount:

@OneToMany(cascade = CascadeType.ALL)
private List<Token> tokens;

Ensuite, faites quelque chose comme ce qui suit (ou une logique similaire)

@Transactional
public void deleteUserToken(Token token){
    userAccount.getTokens().remove(token);
}

Notez l'annotation @Transactional. Cela permettra à Spring (Hibernate) de savoir si vous souhaitez persister, fusionner ou quoi que ce soit que vous fassiez dans la méthode. Si je comprends bien, l'exemple ci-dessus devrait fonctionne comme si vous n'aviez pas défini CascadeType et appelez JPARepository.delete(token).

0
venge