Spring Projections not returning the State Details

3.2k Views Asked by At

I have a Country and State table for which I have integrated with Spring Data JPA. I have created a function public Page<CountryDetails> getAllCountryDetails in my CountryServiceImpl for getting all the Country and the corresponding State details. The service is working fine and is giving me the below output:

{
  "content": [
    {
      "id": 123,
      "countryName": "USA",
      "countryCode": "USA",
      "countryDetails": "XXXXXXXX",
      "countryZone": "XXXXXXX",
      "states": [
        {
          "id": 23,
          "stateName": "Washington DC",
          "countryCode": "USA",
          "stateCode": "WAS",
          "stateDetails": "XXXXX",
          "stateZone": "YYYYYY"
        },
        {
          "id": 24,
          "stateName": "Some Other States",
          "countryCode": "USA",
          "stateCode": "SOS",
          "stateDetails": "XXXXX",
          "stateZone": "YYYYYY"
        }
      ]
    }
  ],
  "last": false,
  "totalPages": 28,
  "totalElements": 326,
  "size": 12,
  "number": 0,
  "sort": null,
  "numberOfElements": 12,
  "first": true
}

My Complete code is as given below:

CountryRepository.java

@Repository
public interface CountryRepository extends JpaRepository<CountryDetails, Integer> {

    @Query(value = "SELECT country FROM Country country GROUP BY country.countryId ORDER BY ?#{#pageable}", 
    countQuery = "SELECT COUNT(*) FROM Country country GROUP BY country.countryId ORDER BY ?#{#pageable}")
    public Page<CountryDetails> findAll(Pageable pageRequest);
}

CountryServiceImpl.java

@Service
public class CountryServiceImpl implements CountryService {

    @Autowired
    private CountryRepository countryRepository;

    @Override
    public Page<CountryDetails> getAllCountryDetails(final int page, final int size) {
        return countryRepository.findAll(new PageRequest(page, size));
    }
}

CountryDetails.java

@Entity
@Table(name = "country", uniqueConstraints = @UniqueConstraint(columnNames = "id"))
public class CountryDetails {

    @Id
    @GeneratedValue
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;
    private String countryName;
    private String countryCode;
    private String countryDetails;
    private String countryZone;

    @JsonManagedReference
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "countryDetails")
    private List<State> states;

    // getters / setters omitted
}

State.java

@Entity
@Table(name = "state", uniqueConstraints = @UniqueConstraint(columnNames = "id"))
public class State {

    @Id
    @GeneratedValue
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;
    private String stateName;
    private String countryCode;
    private String stateCode;
    private String stateDetails;
    private String stateZone;

    @JsonBackReference
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "countryCode", nullable = false, insertable = false, updatable = false, foreignKey = @javax.persistence.ForeignKey(name="none",value = ConstraintMode.NO_CONSTRAINT))
    private CountryDetails countryDetails;

    // getters / setters omitted
}

Now the Problem

Actually what I want the country service to return with minimal information like as shown below

{
  "content": [
    {
      "countryName": "USA",
      "countryCode": "USA",
      "states": [
        {
          "stateCode": "WAS"
        },
        {
          "stateCode": "SOS"
        }
      ]
    }
  ],
  "last": false,
  "totalPages": 28,
  "totalElements": 326,
  "size": 12,
  "number": 0,
  "sort": null,
  "numberOfElements": 12,
  "first": true
}

So for achieving that I have used Projections like as shown below

CountryProjection .java

public interface CountryProjection {
    public String getCountryName();
    public String getCountryCode();
    public List<StateProjection> getStates();
}

StateProjection .java

public interface StateProjection {
    public String getStateCode();
}

CountryServiceImpl.java

@Repository
public interface CountryRepository extends JpaRepository<CountryDetails, Integer> {

    @Query(value = "SELECT country.countryName AS countryName, country.countryCode AS countryCode FROM Country country GROUP BY country.countryId ORDER BY ?#{#pageable}", 
    countQuery = "SELECT COUNT(*) FROM Country country GROUP BY country.countryId ORDER BY ?#{#pageable}")
    public Page<CountryProjection> findAll(Pageable pageRequest);
}

But now the service is returning any of the state details like as shown below

{
  "content": [
    {
      "countryName": "USA",
      "countryCode": "USA"
    }
  ],
  "last": false,
  "totalPages": 28,
  "totalElements": 326,
  "size": 12,
  "number": 0,
  "sort": null,
  "numberOfElements": 12,
  "first": true
} 

How can we get the minimal state details also like as shown below

{
  "content": [
    {
      "countryName": "USA",
      "countryCode": "USA",
      "states": [
        {
          "stateCode": "WAS"
        },
        {
          "stateCode": "SOS"
        }
      ]
    }
  ],
  "last": false,
  "totalPages": 28,
  "totalElements": 326,
  "size": 12,
  "number": 0,
  "sort": null,
  "numberOfElements": 12,
  "first": true
}

Can anyone please help me on this

5

There are 5 best solutions below

2
On

Try using JsonIgnore with the fields which you don't want in the return JSON

@JsonIgnore
private String stateDetails;
0
On

You can make your CountryService return a DTO instead of the entity with only the fields you need.

Service

@Service
public class CountryServiceImpl implements CountryService {

    @Autowired
    private CountryRepository countryRepository;

    @Override
    public Page<CountryDetailsDto> getAllCountryDetails(final int page, final int size) {
        return countryRepository.findAll(new PageRequest(page, size))
                .map(c -> {
                    CountryDetailsDTO dto = new CountryDetailsDTO();
                    dto.setCountryCode(c.getCountryCode());
                    dto.setCountryName(c.getCountryName());

                    dto.setStates(c.getStates().stream().map(s -> {
                        StateDto stateDto = new StateDto();
                        stateDto.setStateCode(s.getStateCode());

                        return stateDto;
                    }).collect(Collectors.toSet()));

                    return dto;
                });
    }
}

DTO

public class CountryDetailsDTO {

    private String countryName;

    private String countryCode;

    private Set<StateDto> states;
}
public class StateDto {

    private String stateCode;
}
1
On

You can use transient keyword with the variable which you don't want within json.

ya otherwise use

@Expose String myString;
8
On

First idea

IMO there is something not right on the way you are doing things. I don't understand why are you defining the query directly like this:

@Query(value = "SELECT country.countryName AS countryName, country.countryCode AS countryCode FROM Country country GROUP BY country.countryId ORDER BY ?#{#pageable}", 
    countQuery = "SELECT COUNT(*) FROM Country country GROUP BY country.countryId ORDER BY ?#{#pageable}")

which is basically, creating a projection by select.

On the other hand, you are using Interface-based Projections which is yet again doing the projection, but by exposing the getters of the properties that you want to be projected. As far as I could see, you defined well the hierarchy through interfaces and it should be valid approach.

So what I am asking is, have you tried removing the @Query part all together?

Second idea (in eye of the comment)

Another idea can be to use the join fetch construct in jpql which is used tell to hibernate to load the association eagerly with the query.

@Query(value = "SELECT country.countryName AS countryName, country.countryCode AS countryCode, countryStates FROM Country country join fetch country.states countryStates GROUP BY country.countryId ORDER BY ?#{#pageable}"
2
On

You can check the documentation of annotation :

@JsonIgnoreProperties("fieldname")

This annotation needs to be applied on your POJO classes in your case Country*, State* mentioning the comma separated list of fields you don't need to be part of the response.

You can try this approach instead of changing to the implementation on projection style.

@JsonIgnoreProperties({ "id","countryDetails","countryZone"})
public class CountryDetails

@JsonIgnoreProperties({ "id","stateName","countryCode","stateDetails","stateZone"})
public class State