Why I don't want use JPA anymore
转自: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 StackOverFlow. 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> update(Map<String, Object> properties) throws ValidationException { validate(properties, "Employee", Arrays.asList(Employee.EMPLOYEE.ID)); return this.updateRecord(properties, Employee.EMPLOYEE). intoMap(); } @Transactional public void delete(Long id) { dsl.fetchOne(Employee.EMPLOYEE, Employee.EMPLOYEE.ID.eq(id)).delete(); } } // Controller @RestController(value = "/") public class EmployeeController { @Autowired private EmployeeRepository employeeRepo; @GetMapping public ResponseEntity<Map<String, Object>> getAll() { return new ResponseEntity<>(employeeRepo.findAll(), HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<Map<String, Object>> getOne(@PathVariable Long id) { return new ResponseEntity<>(employeeRepo.findOne(id), HttpStatus.OK); } @PostMapping public ResponseEntity<> post(@RequestBody Wrapper wrapper) { try { return new ResponseEntity<>(employeeRepo.create(wrapper.getProperties()), HttpStatus.CREATED); } catch(ValidationException e) { return new ResponseEntity<>(e.getErrors(), HttpStatus.BAD_REQUEST); } } @PutMapping public ResponseEntity<?> put(@RequestBody Wrapper wrapper) { try { return new ResponseEntity<>(employeeRepo.update(wrapper.getProperties()), HttpStatus.ACCEPTED); } catch(ValidationException e) { return new ResponseEntity<>(e.getErrors(), HttpStatus.BAD_REQUEST); } } @DeleteMapping public ResponseEntity <?>delete( @RequestParam Longid ) {employeeRepo.delete(id ); return newResponseEntity<>(null ,HttpStatus.ACCEPTED ); } } @Data public class Wrapper {Map<String ,Object > properties = newHashMap <>(); }
POST usage
{ "properties": { "age":32, "name":"anthony lagrede", "years":2, "department_id":1 } }
We just wrote here exactly the same code as before. But although a little less succinct, this code has many advantages.
Advantages
-
0 trap
-
As the selection of datas is manual, it is extremely easy to limit the attributes that we want to expose on our API (eg: exclude the salary or password attribute)
-
Modifying the query to complete our API or make a new SQL query is a breeze
-
No need to use DTO, ancillary framework and therefore introduce additional complexity
-
To make an insertion, no need to make selection on related entities. 0 select 1 insert
-
Only ids are needed to specify a relationship
-
The Put and Patch methods, for partially update the entity, become very simple to manage (except for deletion ...)
Disadvantages
- As there is no object, it is impossible to use the javax.constraint. Data validation must be done manually
- Difficult or impossible to use Swagger that relies on the code to generate the documentation of our API
- Loss of visibility in the Controllers (systematic use of Map)
- JOOQ is not completely free with commercial databases
To conclude
I hope this article will help you implement the necessary actions on your JPA project and that the JOOQ example will give you keys to look things differently.
For those interested, the prototype JOOQ is available on my github.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2012-10-24 移除夏普安全管家