JPA中,实体类(Entity)是一个对应关系型数据库表的类,实体类上通常标注了JPA注解,JPA读取这些注解来映射Java对象和数据库中的数据记录。而我们的Java代码中,所有的操作都需要通过实体管理器EntityManager提供的方法来实现。
与Hibernate类似,JPA规范定义了实体对象的4种状态,状态转换如下图所示。
新建状态(New):实体对象刚被创建,尚未与持久化上下文(Persistence Context)关联,且未保存到数据库中。
托管状态(Managed):实体对象与持久化上下文关联,并在数据库中有对应的记录。新建状态的对象被保存或是从数据库中查询出来的对象通常处于托管状态。
游离状态(Detached):实体对象曾经与持久化上下文关联,但当前持久化上下文已关闭或对象被从持久化上下文中清除,此时对象不再由EntityManager
托管,因此被称为Detached状态。
删除状态(Removed):实体对象已经被标记为删除,但尚未从数据库中删除。通常是通过调用EntityManager.remove()
方法将对象标记为删除状态。
JPA中实体类和数据库表的关联关系是通过映射配置实现的,和Hibernate类似,JPA也支持XML和注解两种方式定义映射配置,不过实际开发中XML映射配置几乎不会使用,XML配置方式不仅编写复杂而且代码可读性很低,它不是一种好的方式,大多数情况下我们都应该使用基于JPA注解的映射配置。下面例子是我们使用JPA注解定义的一个实体类。
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
// ... 省略构造函数、Get/Set方法、equals()和hashCode()方法
}
@Entity:该注解标注这个类是一个JPA的Entity实体类。
@Table:虽然实体类会自动对应一个数据表名,但实际开发中,不同的数据库可能有着不同的命名规范,例如MySQL中的数据库表名通常要求使用前缀_表名
的SnakeCase格式,@Table
注解可以明确手动设置实体类对应的数据表名,而不是使用默认的名字。
@Id:标识该字段为实体类的主键。每个JPA实体类必须有一个主键,用于唯一标识实体。
@GeneratedValue:用于指定主键生成策略,IDENTITY
即由数据库自动生成主键,常用于自增主键列。如果我们的主键是手动设置的,可以不设置该注解。
@Column:该注解用于明确指定数据库表内的字段名。
@Transient:标识该字段不需要持久化,即该字段不会映射到数据库表的列。
除了这些注解,JPA还有几个重要的关于关联映射的注解,有关关联映射我们将在下一节介绍。
实际上你仔细观察@Column
注解会发现其中还有许多可以设置的属性,比如length
、nullable
等,设置我们还能使用columnDefinition
完全指定字段的SQL定义。然而我们的数据库表是手动使用SQL语句在数据库中创建的,JPA注解中这些属性有什么用?
实际上,JPA支持自动建表,也支持检查实体类定义和表结构是否相同,并能够自动更新表结构,使用自动建表能够省去我们手动操作数据库的麻烦。如果你熟悉其它ORM框架,例如Python的DjangoORM,或是.NET平台下的EntityFramework等,就会熟悉先定义实体类然后根据实体类自动建表这种非常方便的开发模式,它通常被称为Code First模式。然而在Java中,JPA规范和ORM框架虽然能自动建表,但缺失了一个重要特性,那就是数据迁移(Data Migration)!没有数据迁移,JPA对表结构的自动更新很可能失控,这种更新也无法回退,很可能造成表结构定义丢失、数据丢失的情况,总而言之不推荐使用。
Java通常用于大型企业级项目中,这种项目的数据库管理通常也有着明确的流程和严格的规范,使用Code First开发模式其实也是不太可能的。Java开发中,我们大多数都是采用Database First开发模式,即先定义表结构,再根据表结构编写实体类。生产环境也是由程序员手动编写建表或增量修改的SQL脚本,并交给DBA执行,这种开发模式自然也不需要在实体类中指定什么length
、nullable
等,只要数据类型能映射成功即可。这也是为什么我们很少听说Java领域的ORM“数据迁移工具”的原因(当然并不是完全没有,也有Liquibase、Flyway等,只是企业级项目较少使用)。
下面例子代码中,我们针对实体类Student
的对象实现了增删改查功能,EJB接口如下。
package com.gacfox.netstore.api;
import com.gacfox.netstore.api.model.StudentDto;
import javax.ejb.Local;
@Local
public interface StudentService {
void createStudent(StudentDto studentDto);
void deleteStudent(Long id);
void updateStudentById(StudentDto studentDto);
StudentDto getStudentById(Long id);
}
下面EJB实现类中,我们通过@PersistenceContext
注入了EntityManager
,然后实现了增删改查方法。
package com.gacfox.netstore.ejb;
import com.gacfox.netstore.api.StudentService;
import com.gacfox.netstore.api.model.StudentDto;
import com.gacfox.netstore.ejb.entity.Student;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless
public class StudentServiceImpl implements StudentService {
@PersistenceContext(unitName = "jpa-netstorer-pu")
private EntityManager entityManager;
@Override
public void createStudent(StudentDto studentDto) {
Student student = new Student();
student.setId(studentDto.getId());
student.setName(studentDto.getName());
student.setAge(studentDto.getAge());
student.setCreateTime(studentDto.getCreateTime());
entityManager.persist(student);
}
@Override
public void deleteStudent(Long id) {
Student student = entityManager.find(Student.class, id);
if (student != null) {
entityManager.remove(student);
}
}
@Override
public void updateStudentById(StudentDto studentDto) {
Student student = entityManager.find(Student.class, studentDto.getId());
if (student != null) {
student.setName(studentDto.getName());
student.setAge(studentDto.getAge());
}
}
@Override
public StudentDto getStudentById(Long id) {
Student student = entityManager.find(Student.class, id);
if (student != null) {
return new StudentDto(student.getId(), student.getName(), student.getAge(), student.getCreateTime());
} else {
return null;
}
}
}
代码比较简单,我想只要理解了JPA的基本概念,代码应该不需要太多解释了。注意更新托管状态的实体对象并没有一个update()
方法,我们直接修改其中的字段即可,JPA会生成DML语句,数据库事务会自动提交,这是和Hibernate的一点区别。
JPA中并没有对批量操作的明确规范,对于批量更新和批量删除,我们可以基于JPQL查询语言实现。对于插入,我们可以通过调整persistence.xml
中的配置让Hibernate自动处理批量插入。
<property name="hibernate.jdbc.batch_size" value="50"/>
hibernate.jdbc.batch_size
的默认值是0
,即不开启批量操作。
当然,如果你的JPA底层实现不是Hibernate,那么还需要参考对应ORM框架的文档,查看是否支持类似的配置。