转自:https://dev.to/alagrede/why-i-dont-want-use-jpa-anymore-fl
Great words for what is considered by many as the greatest invention in the Java world. JPA is everywhere and it is inconceivable today to writing a Java application without him. Nevertheless, each time we start a new project integrating JPA, we encounter performance problems and find traps, never met or just forgotten ...
So many failed projects are causing my bitterness for this framework. Nevertheless, it allows me today to express my feeling and to point out a crucial point explaining why Java projects are so hard to achieve.
I will try to expose you in this article, why JPA is complex in spite of its apparent simplicity and to show you that it is one of the root cause of the project issues
The belief
The evolutions of the Java Persistence API as well as the Spring integrations allow today to write backends in a very fast way and to expose our Rest APIs with very little line of code.
To schematize the operation quickly: our Java entities (2) make it possible to generate the database (1). And eventually (to not offend anyone, because this is not the debate) use a DTO layer to expose our datas (3).
So we can write without lying the following example:
//Entity @Data @NoArgsConstructor(access = AccessLevel.PRIVATE) @ToString(exclude = {"department"}) @EqualsAndHashCode(exclude = {"department"}) @Entity public class Employee { private @Id @GeneratedValue Long id; private String name; private int age; private int years; private Double salary; @ManyToOne private Department department; } @NoArgsConstructor(access = AccessLevel.PRIVATE) @Data @Entity public class Department { private @Id @GeneratedValue Long id; @NotNull private String name; } // Repository public interface EmployeeRepository extends JpaRepository<Employee, Long> { } // Controller @RestController(value = "/") public class EmployeeController { @Autowired private EmployeeRepository employeeRepo; @GetMapping public ResponseEntity<Page<Employee>>; getAll(Pageable pageable) { return new ResponseEntity<>(employeeRepo.findAll(pageable), HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<Employee> getOne(@PathVariable Long id) { return new ResponseEntity<>(employeeRepo.findOne(id), HttpStatus.OK); }
Some of you will recognize annotations Lombok that make it much easier to read the code in our example.
But in this apparent simplicity you'll encounter many bugs, quirks and technical impossibilities.
The "Best Practices" of reality
To save your time, I give you the Best Practices that you will deduce, from your years of experience with JPA and that can be found (unfortunately), scattered here and there on some blogs and *. They are obviously not complete and primarily concern the mapping of Entities.
-
Always exclude JPA associations from equals/hashcode and toString methods
- Avoid untimely collections lazy loading
- Prefer Lombok declaration for simplify reading
(@ToString(exclude = {"department"})
@EqualsAndHashCode(exclude = {"department"}))
-
Always use Set for associations mapping
- If List<> used, it will be impossible to fetch many association
-
Avoid as much as possible bidirectional declaration
- Causes bugs and conflits during save/merge
-
Never define cascading in both ways
-
Prefer use of fetchType LAZY for all associations types. (ex:
@OneToMany(fetch = FetchType.LAZY)
)- Avoid to load the graph object. Once the mapping is defined as EAGER and the code based on this behavior, it's nearly impossible to refactor it.
- Hibernate always make a new request for load a EAGER relation. (this is not optimized)
- For ManyToOne and OneToOne relations, LAZY is possible only if you use optional = false (ex:
@ManyToOne(optional = false, fetch = FetchType.LAZY)
)
-
For add element in lazy collection, use a semantic with a specific method
(ex: add()) rather than the collection getter (avoid:myObject.getCollection().add()
)- Allow to indicate the real way of the cascading (Avoid mistake)
-
Always use a join table for map collections (@ManyToMany and @OneToMany) if you want Cascade collection.
- Hibernate can't delete foreign key if it's located directly on the table
The problems of real life
Now let's talk about some basic software development problems. Example: hide from the API some attributes like salary or a password, depending on the permissions of the person viewing the Employee data.
Solution: Use JsonView (Well integrated with Spring but Hibernate a little less: hibernate-datatype)
In this case, you will have to add an annotation by use cases on all your attributes (not very flexible)
public class Employee { @JsonView(View.Admin.class) @JsonView(View.Summary.class) private @Id @GeneratedValue Long id; @JsonView(View.Admin.class) @JsonView(View.Summary.class) private String name; @JsonView(View.Admin.class) @JsonView(View.Summary.class) private int age; @JsonView(View.Admin.class) @JsonView(View.Summary.class) private int years; @JsonView(View.Admin.class) private Double salary; @JsonView(View.Admin.class) @ManyToOne private Department department;
But if I prefer to use DTO because my persistence model starts to diverge from the information to display, then your code will start to look like a set of setters to pass data from one object formalism to another:
DepartmentDTO depDto = new DepartmentDTO(); depDto.setName(dep.getName()); UserDTO dto = new UserDTO(); dto.setName(emp.getName()); dto.setAge(emp.getAge()); dto.setYears(emp.getYears()); dto.setSalary(emp.getSalary()); dto.setDep(depDTO); ...
Again, there are obviously other frameworks and libraries, more or less intelligent, magical and fast to move from an entity to a DTO is inversely like Dozer, mapstruct... (once past the cost of learning.)
The dream of insertion
Let's move on to insertion. Again, the example is simple. It's simple, it's beautiful but...
@PostMapping public ResponseEntity<?> post(@RequestBody Employe employe, BindingResult result) { if (result.hasErrors()) { return new ResponseEntity<>(result.getAllErrors(), HttpStatus.BAD_REQUEST); } return new ResponseEntity<>(employeeRepo.save(employe), HttpStatus.CREATED); }
There is the reality of objects
What will happen if I want to associate a department when creating an employee?
Not having a setDepartmentId() in Employee but a setDepartment() because we are working on with objects, and adding annotations javax.constraint as @NotNull for validation, we will have to pass all mandatory attributes of a department in addition to the Id which ultimately is the only data that interests us.
POST example:
{"name":"anthony lagrede", "age":32, "years":2, "departement": {"id":1,-->"name":"DATALAB"}<-- }
When you understand that it's going to be complicated
Now to make things worse, let's talk about detached entity with the use of Cascading.
Let's modify our Department entity to add the following relation:
@Data @Entity public class Department { private @Id @GeneratedValue Long id; @NotNull private String name; @OneToMany(mappedBy="department", cascade={CascadeType.ALL}, orphanRemoval = false) private Set<Employee> employees = new HashSet<Employee>(); }
In this code, it is the Department that controls the registration of an employee.
If you are trying to add a new department but with an existing employee (but the employee is detached), you will have to use the entityManager.merge() method.
But if you're working with JpaRepository, you've probably noticed the lack of this merge method.
Indeed, the use of em.merge() is hidden by Spring Data JPA in the implementation of the save method. (Below, the implementation of this save method)
// SimpleJpaRepository @Transactional public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } }
So, when the entity already exists (ie, already has an Id), the merge method will be called.
The problem is: when you try to add a new entity that contains an existing relationship, the em.persist() method is wrongly called. The result you will get, will be:
org.hibernate.PersistentObjectException: detached entity passed to persist: com.tony.jpa.domain.Employee
The pseudo code below tries to illustrate the problem:
// On the first transaction session = HibernateUtil.currentSession(); tx = session.beginTransaction(); // Already contains employee entries List<Employee> emps = session.createQuery("from Employee").list(); tx.commit(); HibernateUtil.closeSession(); // More later after a POST session = HibernateUtil.currentSession(); tx = session.beginTransaction(); // Create a new department entity Department dep1 = new Department(); dep1.setName("DEP 1"); // this new department already contains existing employees dep1.getEmployees().add(emps.get(0)); // employees list has a Cascading session.persist(dep1); tx.commit(); HibernateUtil.closeSession(); // Give at Runtime an Exception in thread "main" org.hibernate.PersistentObjectException: detached entity passed to persist: com.tony.jpa.domain.Employee
At this point, you have 2 choices:
- In the second transaction, reload the employee from the database and attach it to the new department
- Use session.merge instead of session.persist
So, to avoid making all the selections manually (and finally replicate the Hibernate job), you will have to manually inject the EntityManager and call the merge method.
Farewell the repository interfaces of Spring data!
Otherwise, you must reproduce all SELECT manually to reconstruct the graph of the linked entities. (DTO or not).
// manual graph construction List<Employee> emps = employeeRepo.findByIdIn(employeesId); Department dep = new Department(); dep.setName(depDTO.getName()); dep.setEmployees(emps); ...
Here the example remains simple, but we can easily imagine having to make 5, 10 selections of linked entities for 1 single insertion.
Some have even tried to write their framework to automate these selections as gilead.
The object graph literally becomes a burden when we need to manipulate data.
It's counterproductive!
What you must remember
- Many subtleties for simple mappings
- JSON representation of entities is quite rigid / or very verbose if using DTOs
- Using entities to insert, requires to manipulate objects and send too much data
- The use of Cascading give problems with the detached entities
- You have to do as many SELECTs as relations in the entity to add a single new object
We only touched few JPA problems. More than 10 years of existence and still so many traps on simple things.
We have not even talked about JPQL queries that deserve a dedicated article ...
An alternative?
For awareness, I propose to imagine for this end of article, what could be a development without JPA and without object.
Rather than starting with JDBC, which is "steep" in 2017, I suggest you take a look on JOOQ which can be an acceptable solution to manipulate data instead of JPA.
For those who do not know, JOOQ is a framework that will generate objects for each of your SQL(1) tables. Here no JPA mapping, but a JAVA-typed match for each of the columns for write SQL queries in Java.
Let's try the following concept: select employees and expose them in our API only with a map. Once again, we want to manipulate data, not the object.
// Repository public class EmployeeRepository extends AbstractJooqRepository { public static final List<TableField> EMPLOYEE_CREATE_FIELDS = Arrays.asList(Employee.EMPLOYEE.NAME, Employee.EMPLOYEE.AGE, Employee.EMPLOYEE.YEARS); @Transactional(readOnly = true) public Map<String, Object> findAll() { List<Map<String, Object>> queryResults = dsl .select() .from(Employee.EMPLOYEE) .join(Department.DEPARTMENT) .on(Department.DEPARTMENT.ID.eq(Employee.EMPLOYEE.DEPARTMENT_ID)) .fetch() .stream() .map(r -> { // Selection of attributs to show in our API Map<String, Object> department = convertToMap(r, Department.DEPARTMENT.ID, Department.DEPARTMENT.NAME); Map<String, Object> employee = convertToMap(r, Employee.EMPLOYEE.ID, Employee.EMPLOYEE.NAME, Employee.EMPLOYEE.AGE, Employee.EMPLOYEE.YEARS); employee.put("department", department); return employee; }).collect(Collectors.toList()); return queryResults; } @Transactional public Map<String, Object> create(Map<String, Object> properties) throws ValidationException { validate(properties, "Employee", EMPLOYEE_CREATE_FIELDS); return this.createRecord(properties, Employee.EMPLOYEE).intoMap(); } @Transactional public Map<String, Object