Search code examples
javaspring-bootpersistence

TransientObjectException when saving a Map attribute in Spring Boot


I'm getting the following error when looking up a persisted object which has a Map attribute:

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: [package].MapKey; nested exception is java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: [package].MapKey

Most explanations I found refer to adding CascadeType.ALL, which I have done.

The error appears only when I execute a custom query, not with the findById method:

EntityWithMap saved = service.save(entity);
    
assertEquals(entity.getMap(), service.findById(saved.getId()).get().getMap()); //No error

assertEquals(entity.getMap(), service.findByName("test entity").get(0).getMap()); //InvalidDataAccessApiUsageException

EntityWithMap:

@Entity
public class EntityWithMap {

    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "mapping_mapkey_mapvalue", 
      joinColumns = {@JoinColumn(name = "value_id", referencedColumnName = "id")},
      inverseJoinColumns = {@JoinColumn(name = "entity_id", referencedColumnName = "id")})
    @MapKeyJoinColumn(name = "key_id", referencedColumnName = "id")
    private Map<MapKey, MapValue> map = new HashMap<>();

    private String name;
    
    public EntityWithMap(String name) {
        this.name = name;
    }

    public Map<MapKey, MapValue> getMap() {
        return map;
    }
    
    public Long getId() {
        return id;
    }
    
    public void addToMap(MapKey key, MapValue value) {
        map.put(key, value);
    }
    
}

MapKey:

@Entity
public class MapKey {

    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    
}

MapValue:

@Entity
public class MapValue {

    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    
}

Test class:

@DataJpaTest
@Import(EntityWithMapService.class)
public class PersistMappingTest {

    @Autowired private EntityWithMapService service;
    
    @Test
    public void testPersistence() {
        
        EntityWithMap entity = new EntityWithMap("test entity");
        entity.addToMap(new MapKey(), new MapValue());
        entity.addToMap(new MapKey(), new MapValue());
        entity.addToMap(new MapKey(), new MapValue());

        EntityWithMap saved = service.save(entity);
        
        assertEquals(entity.getMap(), service.findById(saved.getId()).get().getMap()); //No error

        assertEquals(entity.getMap(), service.findByName("test entity").get(0).getMap()); //InvalidDataAccessApiUsageException
    }
}

EntityWithMapService:

@Service
public class EntityWithMapService {

    private EntityWithMapRepository repository;

    public EntityWithMapService(EntityWithMapRepository repository) {
        this.repository = repository;
    }

    public EntityWithMap save(EntityWithMap entity) {
        return repository.save(entity);
    }

    public Optional<EntityWithMap> findById(Long id) {
        return repository.findById(id);
    }
    
    public List<EntityWithMap> findByName(String name) {
        return repository.findByName(name);
    }
    
}

EntityWithMapRepository:

@Repository
public interface EntityWithMapRepository extends JpaRepository<EntityWithMap, Long> {
    
    @Query("FROM EntityWithMap e WHERE e.name = :name")
    public List<EntityWithMap> findByName(@Param("name") String name);

}

Solution

  • There's a couple of things that seem to be off in your example.

    1. Your test PersistMappingTest is trying to persist a record of EntityWithMap with references to instance of MapKey and MapValue without persisting them first. You need to persist the MapKey and MapValue records before you can use them as references in the EntityWithMap record. This might be the primary reason you get TransientObjectException.

    Example (pseudo code):

    MapKey mapKey1 = mapKeyService.save(new MapKey());
    MapKey mapKey2 = mapKeyService.save(new MapKey());
    MapKey mapKey3 = mapKeyService.save(new MapKey());
    
    MapValue mapValue1 = mapValueService.save(new MapValue());
    MapValue mapValue2 = mapValueService.save(new MapValue());
    MapValue mapValue3 = mapValueService.save(new MapValue());
    
    EntityWithMap entity = new EntityWithMap("test entity");
    entity.addToMap(mapKey1, mapValue1);
    entity.addToMap(mapKey2, mapValue2);
    entity.addToMap(mapKey3, mapValue3);
    

    NOTE: if it's intentional to not persist the MapKey and MapValue map in the DB and it's only for in-memory use then, try adding the @Transient annotation to the map field in EntityWithMap.

    1. You MapValue entity is not referencing MapKey at all. How can MapKey be the key of MapValue if MapValue is not aware of it.

    Example (pseudo code):

    @Entity
    public class MapValue {
    
        @Id @GeneratedValue(strategy=GenerationType.AUTO)
        private Long id;
    
        @ManyToOne(cascade = CascadeType.ALL)
        @JoinColumn(name = "mapkey_id")
        private MapKey mapKey;
        
    }
    
    1. You shouldn't need to create a new instance of HashMap<> in the declaration of the map in entity EntityWithMap. JPA should do that for you. This could also be a reason you get the exception.

    Look at this article for more information: https://www.baeldung.com/hibernate-persisting-maps