Javers - DiffIgnore on bidirectional OneToMany - java

I am trying to integrate Javers with a Spring Data REST project. Currently I have the following entities in my domain.
Student.class
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private Long dob;
#OneToOne
private Gender gender;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "student", orphanRemoval = true)
private List<ContactNumber> contactNumbers = new ArrayList<>();
}
ContactNumber.class
#Entity
public class ContactNumber {
#Id
#GeneratedValue
private Long id;
private String phoneNumber;
private Boolean isPrimary;
#ManyToOne
private Student student;
}
In the javers docs it is mentioned that:
In the real world, domain objects often contain various kind of noisy
properties you don’t want to audit, such as dynamic proxies (like
Hibernate lazy loading proxies), duplicated data, technical flags,
auto-generated data and so on.
So does that mean I put a #DiffIgnore on the #ManyToOne student field in the contact number class or the #OneToMany contacts field in the student class?

It depends how you're logging the objects and what you want to log. Consider these two lines (suppose that you have a link between p and contactNumber)
//This logs p and contactNumber as part of the array part of p. If you want to ignore contactNumber here,
//add #DiffIgnore on the #OneToMany property. If you want to ignore
javers.commit("some_user", p);
//This would log contactNumber and p as the student. You can add #DiffIgnore here on the student property (#ManyToOne)
javers.commit("some_user", contactNumber);
Note that there is another annotation #ShallowReference that will log the id of the object instead of logging the entire object. E.g. if you add #ShallowReference to the student property it will not log the entire Person object, but only its ID. You can use that instead to have a link between those objects.
UPDATE:
Looking at your model, I'd recommend that you remove the student property. It doesn't make sense to have a link to the student from the phone number. The student has a number assigned, not the other way around. Thus your model would look like this.
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private Long dob;
#OneToOne
private Gender gender;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "student", orphanRemoval = true)
private List<ContactNumber> contactNumbers = new ArrayList<>();
}
ContactNumber.class
#Entity
public class ContactNumber {
#Id
#GeneratedValue
private Long id;
private String phoneNumber;
private Boolean isPrimary;
}
If you really need to find a Person/Student starting with a phone number you can have a Repository for your Person class that enables you to do that search. That would look like this:
//extend from the corresponding Spring Data repository interface based on what you're using. I'll use JPA for this example.
interface PersonRepository extends JpaRepository<Person, Long> {
Person findByPhoneNumber(String phoneNumber);
}
With this, your model is cleaner and you don't need to use DiffIgnore at all.

Related

Spring data JPA save and updating parent entity

I have two entities called Student and Subject. They are stored in tables in the following format
student_id
name
grade
1
John
1
subject_id
name
1
English
2
Math
subject_id
student_id
mark
1
1
75
2
1
75
**Student:**
#Table(name = "student")
#Data
public class Student {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "grade")
private int grade;
//getters and setters left out for this example
}
**Subject:**
#Table(name = "subject")
#Data
public class Subject {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(name = "name")
private String name;
//getters and setters left out for this example
}
**StudentRepository:**
public interface StudentRepository extends JpaRepository<Student, Long> {
}
How do I make it so that everytime I add a student using a StudentController, the subjects are automatically added to the student.
Create the third entity for the third table, create the student object and the subject object . put it in the third entity object, create the third repository and save that, all three tables will be updated together. Just make sure your relationships are correctly mentioned and you are done.
Update your Student entity to encapsulate Subject.
The idea is to explicitly define relationship between Student and Subject , and further leverag Cascade to propagate changes :
#Table(name = "student")
#Data
public class Student {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "grade")
private int grade;
#OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE}, orphanRemoval = true)
#JoinColumn(name = "SUBJECT_ID")
Subject subject;
}
Note : You need to make sure that you populate Subject when storing Student.
For more clarity , explore the examples presented here : https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/

How to deal with a OneToOne relationship when implementing the DTO pattern for a REST API?

Given two entities Employee and EmployeeAddress, I am trying to implement the DTO pattern - mainly because my IDE shows a warning when using an entity as parameter in my REST controller.
In this context, I have a question regarding how to deal with the OneToOne relationship between these two entities:
The parent entity:
#Entity
#Table
public class Employee{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "employee_address_id")
private EmployeeAddress employeeAddress;
}
The child entity:
#Entity
#Table
public class EmployeeAddress{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String postalCode;
#OneToOne(mappedBy="employeeAddress")
private Employee employee;
}
My first idea was to introduce the following two DTOs:
#Getter
#Setter
public class EmployeeDTO {
private Long id;
private String firstName;
private String lastName;
private EmployeeAddressDTO employeeAddress;
}
#Getter
#Setter
public class EmployeeAddressDTO {
private Long id;
private String street;
private String postalCode;
}
This doesn't seem to work, however: I have to replace the EmployeeAddressDTO inside my EmployeeDTO with the actual entity EmployeeAddress in order for this to work. This, however, seems a bit contradicting to me - why would I create an EmployeeDTO only for it to contain an entity?
So, I wonder, how do I deal with this OneToOne relationship in my DTO?
Do I have to create an EmployeeDTO as:
#Getter
#Setter
public class EmployeeDTO {
private Long id;
private String firstName;
private String lastName;
private String street;
private String postalCode;
}
Would this be the right approach?
why would I create an EmployeeDTO only for it to contain an entity?, that is the whole question about DTOs.
In small or toy apps there may even no point for DTOs anyway. When your app is growing, things are different. It is good practice to separate the persistence (entities) from the presentation (or API, or interface to the outside world).
Entities defining the app data model (the data that needs your app to do the work), DTOs are what you want to export (in a controlled way) to the world.
Of course, at the beginning it is mostly the same data.

Mapping entities in JPA/Hibernate without adding additional fields (just by using ids)

I'm trying to map those three entities to each other without adding any additional fields to any of them. They should only contain the fields that already exist. I'm also trying to only get columns in the tables that represent the currently existing entity fields- and no additional columns.
#Entity
public class Order {
#Id
private Integer orderId;
private String title;
private Customer customer;
private List<Comment> comments;
}
#Entity
public class Customer {
#Id
private Integer customerId;
private String name;
}
#Entity
public class Comment {
#Id
private Integer commentId;
private Integer orderId;
private String details;
}
My understanding is that I can't simply use #OneToOne, #OneToMany and #ManyToOne mappings, because neither Customer nor Comment has a reference to Order . I'm trying to somehow reference the ids of Customer and Comment directly from Order.
I've tried using #MapsId and #JoinColumn but either I don't know how to properly use them, or they don't do what I think they do.
Is this task at all possible? If so, how to map them to each other?
For the reference to Comment you must use #JoinColum
The Customer reference assumes that there is a customer_id on the order table.
#Entity
public class Order {
#Id
private Integer orderId;
private String title;
#ManyToOne
#JoinColumn(name = "customer_id")
private Customer customer;
#OneToMany
#JoinColumn(name = "comment_id")
private List<Comment> comments;
}

Json Result has duplicates in ManyToMany with a New Entity in Spring Boot

I'm trying to fetch all Employees with a relational table EmployeeCourse and to see how many Courses the employee has. When I use employeeRepo.findAll() method inside my Controller I got JSON result like this:
[{"firstName":"Pera","lastName":"Peric","employeeCourses":[],"employeeId":1},{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":{"firstName":"Marko","lastName":"Markovic","employeeCourses":[{"id":1,"employee":
A lot of rows were removed for brevity and other entities employees were not loaded.
my Entities look like this:
#Entity
public class Employee{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "employee_id")
private Integer id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "employee", cascade = CascadeType.ALL)
Set<EmployeeCourse> employeeCourses;
// getters and setters implemented
// dont have hashCode() and equals overridden
}
#Entity
public class EmployeeCourse {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne
#JoinColumn(name = "employee_id")
Employee employee;
#ManyToOne
#JoinColumn(name = "course_id")
Course course;
private Date startDate;
private Date endDate;
//have getter and setters and overridden hashCode() and equals
}
public class Course {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "course_id")
private Integer id;
private String name;
private String description;
#OneToMany(mappedBy = "course")
Set<EmployeeCourse> employeeCourses;
}
Why do I have this double results, and how to fetch them?
It is pretty hard to read your JSON data, but I guess that with double results I assume you are talking about the huge amount of Employee data in your JSON response. This is due to the bi-directional relationships:
Employee <--> EmployeeCourse: Employee has a reference to Set<EmployeeCourse> and EmployeeCourse has a reference to Employee.
EmployeeCourse <--> Course: EmployeeCourse has a reference to Course and Course has a reference to Set<EmployeeCourse>.
This means that when Jackson serializes your Employees it will serialize all this duplicated information.
Having said that you have now three options:
You turn the bi-directional relationship into a uni-directional one, by, for example, removing the employee attribute in EmployeeCourse.
If for whatever reason 1. is not possible, then you might use #JsonIgnore so that Jackson does not serialize them.
If 2. is also not possible you may consider using DTOs. Using DTOs instead of plain Entities is usually advisable when returning data via a Controller. With DTOs, you clearly define whatever you want to include in the response to the caller. You can even reorganize your model so that it fits better the needs of the clients. You decouple your inner model and the model your API consumers know, making it possible to rework your inner model and still keep the same public model.
Just put #JsonIgnor on top of the object which you don't want to expand. Ex. in course if you don't want to expand the result of employeeCourses then just do something like this
#OneToMany(mappedBy = "course")
#JsonIgnore
Set<EmployeeCourse> employeeCourses;

JPA: How to remove unnecessary SELECT before save()?

I have a Entity Student which is #ManyToOne with another Entity School, Where School is pre-existing in the database and is fixed.
Entity User:
#Data
#Entity(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(nullable = false)
private String username;
#ManyToOne
private School school;
}
Entity School:
#Data
#Entity(name = "school")
public class School {
#Id
#Column(unique = true, nullable = false)
private int id;
#Column(nullable = false)
private String name;
private String shorten;
#JsonProperty(value = "logo_url")
private String logoUrl;
private float longitude;
private float latitude;
#Column(nullable = false)
private boolean opened;
}
When adding a user, I POST the following json from Postman:
{
"username": "abcd",
"school_id": 2
}
Then,
School school = new School();
school.setId(2); //"school_id" above
User user = new User();
user.setUsername("abcd");
user.setSchool(school);
userRepository.save(user);
Because I think that to add a new user, only the School id is enough, and no other School parameters are required. But every time I run, it will run the select statement to select all fields of School by id before save().
My question is: how to remove this unnecessary operation so that before the save(), there is no need to select? (I know that custom sql statements can be implemented, but I feel like this will break the object orientation of JPA)
use below annotation on entity class
#SelectBeforeUpdate(value=false)
You need to use getReference method to avoid this issue
School school = entityManager.getReference(School.class, 2);
If you are using Spring Data JPA, this method is exposed as getOne on the repository.
See How do find and getReference EntityManager methods work when using JPA and Hibernate

Categories