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).

JPA DTO 1

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);
    }

Cool gif

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
  • Always use OptimisticLock for avoid concurrent data changing (@Version)

    problem gif

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;

Wtf1

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);
    ...

Dto pff

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"}<--
}

Why

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
crazy

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);
    ...

Crazy 2
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 ...
dance

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.

JPA DTO 3

// 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 Long id) {
        employeeRepo.delete(id);
        return new ResponseEntity<>(null, HttpStatus.ACCEPTED);
    } 
}

@Data
public class Wrapper {
    Map<String, Object> properties = new HashMap<>();   
}

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.

原文地址:https://www.cnblogs.com/bigben0123/p/9842578.html