JPA zapis do tabel powiązanych relacją OneToOne

0

Witam,

Zmuszony jestem prosić o pomoc/radę, zatrzymałem się na poniższym problemie i od tygodnia nie mogę kontynuować -:(

Stan obecny w kodzie JSF/JPA:

2 encje = klasa POJO z polami odpowiadającymi polom formularza zapisu z web (Student) oraz klasa reprezentująca stan aktywacji danego użytkownika w systemie (UserActivation): poniżej ekstrakty bez getterów i setterów pól definiujących i łączących klucze główne i obce; w załączeniu schemat MySQL dla 2 tabel w relacji 1-1 oraz zrzut debuggingu z konsoli Wfly-a rzuca wyjątek :

Cannot add or update a child row: a foreign key constraint fails (`paw_news`.`signatories_activation`, CONSTRAINT `signatories_activation_ibfk_1` FOREIGN KEY (`id`) REFERENCES `signatories` (`id`) ON UPDATE CASCADE)

Problem:

O ile kod zapisuje dane z formatki rejestracyjnej do tabeli signatories bez problemu ,to nie pozwala na równoległy zapis do tabeli signatories_activation połączonej relacją 1-1.

Co ciekawe jeśli usunę w MYSQL klucz obcy z tabeli signatories_activation , to kod zapisze dane do dwóch tabel, z tym że w signatories_activation w polach klucz podstawowy i obcy ustawi wartość 0.

Zapis dla powyższego schematu zrealizowałem podobnie jak Pan pokazywał na ćwiczeniach: najpierw do tabeli z formularza poprzez studentService i ...Dao a potem do tabeli signatories_activation poprzez studentService i ...Dao.

Może gdzieś w kodzie JPA trzeba wskazać, że generowana wartość w polu signatories.id ma być zapisana w polu signatories_activation.id. Załączam schemat bazy MySQL.

package paw.signup.jpa;

import javax.jws.soap.SOAPBinding;
import javax.persistence.*;


@Entity
@Table(name = "signatories")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable=false)
    private long id;

    @OneToOne
    @JoinColumn(name="id")
    private UserActivation userActivation;


   @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;
..itd


package paw.signup.jpa;
import javax.persistence.*;


@Entity
@Table(name = "signatories_activation")
public class UserActivation {

   @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "activation_id",nullable=false)
    private long activation_id;

    @Column(name = "activated",nullable=false)
    private String activated;

    @Column(name = "id",nullable=false)
    private long id;

    @OneToOne(mappedBy="userActivation")
    private Student student;
..itd
2018-07-12 23:23:42,603 INFO  [StudentDatabaseBean] (default task-3) utw�rz wywo�anie dla: Student{firstName='Manuela', id=0, lastName='Pyza'}
2018-07-12 23:23:43,612 INFO  [stdout] (default task-3) Hibernate: insert into signatories (apartment_number, city, county, identity_card, email, first_name, gender, house_number, iban_account, last_name, login, password, personal_id, phone, zip_code, prefix, street, voivodeship) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

2018-07-12 23:23:43,620 INFO  [pl.edu.agh.paw.examples.IbanBean] (default task-3) zainicjowano obiekt UserActivation a  pole activated jest :N
2018-07-12 23:23:43,621 INFO  [pl.edu.agh.paw.examples.IbanBean] (default task-3) zainicjowano obiekt UserActivation a  pole id jest :0
2018-07-12 23:23:43,621 INFO  [StudentDatabaseBean] (default task-3) utw�rz wywo�anie dla: UserActivation{activation_id='0', activated='N'id='0'}
2018-07-12 23:23:43,627 INFO  [stdout] (default task-3) Hibernate: insert into signatories_activation (activated, id) values (?, ?)

2018-07-12 23:23:43,633 WARN  [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (default task-3) SQL Error: 1452, SQLState: 23000
2018-07-12 23:23:43,633 ERROR [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (default task-3) Cannot add or update a child row: a foreign key constraint fails (`paw_news`.`signatories_activation`, CONSTRAINT `signatories_activation_ibfk_1` FOREIGN KEY (`id`) REFERENCES `signatories` (`id`) ON UPDATE CASCADE)
2018-07-12 23:23:43,638 ERROR [org.jboss.as.ejb3.invocation] (default task-3) WFLYEJB0034: EJB Invocation failed on component UserActivationDao for method public void paw.signup.dao.AbstractDao.create(java.lang.Object): javax.ejb.EJBException: javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
    at org.jboss.as.ejb3.tx.CMTTxInterceptor.handleExceptionInOurTx(CMTTxInterceptor.java:186)

Pozdrawiam,

0

Nie wiem czy to pomoże, ale jak dla mnie to bardzo źle nazwałeś jedną kolumnę w tym activation - signatories_activation.id - Otóż, wbrew pozorom, u ciebie nie jest to id dla signatories_activation :P także weź może jako foreign keya nazwij kolumnę user_id zamiast id. Poza tym, w encji Student masz dwie kolumny o nazwie id -.^

Pozmieniaj to - nawet jeśli nie naprawi błędu wprost, to poprawi czytelność komunikatów

0

Zmieniłem schemat bazy : dodałem nowe pOle signatories_activation_id do tabeli signatories które będzie kluczem obcym z tabeli signatories_activation = linku do id które tam jest kluczem głównym autoinkrementowanym.
Ustawiłem adnotacje w JPA jak niżej ale serwer przy pubie deploy zwraca błąd jak niżej.
Co jest grane, co robię źle, a może czegoś nie rozumiem ?
Załączam zrzut nowego schematu bazy

@Entity
@Table(name = "signatories")
public class Student {

    @Id
    @Column(name = "id", nullable=false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @OneToOne
    @JoinColumn(name="signatories_activation_id")
    private UserActivation userActivation;

@Entity
@Table(name = "signatories_activation")
public class UserActivation implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id",nullable=false)
    private long id;

 @OneToOne(mappedBy="userActivation")
    private Student student;

    @Id
    @Column(name = "activated",nullable=false)
    private String activated;

{
    "WFLYCTL0080: Failed services" => {
        "jboss.persistenceunit."paw-signup-ear.ear/paw-signup-web.war#PawSignup"" => "org.hibernate.AnnotationException: A Foreign key refering paw.signup.jpa.UserActivation from paw.signup.jpa.Student has the wrong number of column. should be 2
    Caused by: org.hibernate.AnnotationException: A Foreign key refering paw.signup.jpa.UserActivation from paw.signup.jpa.Student has the wrong number of column. should be 2",
        "jboss.persistenceunit."paw-signup-ear.ear/paw-signup-db.jar#PawSignup"" => "org.hibernate.AnnotationException: A Foreign key refering paw.signup.jpa.UserActivation from paw.signup.jpa.Student has the wrong number of column. should be 2
    Caused by: org.hibernate.AnnotationException: A Foreign key refering paw.signup.jpa.UserActivation from paw.signup.jpa.Student has the wrong number of column. should be 2"
    },

mysql_tables_sql.JPG85 KBPokaż Pobierz

0

Nad polem activated nie powinno być adnotacji @Id

0

..rzeczywiście !
Mój błąd ewidentny, poprawiłem i projekt się zbudował.

Obecne adnotacje wyglądają obecnie tak

@Entity
@Table(name = "signatories")
public class Student {

@Id
@Column(name = "id", nullable=false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@OneToOne
@JoinColumn(name="signatories_activation_id")
private UserActivation userActivation;

itd....

@Entity
@Table(name = "signatories_activation")
public class UserActivation implements Serializable{

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id",nullable=false)
private long id;
@OneToOne(mappedBy="userActivation")
private Student student;


@Column(name = "activated",nullable=false)
private String activated;

itd....

Niestety przy próbie zapisu do MySQL z formularza xhtml, Wildfly zwraca błąd :

2018-07-17 2008,655 WARN [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (default task-3) SQL Error: 1048, SQLState: 23000
2018-07-17 2008,655 ERROR [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] (default task-3)** Column 'signatories_activation_id' cannot be null**
2018-07-17 2008,655 ERROR [org.jboss.as.ejb3.invocation] (default task-3) WFLYEJB0034: EJB Invocation failed on component StudentDao for method public void paw.signup.dao.AbstractDao.create(java.lang.Object): javax.ejb.EJBException: javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement

....natomiast zapisuje dane do tabeli signatories_activation prawidłowo inkrementując klucz główny :

mysql> select * from signatories_activation;
+----+-----------+
| id | activated |
+----+-----------+
| 3 | N |
| 4 | N |
+----+-----------+

Niestety, ale nic nie zapisuje do tabeli signatories, bo zdaje się że brak mu wartości dla pola signatories_activation_id, jak zresztą zgłasza.

Może jak tego do końca nie rozumiem, ale jeśli w tabelach SQL oraz w encjach JPA jest ustawiony klucz obcy i powiązanie OneToOne tak jak wyżej wkleiłem, to czy samo to już nie wystarczy, aby zapisać signatories.signatories_activation_id = signatories_activation.id (tzn. czy to się nie dzieje automatycznie na podstawie samych adnotacji relacyjnych OneToOne) ?
.
Czy z tego wynika , że w metodzi/ache zapisującej/ych do bazy brak ustawienia , że do pola encji signatories.signatories_activation_id "ładuj" signatories_activation.id ?

Zaznaczam ze w metodzie kontrolera najpierw wywołuję utworzenie tabeli signatories_activation, a potem signatories.(jedna po drugiej)

?

Poniżej dla jasności struktura tabel /może tam jest gdzieś błąd przy definicji klucza obcego ?/

signatories | CREATE TABLE signatories (
id int(11) NOT NULL AUTO_INCREMENT,
first_name varchar(30) COLLATE utf8_polish_ci NOT NULL,
last_name varchar(50) COLLATE utf8_polish_ci NOT NULL,
identity_card varchar(9) COLLATE utf8_polish_ci NOT NULL,
personal_id varchar(11) COLLATE utf8_polish_ci NOT NULL,
iban_account varchar(35) COLLATE utf8_polish_ci NOT NULL,
gender varchar(1) COLLATE utf8_polish_ci NOT NULL,
zip_code varchar(6) COLLATE utf8_polish_ci NOT NULL,
street varchar(255) COLLATE utf8_polish_ci NOT NULL,
house_number varchar(8) COLLATE utf8_polish_ci NOT NULL,
apartment_number varchar(6) COLLATE utf8_polish_ci DEFAULT 'n/a',
city varchar(255) COLLATE utf8_polish_ci NOT NULL,
voivodeship varchar(255) COLLATE utf8_polish_ci NOT NULL,
county varchar(255) COLLATE utf8_polish_ci NOT NULL,
login varchar(10) COLLATE utf8_polish_ci NOT NULL,
password varchar(32) COLLATE utf8_polish_ci NOT NULL,
email varchar(255) COLLATE utf8_polish_ci NOT NULL,
prefix varchar(7) COLLATE utf8_polish_ci NOT NULL,
phone varchar(11) COLLATE utf8_polish_ci NOT NULL,
signatories_activation_id int(11) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY identity_card (identity_card),
UNIQUE KEY personal_id (personal_id),
UNIQUE KEY iban_account (iban_account),
UNIQUE KEY login (login),
UNIQUE KEY email (email),
KEY signatories_activation_id (signatories_activation_id),
CONSTRAINT signatories_ibfk_1 FOREIGN KEY (signatories_activation_id) REFERENCES signatories (id)
ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci |

ysql> show create table signatories_activation;
------------------------+--------------------------------------------------------------------------------------
-------------------------------------------------------+
Table | Create Table
|
------------------------+--------------------------------------------------------------------------------------
-------------------------------------------------------+
signatories_activation | CREATE TABLE signatories_activation (
id int(11) NOT NULL AUTO_INCREMENT,
activated enum('T','N') COLLATE utf8_polish_ci NOT NULL,
PRIMARY KEY (id)
ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci |

0

Z tego co widzę to chyba nie ustawiłeś pola userActivation w twoim javowym obiekcie klasy Student, a powinieneś. Okej, Adnotacje hibernatowe ci ładnie zamienią obiekty javowe na encje w bazie danych, ale musisz powiedzieć Hibernetowi, który dokładnie wiersz ma ci zmapować.

0

No tak, ale czy ja to dobrze rozumiem :

Czy pole userActivation klasy Student jest odpowiednikiem kolumny signatory_activation_id tabeli SQL, czy jest to tylko adnotacja Hibernate , mówiąca ze ta kolumna ma być w asocjacji z polem id klasy UserActivation ?
One (userActivation i signatory_activation_id) mają niezgodne typy tzn. Object versus long, wiec jak ustawic userActivation/signatory_activation_id klasy Student , żeby tam się zapisywał aktualny id z klasy UserActivation ?
.
To chyba nie jest tak banalne, bowiem JPA nie pozwala na dodanie pola private long signatory_activation_id
aby dokonać ustawienia wartością id z klasy UserActivation

?

0

Na tym polega cała siła Hibernate'a, że na backendzie, w javie, mamy pole o typie jakiejś naszej klasy, a nie samo id. Samo id, zamiast obiektu, używa się częściej np po stronie Many w @OneToMany.

0

...ale w takim razie jak to ustawić w kodzie tzn. upraszczając : Student.signatory_activation_id=userActivation.id , bo w IDE takie kombinacje świecą na czerwono i nie da się zrobić deploy

?

0

Niech Student ma pole UserActivation userActivation, i ustaw obiekt typu UserActivation do obiektu studenta.

0

Klasa Student ma od początku pole userActivation :

@OneToOne
@JoinColumn(name="signatories_activation_id")
private UserActivation userActivation;****

a klasa UserActivation ma równiez pole student :

@OneToOne(mappedBy="userActivation")
private Student student;

Jakie i w którym miejscu ma być podstawienie :

userActivation=student ? / na takie przypisanie IDE jest na czerwono bo typy są niekompatybilne /
rzutowanie na typ userActivation=(UserActivation) student też nic nie daje

przypisanie w metodach czy tez w definicji klasy np w konstruktorze

public Student( UserActivation userActivation )
{
?
}

?

0

Ziomeczku my się chyba nie zrozumieliśmy :D mi chodziło o to, żebyś poprzez setter ustawiał wartość pola userActivation. Pisałeś że obiekt typu UserActivation ci się zapisuje. No i fajnie. Czyli masz zapisany do bazy jakiś obiekt userActivationObject. To teraz zrób

studentObject.setUserActivation(userActivationObject);

i potem save studenta.

by the way, tak mało kodu a tak dużo nieczytelności - student, signator, czy user? :|

0

...sorry, masz całkowitą rację - pytania biorą się stąd , że się dopiero uczę i nie wszystko wiem :-(
.
Dziś już późno, jutro wezmę się do tego wg Twojego wskazania.

0

Poprawiłem kod w następujący sposób :

Poczatke klas Student oraz UserActivation wygląd inaczej (zrezygnowałem z relacji @OneToOne pomiędzy encjami, na rzecz ustawienia relacji 1-1 na poziomie MySQL) :

@Entity
@Table(name = "signatories")
public class Student implements Serializable{

@Id
@Column(name = "id", nullable=false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;


 @Column(name="signatories_activation_id")
 private long signatories_activation_id;

 public long getSignatories_activation_id() {
    return signatories_activation_id;
}

public void setSignatories_activation_id(long signatories_activation_id) {
    this.signatories_activation_id = signatories_activation_id;
}

@Entity
@Table(name = "signatories_activation")
public class UserActivation implements Serializable{

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id",nullable=false)
private long id;

@Column(name = "activated",nullable=false)
private String activated;

W klasie kontrolera metod walidacji formularza -z której po zwalidowaniu poprawności danych uruchamiam procedurę zapisu do bazy danych - mam obecnie :

//tworzę nowy obiekt klasy UserActivation

UserActivation temporaryActivator= (new NewUserDataBaseCreator()).newSignActivatorCreate();

//wołam metodę tworzącą tabelę SQL z encji JPA dla klasy UserActivator czyli kreuję wstępnie zapis do tabeli nowego zapisującego się
z aktywacją ustawioną na N = niekatywny

            studentService.create(temporaryActivator);

//wołam metodę tworzącą tabelę SQL z encji JPA dla klasy Student czyli kreuję wstępnie zapis do tabeli nowego zapisującego się
z pełnymi danymi osobowymi poprzez przekazanie danych osobowych klasy User() oraz temporaryActivator potrzebnych do usatwoenia wartości klucza obcego signatory_activation_id w tabeli SQL

            studentService.create((new NewUserDataBaseCreator()).newSignatoryCreate(getUser(),temporaryActivator));

Metody tworzące obiekty JPA są następujące :

public Student newSignatoryCreate(User user,UserActivation userActivation)
{

                s.setFirstName(user.getFirstName());
            s.setLastName(user.getLastName());
            s.setDowosob(user.getDowosob());
            s.setPesel(user.getPesel());
            s.setIban(user.getIban());
            s.setGender(user.getGender().equals(gender_indicator)? female_indicator:male_indicator);
            s.setPostalCode(user.getPostalCode());
            s.setStreet(user.getStreet());
            s.setHouseNumber(user.getHouseNumber());
                /**
                 * tutaj jeśli nie wprowadzono wartości do pola numer lokalu atrybut klasy JPA doyczący tego kontekstu nie jest
                 * ustawiany i nie jest zapisywany do MySQL
                 */

            if(!user.getApartmentNumber().equals(""))
            {
                    s.setApartmentNumber(user.getApartmentNumber());
            }
            s.setCity(user.getCity());
            s.setVoivodeship(user.getVoivodeship());
            s.setCounty(user.getCounty());
            s.setLogin(user.getLogin());

                /**
                 * tutaj wywołanie metody haszującej hasło wprowadzone w postaci inputSecret
                 * do pola hasło formularza rejestracyjnego
                 * @param user.getPassword
                 */
                s.setPassword((new PasswordEncryptorBeforeDataBaseInput()).encryptePassword(user.getPassword()));

            s.setEmail(user.getEmail());
            s.setPrefix(user.getPrefix());
            s.setPhone(user.getPhone());
          
                logger.log(Level.INFO, "...a pole id z UserActivation przed zapisem do Student wynosi:" + userActivation.getId());

                s.setSignatories_activation_id(userActivation.getId());
           


                return s;
            }

            public UserActivation newSignActivatorCreate (){

                userActivation.setActivated(initial_activation_indicator);

                logger.log(Level.INFO, "zainicjowano obiekt UserActivation a  pole activated jest :" + userActivation.getActivated());
                logger.log(Level.INFO, "zainicjowano obiekt UserActivation a  pole id jest :" + userActivation.getId());

                return userActivation;
}

Rezultat jest obecnie taki jak poniżej po wprowadzeniu kilku danych testowych :

mysql> select first_name,last_name,signatories_activation_id from signatories;
+-----------------+-----------+---------------------------+
| first_name | last_name | signatories_activation_id |
+-----------------+-----------+---------------------------+
| Magdalena | Buc | 1 |
| Kazimierz | Matołek | 2 |
| Karol Boromeusz | Pyza | 3 |
+-----------------+-----------+---------------------------+
3 rows in set (0.00 sec)

mysql> select first_name,last_name,signatories_activation_id as "klucz obcy" from signatories,signatories_activation where signatories_activation_id=signato
+-----------------+-----------+------------+
| first_name | last_name | klucz obcy |
+-----------------+-----------+------------+
| Magdalena | Buc | 1 |
| Kazimierz | Matołek | 2 |
| Karol Boromeusz | Pyza | 3 |
+-----------------+-----------+------------+
3 rows in set (0.00 sec)

Tablele są sprzężone relacja 1-1, ale w klasach JPA nie ma żadnych adnotacji.

Pytanie nr 1 :

czy zrobiłem poprawnie, czy tez należy to poprawić na inny sposób ?

Pytanie nr 2 :

Chciałbym w klasie metod walidacyjnych kontrolera po zwalidowaniu poprawności formalnych zapisującego się oraz przed zapisem
ich do bazy danych zweryfikować czy wybrane dane personalne /PESEL, IBAN itd../ nie występują już w bazie danych aby nie dokonywać zapisu duplikowanych danych takich samych osób/
.
Zapis/Odczyt itd na bazie danych jest dokonywany za pośrednictwem klasy jak niżej :

protected abstract Class<T> getType();

<R> Optional<R> checkForSingleResult(List<R> resultList) {
    return resultList.isEmpty() ? Optional.empty() : Optional.of(resultList.get(0));
}

public void create(T object) {
    entityManager.persist(object);
}

public T merge(T object) {
    return entityManager.merge(object);
}

public void remove(Object id) {
    T o = entityManager.find(getType(), id);

    entityManager.remove(o);
}

public List<T> all() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<T> cq = cb.createQuery(getType());

    cq.select(cq.from(getType()));
    return entityManager.createQuery(cq).getResultList();
}

public List<T> list(int offset, int limit) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<T> cq = cb.createQuery(getType());

    cq.select(cq.from(getType()));
    return entityManager.createQuery(cq).setFirstResult(offset).setMaxResults(limit).getResultList();
}

public Optional<T> get(Object id) {
    T t = entityManager.find(getType(), id);
    return t == null ? Optional.empty() : Optional.of(t);
}

}

Której metody i jak jej użyć , aby odczytać z bazy danych zawartość tabel(i), ponieważ nie za bardzo
rozumiem jeszcze składnię (jestem w trakcie czytania książki do Hibernate i na razie nie napotkałem na opis metod DAO z adnotacjami)

?

(..mam nadzieję , że teraz jaśnie to wygląda niż wczoraj)

Pozdrawiam

0

...dasz radę zerknąć ?

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