springdatarepository ElasticSearch works on save but fails on update with java.lang.StackOverflowError: null

Viewed 617

I am getting java.lang.StackOverflowError: null when updating my data through springdataelasticsearch. The application was generated using Jhipster 7.0.1. Spring Boot version is 2.4.4.. It works fine when creating a new object but an update results in error. Here is the relevant portion of CountryAdminUnitTypeService.java.

     * Save a countryAdminUnitType.
     *
     * @param countryAdminUnitType the entity to save.
     * @return the persisted entity.
     */
    public CountryAdminUnitType save(CountryAdminUnitType countryAdminUnitType) {
        log.debug("Request to save CountryAdminUnitType : {}", countryAdminUnitType);
        CountryAdminUnitType result = countryAdminUnitTypeRepository.save(countryAdminUnitType);
        countryAdminUnitTypeSearchRepository.save(result);
        return result;
    }

When creating, here's how the object looks on call to countryAdminUntiTypeSearchRepository.save(result);.

result = {CountryAdminUnitType@23268} "CountryAdminUnitType{id=200102, name='Province'}"
   id = {Long@23292} 200102
   name = "Province"
   parent = null
   country = {Country@23275} "Country{id=183778, name='Turkey', isoCode='TR'}"
    id = {Long@23281} 183778
    name = "Turkey"
    isoCode = "TR"
    countryAdminUnitTypes = null

And here's how it looks on updating.

result = {CountryAdminUnitType@24746} "CountryAdminUnitType{id=200102, name='Province'}"
 id = {Long@24752} 200102
 name = "Province"
 parent = null
 country = {Country@24754} "Country{id=183778, name='Turkey', isoCode='TR'}"
  id = {Long@24756} 183778
  name = "Turkey"
  isoCode = "TR"
  countryAdminUnitTypes = {PersistentSet@24761}  size = 1
   0 = {CountryAdminUnitType@24746} "CountryAdminUnitType{id=200102, name='Province'}"
    id = {Long@24752} 200102
    name = "Province"
    parent = null
    country = {Country@24754} "Country{id=183778, name='Turkey', isoCode='TR'}"

The only difference I see is that countryAdminUnitTypes is not null in second case. However this should be taken care of by JsonIgnoreProperties given in Country.java below.

Here's the beginning of a very long log file.

ERROR 91444 --- [  XNIO-5 task-1] c.s.c.s.CountryAdminUnitTypeService      : Exception in save() with cause = 'NULL' and exception = 'null'

java.lang.StackOverflowError: null
    at org.springframework.data.util.Streamable.stream(Streamable.java:87)
    at org.springframework.data.util.Streamable.lambda$map$1(Streamable.java:101)
    at org.springframework.data.util.LazyStreamable.stream(LazyStreamable.java:55)
    at org.springframework.data.util.LazyStreamable.iterator(LazyStreamable.java:46)
    at java.base/java.lang.Iterable.forEach(Iterable.java:74)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeCollectionValue(MappingElasticsearchConverter.java:710)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.getWriteComplexValue(MappingElasticsearchConverter.java:620)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperty(MappingElasticsearchConverter.java:601)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperties(MappingElasticsearchConverter.java:553)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:511)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:636)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.getWriteComplexValue(MappingElasticsearchConverter.java:627)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperty(MappingElasticsearchConverter.java:601)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperties(MappingElasticsearchConverter.java:553)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:511)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:636)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.getWriteComplexValue(MappingElasticsearchConverter.java:627)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.lambda$writeCollectionValue$7(MappingElasticsearchConverter.java:709)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
    at java.base/java.util.Spliterators$IteratorSpliterator.tryAdvance(Spliterators.java:1812)
    at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.lambda$initPartialTraversalState$0(StreamSpliterators.java:294)
    at java.base/java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.fillBuffer(StreamSpliterators.java:206)
    at java.base/java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.doAdvance(StreamSpliterators.java:161)
    at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.tryAdvance(StreamSpliterators.java:300)
    at java.base/java.util.Spliterators$1Adapter.hasNext(Spliterators.java:681)
    at java.base/java.lang.Iterable.forEach(Iterable.java:74)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeCollectionValue(MappingElasticsearchConverter.java:710)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.getWriteComplexValue(MappingElasticsearchConverter.java:620)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperty(MappingElasticsearchConverter.java:601)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperties(MappingElasticsearchConverter.java:553)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:511)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:636)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.getWriteComplexValue(MappingElasticsearchConverter.java:627)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperty(MappingElasticsearchConverter.java:601)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeProperties(MappingElasticsearchConverter.java:553)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:511)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.writeEntity(MappingElasticsearchConverter.java:636)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.getWriteComplexValue(MappingElasticsearchConverter.java:627)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.lambda$writeCollectionValue$7(MappingElasticsearchConverter.java:709)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
    at java.base/java.util.Spliterators$IteratorSpliterator.tryAdvance(Spliterators.java:1812)
    at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.lambda$initPartialTraversalState$0(StreamSpliterators.java:294)
    at java.base/java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.fillBuffer(StreamSpliterators.java:206)
    at java.base/java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.doAdvance(StreamSpliterators.java:161)
    at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.tryAdvance(StreamSpliterators.java:300)
    at java.base/java.util.Spliterators$1Adapter.hasNext(Spliterators.java:681)
    at java.base/java.lang.Iterable.forEach(Iterable.java:74)

Here are the model classes.

CountryAdminUnitType.java

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.io.Serializable;
import javax.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
 * A CountryAdminUnitType.
 */
@Entity
@Table(name = "country_admin_unit_type")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@org.springframework.data.elasticsearch.annotations.Document(indexName = "countryadminunittype")
public class CountryAdminUnitType implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    private Long id;

    @Column(name = "name")
    private String name;

    @ManyToOne
    @JsonIgnoreProperties(value = { "parent", "country" }, allowSetters = true)
    private CountryAdminUnitType parent;

    @ManyToOne
    @JsonIgnoreProperties(
        value = { "defaultResidenceMeasurementUnit", "countryAdminUnitTypes", "preferences", "personNationalities" },
        allowSetters = true
    )
    private Country country;

    // jhipster-needle-entity-add-field - JHipster will add fields here
    public Long getId() {
        return id;
    }

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

    public CountryAdminUnitType id(Long id) {
        this.id = id;
        return this;
    }

    public String getName() {
        return this.name;
    }

    public CountryAdminUnitType name(String name) {
        this.name = name;
        return this;
    }

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

    public CountryAdminUnitType getParent() {
        return this.parent;
    }

    public CountryAdminUnitType parent(CountryAdminUnitType countryAdminUnitType) {
        this.setParent(countryAdminUnitType);
        return this;
    }

    public void setParent(CountryAdminUnitType countryAdminUnitType) {
        this.parent = countryAdminUnitType;
    }

    public Country getCountry() {
        return this.country;
    }

    public CountryAdminUnitType country(Country country) {
        this.setCountry(country);
        return this;
    }

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

    // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof CountryAdminUnitType)) {
            return false;
        }
        return id != null && id.equals(((CountryAdminUnitType) o).id);
    }

    @Override
    public int hashCode() {
        // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
        return getClass().hashCode();
    }

    // prettier-ignore
    @Override
    public String toString() {
        return "CountryAdminUnitType{" +
            "id=" + getId() +
            ", name='" + getName() + "'" +
            "}";
    }
}

Country.java

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
 * A Country.
 */
@Entity
@Table(name = "country")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@org.springframework.data.elasticsearch.annotations.Document(indexName = "country")
public class Country implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "iso_code")
    private String isoCode;

    @OneToMany(mappedBy = "country")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @JsonIgnoreProperties(value = { "parent", "country" })
    private Set<CountryAdminUnitType> countryAdminUnitTypes = new HashSet<>();

    // jhipster-needle-entity-add-field - JHipster will add fields here
    public Long getId() {
        return id;
    }

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

    public Country id(Long id) {
        this.id = id;
        return this;
    }

    public String getName() {
        return this.name;
    }

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

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

    public String getIsoCode() {
        return this.isoCode;
    }

    public Country isoCode(String isoCode) {
        this.isoCode = isoCode;
        return this;
    }

    public void setIsoCode(String isoCode) {
        this.isoCode = isoCode;
    }

    public Set<CountryAdminUnitType> getCountryAdminUnitTypes() {
        return this.countryAdminUnitTypes;
    }

    public Country countryAdminUnitTypes(Set<CountryAdminUnitType> countryAdminUnitTypes) {
        this.setCountryAdminUnitTypes(countryAdminUnitTypes);
        return this;
    }

    public Country addCountryAdminUnitType(CountryAdminUnitType countryAdminUnitType) {
        this.countryAdminUnitTypes.add(countryAdminUnitType);
        countryAdminUnitType.setCountry(this);
        return this;
    }

    public Country removeCountryAdminUnitType(CountryAdminUnitType countryAdminUnitType) {
        this.countryAdminUnitTypes.remove(countryAdminUnitType);
        countryAdminUnitType.setCountry(null);
        return this;
    }

    public void setCountryAdminUnitTypes(Set<CountryAdminUnitType> countryAdminUnitTypes) {
        if (this.countryAdminUnitTypes != null) {
            this.countryAdminUnitTypes.forEach(i -> i.setCountry(null));
        }
        if (countryAdminUnitTypes != null) {
            countryAdminUnitTypes.forEach(i -> i.setCountry(this));
        }
        this.countryAdminUnitTypes = countryAdminUnitTypes;
    }


    // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Country)) {
            return false;
        }
        return id != null && id.equals(((Country) o).id);
    }

    @Override
    public int hashCode() {
        // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
        return getClass().hashCode();
    }

    // prettier-ignore
    @Override
    public String toString() {
        return "Country{" +
            "id=" + getId() +
            ", name='" + getName() + "'" +
            ", isoCode='" + getIsoCode() + "'" +
            "}";
    }
}
2 Answers

This is because of how JHipster sets up save. It was only happening on updating objects with bi-directional relationships.

    * Save a countryAdminUnitType.
    *
    * @param countryAdminUnitType the entity to save.
    * @return the persisted entity.
    */
    public CountryAdminUnitType save(CountryAdminUnitType countryAdminUnitType) {
        log.debug("Request to save CountryAdminUnitType : {}", countryAdminUnitType);
        CountryAdminUnitType result = countryAdminUnitTypeRepository.save(countryAdminUnitType);
        countryAdminUnitTypeSearchRepository.save(result);
        return result;
    }

The object to be updated is set correctly when being passed to jpa repository countryAdminUnitTypeRepository.save(countryAdminUnitType);. However, the returned result object has circular dependencies and is directly forwarded to elasticsearch repository countryAdminUnitTypeSearchRepository.save(result); for update. I resolved it by only using the id of the object.

Line

CountryAdminUnitType result = countryAdminUnitTypeRepository.save(countryAdminUnitType);
countryAdminUnitTypeSearchRepository.save(result);

has been changed to

CountryAdminUnitType result = countryAdminUnitTypeRepository.save(countryAdminUnitType);
countryAdminUnitType.setId(result.getId());
countryAdminUnitTypeSearchRepository.save(countryAdminUnitType);
return result;
Related