Nie mogę zapisać kaskadowo encji do bazy

0

Cześć, potrzebuję pomocy. Dalej ćwiczę sobie relacje pomiędzy tabelami i znów utknąłem. Pisze sobie prosty program - listę kontaktów. Składa się z trzech encji: "Contact", "Email" i "Group". Encja "Contact" jest główną encją, która jest związana relacją jeden-do-jeden z encją "Email" oraz relacja jeden do wielu z encją "Group" (każdy kontakt może być przypisany do wielu grup typu praca, rodzina etc.) "Contact" jest główną encją, pozostałe nie mają bez niej sensu, dlatego stwierdziłem, że najsensowniej będzie zapisać wszystkie pozostałe encje do bazy kaskadowo przy okazji zapisu encji głównej. Niestety program się wywala właśnie przy próbie zapisu tej encji. Dostaję taki komunikat:

org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: domain.Group

Wygląda mi to tak, jakby kaskada nie zadziałała, tylko nie rozumiem dlaczego...

Wklejam fragmenty kodu, które wydają mi się najważniejsze, gdyby to nie było wystarczająco czytelne to wrzucę na githuba, żeby tutaj tematu nie zaśmiecać.

Encje:


package domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "contacts")
public class Contact {
    @Id
    @GeneratedValue
    private int contactId;
    private String firstName;
    private String lastName;
    @OneToOne(targetEntity = Email.class,
              cascade = CascadeType.ALL)
    private Email email;
    @ManyToMany(targetEntity = Group.class,
                cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Group> groups = new ArrayList<Group>();

    // Constructor

    public Contact(){}

    public Contact(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "Contact{" +
                "contactId=" + contactId +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", email=" + email +
                ", groups=" + groups +
                '}';
    }

    // Getters and Setters


    public int getContactId() {
        return contactId;
    }

    public void setContactId(int contactId) {
        this.contactId = contactId;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Email getEmail() {
        return email;
    }

    public void setEmail(Email email) {
        this.email = email;
    }

    public List<Group> getGroups() {
        return groups;
    }

    public void setGroups(List<Group> groups) {
        this.groups = groups;
    }
}


package domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "emails")
public class Email {
    @Id
    @GeneratedValue
    private int emailId;
    private String email;

    // Constructor

    public Email(String email) {
        this.email = email;
    }

    // Getters and Setters

    public int getEmailId() {
        return emailId;
    }

    public void setEmailId(int emailId) {
        this.emailId = emailId;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}


package domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "groups")
public class Group {

    @Id
    @GeneratedValue
    private int groupId;
    private String groupName;
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Contact> contacts = new ArrayList<Contact>();

    // Constructors

    public Group(){}

    public Group(String groupName) {
        this.groupName = groupName;
    }

    @Override
    public String toString() {
        return "Group{" +
                "groupId=" + groupId +
                ", groupName='" + groupName + '\'' +
                ", contacts=" + contacts +
                '}';
    }

    // Getters and Setters

    public int getGroupId() {
        return groupId;
    }

    public void setGroupId(int groupId) {
        this.groupId = groupId;
    }

    public String getGroupName() {
        return groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }

    public List<Contact> getContacts() {
        return contacts;
    }

    public void setContacts(List<Contact> contacts) {
        this.contacts = contacts;
    }
}

Fragment repozytorium:

    public static void addNewContact(Contact contact){

        Session session = null;
        try {
            session = HibernateUtils.openSession();
            session.getTransaction().begin();
            session.saveOrUpdate(contact);
            session.getTransaction().commit();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (session != null && session.isOpen()) {
                session.close();
            }
        }
    }   

Metoda, która tworzy cały wpis do bazy:

 public void addNewContact(){
        System.out.println("Add New Contact");
        System.out.println("===============");
        System.out.println("");

        System.out.println("First Name: ");
        String firstName = scanner.next();
        System.out.println("Last Name: ");
        String lastName = scanner.next();

        Contact contact = new Contact(firstName, lastName);

        System.out.println("Email: ");
        String email_ = scanner.next();

        Email email = new Email(email_);
        contact.setEmail(email);

        boolean groupRun = true;
        do{
            System.out.println("Group name: ");
            String groupName = scanner.next();
            Group group = new Group(groupName);
            contact.getGroups().add(group);
            group.getContacts().add(contact);
           
            System.out.println("Would you like to add another group? y/n");
            String choice = scanner.next();
            switch (choice.charAt(0)){
                case 'y':
                    break;
                case 'n':
                    groupRun = false;
                    break;
            }

        }while(groupRun);

        ContactRepository.addNewContact(contact);
    }
0

@s-kaczmarek: na pierwszy rzut oka wydaje mi się że problemem jest brak @JoinTable. Po 2 radzę korzystać z Set zamiast List w JPA/Hibernate (chyba że potrzebujesz zachowac kolejnośc elementow), ale to akurat nie ma nic wspólnego z niezapisywaniem obieków :)

0

czy @JoinTable nie jest przypadkiem potrzebne jedynie, żeby wyeliminować tabelę pośrednią? Nie rozumiem związku z niezapisywaniem...

0

@s-kaczmarek: co to znaczy wyeliminowac tabele pośrednią? W bazach relacyjnych tabela pośrednia jest zawsze potrzebna w związkach wielu-do-wielu. Wytłumacze na przykładzie: masz model Book, masz model Author, i teraz w bazie danych takiej jak PostgreSQL na przykład robisz table book, author no i musisz dodać book_author. @JoinTable pozwala na uniknięcie pisania encji Javovej BookAuthor która miala by tylko referencje na Book i Author, ale w samej bazie ta table musi istnieć. Natomiast JPA (w tym jego implementacja Hibernate) musi wiedzieć jak "pożenić" te tabele, więc bez @JoinTable nie da razy za bardzo, nigdy nie widziałem żeby ktoś tak kombinował. Tutaj masz opisaje jak sie robi Many-To-Many

0

Dzięki za odpowiedzi i linki, pokombinowałem ale dalej nie działa. Zrobiłem tak:

encja Contact:


@Entity
@Table(name = "contacts")
public class Contact {
    @Id
    @GeneratedValue
    private int contactId;
    private String firstName;
    private String lastName;
    @OneToOne(targetEntity = Email.class,
              cascade = CascadeType.ALL)
    private Email email;
    @ManyToMany(targetEntity = Group.class,
                cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
            name="Contact_Groups",
            joinColumns=@JoinColumn(name="CONTACT_ID", referencedColumnName="contactId"),
            inverseJoinColumns=@JoinColumn(name="GROUP_ID", referencedColumnName="groupId"))
    private List<Group> groups = new ArrayList<Group>();

encja Group:

@Entity
@Table(name = "groups")
public class Group {

    @Id
    @GeneratedValue
    private int groupId;
    private String groupName;
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "groups")
    private List<Contact> contacts = new ArrayList<Contact>();

Dostaję taki wyjątek:

Caused by: org.sqlite.SQLiteException: [SQLITE_ERROR] SQL error or missing database (table Contact_Groups has no column named GROUP_ID)

1

Ale przecież ten wyjątek wszystko mowi...

0

Mówi, że tabela pośrednia nie ma kolumny GROUP_ID, ale przecież po to robimy te adnotacje, żeby tabela pośrednia się utworzyła z takimi kolumnami jakie są potrzebne.

edit:

Zerknąłem do bazy i faktycznie w tabeli pośredniej kolumna miała błędną nazwę (została utworzona przy wcześniejszych próbach), nie rozumiem dlaczego nazwa tej kolumny nie została zmieniona przy kolejnym uruchomieniu programu - konfigurację hibernate mam ustawioną na "update". Usunąłem tabele ręcznie, odpaliłem program i teraz tabela pośrednia wygląda tak jak wynika to z adnotacji, ale niestety wciąż dane nie są zapisywane :/

Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: domain.Group

0

Teraz to już w ogóle nie wiadomo, co tam masz w tym kodzie. Zrób jakiś mini projekt, w którym są te klasy i wrzuć na githuba. Tam będziemy mogli to odpalić bez męczącego skrolowania po forum.

Jak chcesz, to wzoruj się na moim testowym projekcie. Zrobiłem na próbę klasy Student i Club, które obsługują ManyToMany. Działa zgodnie z oczekiwaniami, bez problemów po drodze. Zacząłem od minimalnej liczby adnotacji, patrzyłem na efekty (głównie powstające tabele). Ostatecznie doprowadziłem do tego, że testy (CascadeMMTests) przeszły.

Było to pouczające. Na początku widać, że hib tworzy dla każdej strony relacji osobną tabelę łączącą. Potem trzeba jakoś mu zakomunikować, żeby użył jednej do obydwu mapowań. A potem już działa. Cascade cały czas robił, co trzeba. Ani razu nie miałem unsaved transient entity.

Testy są po to, żebyś nie musiał 50 razy klepać po konsoli. Ale jak nie znasz junit, to napisz funkcję main, które nie pyta o nic usera, tylko sama robi wszystko. Dopiero jak zadziała, dołóż interfejs użytkownika.

0

wrzuciłem kod na githuba, jakbyście mogli zerknąć to będę wdzięczny. Ja w tym czasie douczę się z JUnita i postaram się zaimplementować test z Twojego projektu.

1

Nie było to łatwe :)

JPA a Hibernate to nie jest dokładnie to samo. W repozytorium używasz api hibernate (session.saveOrUpdate), a w adnotacjach JPA. O kaskadach w Hibernate Vlad też pisze. I odradza tam używanie saveOrUpdate. Widać to na Twoim przykładzie. W typach kaskadowych JPA nie ma takiego przypadku jak CascadeType.SAVE_UPDATE. Musiałbyś użyć ALL. Albo użyć hibernatowego stylu na kaskady:

@ManyToMany(targetEntity = Group.class)
@Cascade({CascadeType.PERSIST, CascadeType.MERGE, CascadeType.SAVE_UPDATE})
@JoinTable(

Wyrzuciłem z Twojego kodu import z gwiazdką javax.persistence.*. Zamiast tego importowałem pojedyncze klasy, a CascadeType wziąłem ten: org.hibernate.annotations.CascadeType.

Chyba łatwiej by było po prostu zrezygnować z saveOrUpdate.

0

Hmmm, teraz to dopiero ciekawy przypadek! Jeszcze nic nie zmieniłem z tego co napisałeś, za to napisałem test i o dziwo przeszedł mimo, że w konsoli cały czas te same wyjątki. Widocznie jakoś źle ten test napisałem...

Dla jasności - w tym projekcie chcę użyć Hibernate'a samego w sobie, nie jako implementacji JPA. Dlatego tak katuję tu na forum, bo chcę sobie przygotować serię takich szablonów dla Hibernate i JPA z różnymi bazami danych.

Wracając do Twojego posta, co znaczy, że "łatwiej byłoby zrezygnować z saveOrUpdate"? Co zamiast tego powinienem zrobić i czy wtedy adnotacje powinienem zmienić na to co pokazałaś, czy zostawić tak jak mam teraz?

edit:
@jarekczek próbowałem zmienić adnotację cascade na to co pokazałeś ale miałem problem z importem przy CascadeType.SAVE-UPDATE. Ostatecznie zmieniłem adnotację na:

 @ManyToMany(targetEntity = Group.class, cascade = CascadeType.ALL)

I teraz działa, zapisuje do bazy wszystko. Oczywiście pojawiają się kolejne problemy - napisałem test tak, żeby po zapisie do bazy sprawdził, czy ten obiekt w niej istnieje i po sprawdzeniu go usunął no i z tym usuwaniem jest problem, bo tego nie robi. Metoda do usuwania jest w ContactRepository. Ciekawą kwestią jest również to, że id w bazie zwiększają się co 4! Ciekawe dlaczego tak się dzieje...

1 użytkowników online, w tym zalogowanych: 0, gości: 1