I am using the following class as an entity, and a controller class to write data on it:
#Entity
#Table(name = "TableA")
public class TableA {
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, name="Id")
private BigInteger id;
#Column(nullable = false, name="Name")
private String name;
//Setters Getters
}
#RestController
public class TableAController {
#Autowired
TableARepository tableARepository;
#Transactional(rollbackFor = Exception.class)
#PostMapping(value="/CreateTableA")
public void createTableA(#RequestBody TableA newTableA){
TableA tableA = new TableA();
tableA = newTableA;
tableARepository.save(tableA);
}
}
The Id column value will be generated by the DB, so I used the #JsonProperty. But, when I test the REST API using the following as a request:
{
"name" : "Leo Messi"
}
I am getting the aforementioned error message. I have also tried the #JsonIgnore property with the same result. Is there a way to except the id property from the deserialization process? Or should I use another class dedicated the API Request? I am not comfortable with creating different models for every new API.
I am not sure if I should focus on resolving the error, or if I should design the classes using a Design Pattern that never produces it.
Related
In a simple Spring Boot Application, I'm facing with a JpaObjectRetrievalFailureException when I'm trying to save an entity with one-to-many association and client-assigned ids.
Please take a look on these entities and on this simple repository:
#Entity
#Table(name = "cart")
public class Cart {
#Id
#Column(name = "id")
private UUID id;
#Column(name = "name")
private String name;
#OneToMany
#JoinColumn(name = "cart_id")
private List<Item> items;
// constructors, getters, setters, equals and hashCode ommitted
}
#Entity
#Table(name = "item")
public class Item {
#Id
#Column(name = "id")
private UUID id;
#Column(name = "name")
private String name;
// constructors, getters, setters, equals and hashCode ommitted
}
public interface CartRepository extends JpaRepository<Cart, UUID> {
}
I wrote this test:
#DataJpaTest
class CartRepositoryTest {
#Autowired
private CartRepository cartRepository;
#Test
void should_save_cart() {
// GIVEN
final var cart = new Cart(UUID.randomUUID(), "cart");
final var item = new Item(UUID.randomUUID(), "item");
cart.setItems(List.of(item));
// WHEN
final var saved = cartRepository.save(cart);
// THEN
final var fetched = cartRepository.findById(saved.id());
assertThat(fetched).isPresent();
}
}
When I run the test, call to cartRepository.save(cart) fails with:
Unable to find com.example.testjpaonetomany.domain.Item with id f5658508-f3d0-4d9b-a1f0-17b614753356; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.example.testjpaonetomany.domain.Item with id f5658508-f3d0-4d9b-a1f0-17b614753356
org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.example.testjpaonetomany.domain.Item with id f5658508-f3d0-4d9b-a1f0-17b614753356; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.example.testjpaonetomany.domain.Item with id f5658508-f3d0-4d9b-a1f0-17b614753356
at app//org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:379)
at app//org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:235)
at app//org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
at app//org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at app//org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at app//org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at app//org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at app//org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at app//org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at app//org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at app/jdk.proxy3/jdk.proxy3.$Proxy105.save(Unknown Source)
at app//com.example.testjpaonetomany.repository.CartRepositoryTest.should_save_cart(CartRepositoryTest.java:28)
If I modify my entities by adding #GeneratedValue for ids, and in my test, I replace UUID.randomUUID() by null to delegate to Hibernate the ID generation, the test passes.
How to deal with client-generated ids?
The cause is that you save the parent object only (which is absolutely correct and fine) but still need to explain JPA that the operation should be propagated i.e.
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "cart_id")
private List<Item> items;
As minor improvements I would suggest to put the UUID generation into constructors and establish the relation via the dedicated method i.e.
final var cart = new Cart("cart");
cart.addItem(new Item("item"));
and probably consider using em.persist() instead of repository.save() as it makes a select request first in case of using uuids as #Augusto mentioned
I'm trying to create a Rest API for a school project.Therefor I'm trying to save/edit a nested Object.
I have two bidirectional entities which look like this:
EntityA
#Entity
public class EntityA {
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Id
#Column(name = "id", nullable = false)
#JsonProperty("id")
private int id;
#Column(name = "field1", nullable = false, length = -1)
#JsonProperty("field1")
private String field1;
#Column(name = "field2", nullable = false, length = -1)
#JsonProperty("field2")
private String field2;
#OneToMany(mappedBy = "entityA", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonProperty("entityB")
private List<EntityB> entityB;
public EntityA() {
}
//Getter+Setter
}
EntityB
#Entity
public class EntityB {
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Id
#Column(name = "id", nullable = false)
#JsonProperty("id")
private int id;
#Column(name = "field1", nullable = false)
#JsonProperty("field1")
private Date field1;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(...)
#JsonProperty("entityA")
private EntityA entityA;
public EntityB() {
}
//Getter+Setter
}
As RequestBody I will get JSON which should look like this.
{
"field1": "Test",
"field2": "User",
"entityB": [
{
"field1": "30.03.2022"
}
]
}
Right now Spring will automatically map the fields but as soon I try to save it to my DB I will get an error, because the relation in EntityB for EntityA is empty.
I've seen a solution, that I should loop through the EntityB list and add EntityA. I tried it with a for-each but it still sais it null.
What am I doing wrong?
public EntityA createEntityA(EntityA entityA) {
for(EntityB entityB : entityA.getEntityB()){
entityB.setEntityA(entityA);
}
return entityARepository.save(entityA);
}
Edit:
Controller
#PostMapping(value = {"/json/entitya/"})
#ResponseBody
public EntityA createEntityAJson(#RequestBody EntityA entityA) {
return entityAService.createEntityA(entityA);
}
Service
#Service
public class EntityAService {
#Autowired
private EntityARepository entityARepository;
public EntityA createEntityA(EntityA entityA) {
return entityARepository.save(entityA); //in this line the error appears
}
}
Error message
null value in column "entityA" violates not-null constraint
#Service
public class EntityAService {
#Autowired
private EntityARepository entityARepository;
#Autowired
private EntityBRepository entityBRepository;
public EntityA createEntityA(EntityA entityA) {
// create an empty arrayList to stock the entities B retrieveed from the DB
List<EnityB> lst = new ArrayList<>();
// get the entities B from the JSON and sabe it to the DB
for(EntityB entityB : entityA.getEntityB()){
entityB.setEntityA(entityA);
entityBRepository.save(entityB); // you should save entities B to the DataBase before
Optional<EntityB > opt = entityBRepository.findById(entityB.getId());
EntityB b = opt.get();
// add the entities B retrieved from the DB to the arrayList
lst.add(b);
}
// set the EntityB list with the new List from the DB ( include ids ..)
entityA.setEntityB(lst);
// save the entityA to the DB
return entityARepository.save(entityA);
}
}
I'm guessing that what is happening here is that the id fields which are of a non-nullable datatype or some other hidden field from the JPA annotations get set to the wrong value by the json deserialization for JPA to understand that they are new entities. Creating these entities manually in the Java code might solve the issue.
You shouldn't reuse your entity classes as data transfer object for your API. Having classes containing both database-specific annotations and annotations for JSON serialization is a bad idea and it goes against the single-responsibility principle (SRP).
Create separate DTO classes for your API endpoint, then read the entities from the database an copy the values from the DTO object to the entities before saving.
// Receive DTO
// Read entity from DB if update or create new entities if insert
// Copy values from DTO to entitiy
// Save entity
I think your problems will go away if you apply this pattern.
This is my Entity class (shortened for obvious reasons):
Song.java:
#Entity
#Table(name = "songs", schema = "dbo")
public class Song {
#Id
#GeneratedValue
#Column(name = "song_id")
private Integer songId;
// other properties and getter setters not needed for the question
}
Here's my controller (again, shortened for obvious reasons):
SongsController.java:
#RestController
#RequestMapping("/songs")
public class SongsController {
#Autowired
private SongRepository songs;
//GET, DELETE and PUT mappings not neeeded for the question
//POST: adds a new song to the repository
#PostMapping("/add")
public void addSong(#RequestBody(required = true) Song song) throws DuplicateItemException {
if(songs.existsById(song.getSongId())) {
throw new DuplicateItemException(); //simplest possible custom exception handler imaginable
}
songs.save(song);
}
}
Here's my POST request from POSTMAN:
{
"songName": "Song3",
"songDuration": "490"
}
Here's what the function receives:
songName: "Song3"
songDuration: "490"
songId: null
The Exception that gets thrown is "songId cannot be null". How do I fix this?
My database is PostgreSQL (12.10.1).
You need to explicitly add strategy to generated value. Otherwise it will not work. Try this:
#GeneratedValue(strategy = GenerationType.AUTO)
Your #GeneratedValue is missing how to generate the value! Given that you're using PostgreSQL and such RDMS allows the creation of database sequences, I'll suggest the following configuration ... In your Song class, you need to add the #SequenceGenerator annotation and make the following changes:
#Entity
#Table(name = "songs", schema = "dbo")
#SequenceGenerator(
name = "<AnyNameThatFitsYou>",
sequenceName = "<TheNameOfYourDatabaseSequence>",
initialValue = <DatabaseSeqInitVal>,
allocationSize = <HowManyOfTheseValuesShouldBeAllocatedInJPAMemoryForFastAccess>
)
public class Song {
#Id
#GeneratedValue(
generator = "<TheNameYouGiveToTheSequenceGenerator>",
strategy = GenerationType.SEQUENCE
)
#Column(name = "song_id")
private Integer songId;
// other properties and getter setters not needed for the question
}
That should be all ...
I have entity called Franchise and this entity looks like this:
#Data
#Entity
#Table(name = "franchises")
public class Franchise {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#Enumerated(EnumType.STRING)
private FranchiseType type;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "country_id")
private Country country;
}
I have following api endpoint for creating new Franchise entity: POST /api/franchise
And fields that I need to send are: name, type and countryId. countryId is optional field and can be null in the database (column country_id).
I am using mapstruct to map CreateFranchiseDTO to Franchise:
#Data
public class CreateFranchiseDTO {
private String name;
private String type;
private Long countryId;
}
#Mapper(componentModel = "spring")
public interface FranchiseDTOMapper {
#Mapping(source = "countryId", target = "country.id")
Franchise fromCreateFranchiseDTO(CreateFranchiseDTO dto);
}
When I call above mentioned api endpoint with countryId set to some integer (which exists in countries table) everythng works. But if I call endpoint without countryId field in request body and when the following code executes I get exception object references an unsaved transient instance:
Franchise franchise = franchiseDTOMapper.fromCreateFranchiseDTO(dto);
franchiseRepository.save(franchise);
When I look at the debugger I can see that franchise.getCountry() is equal to Country object with all fields set to NULL or zero/false (default values for primitives)
I have few options to solve this:
using #AfterMapping and check for franchise.getCountry().getId() == null
create following method in mapper: Country longToCountry(Long id)
But it seems to me that I am missing something or I am wrong. So how do I map an optional ID (relation) to field using mapstruct?
If I understand correctly, you have the same problem as this person.
What seems to have worked for that person was to use optional = true:
#ManyToOne(optional = true, fetch = FetchType.LAZY)
I'm saving two entity related to each other. After it, I can get the first entity, but I get a NullPointerException when I try to get the second entity from the first entity. This is the example:
#Entity
#Table(name = "PARAMETRIZACION")
public class Parametrizacion {
#Id
#Column(name = "id_param", unique = true, nullable = false)
private Integer idParam;
#OneToMany(fetch = FetchType.LAZY)
private List<Arreglo> listArreglo;
}
And
#Entity
#Table(name = "ARREGLO")
public class Arreglo {
#Id
#Column(name = "id_arreglo", unique = true, nullable = false)
private Integer idArreglo;
}
And my Service:
#Service
#Repository
public class TestServiceImpl implements TestService {
#Override
#Transactional(rollbackFor = Exception.class)
public void methodTest(){
...
parametrizacionRepository.saveAndFlush(parametrizacion);//Id=1
...
arregloRepository.saveAndFlush(listArreglo);//Id=1
Parametrizacion paramFetch = parametrizacionRepository.findById(1);
Log.info("Param.Id=" + paramFetch.getIdParam());
Log.info("Size=" + paramFetch.getListArreglo().size());
}
}
The result for first log is: Param.Id=1
The result for second log is: NullPointerException
How can I get the full entity including his childrens? Only If I do this query after commit transaction I can get the data but I need Save data, Update data and Find data before do Commit on finish transaction.
Maybe there is a problem with the unidirectional relationship. Try adding some #ManyToOne field in the Arreglo class and declare how should they match by adding mappedBy="" to the #OneToMany annotation.
There are some nice examples how the relations should look like:
https://en.wikibooks.org/wiki/Java_Persistence/OneToMany
What you are doing is saving parametrizacion and listArreglo separately. And this don't set any relation for parametrizacion with Arreglo. You have to set listArreglo to parametrizacion's listArreglo variable and save only parametrizacion.