JPA OneToMany Id mezők nem frissülnek merge után

Fórumok

Van egy OneToMany reláció, pl. egy Post-Comments tankönyvi lépda:

public class Post implements Serializable {
    private int id;
    private List<Comment> comments;



    @Id
    @Column(name = "ID")
    @TableGenerator(...)
    @GeneratedValue(...)
    public int getId() {
        return id;
    }

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    public List<Comment> getComments() {
        return comments;
    }
}
public class Comment implements Serializable {
    private int id;
    private int idPost;
    private Post post;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public int getId() {
        return id;
    }

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

    @Column(name = "ID_POST", insertable = false, updatable = false)
    public int getIdPost() {
        return idPost;
    }

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "ID_POST", referencedColumnName = "ID")
    public Post getPost() {
        return post;
    }
}

Az adatbázis kezelő alatta MySql és a Comment tábla ID mezője AUTOINCREMET.

Abban az esetben ha entityManager.merge(post) utasítással mentek egy Post entitást melynek a comments listájához adok egy Comment entitást, akkor szépen elment, viszont a Comment entitás ID mezője, melynek a MySql ad értéket az autoinc miatt nem frissül. Csak ha egy entityManager.refres() vagy bármilyen más módon újra lekérem. Mit kell még tenni hogy a merge hatására frissítse az entitást úgy hogy az id értékét visszakapja anélkül, hogy újra lekérdezném?

A JPA provider EclipseLink.

Hozzászólások

Ez most egy programozoi forum? Ebben mi az unix?

Egyebkent lehet nem kezeli ez az ORM a db altal adott dolgokat: uj ID, trigger altali modositasok, alapertekek amikrol az ORM nem tud. Szerintem kulon kell bekapcsolni valahol hogy mely entitasok sorainal legyen ReRead az DML-ek utan, alapbol nem olvasna vissza ha en irtam volna. 

A #merge() visszaadja a PersistenceContext-ben lévő entitást, így nem kapod meg az id tartalmát?

entity = entityManager.merge(entity);
System.out.println(entity.getId());

Ahogy az előttem szóló is írta, a merge visszatérési értéke fogja tartalmazni a perzisztens entitást, azaz annak lesz id-je.

Nem tudom, ki hogy van vele, de valahogy mindig is utáltam a JPA-t. A Hibernate API-ja sokkal érthetőbb és nekem jobban kézre áll.

Más: a cascade-ra figyelj, nem biztos, hogy jó ötlet mindkét irányból Cascade.ALL-t használni. Tipikusan a child oldalról nem szokás, mivel a child törlése nem szabadna, hogy a parent-et is törölje. Én általában csak legfeljebb az egyik irányból szoktam cascade-ot engedélyezni, még @OneToOne vagy @ManyToMany esetben is.

Más2: teljesen felesleges a Comment-be a getIdPost property. A getPost().getId() is jó (ugyanez JPQL-ben is)! Ha lazy fetch van, akkor sem fogja a teljes Posta objektumot felolvasni, mert itt amúgy is csak egy lazy proxy van, ami már eleve rendelkezik az ID-vel. Ha meg nincs lazy fetch, akkor pláne felesleges.

A merge visszatérési értékének nincs id-je. Az adatbázisba szépen bekerül a szülő és a gyerek rekord is, de a merge visszatérési értékében nincs benne az id (értéke 0). De ha utána refresh()-el vagy ha lekérdezem bármilyen módon find(), vagy query akkor már megkapom az id értékét amit a MySql kiosztott neki.

Én amúgy megnézném a naplófájlokban hogy egyáltalán milyen SQL-eket ad ki az EclipseLink. Ha a merge-kor tényleg nem történik select, akkor bizony szükséged lesz arra refreshre, mert lekérdezés nélkül nem fogja tudni az ORM, hogy mi lett az azonosító. A persistence.xml-ben ezt kell beállítani, hogy az EclipseLink kinaplózza az SQL utasításokat:

<property name="eclipselink.logging.level.sql" value="FINE"/>
Szerkesztve: 2021. 11. 05., p – 09:07

Írsz komplett példát arra, ahogy nálad nem működik? Ezek az entitások, de a lényeg nem az entitásokban van, hanem különösen a tranzakciós határoknál.

CREATE TABLE POST
(
    ID  INTEGER AUTO_INCREMENT,
    POST VARCHAR(100),
    CONSTRAINT PK_POST PRIMARY KEY (ID)
);

CREATE TABLE COMMENT
(
    ID  INTEGER AUTO_INCREMENT,
    ID_POST INTEGER,
    COMMENT VARCHAR(100),
    CONSTRAINT PK_COMMENT PRIMARY KEY (ID),
    CONSTRAINT FK_COMMENT_01 FOREIGN KEY (ID_POST) REFERENCES POST (ID)
);

 

@Entity
public class Post {
    private int id;
    public String post;
    public List<Comment> comments;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public int getId() {
        return id;
    }

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

    @Basic
    @Column(name = "POST")
    public String getPost() {
        return post;
    }

    public void setPost(String post) {
        this.post = post;
    }

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    public List<Comment> getComments() {
        return comments;
    }

    public void setComments(List<Comment> comments) {
        this.comments = comments;
    }

}

 

@Entity
public class Comment {
    private int id;
    public String comment;
    public Post post;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public int getId() {
        return id;
    }

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

    @Basic
    @Column(name = "COMMENT")
    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    @ManyToOne
    @JoinColumn(name = "ID_POST", referencedColumnName = "ID")
    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }

    public Comment() {
    }

    public Comment(String comment, Post post) {
        this.comment = comment;
        this.post = post;
    }
}

A kódrészlet amit az entitásokat létrehozza és perzisztálja:

        EntityManager em =  Persistence.createEntityManagerFactory(Constants.PERSISTENCE_UNIT_NAME).createEntityManager();

        Post post = new Post();
        post.setPost("blablabla");
        post.setComments(new ArrayList<>());
        post.getComments().add(new Comment("comment 1", post));

        em.getTransaction().begin();
        em.persist(post);
        em.getTransaction().commit();

        System.out.println(post.getComments().get(0).getId());

        em.getTransaction().begin();
        post.getComments().add(new Comment("comment 2", post));
        post.getComments().add(new Comment("comment 3", post));
        post = em.merge(post);

        System.out.println(post.getComments().get(1).getId());
        System.out.println(post.getComments().get(2).getId());
        em.getTransaction().commit();

        em.refresh(post);
        System.out.println(post.getComments().get(1).getId());
        System.out.println(post.getComments().get(2).getId());

 

A kimenet:

1
0
0
2
3

Aham:

        em.getTransaction().begin();
        post.getComments().add(new Comment("comment 2", post));
        post.getComments().add(new Comment("comment 3", post));
        post = em.merge(post);

        System.out.println(post.getComments().get(1).getId());
        System.out.println(post.getComments().get(2).getId());
        em.getTransaction().commit();

        em.refresh(post);
        System.out.println(post.getComments().get(1).getId());
        System.out.println(post.getComments().get(2).getId());

Naszóval, röviden és elvileg: commit() vagy flush() vagy refresh() esetén lesznek id-k így ebben a formában. Amíg tranzakcióban vagy és az nem zárul le, addig nem lesznek id-k, ha azok adatbázis által autogenerated típusúak.

Ha így írod, akkor jobb lesz?
 

        // ...
        post = em.merge(post);
        em.getTransaction().commit();

        System.out.println(post.getComments().get(1).getId());
        System.out.println(post.getComments().get(2).getId());
        // ...

Gondolom mert a commit is csinál refersh-t, nem a commit az érdekes.

Ennél azért komplexebb a tranzakció kezelése, arra kell gondolni, hogy egy JavaEE környezetben tipikusan elosztott kétfázisú tranzakciók vannak, amíg nem zárul le a tranzakció, addig nem kapsz vissza olyan azonosítókat, amelyek később érvénytelenek lennének, ha beüt valami baj. Például ebben az esetben létrehozod a struktúrát az EntityManager oldalán, lekérdezed az azonosítót, megy a commit(), elhasal és nálad meg lennének olyan azonosítók, amelyek amúgy nem léteznek az adatbázisban. Szóval emlékeim szerint ez by design ilyen, de tényleg régen volt dolgom JPA projekttel.

Én mondjuk eleve fordítva csinálnám: vagyis nem a szülő mezőjét birizgálnám, hanem a gyereket szúrnám be - azon beállítva a szülőt.

Ezért van a "cascade", hogy ezt megoldja helyetted. És tranzakcióban hiába is szúrod be, elvileg akkor se kapsz vissza id-t, amíg a tranzakció le nem zárul - vagy nem kérsz implicit szinkronizálást flush() vagy refresh() használatával, ami antipattern.

Hja, elkerülte a figyelmem az AUTOINCREMENT :)

Az ok a következő: Mivel az ID-et az adatbázis motor generálja, ezért a JPA réteg nem kapja meg addig, amíg nem beszélgetett vele. Márpedig a JPA réteg (alapértelmezetten) nem beszélget addig a DB motorral, amíg nem feltétlen szükséges, késlelteti a parancsok kiküldését. Ennek teljesítmény oka van elsősorban, hogy objektum-orientált módon tudd manipulálni az objektumaidat, és utána hatékonyan lehessen ezt SQL-re fordítani (ami teljesen más világ). Ha szükséged van tranzakció közepén is az ID-ekre, akkor explicit flush-t kell hívni.

Olvass utána annak, hogyan működik a flushing és a session cache (EntityManager) működése.

Ha van választásod a JPA implementáció kiválasztásában, akkor inkább a Hibernate-et ajánlanám, nagyon jól összerakott szoftver jó dokumentációval.

Ha van választásod a JPA implementáció kiválasztásában, akkor inkább a Hibernate-et ajánlanám, nagyon jól összerakott szoftver jó dokumentációval.

Ízlések és pofonok, én mindig is utáltam a Hibernate megoldásait, mint zsebben a száraz szart. :D

Így már teljesen érthető a dolog. 

Választási lehetőségem van az implementáció kiválasztásában, csak kérdés mennyire fájdalmas a migrálás EclipseLink-ről Hibernate-re. 

Egyszer volt egy halovány próbálkozás, hogy a persitence.xml-ben átírtam a 

<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>

opciót a Hbernate-re

de elkezdett panaszkodni arra, hogy én a gettereknél használtam az annaotációkat, ő meg valamiért azt szerette csak ha a mező volt annotálva. Túl sok osztályt kellet volna átírni emiatt és annak sem volt időm akkor utánaolvasni hogy ez most miért van így, így ott el is halt a dolog. De egyre inkább előtérbe kerül megint hogy át kellene állni.

Szerintem össze van kutyulva a gyerek - szülő hivatkozás. Nézz át még egyszer valami példát, doksit, hogy ne értsd félre a használatát!

pl. a Commentben az idpost mező nem kell, annotációba nem is az a mezőnév kell, hanem a szülő, a post id mező neve (ID).