0

I have the following problem in java/spring.

I tried to simplify at maximum my problem with a more concrete case. To summarize, we have a class Country and each country is linked to many City. Each City is linked to another with an intermediate class Road.

I implemented it that way (see below) but this does not satisfact me because we have a redundancy in the Road object, linked to the Country by its cities and by its country attribute (for the example, let's assume we don't care for the duplicates, ie the Road Paris-Lyon and the Road Lyon-Paris).

Another precision, we don't have cross countries. All the roads and the cities under the country belong to the same country.

public class Country implements Serializable {
    ...

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "country")
    private List<City> cities;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "country")
    private List<Road> roads;
}

public class City implements Serializable {
    ...

    @ManyToOne(optional = false)
    @JoinColumn(name = "countryId", referencedColumnName = "id")
    private Country country;
}

public class Road implements Serializable {
    // Unicity by the corresponding 3 items countryId, cityFromId and cityToId
    ...

    @ManyToOne(optional = false)
    @JoinColumn(name = "countryId", referencedColumnName = "id")
    private Country country;


    @ManyToOne(optional = false)
    @JoinColumn(name = "cityFromId", referencedColumnName = "id")
    private City cityFrom;


    @ManyToOne(optional = false)
    @JoinColumn(name = "cityToId", referencedColumnName = "id")
    private City cityTo;
}

To avoid this duplicate reference, we could imagine to take off the reference to the Country class in the Road class, but this would imply to add the references @OneToMany or the Road in the class City. We would then be on a cyclic reference because of the fact that the Road is linking 2 cities (see below).

public class Country implements Serializable {
    ...

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "country")
    private List<City> cities;
}

public class City implements Serializable {
    ...

    @ManyToOne(optional = false)
    @JoinColumn(name = "countryId", referencedColumnName = "id")
    private Country country;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "cityFrom")
    private List<Road> cityFromRoad;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "cityTo")
    private List<Road> cityToRoad;
}

public class Road implements Serializable {
    // Unicity by the corresponding 3 items countryId, cityFromId and cityToId
    ...


    @ManyToOne(optional = false)
    @JoinColumn(name = "cityFromId", referencedColumnName = "id")
    private City cityFrom;


    @ManyToOne(optional = false)
    @JoinColumn(name = "cityToId", referencedColumnName = "id")
    private City cityTo;
}

Could you please help me to find a better way to make my model?

Thanks a lot!

4
  • 1
    Bonjour , would you mind write in English? Commented Jan 4, 2019 at 14:56
  • Sorry I forgot. I translated! But It did not save the "Hello" at the begining of my message... Commented Jan 4, 2019 at 15:05
  • Few suggestions . (1) Can a road start from one country and end with different country ? If yes , your 1st model cannot handle such case . (2) You don't need to always map the "relationship" in both directions . Some direction such especially some one-to-many are seldom used in the OO manner .Just map the "many-to-one" side is enough. In case when you need to get those "many" entities , you can use query to get them rather than getting the one and navigate to those "many" in order to get them. Commented Jan 4, 2019 at 15:28
  • Hello. I updated my ticket to give the precision that roads and cities belong to the same country. Thanks for the tip for (2) I will try this implementation! Commented Jan 4, 2019 at 15:48

1 Answer 1

3

I don't think you have a good concept of the actual model. A road should be a separate entity, not just a connection between two cities. For example, a road can run through many cities. Also, a city can have many roads. What you are describing is that cities can be connected to many cities.

enter image description here

But what you want to describe is that there is a ManyToMany relationship between roads and cities.

enter image description here

A simple JPA model with only unidirectional mapping would be as follows.

@Entity
public class Country {
    @Id
    private String name;

@Entity
public class City {
    @Id
    private String name;    
    @ManyToOne
    private Country country;
    @ManyToMany
    private Set<Road> roads;

@Entity
public class Road {
    @Id
    private String name;    

In order to persist the map you create a county, add cities, add roads, and describe what roads there are for each city.

tx.begin();
Country france = new Country("France");
City paris = new City("Paris");
paris.setCountry(france);
City nice = new City("Nice");
nice.setCountry(france);
City normandy = new City("Normandy");
normandy.setCountry(france);
Road r1 = new Road("R1");
Road r2 = new Road("R2");
Set<Road> roads = new HashSet<>();
roads.add(r1);
roads.add(r2);
paris.setRoads(roads);
roads = new HashSet<>();
roads.add(r2);
normandy.setRoads(roads);
roads = new HashSet<>();
roads.add(r1);
nice.setRoads(roads);

em.persist(france);
em.persist(paris);
em.persist(nice);
em.persist(normandy);
em.persist(r1);
em.persist(r2);

tx.commit();

In order to get a list of roads for a city you select the city with its list of roads. To see what cities a road connects to you select cities which contain the road in its list of roads.

TypedQuery<City> roadsForCityQuery = em.createQuery("select c from City c join fetch c.roads join fetch c.country where c = :city", City.class);

City normandyRoads = roadsForCityQuery.setParameter("city", normandy).getSingleResult();    
System.out.println("normandyRoads = " + normandyRoads);

City parisRoads = roadsForCityQuery.setParameter("city", paris).getSingleResult();
System.out.println("parisRoads = " + parisRoads);

TypedQuery<City> citiesForRoadQuery = em.createQuery("select distinct c from City c join fetch c.roads road join fetch c.country where road = :road", City.class);

List<City> r1Cities = citiesForRoadQuery.setParameter("road", r1).getResultList();  
System.out.println("r1Cities = " + r1Cities);

List<City> r2Cities = citiesForRoadQuery.setParameter("road", r2).getResultList();  
System.out.println("r2Cities = " + r2Cities);

This gives the following results:

normandyRoads = City(name=Normandy, roads=[Road(name=R2)])
parisRoads = City(name=Paris, roads=[Road(name=R1), Road(name=R2)])
r1Cities = [City(name=Nice, roads=[Road(name=R1)]), City(name=Paris, roads=[Road(name=R1), Road(name=R2)])]
r2Cities = [City(name=Normandy, roads=[Road(name=R2)]), City(name=Paris, roads=[Road(name=R1), Road(name=R2)])]
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.