Issue
I have a list of Question , every object of type Question has a 1:n relationship with objects of type Answer (so every Question object has a list of Answer objects). I'm trying to display on the browser, using Angular material, all the Question objects with their answers after a click event, but when I try to do so, the Question objects are displayed without their answers. After some researches I found out that even if the Answer and Question objects are correcty "connected" and stored in the database, the list of answers results to be undefined as shown in the following console:
{id: 10, question: 'Question1', questionsForUser: Array(0), answers: undefined}
Answers: homepage.component.ts:32
undefined homepage.component.ts:33
How can I deal with this problem?
Here is the relationship between Question and Answer:
homepage.component is the component in which the following click event occurs:
<div class="button">
<button mat-button (click)="getQuestions()" routerLink="questions/getAllQuestions" routerLinkActive="active">Show</button>
</div>
homepage.component.ts:
export class HomepageComponent implements OnInit {
longText = `...`;
public questions: Question[] = [];
constructor(private questionService: QuestionService, private shared: SharedService) { }
ngOnInit(): void {
this.shared.castQuestions.subscribe(questions=>this.questions=questions);
}
public getQuestions():void{
this.questionService.getQuestions().subscribe(
(response: Question[]) => {
this.questions =response;
this.shared.showQuestions(this.questions);
console.log(response);
for(let i=0; i<response.length; i++){
this.questions[i].answers=response[i].answers;
console.log(response[i]);
console.log("Answers:");
console.log(response[i].answers);
}
},
(error: HttpErrorResponse) => {
alert(error.message);
}
);
}
}
After the click event, thanks to Angular routing, the tool.component code should be executed.
tool.component.html:
<table mat-table [dataSource]="questions" class="mat-elevation-z8">
<!-- Question Column -->
<ng-container matColumnDef="question">
<th mat-header-cell *matHeaderCellDef> Question </th>
<td mat-cell *matCellDef="let question"> {{question.question}} </td>
</ng-container>
<!-- Answers Column -->
<ng-container matColumnDef="answers">
<th mat-header-cell *matHeaderCellDef> Answers </th>
<td mat-cell *matCellDef="let question"> {{question.answers}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
tool.component.ts:
export class ToolComponent implements OnInit {
public questions: Question[] = [];
displayedColumns = ['question', 'answers'];
constructor(private shared: SharedService) { }
ngOnInit(): void {
this.shared.castQuestions.subscribe(questions=>this.questions=questions);
}
}
shared.service.ts:
@Injectable({
providedIn: 'root'
})
export class SharedService {
private questions= new BehaviorSubject<Array<Question>>([]);
castQuestions = this.questions.asObservable();
showQuestions(data: Question[]){
this.questions.next(data);
}
}
console.log(response):
question.service.ts:
@Injectable({
providedIn: 'root'
})
public getQuestions(): Observable<Question[]> {
return this.http.get<Question[]>('http://localhost:8080/questions/getAllQuestions');
}
}
Back/api with which I create the response:
Question entity:
@Getter
@Setter
@EqualsAndHashCode
@ToString
@Entity
@Table(name = "question", schema = "purchase")
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private int id;
@Basic
@Column(name = "question", nullable = true, length = 1000)
private String question;
@OneToMany(mappedBy = "question", cascade = CascadeType.MERGE)
private List<QuestionForUser> questionsForUser;
@OneToMany(mappedBy = "question", cascade = CascadeType.MERGE)
@JsonIgnore
private List<Answer> answers;
}
Answer entity:
@Getter
@Setter
@EqualsAndHashCode
@ToString
@Entity
@Table(name = "answer", schema = "purchase")
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private int id;
@Basic
@Column(name = "answer", nullable = true, length = 1000)
private String answer;
@ManyToOne
@JoinColumn(name = "question")
private Question question;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
QuestionRepository:
@Repository
public interface QuestionRepository extends JpaRepository<Question, Integer> {
List<Question> findByQuestionContaining(String question);
boolean existsByQuestion(String question);
}
QuestionService:
@Service
public class QuestionService {
@Autowired
private QuestionRepository questionRepository;
@Transactional(readOnly = true)
public List<Question> getAllQuestions(){
return questionRepository.findAll();
}
}
QuestionController:
@GetMapping("/getAllQuestions")
public List<Question> getAll(){
List<Question> ques = questionService.getAllQuestions();
for(Question q:ques){
System.out.println(q.getQuestion());
if(q.getAnswers()!=null){
System.out.println("The answers are: "+q.getAnswers().size());
}
}
return questionService.getAllQuestions();
}
As suggested, I tried to add a test to getAll() in QuestionController. To make the test return a string, I temporarily changed the method getAll() in this way:
@GetMapping("/getAllQuestions")//funziona
public String getAll(){
List<Question> result = questionService.getAllQuestions();
String answer = result.get(0).getAnswers().get(0).getAnswer();
return answer;
}
Then, I wrote the following test:
class QuestionControllerTest {
@Test
void getAll() {
QuestionController controller = new QuestionController(); //Arrange
String response = controller.getAll(); //Act
assertEquals("a1", response); //Assert
}
}
The first answer of the first question should be a1, but when I execute the test on IntelliJ I have the following result:
java.lang.NullPointerException: Cannot invoke "com.ptoject.demo.services.QuestionService.getAllQuestions()" because "this.questionService" is null
at com.ptoject.demo.controller.QuestionController.getAll(QuestionController.java:64) at com.ptoject.demo.controller.QuestionControllerTest.getAll(QuestionControllerTest.java:12) <31 internal calls> at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)<9 internal calls> at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)<25 internal calls>
Solution
The issue is caused by the @JsonIgnore annotation on List<Answer> answers field in your Question entity class. This is telling Jackson to ignore (i.e. don't include) this field when serializing the object into JSON.
To fix this:
In your
Questionentity class, remove the@JsonIgnoreannotation from theList<Answer> answersfieldOn the other hand, in your
Answerentity class you should add the@JsonIgnoreannotation to theQuestion questionfield -- this is to avoid potential Jackson infinite recursion issue caused by the bidrection relationship.
Question entity:
...
public class Question {
...
// remove @JsonIgnore
@OneToMany(mappedBy = "question", cascade = CascadeType.MERGE)
private List<Answer> answers;
...
}
Answer entity:
...
public class Answer {
...
@ManyToOne
@JoinColumn(name = "question")
@JsonIgnore // add this here
private Question question;
...
}
Answered By - jamesngyz


0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.