Home > Software engineering >  List of objects not displayed because undefined in Spring boot
List of objects not displayed because undefined in Spring boot

Time:02-06

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:

enter image description here

homepage.component is the component in which the following click event occurs:

<div >
          <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" >
 
  <!-- 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):

enter image description here

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>

CodePudding user response:

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:

  1. In your Question entity class, remove the @JsonIgnore annotation from the List<Answer> answers field

  2. On the other hand, in your Answer entity class you should add the @JsonIgnore annotation to the Question 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;
    ...
}
  •  Tags:  
  • Related