Updating entity with One to One relationship using Spring Data - spring-boot

I have an issue when updating entity with OneToOne relationship, it creates record instead of updating the existing one. Below are the sample entities.
#Entity
#Table(schema = "crm", name = "persons")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
#OneToOne(mappedBy = "person", fetch = FetchType.LAZY)
public Employee getEmployee() {
return employee;
}
}
#Entity
#Table(schema = "crm", name = "employees")
public class Employee {
#Id
public Long getId() {
return id;
}
#OneToOne
#MapsId //Use the person PK id value as Employee PK id
#JoinColumn(name = "id")
public Person getPerson() {
return person;
}
}
I am using the PagingAndSortingRepository of Spring Data. Below is the service layer to update the entity.
#Override
#Transactional
public EmployeeResponse updateEmployee(Employee aEmployee) {
EmployeeResponse response = new EmployeeResponse();
try {
Optional<Employee> probableEmployee = employeeRepository.findById(aEmployee.getId());
if (!probableEmployee.isPresent()) {
throw new RecordNotFoundException(String.format(MessageConstants.EMPLOYEE_ID_NOT_FOUND, aEmployee.getId()));
}
Employee existingEmployeeToUpdate = probableEmployee.get();
EmployeeEntityHelper.updateExistingEntity(aEmployee, existingEmployeeToUpdate);
existingEmployeeToUpdate = employeeRepository.save(existingEmployeeToUpdate);
response.setSuccessfulResponse(existingEmployeeToUpdate);
} catch (Exception ex) {
LOGGER.error(ex.getLocalizedMessage(), ex);
response.setErrorAttributes(false, ReturnCode.FAILED.getCode(), ex.getLocalizedMessage());
}
return response;
}
The EmployeeEntityHelper.updateExistingEntity(source, target) will simply copy all properties of entities from source to target.
The save() method will generate an insert for Person even if I am explicitly passing the id existing in DB. But for employee it will generate an update which is expected.
Below is the updateExistingEntity() method:
public static void updateExistingEntity(Employee source, Employee target) {
copyProperties(source, target, Arrays,asList("person", "employeeNumber", "hiredDate", "birthDate"));
}
private static void copyProperties(Object aSource, Object aTarget, Iterable<String> aProperties) {
BeanWrapper sourceWrapper = PropertyAccessorFactory.forBeanPropertyAccess(aSource);
BeanWrapper targetWrapper = PropertyAccessorFactory.forBeanPropertyAccess(aTarget);
aProperties.forEach(p ->
targetWrapper.setPropertyValue(p, sourceWrapper.getPropertyValue(p))
);
}

Generally, a new INSERT instead of UPDATEs could be due to:
Either entity passed to save has no ID set, thus persist is called under the hood
Or entity with given ID is not present in the database thus merge fails (and maybe it is persisted as a fallback, dunno)
Check if entities have set ID field.

In your model , override equal/hash-code and just chek the id parameter
maybe it will help you :)

The issue is already fixed, somehow the copyproperties logic is not right in the sense that I am updating/setting the person also.

Related

Problem when attempting a saveAndFlush commit (JPA ) when primary key is auto-generated from postGres trigger

I am using spring JPA to attempt to write records to a postGres DB. At the time of the commit, I am getting the following error:
Caused by: org.postgresql.util.PSQLException: ERROR: null value in column "col_id" violates not-null constraint
Detail: Failing row contains (null, null, null, null, null)
I have the following repository interface:
public interface MyRepo extends JpaRepository <MyModel, String> {
}
, the following model class:
#Entity
#Validated
#Table(name = "my_table", schema="common")
public class MyModel {
#Id
#Column(name = "col_id")
private String id;
#Column(name = "second_col")
private String secCol;
#Column(name = "third_col")
private String thirdCol;
#Column(name = "fourth_col")
private String fourthCol;
#Column(name = "fifth_col")
private String fifthCol;
public MyModel() {
}
public MyModel(String id, String secCol, String thirdCol, String fourthCol, String fifthCol) {
this.id = id;
this.secCol = secCol;
this.thirdCol = thirdCol;
this.fourthCol = fourthCol;
this.fifthCol = fifthCol;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSecCol() {
return secCol;
}
public void setSecCol(String secCol) {
this.secCol = secCol;
}
public String getThirdCol() {
return thirdCol;
}
public void setThirdCol(String thirdCol) {
this.thirdCol = thirdCol;
}
public String getFourthCol() {
return fourthCol;
}
public void setFourthCol(String fourthCol) {
this.fourthCol = fourthCol;
}
public String getFifthCol() {
return fifthCol;
}
public void setFifthCol(String fifthCol) {
this.fifthCol = fifthCol;
}
}
, and the relevant part of the service class:
public MyModel myModel (MyModel myModel) {
MyModel mm = null;
try {
mm = myRepo.saveAndFlush(myModel);
} catch ( Exception e) {
e.printStackTrace();
}
return mm;
}
UPDATE:
I finally realized that my problem is due to a database trigger that auto-generates primary key against a complex business rule. Also, I assume I might need to use a custom save method rather than the default repo.saveAndFlush? I would be grateful for any ideas given new information. Thanks!
I reproduced the exact same code in a test project with Postgres and it worked well for me. You are absolutely correct that the values of the model class are not populated. You must share your controller also. It may really help me to help you if I can get a look where your service is being called from. Only that will help me to deduce why your model values are being passed as null in the service call.

#Embeddable Composite Key is not throwing integrity exception in with spring data JPA

I have used Spring Data JPA and #Embedabble to create the composite key.
And one Base class BaseDate will be extended by all the Entity.
sysCreationDate will be generated during insertion (not null and non-updatable)
save user is working fine for the first time but there are 3 issues here-
During the second call instead of throwing an exception it is updating the sysUpdateDate and userType
During the first call sysUpdateDate is not null (#UpdateTimestamp)
During the second call in response it returns the sysCreationDate as null
Below is the code-
Embeddable class
#Embeddable
public class CompKey implements Serializable {
#Column(name ="USER_ID")
private String userId;
#Column(name ="USER_NAME")
private String userName;
public CompKey(String userId, String userName) {
super();
this.userId = userId;
this.userName = userName;
}
public CompKey() {
super();
}
//Getters /Setters /Equual and Hashcode
}
Base Class for Date
#MappedSuperclass
public abstract class BaseDate {
#CreationTimestamp
#Column(name = "SYS_CREATION_DATE", updatable=false, nullable=false)
private Calendar sysCreationDate;
#Column(name = "SYS_UPDATE_DATE")
#UpdateTimestamp
private Calendar sysUpdateDate;
public BaseDate(Calendar sysCreationDate, Calendar sysUpdateDate) {
this.sysCreationDate = sysCreationDate;
this.sysUpdateDate = sysUpdateDate;
}
public BaseDate() {
}
//Getters and Setters
}
Entity Class
#Entity
public class User extends BaseDate{
#Column(name = "USER_TYPE")
private String userType;
#EmbeddedId
private CompKey compkey;
public User() {
super();
}
public User(Calendar sysCreationDate, Calendar sysUpdateDate, String userType, CompKey compkey) {
super(sysCreationDate, sysUpdateDate);
this.userType = userType;
this.compkey = compkey;
}
//Getters and setters
}
Repo -
#Repository
public interface UserRepo extends CrudRepository<User, CompKey> {
}
Service and Controller -
#Service
public class UserService {
#Autowired
UserRepo userRepo;
public User saveUser(User user) {
return userRepo.save(user);
}
public Optional<User> getUser(CompKey key) {
return userRepo.findById(key);
}
}
#RestController
#RequestMapping("/user")
public class UserController {
#Autowired
UserService userService;
#PostMapping("/save")
public User saveUser(#RequestBody User user) {
return userService.saveUser(user);
}
#GetMapping("/get")
public Optional<User> getUser(#RequestBody CompKey key) {
return userService.getUser(key);
}
Input -
{
"userType": "K",
"compkey": {
"userId": "1002",
"userName": "ASDF"
}
}
Output 1)-
{
"sysCreationDate": "2021-01-08T18:09:28.802+00:00",
"sysUpdateDate": "2021-01-08T18:09:28.802+00:00",
"userType": "K",
"compkey": {
"userId": "1002",
"userName": "ASDF"
}
{
"sysCreationDate": null,
"sysUpdateDate": "2021-01-08T18:10:43.206+00:00",
"userType": "K",
"compkey": {
"userId": "1002",
"userName": "ASDF"
}
}
Thanks in advance
The integrity constraint violation exception is not thrown because your Spring repository just updates the object.
Spring repositories do not differentiate between insert and update. There is only one general-purpose method -- save. By default, this method persists (inserts) a new object only when a primary key is null or 0; otherwise, it merges (updates) into an existing object. You always have a primary key set, so it always calls merge, which updates the second time.
Its basic implementation in SimpleJpaRepository looks like:
#Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
The key part is isNew method with its default implementation like:
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
}
The available solutions are:
call EntityManager directly.
implement Persistable interface from Spring and implement your own isNew to inform a Spring repository whether your object is new or was already persisted.
use a surrogate primary key (long, #GeneratedValue) and a unique constraint on your logical key
I would recommend the third solution (with a surrogate primary key) as it's simple and has better extensibility. For example, it will be easier to add a foreign key referencing your entity.
There also is a solution with calling find first, just to check if the object exists in a database. However, this solution is prone to a race issue (two concurrent REST requests to create a new object, both call find, both receive null, thus both save, and one data is lost/overwritten).
For #UpdateTimestamp, you've already got a comment, and for #CreationTimestamp null, please, post your controller.

Bidirectional #OneToMany and #ManyToOne - what`s the right way to remove child from collection without removing from database

Suppose I have two mapped entities, Field and Cluster. I would like to remove from Field one of mapped Clusters without deleting this Cluster from database. What`s the best way to do it?
Field.java
#Entity
#Table(name = "field")
public class Field extends Base {
...
#OneToMany(mappedBy = "field", fetch = FetchType.LAZY)
private List<Cluster> clusters = new ArrayList<>();
Cluster.java
#Entity
#Table(name = "cluster")
public class Cluster extends Base {
....
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "field_id")
private Field field;
Now I have to do something like that:
public FieldOutDto save(FieldInDto createRequest) {
Field newField = new Field();
modelMapper.map(createRequest, newField);
setClusters(newField, createRequest);
return modelMapper.map(fieldRepository.save(newField), FieldOutDto.class);
}
public FieldOutDto update(String id, FieldInDto updateRequest) {
Field fromDb = fieldRepository.findById(UUID.fromString(id)).orElseThrow(EntityNotFoundException::new);
modelMapper.map(updateRequest, fromDb);
clusterRepository.findAllByField_Id(UUID.fromString(id)).forEach(cluster -> cluster.setField(null));
setClusters(fromDb, updateRequest);
return modelMapper.map(fieldRepository.save(fromDb), FieldOutDto.class);
}
public void delete(String id) {
findById(id).getClusters()
.forEach(cluster -> cluster.setField(null));
fieldRepository.deleteById(UUID.fromString(id));
}
private void setClusters(Field field, FieldInDto request) {
if (request.getClustersUuid() != null && !request.getClustersUuid().isEmpty()) {
field.setClusters(request.getClustersUuid().stream()
.map(id -> {
Cluster cluster = clusterRepository.findById(UUID.fromString(id)).orElseThrow(EntityNotFoundException::new);
cluster.setField(field);
return cluster;
})
.collect(Collectors.toList()));
} else {
field.setClusters(Collections.EMPTY_LIST);
}
}
I try to find a way to get the same result without it in UPDATE
clusterRepository.findAllByField_Id(UUID.fromString(id)).forEach(cluster -> cluster.setField(null));
and in DELETE methods
findById(id).getClusters()
.forEach(cluster -> cluster.setField(null));
One of the ways to remove a child from collection without removing from the database is by creating utility methods to add or remove children(Clusters) in your Parent(Field) class as below.
#Entity
#Table(name = "field")
public class Field extends Base {
...
#OneToMany(mappedBy = "field", fetch = FetchType.LAZY)
private List<Cluster> clusters = new ArrayList<>();
public void addCluster(Cluster cluster) {
cluster.add(cluster);
cluster.setField(this);
}
public void removeCluster(Cluster cluster) {
cluster.remove(cluster);
cluster.setField(null);
}
}
Then you call these methods to add or remove a child(cluster) from the parent(Field) without deleting it from DB in advance.

Using Spring Data JPA - how to save ONLY UNIQUE items in child entity while saving parent entity and using CascadeType=PERSIST

I am preparing simple Spring app. I have 2 entities :
Book.class (parent) and Author.class (child): with #OneToMany from Author view and #ManyToOne(cascade=CascadeType.PERSIST) from Book view relations. While saving new Book - also Author is being saved and added to DB( mySql)- which is what I want. But I cannot understand why Spring adds Author - if such item already exists. How to change the code to make sure that only unique Authors will be added to DB and there will be no duplicates in author table in DB?
I've added hashCode and equals methods to Author class but it did not help.
I've tried to change also Cascade.Type but also did not help.
The Author.class(part of code):
#Entity
public class Author {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author")
#JsonManagedReference(value = "book-author")
private Set<Book> books = new HashSet<Book>();
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Author author = (Author) o;
return Objects.equals(getFirstName(), author.getFirstName()) &&
Objects.equals(getLastName(), author.getLastName());
}
#Override
public int hashCode() {
return Objects.hash(getFirstName(), getLastName());
}
And the Book.class(part of code):
#Entity
public class Book {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
private String title;
#ManyToOne( cascade=CascadeType.PERSIST)
#JoinColumn(name = "author_id", unique = true)
#JsonBackReference(value="book-author")
private Author author;
Edit 1
BookServiceImpl.class
#Override
public BookDto addBookDto(BookDto bookDto) {
Book book = bookConverter.apply(bookDto);
bookRepository.save(book);
return bookDtoConverter.apply(book);
}
AuthorServiceImpl.class
#Override
public Author findAuthor(String firstName, String lastName) {
Optional<Author> authorByNameOptional = authorRepository.findByFirstNameAndLastName(firstName, lastName);
if (authorByNameOptional.isPresent()) {
return authorByNameOptional.get();
} else {
Author newAuthor = new Author();
newAuthor.setFirstName(firstName);
newAuthor.setLastName(lastName);
return newAuthor;
}
And BookWebController.class
#PostMapping("/addWebBook")
public String addBook(#ModelAttribute(name = "addedBook") BookDto addedBook, Model model) {
Author author1 = addedBook.getAuthor();
Author author = authorService.findAuthor(author1.getFirstName(), author1.getLastName());
addedBook.setAuthor(author);
bookService.addBookDto(addedBook);
return "redirect:/message?msg";
}
Would be greatful for any hint as I am quite new to this area :-)
Let me suggest that your Author object in Book has empty primary key field? Hibernate's logic in this case: empty id means new row to insert. It may work correctly if you set a primary key to author. For example, user can find author (with it's PK) or add new (without PK) and call save(book) method which will cascadely persists only new author. In most cases that usually works like that.
Another moment to pay attention, that if you wanna keep author's uniqueness in database, than you must to put constraint on the author's entity.
For example if each author must have unique first name and last name it may look something like this:
#Entity
#Table(uniqueConstraints= #UniqueConstraint(columnNames={"first_name", "last_name"}))
public class Author {
...
After that, DataIntegrityViolationException will be thrown on duplicating value insertion and your database will stay clean of duplicates.

Relationship Exists in neo4j but not in Spring #NodeEntity

I have a class in my domain called Activity that looks like the following
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
#NodeEntity
public class Activity {
#GraphId
private Long id;
private String title;
private String description;
#Relationship(type = "RELATED_TO", direction = Relationship.UNDIRECTED)
private List<Activity> relatedActivities = new ArrayList<>();
public Activity() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public Collection<Activity> getRelatedActivities() {
System.out.println("getting relatedActivities");
System.out.println(relatedActivities);
return relatedActivities;
}
public void addRelatedActivity(Activity activity) {
this.relatedActivities.add(activity);
}
}
I create relationships using the following repository class:
#RepositoryRestResource(collectionResourceRel = "relationships", path = "relationships")
public interface RelationshipRepository extends GraphRepository<Relationship> {
#Query("MATCH (a1:Activity), (a2:Activity) " +
"WHERE a1.title = {0} AND a2.title = {1}" +
"CREATE (a1)-[:RELATED_TO]->(a2)")
void addRelationship(String a1Title, String a2Title);
}
I have verified that this code works using the neo4j browser, which lets me see existing nodes and relationships between them. However, when I access getRelatedActivities() on an Activity object, it's always an empty array, even if that Activity has other Activity nodes related to it, clearly visible in neo4j.
How can I get the relatedActivites on an Activity to automatically populate based on its relationships correctly?
The problem in your code is that you define the "target" as an Activity here
#Relationship(type = "RELATED_TO", direction = Relationship.UNDIRECTED)
private List<Activity> relatedActivities = new ArrayList<>();
but you also have a RelationshipEntity class in your code base: Relationship with the same type RELATED_TO.
When OGM gets the result it tries to match every field but since it converts the relationship type RELATED_TO to the RelationshipEntity and not an Activity object, it does not fill the list in the Activity class.

Resources