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
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 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":,
"years":,
"departement": {"id":,-->"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()); // 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 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":,
"name":"anthony lagrede",
"years":,
"department_id":
}
}
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.
Why I don't want use JPA anymore的更多相关文章
- 快速搭建springmvc+spring data jpa工程
一.前言 这里简单讲述一下如何快速使用springmvc和spring data jpa搭建后台开发工程,并提供了一个简单的demo作为参考. 二.创建maven工程 http://www.cnblo ...
- 玩转spring boot——结合JPA入门
参考官方例子:https://spring.io/guides/gs/accessing-data-jpa/ 接着上篇内容 一.小试牛刀 创建maven项目后,修改pom.xml文件 <proj ...
- 玩转spring boot——结合JPA事务
接着上篇 一.准备工作 修改pom.xml文件 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q ...
- springmvc+jpa实现分页的两种方式
1.工具类 public final class QueryTool { public static PageRequest buildPageRequest(int pageNumber, int ...
- spring boot(五):spring data jpa的使用
在上篇文章springboot(二):web综合开发中简单介绍了一下spring data jpa的基础性使用,这篇文章将更加全面的介绍spring data jpa 常见用法以及注意事项 使用spr ...
- 转:使用 Spring Data JPA 简化 JPA 开发
从一个简单的 JPA 示例开始 本文主要讲述 Spring Data JPA,但是为了不至于给 JPA 和 Spring 的初学者造成较大的学习曲线,我们首先从 JPA 开始,简单介绍一个 JPA 示 ...
- 一步步学习 Spring Data 系列之JPA(一)
引入: Spring Data是SpringSource基金会下的一个用于简化数据库访问,并支持云服务的开源框架.其主要目标是使得数据库的访问变得方便快捷,并支持map-reduce框架和云计算数据服 ...
- 一步步学习 Spring Data 系列之JPA(二)
继上一篇文章对Spring Data JPA更深( )一步剖析. 上一篇只是简单的介绍了Spring Data JPA的简单使用,而往往在项目中这一点功能并不能满足我们的需求.这是当然的,在业务中查询 ...
- jpa+springmvc+springdata(一)
学习尚硅谷笔记: 首先配置application.xml: <?xml version="1.0" encoding="UTF-8"?> <b ...
随机推荐
- 对B+树,B树,红黑树的理解
出处:https://www.jianshu.com/p/86a1fd2d7406 写在前面,好像不同的教材对b树,b-树的定义不一样.我就不纠结这个到底是叫b-树还是b-树了. 如图所示,区别有以下 ...
- java.lang(StringBuffer)
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharS ...
- oracle查看表结构命令desc
- embed标签的flash层级太高问题
因为客户要求,项目得兼容IE的兼容模式 页面到了flash都会遮挡底部悬浮的导航. 改变浮动窗口和embed的层级还是不可以.应该不是层级的关系. 最后百度解决方案:在embed标签内添加了wmode ...
- 将选中项的value值赋给select的title
$('select').change(function () { $(this).attr("title",$(this).find("option:selected&q ...
- 浅谈基于Prism的软件系统的架构设计
很早就想写这么一篇文章来对近几年使用Prism框架来设计软件来做一次深入的分析了,但直到最近才开始整理,说到软件系统的设计这里面有太多的学问,只有经过大量的探索才能够设计出好的软件产品,就本人的理解, ...
- linux服务器运维
1. grep正则匹配 grep -E "([0-9]{1,3}\.){4}" filepath egrep "([0-9]{1,3}\.){4}" fil ...
- SQL Server中的完全连接(full join)
一.建库和建表 create database scort use scort create table emp ( empno int primary key, ename ), sal int, ...
- 一、IntelliJ IDEA创建java项目
一.IntelliJ IDEA创建java项目 二.IntelliJ IDEA下载并包含jdbc包 1.下载zip格式的驱动包:https://dev.mysql.com/downloads/conn ...
- Nginx http2.0
109/110 HTTP2.0协议 优势必须使用TLS加密 传输数据量大幅减少 1:以二进制格式传输 2:标头压缩(header做压缩) 多路复用及相关功能 : 消息优先级 (比如样式表先渲染页面那 ...