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

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.

Related

Spring JPA CriteriaQuery groupBy based on only one key in a Composite Primary Key (#EmbeddedId)

I am trying to write a criteriaQuery and group results based on emailId, which is one of the keys of a composite PK, embedded into one of my Entity classes.
The method that returns the specification is as follows :
public static Specification < User > getSpecification(Integer id) {
return (root, query, criteriaBuilder) - > {
var predicates = new ArrayList<Predicate()>;
predicates.add(criteriaBuilder.equal(root.get("indexId"), id));
query.groupBy(root.get("details").get("emailId"));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
The entity class:
public class User {
#EmbeddedId private Details details;
private String name;
private String status;
}
#Embeddable
public class Details {
private String emailId;
private Integer branchId;
}
I have skipped some annotations.
I want to group the results in a way where emailId remains unique, even if branchId changes. In essence for data where there are 3 rows of same emailId but different branchId, I should only fetch 1 result.
It seems it throws an error and asks for both components of the composite key to be passed in the query.groupBy statement.
If you could please help me figure out the issue.

JHipster - Insert in the database with the GET method

I have to create an application with Jhipster but i never use it before.
When a user send a GET request to the address http://localhost:8080/api/newmesure/{mac-address}/{value}
I want to insert a new mesure in my database.
First i created 3 entity "Plantes", "Capteurs" and "Mesures" with this format :
Image here : https://i.stack.imgur.com/zJqia.png (I'm not allowed to post)
I activated the JPA Filtering to create a #Query to insert data in my database but i read that was not possible.
In /src/main/java/com/mycompany/myapp/web/rest/MesuresRessources.java :
/**
* REST controller for managing {#link com.mycompany.myapp.domain.Mesures}.
*/
#RestController
#RequestMapping("/api")
public class MesuresResource {
private final Logger log = LoggerFactory.getLogger(MesuresResource.class);
private static final String ENTITY_NAME = "mesures";
#Value("${jhipster.clientApp.name}")
private String applicationName;
private final MesuresService mesuresService;
private final MesuresQueryService mesuresQueryService;
public MesuresResource(MesuresService mesuresService, MesuresQueryService mesuresQueryService) {
this.mesuresService = mesuresService;
this.mesuresQueryService = mesuresQueryService;
}
#GetMapping("/newMesure/{mac}/{value}")
public String newMesure(#PathVariable String mac,#PathVariable int value) {
log.debug("Adresse MAC : "+mac);
log.debug("Valeur : "+value);
#Query("SELECT valeur FROM Mesures WHERE id = 1") //not working
Mesures getValeur(); //not working
return "Mesure ajoutée";
}
}
In /src/main/java/com/mycompany/myapp/domain/Mesures.java :
/**
* A Mesures.
*/
#Entity
#Table(name = "mesures")
public class Mesures implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "valeur")
private Integer valeur;
#ManyToOne(optional = false)
#NotNull
#JsonIgnoreProperties("macs")
private Capteurs mac;
// jhipster-needle-entity-add-field - JHipster will add fields here, do not remove
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getValeur() {
return valeur;
}
public Mesures valeur(Integer valeur) {
this.valeur = valeur;
return this;
}
public void setValeur(Integer valeur) {
this.valeur = valeur;
}
public Capteurs getMac() {
return mac;
}
public Mesures mac(Capteurs capteurs) {
this.mac = capteurs;
return this;
}
public void setMac(Capteurs capteurs) {
this.mac = capteurs;
}
// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Mesures)) {
return false;
}
return id != null && id.equals(((Mesures) o).id);
}
#Override
public int hashCode() {
return 31;
}
#Override
public String toString() {
return "Mesures{" +
"id=" + getId() +
", valeur=" + getValeur() +
"}";
}
}
Louan
Learning java with JHipster is probably not a wise idea, it uses a very rich technology stack which might lose you unless you invest enough time to learn the basics.
There are many things wrong in your code and approach:
You can't use #Query annotation inside the body of method a of your REST controller, it must be used in your #Repository interface, this code can't compile. See https://www.baeldung.com/spring-data-jpa-query for a quick introduction
JPA filtering is not related to inserting into database
In HTTP/REST, GET method is supposed to be idempotent. For making changes in your database you should use POST or PUT methods. See What is idempotency in HTTP methods?
Your entity naming convention is not consistent: use singular for entity classes because each entity object represents one single instance of Mesure. Here you have Plantes (plural), Capteur (singular) and Mesures (plural). For table names, JHipster uses singular but plural is quite common too because a table holds many rows. Of course, this is just a convention and you or your team may decide to apply another (like a prefix for table names) but the key point is to be consistent.

Updating entity with One to One relationship using Spring Data

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.

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.

Hibernate One To Many join column not Saving

I am using Hibernate and Spring/ Spring MVC.
I have two entities, AccommodationRequest and Restriction.
The relationship is AccommodationRequest can have many Restriction.
When I save the AccommodationRequest with a Restriction, both the results are saved to the database
but the Restriction is missing the foreign key / join column (accommodationRequestId).
I am sure there is something I must be missing. Any help is appreciated.
AccommodationRequest is defined:
#Entity
public class AccommodationRequest {
....
#OneToMany(mappedBy="accommodationRequest",
targetEntity=Restriction.class,
fetch=FetchType.EAGER,
cascade=CascadeType.ALL)
private List<Restriction> restrictions;
public List<Restriction> getRestrictions(){
return restrictions;
}
public void setRestrictions(List<Restriction> restrictions){
this.restrictions = restrictions;
}
....
Restriction is defined:
#Entity
public class Restriction {
#Id
#GeneratedValue
private long id;
private String restriction;
#ManyToOne
#JoinColumn(name="accommodationRequestId")
private AccommodationRequest accommodationRequest;
public AccommodationRequest getAccommodationRequest() {
return accommodationRequest;
}
public void setAccommodationRequest(AccommodationRequest accommodationRequest) {
this.accommodationRequest = accommodationRequest;
}
And below is the code that does the saving:
Restriction restriction = new Restriction();
restriction.setRestriction("test restrcition 6");
List<Restriction> restrictions = new ArrayList<Restriction>();
restrictions.add(restriction);
long id = accommodationService.saveOrUpdate(accommodationRequest);
accommodationRequest.setRestrictions(restrictions);
accommodationService.saveOrUpdate(accommodationRequest);
You haven't set AccommodationRequest to your Restriction.
Restriction restriction = new Restriction();
restriction.setRestriction("test restrcition 6");
restriction.setAccommodationRequest(accommodationRequest); // You have to do this
List<Restriction> restrictions = new ArrayList<Restriction>();
restrictions.add(restriction);
long id = accommodationService.saveOrUpdate(accommodationRequest);
accommodationRequest.setRestrictions(restrictions);
accommodationService.saveOrUpdate(accommodationRequest);

Resources