JPA从入门到精通
一、入门JPA
1. 定义实体类
**相关注解: ** @Entity
、@Table
、@Id
、@IdClass
、@GeneratedValue
、@Basic
、@Transient
、@Column
、@Temporal
、@Enumerated
、@Lob
注解 | 说明 |
---|---|
@Entity | 定义对象会成为被JPA 管理的实体 |
@Table | 指定数据库的表名(jdk、jpa自带的) |
@Id | 声明数据库的主键列 |
@IdClass | 主要用于联合主键使用 |
@GeneratedValue | 主键的生成策略 |
@Basic | 表示属性是到数据库表的字段的映射(默认有的) |
@Transient | 表示该属性不是数据库中的字段的映射 |
@Column | 定义该属性对应数据库中的列信息(名、数据类型等) |
@Temporal | 属性映射对应的精度的字段(针对java中的Data类型) |
@Enumerated | 直接映射enum 枚举类型的字段 |
@Lob | 属性映射成数据库支持的大对象类型,(Clob、Blob类型) |
以上都是对于单表操作的相关注解
多表相关的注解::@JoinColum
、@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
、@JoinTable
、@OrderBy
注解 | 说明 |
---|---|
@JoinColum | 定义外键的关联的字段名称 |
@OneToOne | 关联关系(一对一)、常与@JoinColum 结合使用 |
@OneToMany | 关联关系(一对多),一般定义mappedBy 属性 |
@ManyToOne | 关联关系(多对一),常与@JoinColum 结合使用 |
@ManyToMany | 关联关系(多对多) |
@JoinTable | 关联时排序问题 |
@OrderBy | 关联关系表 |
注意:联表的关键在于多表之间的主键或者候选键关系维护(例如:一对一、任意一方都可以维护这个关系,当然也可以新创建一张表进行维护,在开发中需要衡量SQL查询的效率问题来决定)。
2. 定义Repository
常见的Repository
接口: Repository
、CrudRepository
、PagingAndSortingRepository
、QueryByExampleRepository
、JpaRepository
、JpaSpecificationRepository
、QueryDslPredicateExecutor
。
二、实战开发
1. 简单案例
(1) 定义实体Entity
@Entity
@Table(name = File.TABLE)
@Data
public class File {
public final static String TABLE = "file";
private static final long serialVersionUID = 408359656184651337L;
/**
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Getter
@Setter
protected Long id;
/**
* 记录创建时间
*/
@Getter
@Setter
@Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME default current_timestamp comment '创建时间'")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
protected LocalDateTime createTime;
/**
* 记录修改时间
*/
@Getter
@Setter
@Column(name = "update_time", nullable = false, columnDefinition = "DATETIME default current_timestamp comment '最后修改时间'")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@LastModifiedDate
protected LocalDateTime updateTime;
/**
* 创建者
*/
@Getter
@Setter
@Column(name = "created_by", columnDefinition = "varchar(64) default 'system' comment '创建人'")
@CreatedBy
protected String createdBy = "system";
/**
* 修改者
*/
@Getter
@Setter
@Column(name = "last_modified_by", columnDefinition = "varchar(64) default 'system' comment '最后修改人'")
protected String lastModifiedBy = "system";
/**
* 是否删除
* 0-未删除,1-已删除
*/
@Getter
@Setter
@Column(name = "is_deleted", nullable = false, columnDefinition = "TINYINT(1) default 0 comment '是否被删除(0、否 1、是)'")
protected Integer deleted = 0;
/**
* 文件名
*/
@Column(name = "name", columnDefinition = "varchar(128) comment '点名称'")
private String name;
@Column(name = "no", columnDefinition = "varchar(64) comment '文件编号'")
private String no;
/**
* 文件地址
*/
@Column(name = "address", nullable = false, columnDefinition = "varchar(128) comment '文件地址'")
private String address;
/**
* 文件夹
*/
@Column(name = "folder_no", columnDefinition = "varchar(64) comment '文件夹'")
private String folderNo;
/**
* 是否生效(FileType.MAP、FileType.IMAGE isTakeEffect 默认为 true,FileType.EXCEL isTakeEffect 默认为 false)
*/
@Column(name = "is_take_effect", nullable = false, columnDefinition = "bit default true comment '是否生效(type=1、type=2 isTakeEffect 默认为 true,type=3、type=4 isTakeEffect 默认为 false)'")
private Boolean isTakeEffect;
}
(2) 定义实体Repository
@Repository
public interface FileRepository extends JpaRepository<File, Long>, JpaSpecificationExecutor<File> {
}
2. 库表设计问题
根据阿里mysql开发规范,可以得到:设计库表时,必须含有创建时间
、更新时间
、创建人
、更新人
、是否被删除
(只能逻辑删除)
问题: 每次插入数据或更新数据,这类数据很难得到有效的管理,管理不好,则会出现错误数据(更新人没有更该;每次查询都需要加上是否呗删除了)。这类情况都会影响开发效率。
三、Auditing配置
1. Auditing介绍
Auditing
翻译是审计和审核
。这个技术可以解决的一张表的插入数据或修改数据时,同时也要记录创建者
、修改者
、创建时间
、修改时间
(主要通过四个注解实现),并且能让我们方便记录操作日志(主要是通过监听器Listener
功能实现)。
2. 解决方案 — 4个注解
. 注解 | . 说明 |
---|---|
@CreatedBy | 创建的用户 |
@CreatedDate | 创建的时间 |
@LastModifiedBy | 最后一次修改实体的用户 |
@LastModifiedDate | 最后一次修改实体的时间 |
3. Auditing
如何配置?
步骤一:在Entity
里添加上@EntityListeners(AuditingEntityListener.class)
注解
@Entity
@Table(name = "user")
@EntityListeners(AuditingEntityListener.class)
public class User{
@Id
private Long id;
@CreateBy
@Column(name="create_user")
private String createUser;
@CreateDate
@Column(name="create_time")
private Date createTime;
@LastModifiedDate
@Column(name="last_modified_time")
private Data lastModifiedTime;
}
步骤二:实现AuditingAware
接口,指定创建[修改]用户是谁。
public class MyAuditingAware implements AuditingAware<Stirng>{
public String getCurrentAuditor(){
// 应该根据当前使用的安全框架中获取用户名
return "system";
}
}
步骤三:启动类或配置了上,添加注解@EnableJpaAuditing
@EnableJpaAuditing
@SpringBootApplication
public class Main{
public static void main(String[] args){
SpringApplication.run(Main.class,args);
}
public AuditingAware<String> auditingAware(){
return new MyAuditingAware<>();
}
}
4. @MappedSupperclass
的介绍
上述,刚提到了阿里规范,设计数据库表时,需要某些公共的字段(创建人、修改人、id等等)。在实际开发过程中,我们会将这类字段抽象出来,让其他数据库表去继承这个类,从而实现既可以达到要求并且简化了开发。
结论:@MappedSupperclass
可以解决实体Entity
(以及Repository
)的继承问题。
实战案例:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 8328293151203544834L;
/**
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Getter
@Setter
protected Long id;
/**
* 记录创建时间
*/
@Getter
@Setter
@Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME default current_timestamp comment '创建时间'")
@CreatedDate
protected LocalDateTime createTime;
/**
* 记录修改时间
*/
@Getter
@Setter
@Column(name = "update_time", nullable = false, columnDefinition = "DATETIME default current_timestamp comment '最后修改时间'")
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@LastModifiedDate
protected LocalDateTime updateTime;
/**
* 创建者
*/
@Getter
@Setter
@Column(name = "created_by", columnDefinition = "varchar(64) default 'system' comment '创建人'")
@CreatedBy
protected String createdBy = "system";
/**
* 修改者
*/
@Getter
@Setter
@Column(name = "last_modified_by", columnDefinition = "varchar(64) default 'system' comment '最后修改人'")
@LastModifiedBy
protected String lastModifiedBy = "system";
/**
* 是否删除
* 0-未删除,1-已删除
*/
@Getter
@Setter
@Column(name = "is_deleted", nullable = false, columnDefinition = "TINYINT(1) default 0 comment '是否被删除(0、否 1、是)'")
protected Integer isDeleted = 0;
}
这样的话,其他的数据库表实体Entity
就可以继承该类,同时也拥有上述的数据库字段。
5. Listener
事件的扩展
实战问题:操作记录功能实现。例如,记录每个用户对实体数据的添加或者修改动作。
常见解决方案:
- 如果放在
service
层,代码耦合高,不易修改;- 如果通过AOP实现,实现比较复杂,并且只能使用环绕通知实现。
JPA
提供了一种更简洁的解决方案:自定义Listener
.注解 | .说明 |
---|---|
@PostPersist | 更新时,出发的动作 |
@PostRemove | 删除时,触发的动作 |
@PostUpdate | 更新时,触发的动作 |
使用案例
public class MyLogAuditingListener{
@PostPersist
public void postPersist(Object entity) {
// 逻辑处理
}
@PostRemove
public void postRemove(Object entity) {
// 逻辑处理
}
@PostUpdate
public void postUpdate(Object entity) {
// 逻辑处理
}
}
@Entity
@Table(name = "user")
@EntityListeners({AuditingEntityListener.class,MyLogAuditingListener.class})
public class User{
//..定义一些属性字段信息
}
6、逻辑删除的实现
背景描述:
(1)根据阿里开发手册
对MySQL
的描述可以得知:针对数据的删除,不能从物理上删除,而是应该用一个字段表示逻辑的删除(例如:用is_deleted=1
表示数据被删除)
(2) 为什么要这么做?理由:
- 保留原有的数据,方便后续使用;
- 物理删除数据时,磁盘中的数据也会删除该数据,也就可能会导致,磁盘块的
页分裂
、页合并
以及索引结构改变
。
(3)在使用JPA
进行数据查询时,需要添加上where is_deleted = 0 ;
。如果所有的查询都自己添加上这段,这样的话,就体现不了JPA
的优势了。
实战如何解决:@Where
、@SqlDelete
(@SqlUpdate
,这是更新的SQL,根据需求定义)
.注解 | .说明 |
---|---|
@Where | 所有的SQL 语句都添加上后续定义的sql语句(例如:where is_deleted=0 ) |
@SqlDelete | 自定义删除语句 |
@SqlUpdate | 自定义更新语句 |
实战案例
@Entity
@Table(name = File.TABLE)
@Table(appliesTo = File.TABLE, comment = "文件表")
@EqualsAndHashCode(callSuper = false)
@Accessors
@Where(clause = " is_deleted = 0 ")
@SQLDelete(sql = " update " + File.TABLE + " set is_deleted = 1 where id = ? ; ")
@Data
public class File extends BaseEntity {
public final static String TABLE = "file";
private static final long serialVersionUID = 408359656184651337L;
// 自定义属性
}
注意:在实际开发中可能,调用删除相关接口时,可能会报错,这是,需要在删除的接口上添加@Transactional
、@Modifying
两个注解(前者表示事物 必须有,后者是表示更新或删除操作需要的注解)
7. 枚举类型的使用
背景描述:
(1) 在实战开发过程中,避免不了使用枚举类型。在使用枚举数据类型时,如何映射到 mysql
中的数据类型?
(2) 一般情况下,我们都有是一种(key-value
)形式的存在表示枚举类型。默认情况下,数据库存储的值,是枚举型类型中的顺序值(从0开始)
。这样的话,自己当初设计的key-value
存储结构就难以匹配上了。
(3)问题所在:默认情况,java枚举类型
映射到数据库
中的值是枚举值的顺序。但我们期望自己可以指定存储的值。
JPA提供的解决方案
:实现EnumValueConverter
接口解析枚举类型的值问题。
实战案例:
步骤一:自定义注解:@EnumValue
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumValue {
}
步骤二:自定义注解解析器
public class OrdinalEnumValueConverter<E extends Enum> implements EnumValueConverter<E, Integer>, Serializable {
private static final Logger log = Logger.getLogger(OrdinalEnumValueConverter.class);
private static final long serialVersionUID = -4467561563537482266L;
private final EnumJavaTypeDescriptor<E> enumJavaDescriptor;
public OrdinalEnumValueConverter(EnumJavaTypeDescriptor<E> enumJavaDescriptor) {
this.enumJavaDescriptor = enumJavaDescriptor;
}
@Override
public E toDomainValue(Integer relationalForm) {
return enumJavaDescriptor.fromOrdinal(relationalForm);
}
@Override
public Integer toRelationalValue(E domainForm) {
// TODO 修改为Object 看能否支持String
Class<E> e = enumJavaDescriptor.getJavaType();
Field[] fields = e.getDeclaredFields();
Field enumField = null;
for (Field field : fields) {
EnumValue enumValue = field.getAnnotation(EnumValue.class);
if (enumValue != null) {
enumField = field;
break;
}
}
if (enumField != null) {
try {
enumField.setAccessible(true);
return (Integer) enumField.get(domainForm);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
return enumJavaDescriptor.toOrdinal(domainForm);
}
@Override
public int getJdbcTypeCode() {
return Types.INTEGER;
}
@Override
public EnumJavaTypeDescriptor<E> getJavaDescriptor() {
return enumJavaDescriptor;
}
@Override
public E readValue(ResultSet resultSet, String name) throws SQLException {
final int ordinal = resultSet.getInt(name);
final boolean traceEnabled = log.isTraceEnabled();
if (resultSet.wasNull()) {
if (traceEnabled) {
log.trace(String.format("Returning null as column [%s]", name));
}
return null;
}
Class<E> e = enumJavaDescriptor.getJavaType();
Field[] fields = e.getDeclaredFields();
Field enumField = null;
for (Field field : fields) {
EnumValue enumValue = field.getAnnotation(EnumValue.class);
if (enumValue != null) {
enumField = field;
break;
}
}
if (enumField != null) {
Object[] objects = e.getEnumConstants();
try {
for (Object obj : objects) {
enumField.setAccessible(true);
Object enumFieldValue = enumField.get(obj);
// TODO ordinal 改名
if (enumFieldValue.equals(ordinal)) {
return (E) obj;
}
}
throw new SQLException("EnumValue not match:" + ordinal);
} catch (IllegalAccessException ex) {
log.error("", ex);
throw new SQLException(ex);
}
}
final E enumValue = enumJavaDescriptor.fromOrdinal(ordinal);
if (traceEnabled) {
log.trace(String.format("Returning [%s] as column [%s]", enumValue, name));
}
return enumValue;
}
@Override
public void writeValue(PreparedStatement statement, E value, int position) throws SQLException {
final Integer jdbcValue = value == null ? null : toRelationalValue(value);
final boolean traceEnabled = log.isTraceEnabled();
if (jdbcValue == null) {
if (traceEnabled) {
log.tracef("Binding null to parameter: [%s]", position);
}
statement.setNull(position, getJdbcTypeCode());
return;
}
if (traceEnabled) {
log.tracef("Binding [%s] to parameter: [%s]", jdbcValue.intValue(), position);
}
statement.setInt(position, jdbcValue);
}
@Override
public String toSqlLiteral(Object value) {
return Integer.toString(((E) value).ordinal());
}
}
步骤三:定义枚举类型、实体类
// 定义自己的枚举类型
@Getter
@AllArgsConstructor
public enum FileType {
IMAGE(1, "image", "图片"),
MAP(2, "map", "地图文件"),
UNKNOWN(3, "unknown", "未知"),
EXCEL(4, "excel", "Excel 表格");
@EnumValue
private final Integer code;
private final String name;
private final String desc;
}
// 定义自己的 entity 实体
@Entity
@Table(name = File.TABLE,)
@org.hibernate.annotations.Table(appliesTo = File.TABLE, comment = "文件表")
@EqualsAndHashCode(callSuper = false)
@Accessors
@Where(clause = "deleted=0")
@SQLDelete(sql = "update " + File.TABLE + " set deleted = 1 where id = ?")
@Data
public class File extends BaseEntity {
public final static String TABLE = "file";
private static final long serialVersionUID = 408359656184651337L;
/**
* 文件类型
*/
@Column(name = "type", nullable = false, columnDefinition = "tinyint(1) comment '文件类型'")
private FileType type;
}
四、Repository
的理解
常见的Repository
相关接口有:
接口 | 说明 | |
---|---|---|
1 | Repository | |
2 | JpaRepository | |
3 | CrudJpaRepository | |
4 | Repository | |
5 | Repository | |
6 | Repository | |
7 | Repository |
1. Repository
相关接口
接口 | 说明 |
---|---|
Repository | 顶级接口 ,提供了实体的简单查询(根据字段名) |
CrudReposiroty | 继承自Repository ,常见的CRUD 操作 |
PagingAndSortingRepository | 继承自CrudReposiroty ,提供了分页查询和排序操作,集合操作返回Iterable |
JPARepository | 继承自PagingAndSortingRepository ,直接返回了List |
JpaSpecificationExecutor | 这个接口单独存在,主要提供了多条件查询的支持,并且可以在查询中添加分页和排序。 |
在实战开发过程中,只需要继承
JPARepository
和JpaSpecificationExecutor
接口,就能完成全部的条件查询操作。
下面重点介绍PagingAndSortingRepository
、JpaSpecificationExecutor
两个接口的使用
2. PagingAndSortingRepository
接口
根据名字,不难发现这个接口是跟分页
、排序相关的
。所以下面我们就从这两个切入点考虑实战中如实优雅的写出代码。
分页
功能介绍
实战中,可能用到的相关接口:Pageable
、@PageableDefault
、JpaRepository<T, ID>
第一步:分页相关的参数(三个):当前页page
、页大小size
、总页数量totalPages
@Entity
@Table(name = MapFile.TABLE)
public class MapFile extends BaseEntity {
public final static String TABLE = "factory_map_file";
private static final long serialVersionUID = 408359656184651337L;
// 实体相关属性
// ...
}
@Repository
public interface MapFileRepository
extends JpaRepository<MapFile, Long>,
JpaSpecificationExecutor<MapFile> {
}
@RestController
public class MapFileController {
@PostMapping("/page")
public Page<MapFile> page(@RequestBody @PageableDefault(size = 5, page = 1) Pageable pageable) {
Page<MapFile> page = mapFileRepository.findAll(pageable);
System.out.println("获取查询得到的数据:"+page.get());
System.out.println(page);
return page;
}
}
说明:
@PageableDefault
:指定默认的分页配置信息。Pageable
:JpaRepository
接口的重要相关参数
排序
功能介绍
实战中,可能用到的相关接口:Pageable
、@PageableDefault
、JpaRepository<T, ID>
、Sort
、@SortDefault
@RestController
public class MapFileController {
@GetMapping("/page2")
public Page<MapFile> page2(@PageableDefault(size = 5, page = 1, sort = {"id"}) Pageable pageable) {
Page<MapFile> page = mapFileRepository.findAll(pageable);
System.out.println("获取查询得到的数据:" + page.get());
System.out.println(page);
return page;
}
@GetMapping("/sort")
public Object sort(@SortDefault(sort = {"id", "createTime"},direction = Sort.Direction.DESC) Sort sort) {
System.out.println(sort.toString());
// List<MapFile> list = mapFileRepository.findAll(sort.descending()); // 逆序
List<MapFile> list = mapFileRepository.findAll(sort.ascending()); // 正序(默认)
System.out.println(list);
return list;
}
}
说明:
- 既可以使用
Pageable
参数进行排序,也可以使用Sort
接口进行排序 Pageable
接口,似乎不能进行逆序操作。
五、实战中设计实体
1. 枚举类型
在实战开发过程中,设计实体对象时,就避免不了出现枚举类型的数据。但
枚举类型
仅仅只是在Java程序中的数据类型,在数据库中并不存在(其实也有枚举类型,但在实战开发过程中几乎不用)。
根据上述的描述,可以提出问题:在Java程序和数据库之间,枚举数据如何传递、转换?
1.1 @Convert
和AttributeConverter
接口
理论知识铺垫:
基本使用步骤:
- 实现
AttributeConverter
接口- 声明枚举类型
- 设计实体,并使用上该类型,并使用
@Convert(converter = EnumConverter.class)
- 开始进行测试
上案例:
声明枚举类型:
/**
* code 在 java程序中,尽可能要唯一性。(这种程序更具有容错性,更易于扩展)
* 提示:xxxx_xxx 前4个'x'表示不同的枚举类型,后3个'x'表示每种枚举类型中的值(如果觉得不够用可以加大范围)
*/
public interface EnumType {
/**
* 编码
*/
Integer getCode();
/**
* 对应文字描述
*/
String getName();
/**
* 中文描述
*/
String getDesc();
/**
* 英文描述
*/
String getEnDesc();
default String getValue() {
return getDesc();
}
default String toJson() {
return String.format("{\"code\": %d,\"name\": \"%s\",\"desc\": \"%s\"}", getCode(), getName(), getValue());
}
}
@Getter
public enum StatusType implements EnumType {
MAN(103_001, "MAN", "男", "man"),
WOMAN(103_002, "WOMAN", "女", "woman");
private final Integer code;
private final String name;
private final String zhDesc;
private final String enDesc;
StatusType(Integer code, String name, String zhDesc, String enDesc) {
this.code = code;
this.name = name;
this.zhDesc = zhDesc;
this.enDesc = enDesc;
}
public static StatusType codeOf(Integer code) {
StatusType[] values = StatusType.values();
for (StatusType value : values) {
if (value.getCode().equals(code)) {
return value;
}
}
return null;
}
@Override
public String getDesc() {
return zhDesc;
}
}
Converter:
/**
* TODO 这里应该将枚举类型抽象出一个顶级接口,然后再声明一个(如 上述的EnumType) 全局枚举类型的管理类进行管理。
* 这样就不用每个枚举类型都写一个转换接口的了。
*/
public class EnumConverter implements AttributeConverter<StatusType, Integer> {
@Override
public Integer convertToDatabaseColumn(StatusType attribute) {
return attribute.getCode();
}
@Override
public StatusType convertToEntityAttribute(Integer code) {
return StatusType.codeOf(code);
}
}
entity:
@Entity
@Table(name = Person.TABLE_NAME)
//@Where(clause = "deleted=0")
//@SQLDelete(sql = "update " + Person.TABLE_NAME + " set deleted = 1 where id = ?")
@Data
public class Person {
public static final String TABLE_NAME = "person";
private static final Long serialVersionUID = -12345456567434L;
private String name;
private String password;
@Convert(converter = EnumConverter.class)
private StatusType type;
}
repository:
public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {
}
controller:(这里少了serice层)
@RestController
@RequestMapping("/persons")
public class PersonController {
@Autowired(required = false)
private PersonRepository personRepository;
@GetMapping("/{id}")
public Person get(@PathVariable("id") Long id) {
return personRepository.findById(id).get();
}
@PostMapping("/add")
public Person add(@RequestBody Person person) {
return personRepository.saveAndFlush(person);
}
}
请求测试:(这里使用idea自带的测试工具,文件类型:‘http’)
### 查询 person
GET http://localhost:8080/persons/1
### add person
POST http://localhost:8080/persons/add
Content-Type: application/json
{
"name": "zhangsan",
"password": "123456",
"type": "WOMAN"
}
1.2 @Convert
方式处理的缺点
public interface AttributeConverter<X,Y> {
public Y convertToDatabaseColumn (X attribute);
public X convertToEntityAttribute (Y dbData);
}
该接口接受到的参数非常有限,不方便扩展。
问题:程序中有100个枚举类型数据,难道也要定义100个AttributeConverter
接口的实现类?在实际开发中,这肯定是不希望看到的。
解决方案:因此,在实际开发过程中,需要定义好枚举类型
的顶级接口BaseEnumType
,以及全局枚举类型的管理类GlobaEnumManager
。这样,再多的枚举类型,也只需要一个AttributeConverter
接口的实现类。
2. 时间类型
和上述处理的方式一样。