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
Question
entity class, remove the@JsonIgnore
annotation from theList<Answer> answers
fieldOn the other hand, in your
Answer
entity class you should add the@JsonIgnore
annotation to theQuestion question
field -- 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
Answer Checked By - Pedro (JavaFixing Volunteer)