Löschen funktioniert nicht mit JpaRepository

72

Ich habe eine Spring 4-App, in der ich versuche, eine Instanz einer Entität aus meiner Datenbank zu löschen. Ich habe die folgende Entität:

@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
}

Ich habe eine JpaRepository-Schnittstelle definiert:

public interface TokenRepository extends JpaRepository<Token, Long> {

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

}

Ich habe ein Unit-Test-Setup, das mit einer In-Memory-Datenbank (H2) funktioniert, und fülle die Datenbank mit zwei Token vor:

@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));
}

Die erste Behauptung ist erfolgreich, die zweite fehlgeschlagen. Ich habe einen anderen Test versucht, der den Token-Wert ändert und in der Datenbank speichert, und er funktioniert tatsächlich. Daher bin ich mir nicht sicher, warum das Löschen nicht funktioniert. Es werden auch keine Ausnahmen ausgelöst, sondern nur nicht in der Datenbank gespeichert. Es funktioniert auch nicht gegen meine Oracle-Datenbank.


Bearbeiten

Ich habe immer noch dieses Problem. Ich konnte das Löschen in der Datenbank beibehalten, indem ich dies zu meiner TokenRepository-Oberfläche hinzufügte:

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

Dies ist jedoch keine ideale Lösung. Irgendwelche Ideen, was ich tun muss, damit es ohne diese zusätzliche Methode funktioniert?

Twisty McGee
quelle
2
Haben Sie jemals eine Lösung dafür gefunden? das gleiche Problem auch in der neuesten Frühlingsversion. Laut MySQL-Protokolltabelle wird nie eine Löschanweisung ausgegeben, keine Fehlermeldung verfügbar.
Phil294
Unglücklicherweise nicht. Ich musste es hier nicht mehr tun und die anderen Orte, an denen ich es tun musste, schienen gut zu funktionieren. Ich weiß nicht, was an diesem Fall so besonders war.
Twisty McGee
Sie sollten in Betracht ziehen, diese @ Modifying-Löschanweisung als Antwort zu veröffentlichen. Nur so konnte ich es auch lösen. Ich habe die Frage positiv bewertet, aber ich habe auch diese Antwort positiv bewertet.
Taugenichts
Das Überschreiben der Löschmethode (n) hat es auch für mich gelöst, nicht sicher, warum dies passiert ist (Upgrade von Spring Boot 2.1.5 auf 2.1.6)
Brunaldo

Antworten:

46

Höchstwahrscheinlich tritt ein solches Verhalten auf, wenn Sie eine bidirektionale Beziehung haben und nicht beide Seiten synchronisieren, während sowohl Eltern als auch Kind bestehen bleiben (an die aktuelle Sitzung angehängt).

Das ist schwierig und ich werde dies anhand des folgenden Beispiels erklären.

@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;
    }
}

Schreiben wir einen Test (übrigens einen Transaktions-Test)

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
    }
}

Ziemlich einfacher Test, oder? Wir erstellen Eltern und Kinder, speichern sie in der Datenbank, holen dann ein Kind aus der Datenbank, entfernen es und stellen schließlich sicher, dass alles wie erwartet funktioniert. Und das ist es nicht.

Das Löschen hier hat nicht funktioniert, weil wir den anderen Teil der Beziehung, der in der aktuellen Sitzung bestehen bleibt, nicht synchronisiert haben. Wenn Eltern nicht mit der aktuellen Sitzung verbunden wären, würde unser Test bestehen, dh

@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);
    }
} 

und

@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!
}

Natürlich beweist es nur meinen Standpunkt und erklärt das Verhalten, mit dem OP konfrontiert ist. Der richtige Weg ist offensichtlich, beide Teile der Beziehung synchron zu halten, was bedeutet:

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;
    }
}

Offensichtlich @PreRemovekönnte hier verwendet werden.

pzeszko
quelle
Wirklich gute Arbeit bei der Erklärung hier. Ich hatte das gleiche Problem und die gleiche offensichtliche "Problemumgehung" - Löschen durch eine benutzerdefinierte Abfrage, aber ich wollte es nicht verwenden, da es offensichtlich ein Hack war. Der @ PreRemove-Hook hat hier wirklich Wunder gewirkt. Vielen Dank!
Ognjen Mišić
Froh, dass ich helfen konnte!
Pzeszko
38

Ich hatte das gleiche Problem

Möglicherweise verfügt Ihre UserAccount-Entität über ein @ OneToMany mit Cascade für ein Attribut.

Ich habe gerade die Kaskade entfernt, als sie beim Löschen bestehen bleiben könnte ...

Davi Arimateia
quelle
1
Das hat auch bei mir funktioniert. Insbesondere habe ich herumgespielt und konnte diese Kaskadentypen beibehalten: [CascadeType.PERSIST, CascadeType.REFRESH] und trotzdem das Löschen über das untergeordnete Repository ausführen. Was für mich keinen Sinn machte, war, dass ich die Eigentümerentität das Kind über "mappedBy" auf dem Elternteil gemacht habe
GameSalutes
14
Diese Lösung scheint zu funktionieren, gibt aber keine Erklärung.
Ivan Matavulj
16

Sie müssen die PreRemove-Funktion in der Klasse hinzufügen, in der Sie viele Objekte als Attribut haben, z. B. in der Bildungsklasse, die eine Beziehung zu UserProfile Education.java haben

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);
    }
}
Taimur
quelle
6

Eine Möglichkeit besteht darin, cascade = CascadeType.ALLFolgendes in Ihrem userAccount-Dienst zu verwenden:

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

Dann machen Sie etwas wie das Folgende (oder eine ähnliche Logik)

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

Beachten Sie die @TransactionalAnmerkung. Auf diese Weise kann Spring (Ruhezustand) feststellen, ob Sie entweder fortfahren, zusammenführen oder was auch immer Sie in der Methode tun möchten. AFAIK Das obige Beispiel sollte so funktionieren, als ob Sie keinen CascadeType festgelegt hätten, und aufrufen JPARepository.delete(token).

Rache
quelle
4

Dies ist für alle, die von Google erfahren, warum ihre Löschmethode in Spring Boot / Hibernate nicht funktioniert , unabhängig davon, ob sie aus dem JpaRepository / CrudRepository deleteoder aus einem benutzerdefinierten Repository-Aufruf verwendet wirdsession.delete(entity) oderentityManager.remove(entity) .

Ich habe ein Upgrade von Spring Boot 1.5 auf Version 2.2.6 (und Hibernate 5.4.13) durchgeführt und eine benutzerdefinierte Konfiguration für Folgendes verwendet transactionManager:

@Bean
public HibernateTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    return new HibernateTransactionManager(entityManagerFactory.unwrap(SessionFactory.class));
}

Und ich habe es geschafft, es zu lösen, indem ich die obige @EnableTransactionManagementbenutzerdefinierte transactionManagerBean-Definition verwendet und gelöscht habe .

Wenn Sie immer noch eine Art benutzerdefinierten Transaktionsmanager verwenden müssen, kann das Ändern der Bean-Definition in den folgenden Code auch funktionieren:

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
}

Denken Sie abschließend daran, die automatische Konfiguration von Spring Boot zu aktivieren, damit die entityManagerFactoryBean automatisch erstellt werden kann, und entfernen sessionFactorySie auch alle Beans, wenn Sie ein Upgrade durchführen entityManager(andernfalls führt Spring Boot die automatische Konfiguration nicht ordnungsgemäß durch). Stellen Sie schließlich sicher, dass Ihre Methoden funktionieren, @Transactionalwenn Sie Transaktionen nicht manuell bearbeiten.

Toribio
quelle
3

Ich habe das auch gerade durchgemacht. In meinem Fall musste ich dafür sorgen, dass die untergeordnete Tabelle ein nullbares Fremdschlüsselfeld hat, und dann das übergeordnete Element aus der Beziehung entfernen, indem ich null setzte und dann save and delete und flush aufrief.

Ich habe vorher kein Löschen im Protokoll oder eine Ausnahme gesehen.

Lucas Holt
quelle
Ich hatte das gleiche Problem und bin auf diesen Thread gestoßen. Ich weiß nicht, ob ich dies als Lösung oder Problemumgehung bezeichnen würde, aber so werden die untergeordneten Objekte gelöscht.
Brian Kates
2

Wenn Sie eine neuere Version von Spring Data verwenden, können Sie die deleteBy-Syntax verwenden, damit Sie eine Ihrer Anmerkungen entfernen können: P.

Das nächste ist, dass das Verhalten bereits durch ein Jira-Ticket erfasst wird: https://jira.spring.io/browse/DATAJPA-727

Eruvanos
quelle
1

Ihr Anfangswert für ID ist 500. Das bedeutet, dass Ihre ID mit 500 beginnt

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

Und Sie wählen hier einen Artikel mit der ID 1 aus

 Token deleted = tokenRepository.findOne(1L);

Überprüfen Sie also Ihre Datenbank, um dies zu klären

Bitman
quelle
Ich glaube nicht, dass das hier das Problem ist, denn wenn ich den Test mit Testdaten setze, gebe ich die ID als 1 an.
Twisty McGee
1

Ich habe das gleiche Problem, Test ist in Ordnung, aber in der Datenbankzeile wird nicht gelöscht.

Haben Sie der Methode die Annotation @Transactional hinzugefügt? Für mich funktioniert diese Änderung

Federico Quaglia
quelle
1
@Transactional
int deleteAuthorByName(String name);

Sie sollten @Transactional in Repository schreiben, um JpaRepository zu erweitern

M Shafaghi
quelle
0

In meinem Fall war CASCADE.PERSIST, ich habe für CASCADE.ALL geändert und die Änderung durch die Kaskade vorgenommen (Ändern des Vaterobjekts).

Schein Fiorin
quelle
0

CascadeType.PERSIST und orphanRemoval = true funktionieren nicht zusammen.

Dejan
quelle