Frage zu ManyToMany Enties

8u3631984

Bekanntes Mitglied
Hallo zusammen,
Ich habe folgende 2 Enties :

Java:
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "category")
@ToString
public class Category{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    @Getter
    @Column(unique = true)
    private String name;
}

Und :
Java:
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "document")
@ToString(callSuper = true)
public class Document {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @NonNull
    @Getter
    @Builder.Default
    @Enumerated(EnumType.STRING)
    private DocumentType type = DocumentType.IMAGE;

    @NonNull
    private String name;


    @NonNull
    @ManyToMany(fetch = FetchType.EAGER,cascade = {CascadeType.PERSIST})
    @Singular
    private List<Category> categories;

Hier erzeuget ich die 2 Entitäten :
Java:
            var category = Category.builder().name("2022").build();
            var category2 = Category.builder().name("2023").build();
            Document document = Document.builder().name("TestDokument")
                    .type(DocumentType.IMAGE)
                    .category(category)
                    .category(category2)
                    .build();
            repository.saveAndFlush(document);

            var document2 = Document.builder()
                    .name("TestDokument 2")
                    .type(DocumentType.IMAGE)
                    .category(category2).build();
            repository.saveAndFlush(document2);

Nun bekomme ich folgenden Fehler :
nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist:

Das Problem tritt nur auf wenn ich einen bereits gespeicherte Category verwende.
Ich habe mit den cascaden Typen herumgespielt aber leider das Problem noch nicht gelöst.

Eine Alternative ist : Einen Service zu schreiben, der prüft ob die Category bereits existiert. Aber ich dachte mir, dass es evtl. etwas einfacher geht
 

osion

Bekanntes Mitglied
Hallo

Antwort chatGPT:
Es sieht so aus, als hättest du ein Problem mit der Verwendung von "detached" Entitäten in deinem Code. Eine "detached" Entität ist ein Objekt, das von einer Persistence-Context-Sitzung getrennt ist, in der es ursprünglich geladen wurde. Wenn du versuchst, eine detached Entität zu persistieren (also in der Datenbank zu speichern), bekommst du diesen Fehler, weil Hibernate nicht weiß, wie es die Entität verfolgen und verwalten soll.

Um dieses Problem zu lösen, gibt es einige Möglichkeiten:

  1. Du könntest versuchen, die detached Entität zu "reattachen", indem du sie erneut lädst und dann speicherst. Dies kann entweder mit dem EntityManager oder dem Session-Objekt gemacht werden, je nachdem, welches JPA-Implementierung du verwendest.
  2. Du könntest versuchen, die detached Entität zu mergen, anstatt sie zu persistieren. Dies würde Hibernate erlauben, die Entität in die Persistence-Context-Sitzung zu integrieren, ohne dass du sie erneut laden musst.
  3. Du könntest versuchen, die detached Entität "transparent" zu machen, indem du die CascadeType.PERSIST auf die entsprechenden Beziehungen setzt. Dies würde Hibernate erlauben, die Entität automatisch zu persistieren, wenn sie sich in einer Persistence-Context-Sitzung befindet.
Ich hoffe, das hilft dir weiter! Wenn du weitere Fragen hast, zögere nicht, sie zu stellen.

Meine Antwort als Ergänzung:
Es ist nicht klar, wie dein Builder aussieht, aber ich gehe davon aus, dass du die Category nicht richtig lädst und Hibernate keinen Bezug mit der Datenbank herstellen kann. Du kannst, z. B. nicht einfach Schreiben id = 1 und Hibernate sagt, dass ist es Eintrag mit der id = 1 (das ist auch sehr problematisch wenn du was lädst und die id anpasst). Eigentlich solltest du eine vorhandene Category über das Repository nachladen. Hibernate hat dafür die Methoden findById etc., die in deinem Fall ein Optional zurückgeben würden.

Man darf nie vergessen das Hibernate ein Proxy zwischen, z. B. Spring und der Datenbank ist, d. h. es ist nicht nur ein Query generierer. Entweder sagt man explizit, dass man eine Session in die DB schreiben will oder Hibernate macht das, wenn es denkt, dass der passende Moment dafür ist, z. B. es muss was nachladen.
 

KonradN

Super-Moderator
Mitarbeiter
Ich finde es übrigens mässig sinnvoll ChatGPT antworten zu posten...
Nunja - ich finde es nicht uninteressant. Das ist ähnlich wie das Posten von Google Suchergebnissen. Und bei der Hilfe zur Selbsthilfe ist das eine sehr interessante Sache. Damit lässt sich durchaus auch mit weniger Erfahrung ein Problem auch selbst lösen.

Ich sehe das also nicht wirklich als unsinnig an.
 

osion

Bekanntes Mitglied
Da es erst beim zweiten Insert passiert, vermute ich, dir fehlt ein CascadeType.MERGE.

Ich finde es übrigens mässig sinnvoll ChatGPT antworten zu posten...
Es soll zeigen, wie die Antwort von chatGPT auf ein Problem im Vergleich zu einer menschlichen Antwort gewesen wäre (und es wird vermerkt). Gleichzeitig, wenn die Informationen von chatGPT hilfreich sind, warum auch nicht? Ich poste ja auch, wie Konrad sagt, einen Link zu einer Webseite mit weiteren Informationen.

Er schreibt ja selbst, dass er nicht prüft, ob die Daten in der Datenbank existieren, also wird er sie wohl nicht von der Datenbank laden und Hibernate kann keinen Bezug bilden, weil er vorhandene Daten einfügt. Ohne den Builder zu sehen, ist das nur eine Annahme.
 

8u3631984

Bekanntes Mitglied
Hallo

Antwort chatGPT:
Es sieht so aus, als hättest du ein Problem mit der Verwendung von "detached" Entitäten in deinem Code. Eine "detached" Entität ist ein Objekt, das von einer Persistence-Context-Sitzung getrennt ist, in der es ursprünglich geladen wurde. Wenn du versuchst, eine detached Entität zu persistieren (also in der Datenbank zu speichern), bekommst du diesen Fehler, weil Hibernate nicht weiß, wie es die Entität verfolgen und verwalten soll.

Um dieses Problem zu lösen, gibt es einige Möglichkeiten:

  1. Du könntest versuchen, die detached Entität zu "reattachen", indem du sie erneut lädst und dann speicherst. Dies kann entweder mit dem EntityManager oder dem Session-Objekt gemacht werden, je nachdem, welches JPA-Implementierung du verwendest.
  2. Du könntest versuchen, die detached Entität zu mergen, anstatt sie zu persistieren. Dies würde Hibernate erlauben, die Entität in die Persistence-Context-Sitzung zu integrieren, ohne dass du sie erneut laden musst.
  3. Du könntest versuchen, die detached Entität "transparent" zu machen, indem du die CascadeType.PERSIST auf die entsprechenden Beziehungen setzt. Dies würde Hibernate erlauben, die Entität automatisch zu persistieren, wenn sie sich in einer Persistence-Context-Sitzung befindet.
Ich hoffe, das hilft dir weiter! Wenn du weitere Fragen hast, zögere nicht, sie zu stellen.

Meine Antwort als Ergänzung:
Es ist nicht klar, wie dein Builder aussieht, aber ich gehe davon aus, dass du die Category nicht richtig lädst und Hibernate keinen Bezug mit der Datenbank herstellen kann. Du kannst, z. B. nicht einfach Schreiben id = 1 und Hibernate sagt, dass ist es Eintrag mit der id = 1 (das ist auch sehr problematisch wenn du was lädst und die id anpasst). Eigentlich solltest du eine vorhandene Category über das Repository nachladen. Hibernate hat dafür die Methoden findById etc., die in deinem Fall ein Optional zurückgeben würden.

Man darf nie vergessen das Hibernate ein Proxy zwischen, z. B. Spring und der Datenbank ist, d. h. es ist nicht nur ein Query generierer. Entweder sagt man explizit, dass man eine Session in die DB schreiben will oder Hibernate macht das, wenn es denkt, dass der passende Moment dafür ist, z. B. es muss was nachladen.
Danke für deine / eure sehr ausfürhliche Antwort.
Ich habe es jetzt so gelöst, dass ich einen eigenen Service und ein eigenes Repsotitory Interface für jedes Entity habe. Damit klappt es super, weil der Service vorher prüft ob ein etsprechendes Entity bereits vorhanden ist.

Bei der ganzen Sachen habe ich noch ein Verständnis Problem :
Wann ist es ratsam bzw notwendig ein Repsository Interface für jedes Entity zu verwenden. Denn auch wenn ich kein solches Interface habe. schafft es Spring Boot das Enitity zu persitieren
 

mihe7

Top Contributor
Wann ist es ratsam bzw notwendig ein Repsository Interface für jedes Entity zu verwenden.
Ein Repository ist ein Pattern aus dem Domain-Driven-Design und stellt eine Abstraktion einer (in der Regel persistenten) Menge von Entities dar, die den tatsächlichen Zugriff vor dem Nutzer des Repos verbirgt.

Im DDD gilt die Regel, dass nur auf solche Entities über ein Repository zugegriffen wird, die die Wurzel eines Aggregats darstellen. Zu den sonstigen Teilen des Aggregats soll dann über die Entity navigiert werden(*). Ein Aggregat bildet wiederum eine transaktionale Einheit.

Beispiel: Rechnung und Rechnungsposition bilden ein Aggregat, die Wurzel des Aggregats ist die Rechnung. Man benötigt also ein Repository, um auf eine Rechnung zuzugreifen, über diese erhält man dann Zugriff auf ihre Positionen.

Die Frage, ob man verschiedene Aggregate in ein und demselben Repository verwalten soll, würde ich regelmäßig verneinen: Dinge, die nicht zusammengehören, sollten getrennt behandelt werden (Stichwort: Kohäsion/Single Responsibility Principle).

(*) Im Domain Model geht es nicht um banale Abfragen, wie man sie für z. B. einen Datenexport benötigt. Solche Dinge wären separat zu betrachten.
 

8u3631984

Bekanntes Mitglied
ICh muss das Thema noch einmal aufmachen - da ich leider immer noch auf dem Schlauch stehe. Also erstmal danke für eure zahlreichen Antworten, sie haben bei etwas Licht ins Dunkle gebracht.
Also noch mal kurz mein Setup :

Category class :
Java:
@Builder(setterPrefix = "with")
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "category")
@ToString
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    @Getter
    @Column(unique = true)
    private String name;
}

Image Class :
Code:
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "image")
@ToString
public class Image implements Document {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NonNull
    @Getter
    private String hashValue;

    @NonNull
    @Getter
    private String path;

    @NonNull
    @Getter
    private DocumentType type;


    @NonNull
    @Getter
    @ManyToMany(cascade = CascadeType.MERGE)
    @JoinTable(
            name = "image_categories",
            joinColumns = {
                    @JoinColumn(name = "image_id")
            },
            inverseJoinColumns = {
                    @JoinColumn(name = "category_id")
            })
    @Singular
    private List<Category> categories;

Image repository class :
Code:
@Repository
public interface ImageRepository extends JpaRepository<Image, Long>{
}

Code:
    @Bean
    CommandLineRunner cmd(ImageRepository repository){
        return  args -> {
            var category = Category.builder().withName("test - category").build();

            var image = Image.builder().hashValue("1234").path("1234").type(DocumentType.UNKNOWN).category(category).build();
            image = repository.saveAndFlush(image);
            System.out.println("save image : " + image);


            // BEIM SPEICHERN VON IMAGE 2 KOMMT DER FEHLER
            var image2 = Image.builder().hashValue("2222").path("22222").type(DocumentType.UNKNOWN).category(category).build();
            image2 = repository.saveAndFlush(image2);
            System.out.println("save image : " + image2);

        };
    }

Wenn ich nur ein Image Objekt anlege (mit einer Category) wird beides richtig gepseichert. Nur wenn ich wenn ich ein weiteres Images Objekt mit einer bereits gespeicherten Categegory anlege bekomme ich den Fehler.
Ich verstehe weiterhin nicht was an der Cascade falsh ist. Hat jemand noch einen Hinweis
 

8u3631984

Bekanntes Mitglied
Aber ich habe doch CascadeType.ALL beinhaltete der nicht auch den PERSIST Type.

Falls es relevant ist . HIer ist meine application.yml
Java:
server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:file:./document
    username: document
    password: document
    driverClassName: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create-drop
      generate-ddl: true
    dialect: org.hibernate.dialect.H2Dialect

  h2:
    console:
      enabled: true
      path: /h2-console

  show-sql: true
 

mihe7

Top Contributor
Ja, bei CascadeType.ALL bekommst Du zunächst keinen Fehler. Danach hast Du das Problem, dass persist auf einer Entity aufgerufen, die mit einer detached-Entity in Beziehung steht. Das funktioniert so nicht, weil die detached-Entity nicht gemerged wird (s. dazu den Link auf Baeldung aus #9).

Du möchtest:
a) eine neue Image-Entity mit
b) einer bestehenden Category

Dazu müssen sich in der Image-Entity die Category-Entities im Zustand managed befinden. Ansonsten erhältst Du entweder neue Kategorien falls diese transient sind oder aber einen Fehler, falls diese detached sind.

Wie Du es genau mit Spring Data JPA hinbekommst bzw. was da die best practice ist, kann ich Dir nicht 100-%ig sagen, da ich auf Java EE unterwegs bin. Vermutlich brauchst Du eine mit @Transactional annotierte Methode, um das alles in einer Transaktion ablaufen zu lassen, dann dürfte category nicht detached werden. Da wissen evtl. @LimDul oder @KonradN oder andere mehr.
 

LimDul

Top Contributor
Aus dem Bauch heraus, würde ich die Category zwischendurch neuladen.

Code:
    @Bean
    CommandLineRunner cmd(ImageRepository repository){
        return  args -> {
            var category = Category.builder().withName("test - category").build(); // Nicht persistiert / Managed

            var image = Image.builder().hashValue("1234").path("1234").type(DocumentType.UNKNOWN).category(category).build(); // Hier wird die Category das erste mal persistiert, aber das category Objekt selber ist immer noch das alte. 
            image = repository.saveAndFlush(image);
            System.out.println("save image : " + image);


            // BEIM SPEICHERN VON IMAGE 2 KOMMT DER FEHLER
            var image2 = Image.builder().hashValue("2222").path("22222").type(DocumentType.UNKNOWN).category(category).build(); // Hier wird versucht das gleiche categoriy objekt noch mal zu speichern, das geht schief
            image2 = repository.saveAndFlush(image2);
            System.out.println("save image : " + image2);

        };
    }
Es gibt einen Grund, war die save Methoden dir das gespeicherte Objekt zurückgeben, damit es eben unter Kontrolle der persistence steht.

Der sauberste Weg wäre meines Erachtens, erst die Category separat zu speichern und die dann zu verwenden. Alternativ für das speichern von image2 die category zu nehmen, die im image häng Objekt, dass aus saveAndFlush zurückkam hängt.
 

Neue Themen


Oben