Spring - Automatyczne nadawanie zewnętrznego klucza

0

Witam. Próbuję zrobić projekt springowy, w którym możliwe będzie tworzenie egzaminów z pytań wraz z dostępnymi odpowiedziami. Klasa Answer wygląda tak:

@Entity
@Table(name = "answers")
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    int id;
    String content;
    boolean isTrue;
    @ManyToOne
    @JoinColumn(name = "questionId")
    private Question question;

A klasa Question wygląda tak:

@Entity
@Table(name = "questions")
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String description;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "question")
    private Set<Answer> answers;

Migracja:

create table Exams(
id int primary key auto_increment
)
create table Questions(
id int primary key auto_increment,
qstContent varchar(100) not null
)
create table Answers(
    id int primary key auto_increment,
    content varchar(100) not null,
    isTrue bit,
    questionId int not null,
    foreign key (questionId) join Questions(id)
)

Metoda w kontrolerze:

@PostMapping("/questions")
public ResponseEntity<Question> createQuestion(@RequestBody Question toCreate) throws Exception {
    Question question = repository.save(toCreate);
    if (toCreate == null) {
        throw new Exception();
    } else {
        return new ResponseEntity<>(toCreate, HttpStatus.CREATED);
    }
}

JSon, który wysyłam:

{
    "description": "jakiś opis",
    "answers": [{"content": "treść odpowiedzi", "isTrue": false},{"content": "treść odpowiedzi2", "isTrue": true}]
}

W bazie danych zostają zapisane pytania i odpowiedzi, ale odpowiedzi nie mają klucza zewnętrznego identyfikującego je z pytaniem. W sumie jak patrzę na kod to jest to nawet jasne, bo próbowałem całość tworzyć przez analogię do innego projektu. Ktoś mógłby mi jednak podpowiedzieć jak to naprawić, żeby odpowiedź zawierała prawidłowy klucz zewnętrzny? Ponadto podejrzewam, że tworzenie ID dla klasy Answer jest również nienajlepsze, tzn dodając pytanie z dwoma odpowiedziami, pytanie będzie miało klucz 1, a odpowiedzi kolejno 2 i 3.

0

Proponowałbym lekką przebudowę. Wysyłasz requesta z odpowiedzią, ale widzę że to klient mówi czy ona jest prawidłowa co jest łatwe do oszukania. W backendzie (czyli w Springu) powinieneś wyciągnać question, porównać jego odpowiedzi z tym co dostajesz i zinterpretować czy ona jest prawidłowa czy nie, wówczas klient nie będzie mógł ingerować w poprawność odpowiedzi
Kolejna rzecz to zbyt stanowe odpowiedzi. Potrzebujesz w zwrotce prawidłowy klucz zewnętrzny, ale w obecnej architekturze mógłbyś to zidentyfikować tylko raz, bo jak następnym razem poprosisz o zwrotkę (jakimś GETem) to nie będziesz wiedział jakie udzielone odpowiedzi zwrócić. Powinieneś utworzyć jakąś encję typu sesja, w ramach której będziesz mieć listę pytań i odpowiedzi na nie. Wówczas w każdej sesji mogą być rekordy, które będą mieć swoje klucze oraz klucze do pytania i udzielonej odpowiedzi

0

Jesteś pewien że odpowiedzi nie mają klucza „zewnętrznego”?

questionId int not null,
foreign key (questionId) join Questions(id)

oznacza, że wiersze w tabeli questions MUSZĄ mieć klucz obcy.

Raczej problem leży po stronie ORM. W JPA jak wypełniasz jedną stronę relacji w nowych obiektach, to też musisz wypełnić ręcznie relację odwrotną. Pewnie dlatego Answer.question pozostaje nullem po stronie javy.

Poza tym, ta relacja nie wygląda na dobrze zmapowaną:

  • Nie mapuj do Seta, ale do Listy. Odpowiedzi na pewno są ponumerowane w ramach Question. Poczytaj jak zmapować ponumerowaną listę.
  • Answer nie potrzebuje osieroconego (i generowanego) klucza głównego id. Wystarczy złożony klucz question_id + answer_order.

Inny problem to cykl w obiekcie zwracanym. Jak rozwiążesz problem relacji odwrotnej, to od razu pojawi się problem z zapętleniem w Jsonie. Generalnie operowanie na encjach w api kontrolera to słaby pomysł. Powinieneś wprowadzić jakieś pośrednie dto.

0

@Ephyron: Tzn. może powinienem zacząć od tego co w ogóle chcę zrobić. Plan był taki. Najpierw zrobić to tak żeby JSonem można było wysłać pytanie z dostępnymi odpowiedziami. Wtedy dałoby się stworzyć zbiór pytań metodą post poprzez podanie powiedzmy tablicy id różnych pytań. Jakby to zagrało, to chciałem do tego jakiś front z logowaniem stworzyć dla nauczyciela, który tworzy pytania. Potem dopiero chciałem stworzyć funkcje dla osoby egzaminowanej. Dane logowania, wynik dla wybranego testu itd. Prawdę mówiąc nie zaszedłem za daleko w Springu, ale wydawało mi się, że jeśli zrobię oddzielny front dla egzaminującego i egzaminowanego, to raczej nie powinno już być problemem, żeby temu drugiemu zabronić czegoś robić przez protokół http.

0

Ja bym się zastanowił, czy chcę od razu zaprzęgać JPA/hibernate do projektu z 3 tabelkami i czy w ogóle osobne tabele są potrzebne. Czy jest sens trzymać odpowiedzi dla jednego pytania w osobnych wierszach tabeli (json mapping ftw)? Jak widać, hibernate zaczyna boleć już przy pierwszej kolekcji.

0

@ArchitektSpaghetti: No to nawet zakładając, że nie chcę nic w Springu robić, tylko mieć relacyjną bazę danych, która ma zbiory pytań z odpowiedziami... Wydawało mi się, że inaczej tego nie mogę zrobić, bo nie ma żadnego typu w SQL, który by wprost odpowiadał tablicy albo czemuś podobnemu.

0

Większość baz danych ma wsparcie do kolumn typu Json.

0

Zrobiłem dwie DTO do klasy Question (teraz jak patrzę, chyba niepotrzebnie, jedna by wystarczyła) i identyfikację Answer po kluczu obcym zamieniłem na embedded id. No i mogę dodawać i odczytywać pytania. Teraz chciałbym się zabrać za klasę Exam. Jako że jeden egzamin może zawierać wiele pytań, a pytanie może pojawiać się w wielu egzaminach wydaje mi się, że powinna to być relacja many to many, tylko coś robię źle. Do migracji dodałem:

create table exams_questions(
exam_id int,
question_id int,
FOREIGN KEY (exam_id) REFERENCES Exams(id),
FOREIGN KEY (question_id)  REFERENCES Questions(id)
);

Id i encję zrobiłem analogicznie do jakiegoś przykładu z baeldung.com. Obie klasy zawierają konstruktory, gettery i settery, a ExamQuestionId wygenerowaną metodę equals i hashcode:

@Entity
@Table(name = "exams_questions")
public class ExamQuestion {

    @EmbeddedId
    ExamQuestionId id = new ExamQuestionId();

    @ManyToOne
    @MapsId("examId")
    @JoinColumn(name = "exam_id")
    private Exam exam;

    @ManyToOne
    @MapsId("questionId")
    @JoinColumn(name = "question_id")
    private Question question; 
@Embeddable
public class ExamQuestionId implements Serializable {

    @Column(name = "exam_id")
    private int examid;

    @Column(name = "question_id")
    private int questionid;

Metodę, która ma zapisać JSona wygląda tak:

@PostMapping("/exams")
public ResponseEntity<ExamWriteModel> createAnExam (@RequestBody ExamWriteModel ewm){
    Exam exam = new Exam();
    examRepository.save(exam);
    for (Question question: ewm.getQuestions()){
        questionRepository.save(question);
        ExamQuestion eq = new ExamQuestion(exam, question);
        examQuestionRepository.save(eq);
    }
    return new ResponseEntity<ExamWriteModel>(ewm, HttpStatus.CREATED);
}

a jSon:

{
"questions": [
{
    "description": "jakiś opis",
    "answers": 
    [{"content": "treść odpowiedzi", "true": false},
    {"content": "treść odpowiedzi2", "true": true},
    {"content": "treść odpowiedzi3", "true": true}]
}    
] 
}

Dostaję błąd:
Naruszenie więzów integralności: "FKRN7VRIEQS1E8NB1V462B1MJET: PUBLIC.EXAMS_QUESTIONS FOREIGN KEY(EXAM_ID) REFERENCES PUBLIC.EXAMS(ID) (0)"
I nie do końca rozumiem. Tzn. spodziewałbym się tego gdybym próbował stworzyć examQuestion zanim zapisałem Exam i Question, bo nie zostało wygenerowane id, ale tak to nie wiem. :/

0

Po co jest ta encja ExamQuestion?

Poza tym nie jestem przekonany, że te encje są managed, sprobuj zrobic na zasadzie:

exam = examRepo.save(exam);
0

@Charles_Ray: Żeby można było wprowadzając id pytania znaleźć w jakich egzaminach się znajduje no i żeby można było pytania przypisać do egzaminu.

0

No to od biedy mozesz uzyc @ManyToMany. Rozumiesz, że encje i tabelki w bazie nie sa jeden do jednego?

Sprobuj z tym przypisywaniem wyniku save na zmienna, to moze byc to.

0

Rozumiesz, że encje i tabelki w bazie nie sa jeden do jednego?

No chyba. Jeśli Cię dobrze rozumiem, to już kiedy ma się obiekt z polem przyjmującym listę jako wartość musi wyjść. :)
Koniec końców zrobiłem tak żeby nie musieć robić encji dla exams_questions:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String description;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "questions")
private List<Exam> exams = new ArrayList<>();
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "exams_questions",
        joinColumns = { @JoinColumn(name = "exam_id")},
        inverseJoinColumns = { @JoinColumn (name = "question_id")})
private List<Question> questions = new ArrayList<>();
@PostMapping("/exams")
public ResponseEntity<List<QuestionWriteModel>> createAnExam (@RequestBody List<QuestionWriteModel> qwmList){
    Exam exam = new Exam();
    for(QuestionWriteModel qwm: qwmList){
        Question question = qwm.toQuestion();
        exam.getQuestions().add(question);
        question.getExams().add(exam);
    }
    examRepository.save(exam);
    return new ResponseEntity<List<QuestionWriteModel>>(qwmList, HttpStatus.CREATED);
}

Teraz mam tylko problem z PUT. Nie wiem czy zła jest metoda czy złe żądanie wysyłam, ale dostaję status: 400 :/ :

@PutMapping("/exams")
public String updateExam(@RequestParam("exam_id")int examId,
                         @RequestParam("question_id")int questionId) {
Exam exam = examRepository.findById(examId).get();
Question question = questionRepository.findById(questionId).get();
exam.getQuestions().add(question);
examRepository.save(exam);
    return "done";
}

To, co wysyłam w Postmanie:
localhost:8080/exams?exam_id=1&qustion_id=3

0

Teraz mam tylko problem z PUT. Nie wiem czy zła jest metoda czy złe żądanie wysyłam, ale dostaję status: 400

Musisz w logach sprawdzic, co dokladnie sie dzieje.

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