博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Why I don't want use JPA anymore
阅读量:6984 次
发布时间:2019-06-27

本文共 12444 字,大约阅读时间需要 41 分钟。

转自: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
{ } // Controller @RestController(value = "/") public class EmployeeController { @Autowired private EmployeeRepository employeeRepo; @GetMapping public ResponseEntity
>; getAll(Pageable pageable) { return new ResponseEntity<>(employeeRepo.findAll(pageable), HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity
getOne(@PathVariable Long id) { return new ResponseEntity<>(employeeRepo.findOne(id), HttpStatus.OK); }

Some of you will recognize annotations 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

 

  • Always use OptimisticLock for avoid concurrent data changing (@Version)

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 (Well integrated with Spring but Hibernate a little less: )
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 , ... (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@Entitypublic class Department { private @Id @GeneratedValue Long id; @NotNull private String name; @OneToMany(mappedBy="department", cascade={ CascadeType.ALL}, orphanRemoval = false) private Set
employees = new HashSet
(); }

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 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 transactionsession = HibernateUtil.currentSession(); tx = session.beginTransaction(); // Already contains employee entries List
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
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 .
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 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.

// Repositorypublic class EmployeeRepository extends AbstractJooqRepository { public static final List
EMPLOYEE_CREATE_FIELDS = Arrays.asList(Employee.EMPLOYEE.NAME, Employee.EMPLOYEE.AGE, Employee.EMPLOYEE.YEARS); @Transactional(readOnly = true) public Map
findAll() { List
> 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
department = convertToMap(r, Department.DEPARTMENT.ID, Department.DEPARTMENT.NAME); Map
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
create(Map
properties) throws ValidationException { validate(properties, "Employee", EMPLOYEE_CREATE_FIELDS); return this.createRecord(properties, Employee.EMPLOYEE).intoMap(); } @Transactional public Map

转载地址:http://yktpl.baihongyu.com/

你可能感兴趣的文章
CodeMirror 5.46.0 发布,多功能在线代码编辑器
查看>>
zabbix配置邮箱报警
查看>>
使用ulimit设置文件最大打开数
查看>>
XmlHttpRequest 对象详解
查看>>
浅析Banner设计
查看>>
SQL Server扩展事件(Extended Events)-- 将现有 SQL 跟踪脚本转换为扩展事件会话
查看>>
传说中的裸奔节--认识及体验CSS
查看>>
二十年后的回眸(2)——顺风顺水的前三年
查看>>
烂泥:为KVM虚拟机添加网卡
查看>>
数据驱动安全架构升级---“花瓶”模型迎来V5.0(二)
查看>>
Sql Server 常用日期格式
查看>>
让“云”无处不在-Citrix Xenserver之一 环境搭建
查看>>
CentOS 5.5下LVM的分区管理
查看>>
Vsftp与PAM虚拟用户
查看>>
GoogleAppEngine是什么?
查看>>
利用 UML 进行实体关系建模
查看>>
WCF中的Stream操作
查看>>
.NET实现之(WebService数据提供程序)
查看>>
Spread for Windows Forms快速入门(8)---单元格中用户动作触发的事件
查看>>
XXX管理平台系统——概要
查看>>