ThirstyAs42
ThirstyAs42

Reputation: 21

Spring REST API list always empty

So, I want to create a REST API with Spring in Intellij (using Gradle) that manages countries. Every country has multiple regions (as a list) and every region has multiple locations. I'm using Hibernate and I have a class named DatabaseLoader that preloads enteries in the db. It preloads countries, but their lists are empty. Can anyone explain me what I'm doing wrong?

Here's the Country Class:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "Countries")
public class Country {
    private @Id @GeneratedValue Long id;
    private String name;

    @Autowired
    @OneToMany(mappedBy="country")
    private List<Region> regions = new ArrayList<>();

    Country (String name, List<Region> regions) {
        this.name = name;
        this.regions = regions;
    }

    Country(String name) {
        this.name = name;
    }

    Country() {}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<Region> getRegions() {
        return regions;
    }

    public void setRegions(List<Region> regions) {
        this.regions = regions;
    }

    @Bean
    @Autowired
    public void addRegion(Region region) {
        regions.add(region);
    }

    @Bean
    @Autowired
    public void listRegions() {
        System.out.println(name);
        System.out.println("xxxx");
        for (Region region : regions) {
            region.listLocations();
        }
    }
}

And the Region class:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "Regions")
public class Region {
    private @Id @GeneratedValue Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    @OneToMany(mappedBy="region")
    private List<Location> locations = new ArrayList<>();

    public Country getCountry () {
        return country;
    }

    public void setCountry (Country country) {
        this.country = country;
    }

    public Region(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<Location> getLocations() {
        return locations;
    }

    public void setLocations(List<Location> locations) {
        this.locations = locations;
    }

    @Bean
    public void addLocation(Location location) {
        this.locations.add(location);
    }

    @Bean
    @Autowired
    public void listLocations() {
        System.out.println(this.name);
        System.out.println("xxx");
        for(int i =0; i < locations.size(); i++) {
            System.out.print(locations.get(i).getId() + " ");
            locations.get(i).showAvailableSports();
        }
    }
}

And this is the DatabaseLoader:

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class DatabaseLoader {
    private static final Logger log = LoggerFactory.getLogger(DatabaseLoader.class);

    @Bean
    CommandLineRunner initDatabase(CountryRepo repo) {
        return args -> {
            Region lyon = new Region("Lyon");
            List<Region> regions = new ArrayList<>();
            Country France = new Country("France");
            France.addRegion(lyon);
            log.info("Preloading " + repo.save(France)  + " " + France.getRegions().get(1).getName());
        };
    }
}

At the output, it displays the region's name correctly, but when using Postman for a GET request, the list is empty.

Upvotes: 1

Views: 993

Answers (2)

code_mechanic
code_mechanic

Reputation: 1148

Since you have two different entities and you are saving only one (parent) here, therefore you need to cascade your persist effect to child also as given below

@OneToMany(mappedBy="country", cascade = {CascadeType.PERSIST})
private List<Region> regions = new ArrayList<>();

This will instruct the JPA provider(default hibernate) to save the child entities as well when parent (Country is saved).

Cascade.PERSIST is the one cascade type, you can have similar effects when updating/removing parent entity i.e Cascade.MERGE, Cascade.REMOVE etc, if you want to choose selectively then add some of them otherwise use Cascade.ALL, but beware this will apply on all effects, so do read the docs about it.

The name of region, you are seeing in logs, is set by you, try switching on the hibernate query logs and see if queries are fired to children table.

EDIT

Short answer: Keep your both entities in synch

public void addRegion(Region region) {
   regions.add(region);
   region.setCountry(this);
}

TL;DR

I saw one more issue with your code after the comment below and I missed it while writing this answer.

The problem is that you have bi-directional relationship between your entities and when you have that, you must keep them in synch while performing operation on one or other.

If you would check your logs(If you don't know, find the reference at bottom of this answer), then JPA (hibernate) issuing query to your DB for region entity as well. You can also query your DB and check that.

But you would find that foreign key, country_id would be null, hence your relation between two tables is not established and when you query the country, hibernate will issue a second query to fetch related region (If your fetch strategy is LAZY)

select regions0_.country_id as country_3_3_0_, regions0_.id as id1_3_0_, regions0_.id as id1_3_1_, regions0_.country_id as country_3_3_1_, regions0_.name as name2_3_1_ from region regions0_ where regions0_.country_id=?

Since, the country_id field in region would be null, you would not receive the regions with country id.

So, to keep your bidirectional relationship in synch, you can do two things:

  • Either call the setter on both entities.
  • Or, call region setter inside the addRegion function of your country entity.

Call setter on both entities

You have addRegion function in country entity, which is adding region to its List<Region>

public void addRegion(Region region) {
   regions.add(region);
}

and you have setCountry in region entity

public void setCountry (Country country) {
   this.country = country;
}

You need to call both methods in your command line runner, on both entity, so that hibernate can apply that foreign key for you in table.

Region lyon = new Region();
lyon.setName("Lyon");

Country france = new Country();
france.setName("France");
france.addRegion(lyon);

lyon.setCountry(france);

//other code below it to save

Call region setter inside the addRegion function

Problem with earlier approach that client has to know that it needs to call setter on both entities.

To resolve this, you can have both operation hidden from client inside addRegion function as given below

public void addRegion(Region region) {
   regions.add(region);
   region.setCountry(this);
}

This will update the country_id field in region table and you must get the country with regions in your get request.

These are the queries it fired, when application run (yours may have more fields)

insert into country (name) values (?)
insert into region (country_id, name) values (?, ?)

If you are wondering how I got this SQL to be printed, then worry not its easy and you must have it on in development to check the queries.

You can simply add these two properties in your application.properties

spring.datasource.jpa.show-sql=true
logging.level.org.hibernate.SQL=debug

Note : You must annotate the @JsonIgnore on your getCountry() inside your Region class, otherwise it would through stackoverlow error, because of Jackson trying to recursively serialize your Country object and all its properties, if you are returning entity object as response.

Also, why you have these @Bean annotation over getters or setters of your entity class? Either you have misunderstood the @Bean annotation or you are trying to do something which you should not do.

Upvotes: 1

ThirstyAs42
ThirstyAs42

Reputation: 21

So, after I POST another country, it has the id 3, which means that id 2 is set to the region, which is saved in the table.screenshot of POST But the problem is that when I use GET request, the regions list is empty.screenshot of GET

Upvotes: 0

Related Questions