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.
Related
I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading
There is a given database structure and graphql schema.
Fortunately they have a lot in common but unfortunately there are some difference.
Let's say there are entities in java to match the following database structure.
SQL:
TABLE ANIMAL
+ID NUMBER(19)
+NR_OF_LEGS NUMBER(19)
TABLE SHEEP
+ID NUMBER
+LAST_TIME_SHEARED DATETIME
+ANIMAL_ID NUMBER(19)
TABLE COW
+MILK_IN_L NUMBER(3)
+ANIMAL_ID NUMER(19)
Java:
#Entity
#Table(name = "ANIMAL")
public class Animal
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="nrOfLegs", nullable=false)
private long nrOfLegs;
}
#Entity
#Table(name = "SHEEP")
public class SheepE
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="lastTimeSheared", nullable=false)
private Datetime lastTimeSheared;
#ManyToOne(targetEntity = AnimalE.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "animalId", nullable = false, insertable = false, updatable = false)
private Animal animal;
}
#Entity
#Table(name = "COW")
public class CowE
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="milkInL", nullable=false)
private int milkInL;
#ManyToOne(targetEntity = AnimalE.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "animalId", nullable = false, insertable = false, updatable = false)
private Animal animal;
}
The existing GraphQl schema is considered to be like this:
type Sheep{
id: int!
lastTimeSheard: String!
nrOfLegs: int!
}
type Cow {
id: int!
milkInL: int!
nrOfLegs: int
}
The project uses graphql-java in version 11.0 (guess we should update soon)
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>11.0</version>
</dependency>
The graphql works fine and isimplemented like this:
#Component
public class GraphQLProvider {
#Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
#PostConstruct
public void init() {this.graphQL = /*init;*/null;}
private RuntimeWiring buildWiring() {
RuntimeWiring.Builder b = RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("freightCarrier", graphQLDataFetchers.getCow()))
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("personCarrier", graphQLDataFetchers.getSheep())));
return b.build();
}
}
#Component
public class GraphQLDataFetchers {
#AutoWired
private CowRepository cowRepo;
#AutoWired
private sheepRepository sheepRepo;
public DataFetcher getCow() {
DataFetcher dataFetcher = (DataFetchingEnvironment dfe) -> {
int id = dfe.getArgument("id");
return getGraphQlCowFromCowEntity(cowRepo.getById(id));//dirty!
};
return dataFetcher;
}
public DataFetcher getCow() {
DataFetcher dataFetcher = (DataFetchingEnvironment dfe) -> {
int id = dfe.getArgument("id");
return getGraphQlSheepFromSheepEntity(cowRepo.getById(id));//dirty!
};
return dataFetcher;
}
private Cow getGraphQlCowFromCowEntity(CowE ce){//dirty!
return new Cow(ce.getId(), ce.getMilkInL(),ce.getLegs());
}
private Sheep getGraphQlSheepFromSheepEntity(SheepE se){//dirty!
return new Sheep(se.getId(), se.getLastTime(),se.getLegs());
}
public class Sheep
private long id;
private Datetime lastTimeSheared;
private int nrOfLegs;
public Sheep(long id, DateTime lasttimeSheared, int nrOfLegs){
//u know what happens here
}
}
public class Cow
private long id;
private int milkInL;
private int nrOfLegs;
public Sheep(long id, int milkInL, int nrOfLegs){
//u know what happens here
}
}
So how to get rid of getGraphQlCowFromCowEntity and getGraphQlSheepFromSheepEntity. It double ups the code and also is in direct conflict to what graphql is suppose to be abstraction of the data. With this design here each time all fields are loaded through jpa and not only requested fields.
Imagine this is a way more complex environment with more fields.
The graphql schema can't be changed as it's not my responsibility, changing the entire back-end to match schema is also not what I want to archive.
Kind regards
You should use DTO. Retrieving and sending entity object is bad practice as you do not want your grahql api to change every time you refactor you database model, or in your case. Your Sheep and Cow objects are DTO, but you will need some way to convert your entity to DTO (getGraphQlCowFromCowEntity is fine, but you could use polymorphism - CowEntity.toDTO() - or have a service layer do the conversion, there are plenty of way to do this).
To answer your concerns about loading only the requested data, you want your DTO object to only be populated with the requested fields. One way to do this is, instead of populating all fields, have the DTO own a reference to the entity object and retrieve the data from the entity object only when requested.
public class Sheep {
private SheepE entity;
public Sheep(SheepE entity){
this.entity=entity;
}
public getId() {
return entity.getId();
}
public getLastTimeSheared() {
return entity.getLastTimeSheared();
}
...
}
Please see this answer I wrote to a similar question: Graphql Tools: Map entity type to graphql type
I have the following problem: I have three connected classes. I have annotated them but I am getting wrong results (described below):
#Entityd
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassBList")
private List<ClassB> listB;
...
}
#Entity
#Table(name="ClassB")
public class ClassB {
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassCList")
private List<ClassC> listC;
...
}
#Entity
#Table(name="ClassC")
public class ClassC {
#Id
#GeneratedValue()
private Long id = 0L;
...
#ElementCollection
private List<String> listD;
...
}
When I work with this structure for the first ClassA I create,save and load everything is ok. For a new instance of ClassA which I save to repo and load again, I suddenly have the strings of the first ClassA in listD.
The result I need is that every class is "independently" saved. So the collections of each class should hold unique (each one with its own id and sublists) objects.
What would be the best way (annotations) to model this classes in Java 8 with Spring Boot 2.2.0.M5 and javax.persistence-api 2.2 ?
EDIT:
I have now removed class B and rewrote classA to:
#Entity
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
#MapKey(name = "type")
private Map<String,Set<ClassC>> classCmap;
...
}
This is giving me an error like:
org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class
How can I model/solve/annotate this?
If you don't need to query data based on listD, I would suggest to keep the list as text in the database and use a converter:
#Converter
public class ListDConverter implements AttributeConverter<List<String>, String> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(List<String> listD) {
try {
return objectMapper.writeValueAsString(listD);
} catch(IOException e){
return null;
}
}
#Override
public List<String> convertToEntityAttribute(String stringListD) {
if(stringListD == null){
return Collections.emptyList();
}
try {
return objectMapper.readValue(stringListD, new TypeReference<List<String>>() {});
}catch(IOException e){
return Collections.emptyList();
}
}
}
and in your ClassC entity class :
#Convert(converter = ListDConverter.class)
private List<String> listD;
Why do I like this approach :
No extra table and joins => better performance
Easier to read listD in the database
#ElementCollection describes a table. So your code is probably creating a "listD" table with one column of type string, with no primary key.
Also, do you really want to use the SELECT fetch mode? That's going to generate 1 + b + b*c queries when you could just implement your data as sets (since you have unique identifiers) and use JOIN, which would result in one and only one query.
See this site for an explanation on how to use #ElementCollection.
I have 2 domain models and one Spring REST Controller like below:
#Entity
public class Customer{
#Id
private Long id;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name="COUNTRY_ID", nullable=false)
private Country country;
// other stuff with getters/setters
}
#Entity
public class Country{
#Id
#Column(name="COUNTRY_ID")
private Integer id;
// other stuff with getters/setters
}
Spring REST Controller:
#Controller
#RequestMapping("/shop/services/customers")
public class CustomerRESTController {
/**
* Create new customer
*/
#RequestMapping( method=RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
#ResponseBody
public com.salesmanager.web.entity.customer.Customer createCustomer(#Valid #RequestBody Customer customer, Model model, HttpServletRequest request, HttpServletResponse response) throws Exception {
customerService.saveOrUpdate(customer);
return customer;
}
// other stuff
}
I am trying to call above REST service with below JSON as body:
{
"firstname": "Tapas",
"lastname": "Jena",
"city": "Hyderabad",
"country": "1"
}
Where country code 1 is already there in Country table. The problem is when I am calling this service getting below error:
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation: com.test.model.Customer.country -> com.test.model.Country; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation: com.test.model.Customer.country -> com.test.model.Country
Any help will be appreciated!
Try putting CascadeType.ALL
#OneToOne(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#JoinColumn(name="COUNTRY_ID", nullable=false)
private Country country;
I had a similar problem. Two entities: Document and Status.
Document had a relationship OneToMany with Status, that represented the history of Status the Document had.
So, there was a #NotNull #ManyToOne reference of Document inside Status.
Also, I needed to know the actual Status of Document. So, I needed another relationship, this time #OneToOne, also #NotNull, inside Document.
The problem was: how can I persist both entities the first time if both had a #NotNull reference to the other?
The solution was: remove #NotNull reference from actualStatus reference. This way, it was able to persist both entities.
Just to add an additional scenario that led me to this exact same error:
Make sure that any backward references that may exist are not null.
Specifically in my case, I was using Mapstruct to update some fields of the entity, e.g.
MyClass newInstance = //...
MyClass dbInstance = repository.findByField(someField);
MyClassMapper.MAPPER.update(dbInstance, newInstance);
repository.save(dbInstance);
And my poor implementation of MyClassMapper led the backward references of dbInstance fields to be set to null when they should be pointing back to dbInstance.
I got same error and this is how I solved it:
1st Entity:
#Entity
public class Person implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int personId;
private String name;
private String email;
private long phoneNumber;
private String password;
private String userType;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "personCustomer", cascade
= CascadeType.ALL)
private Customer customer;
2nd Entity:
#Entity
public class Customer implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int customerId;
#OneToOne(fetch = FetchType.LAZY, optional = false, cascade =
CascadeType.ALL)
#JoinColumn(name = "person_customer")
#JsonIgnore
private Person personCustomer;
My Controller:
#PostMapping("/customer/registration")
public PersonCustomer addCustomer(#RequestBody Person person)
{
Customer customer = new Customer(person);
person.setCustomer(customer);
Customer cust = customerRepo.save(customer);
logger.info("{}", cust);
Optional<Person> person_Cust =
personRepo.findById(cust.getPersonCustomer().getPersonId());
Person personNew = person_Cust.get();
PersonCustomer personCust = new PersonCustomer();
if(cust.equals(null))
{
personCust.setStatus("FAIL");
personCust.setMessage("Registration failed");
personCust.setTimestamp(personCust.timeStamp());
}
personCust.setStatus("OK");
personCust.setMessage("Registration OK");
personCust.setTimestamp(personCust.timeStamp());
personCust.setPerson(personNew);
return personCust;
}
The problem got solved when I added "person.setCustomer(customer);".
As both POJO classes has each others reference, so we have to "set" each others reference before using the JPA repository method(customerRepo.save(customer));
I had the exact same problem. The solution seems to be to send the JSON like this:
{
"firstname": "Tapas",
"lastname": "Jena",
"city": "Hyderabad",
"country": {"id":"1"}
}
I guess #RequestBody tries to map an entity not a single field since the Customer instance is referencing a Country instance.
(I have similarly two entities, joined. In the DB, records for the referenced entity (Country in your case) were already created but the entity creation (Customer in your case) with a json, provided the same error message. For me CascadeType.ALL not helped but the above written change in the JSON solved the problem. For further config of course CascadeType can be considered.)
you should change :
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name="COUNTRY_ID", nullable=false)
private Country country;
to :
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name="COUNTRY_ID")
private Country country;
just delete nullable setting.
I have one entity called Change where I need log changes in database like inserting, updating or deleting rows.
So my Change table contains some data and now I would like to add foreign key to record changes in another table, but I have different tables. For example I have Weather table, Group table,... So I have done some searching and I have found a little bit about #Any annotation. So I added some columns to my Change entity:
#Entity
#Table(name = "CHANGE")
public class Change {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "CHANGE_ID")
private int changeId;
...
#Any(metaColumn = #Column(name = "RECORD_TABLE"))
#AnyMetaDef(idType = "int", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Weather.class, value = "WEATHER"),
#MetaValue(targetEntity = Group.class, value = "GROUP"),
...
})
#JoinColumn(name="recordID")
private Object record;
#ManyToOne
#JoinColumn(name = "USER_ID")
private User user;
public Object getRecord() {
return record;
}
public void setRecord(Object record) {
this.record = record;
}
...
And my stupid question is:
How can I insert data into database (like foreign ID and class name) and how could I retrieve them?
Please go through this Link
You should care about your entity relationship (1-1 or 1-M or M-M)