Hironobu Nagaya Hironobu Nagaya - 25 days ago 10
Java Question

Joining entity objects by foreign key with Java Stream API

I am training for using Java Steam API and implemented what mentioned in title.

But I am dissatisfied with my code.

For example, there are three entity classes which is simple immutable bean.

CODE:

public class Country {
private final Integer countryId; // PK, primary key
private final String name;

public Country(Integer countryId, String name) {
this.countryId = countryId;
this.name = name;
}
// omitting getters
}

public class State {
private final Integer stateId; // PK
private final Integer countryId; // FK, foreign key
private final String name;

public State(Integer stateId, Integer countryId, String name) {
this.stateId = stateId;
this.countryId = countryId;
this.name = name;
}
// omitting getters
}

public class City {
private final Integer cityId; // PK
private final Integer stateId; // FK
private final String name;

public City(Integer cityId, Integer stateId, String name) {
this.cityId = cityId;
this.stateId = stateId;
this.name = name;
}
// omitting getters
}


These entities relation is one to many, not many to many.

I want to create
Map<Country, Map<State, City>>
object from entity collections as
Collection<Country>
,
Collection<State>
and
Collection<City>
using PK and FK relations.

My implementation is here.

CODE:

// entity collections
Set<Country> countries = Collections.singleton(new Country(1, "America"));
Set<State> states = Collections.singleton(new State(30, 1, "Wasington"));
Set<City> cities = Collections.singleton(new City(500, 30, "Wasington, D.C."));

// intermediate maps
Map<Integer, City> fkCityMap = cities.stream()
.collect(Collectors.toMap(City::getStateId, Function.identity()));
Map<Integer, State> fkStateMap = states.stream()
.collect(Collectors.toMap(State::getCountryId, Function.identity()));
Map<Integer, Map<State, City>> fkStateCityMap = fkStateMap.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey, entry -> Collections.singletonMap(
entry.getValue(), fkCityMap.get(entry.getValue().getStateId()))));

// result
Map<Country, Map<State, City>> mapWhatIWant = countries.stream()
.collect(Collectors.toMap(Function.identity(),
country -> fkStateCityMap.get(country.getCountryId())));


It works, but not elegant especially commented "intermediate maps" part, I think.

Is there better way to implement this?




UPDATE

There are mistakes mentioned by Holger.


  1. The type what I want is
    Map<Country, Map<State, Collection<City>>>
    ,

    not
    Map<Country, Map<State, City>>
    .

  2. I misunderstood about Washington and Washington D.C.

    So, code commented entity collections is bad example.


Answer

The way, you have defined your intermediate maps, i.e. Map<Integer, City> fkCityMap, mapping from state id to city, and Map<Integer, State> fkStateMap, mapping from country id to state, you are establishing the assumption that there can be only one State in a Country and exactly one City in a State, which indirectly also allows only one City in an entire Country.

Of course, you won’t notice as long as your test data consist only of exactly one Country, one State and one City. Even worse, even your intended result type Map<Country, Map<State, City>> works only if there is exactly one City per State. So not only your implementation is broken, even the task definition is.

Let’s redefine the result type as Map<Country, Map<State, Set<City>>>, to allow more than one City per State, so you can implement the operation as

Map<Country, Map<State, Set<City>>> mapThatYouWant = countries.stream()                
    .collect(Collectors.toMap(Function.identity(), c->states.stream()
        .filter(s -> Objects.equals(s.getCountryId(), c.getCountryId()))
        .collect(Collectors.toMap(Function.identity(), s->cities.stream()
            .filter(city -> Objects.equals(city.getStateId(), s.getStateId()))
            .collect(Collectors.toSet())))));

but note that creating intermediate maps for looking up items might be actually more efficient than searching the sets linearly multiple times. You only have to care to create the right kind of maps.

Map<Integer,Country> countryById = countries.stream()
    .collect(Collectors.toMap(Country::getCountryId, Function.identity()));
Map<Integer,Set<City>> citiesByStateId = cities.stream()
    .collect(Collectors.groupingBy(City::getStateId, Collectors.toSet()));

Map<Country, Map<State, Set<City>>> mapThatYouWant = states.stream()
    .collect(Collectors.groupingBy(s -> countryById.get(s.getCountryId()),
        Collectors.toMap(Function.identity(),
            s -> citiesByStateId.getOrDefault(s.getStateId(), Collections.emptySet()))));

By the way, Washington, D.C. is not in the state Washington.