Reputation: 21
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
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:
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
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