conanan's blog conanan's blog
首页
关于
  • 分类
  • 标签
  • 归档
  • Java
  • Java Web
  • 工具

    • Maven
  • MySQL
  • Redis
  • Git
  • Vim
  • Nginx
  • Docker
GitHub

Evan Xu

前端界的小学生
首页
关于
  • 分类
  • 标签
  • 归档
  • Java
  • Java Web
  • 工具

    • Maven
  • MySQL
  • Redis
  • Git
  • Vim
  • Nginx
  • Docker
GitHub
  • Jakarta EE

  • Tomcat

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringDataJPA

    • JPA基础
    • 总览入门
      • 持久层开发的问题
        • 持久层开发的问题
        • 什么是 Spring Data?
        • Spring Data 的子项目
        • Spring Data JPA 关系图 🔥
      • Spring Data Common 之 Repository 🔥
        • Spring Data JPA 的依赖关系
        • Repository 接口、层次关系 🔥
        • 测试 Entity—User
        • Repository 接口示例—方法名称规则查询 🔥
        • CrudRepository 接口示例 🔥
        • PagingAndSortingRepository 接口示例 🔥
        • JpaRepository 接口示例 🔥
        • 具体实现类 SimpleJpaRepository 🔥
        • 启发 🔥
      • @Entity 中的 JPA 注解 🔥
        • Entity 实体是什么?
        • 详细的注解都有哪些?
        • @Entity
        • @Table
        • @Access
        • @Id
        • @GeneratedValue
        • @Enumerated(没啥用)
        • @Basic
        • @Transient
        • @Column
        • @Temporal
        • 完整的例子
      • 联合主键
        • 通过@Id 和 @IdClass 做到联合主键
        • @Embeddable 与 @EmbeddedId 注解使用
      • 实体之间的继承关系如何实现?🔥
        • 总结
      • 附. Spring MVC 测试
      • 未整理 🔥
    • 基础操作
    • 多表操作
    • 审计功能
    • JPA示例(旧)
  • Test
  • Shiro
  • Thymeleaf
  • Java Web
  • SpringDataJPA
conanan
2021-06-21

总览入门

# 总览入门

# 持久层开发的问题

# 持久层开发的问题

随着互联网技术的发展,现在的企业开发中用到的用于数据存储的产品,不再仅仅是关系型数据库,而是要根据场景需要选择不同的存储技术,比如用于缓存热点数据的redis,用于存储文档数据的mongodb,用于支持强大搜索功能的elasticsearch等等。

在Java中,对于上面所说的产品都提供了优秀的访问技术。比如针对关系型数据库的mybatis、jpa等技术,针对于redis的jedis技术等等..... 这些技术虽然可以很好的针对各个存储产品进行访问操作,但同时也带来了新的问题,那就是不同的持久层技术的API是不一样的。这样一来,开发人员就必须同时掌握多种数据访问技术,这无疑增加了开发成本。那么我们会想,有没有这样一种技术,它可以使用一套API支持各个不同的存储的访问呢?就在这样的需求下,SpringData产生了。

# 什么是 Spring Data?

Spring Data 项目是从 2010 年开发发展起来的,Spring Data 利用一个大家熟悉的、一致的、基于“注解”的数据访问编程模型,做一些公共操作的封装,它可以轻松地让开发者使用数据库访问技术,简化 DAO 层开发,包括关系数据库、非关系数据库(NoSQL)。同时又有不同的数据框架的实现,保留了每个底层数据存储结构的特殊特性。

Spring Data Common 是 Spring Data 所有模块的公共部分,该项目提供了基于 Spring 的共享基础设施,它提供了基于 repository 接口以 DB 操作的一些封装,以及一个坚持在 Java 实体类上标注元数据的模型。

Spring Data 不仅对传统的数据库访问技术如 JDBC、Hibernate、JDO、TopLick、JPA、MyBatis 做了很好的支持和扩展、抽象、提供方便的操作方法,还对 MongoDb、KeyValue、Redis、LDAP、Cassandra 等非关系数据的 NoSQL 做了不同的实现版本,方便我们开发者触类旁通。

# Spring Data 的子项目

下图为目前 Spring Data 的框架分类结构图,里面都有哪些模块可以一目了然,也可以知道哪些是我们需要关心的项目

img

主要项目(Main Modules):

  • Spring Data Commons,相当于定义了一套抽象的接口
  • Spring Data JDBC,对JDBC的Spring Data存储库支持
  • Spring Data JPA,我们关注的重点,对 Spring Data Common 的接口的 JPA 协议的实现
  • Spring Data Gemfire
  • Spring Data KeyValue
  • Spring Data LDAP
  • Spring Data MongoDB,对MongoDB的基于Spring对象文档的存储库支持
  • Spring Data REST
  • Spring Data Redis,封装Jedis技术,对redis实现访问操作
  • Spring Data for Apache Cassandra
  • Spring Data for Apache Solr

社区支持的项目(Community Modules):

  • Spring Data Aerospike
  • Spring Data Couchbase
  • Spring Data DynamoDB
  • Spring Data Elasticsearch,对Elasticsearch实现访问操作(一般不太用,因为版本更新比较慢)
  • Spring Data Hazelcast
  • Spring Data Jest
  • Spring Data Neo4j
  • Spring Data Vault

其他(Related Modules):

  • Spring Data JDBC Extensions
  • Spring for Apache Hadoop
  • Spring Content

关于 Spring Data 的子项目,除了上面这些,还有很多开源社区版本,比如 Spring Data、MyBatis 等

# Spring Data JPA 关系图 🔥

只需在 JPA 规范上面再添加一个 Spring Data JPA 即可

image-20201217220824953

# Spring Data Common 之 Repository 🔥

Spring Data Common 定义了很多公用的接口和一些相对数据操作的公共实现(如分页排序、结果映射、Autiting 信息、事务等),而 Spring Data JPA 就是 Spring Data Common 的关系数据库的查询实现。本节了解下 Spring Data Common 的核心内容——Repository,从 Repository 的所有子类着手,逐步掌握 CrudRepository、PageingAndSortingRepository、JpaRepository的使用。

# Spring Data JPA 的依赖关系

先来看看 Spring Data JPA 所依赖的 jar 包关系是什么样的

image-20201219193544969

数据库连接用的是 JDBC,连接池用的是 HikariCP(看 starter-jdbc),强依赖 Hibernate(不能切换实现了JPA的其他框架);Spring Boot Starter Data JPA 依赖 Spring Data JPA;而 Spring Data JPA 依赖 Spring Data Commons。Spring Data Commons 是终极依赖。下面学习 DB 操作的入口 Repository及其子类

# Repository 接口、层次关系 🔥

# Repository 接口

Repository 是 Spring Data Common 里面的顶级父类接口,操作 DB 的入口类。首先介绍 Repository 接口的源码、类层次关系和使用实例。

package org.springframework.data.repository;
import org.springframework.stereotype.Indexed;

@Indexed
public interface Repository<T, ID> {

}
1
2
3
4
5
6
7

Resposiory 是 Spring Data 里面进行数据库操作顶级的抽象接口,里面什么方法都没有,但是如果任何接口继承它,就能得到一个 Repository,还可以实现 JPA 的一些默认实现方法(如方法名称规则查询)。这是 Spring 利用 Respository 作为 DAO 操作的 Type,以及利用 Java 动态代理机制实现的

Spring 在做动态代理的时候,只要是它的子类或者实现类,再利用 T 类以及 T 类的 主键 ID 类型作为泛型的类型参数,就可以来标记出来、并捕获到要使用的实体类型,就能帮助使用者进行数据库操作。

# Repository 类层次关系

Type hierarchy 类的层次结构,使用 Ctrl + H(mac)即可看到

image-20201219195036228

通过该层次结构视图,你就会明白基类 Repository 的用意,由此可知,存储库分为以下 4 个大类。

  • ReactiveCrudRepository 这条线是响应式编程,主要支持当前 NoSQL 方面的操作,因为这方面大部分操作都是分布式的,所以由此我们可以看出 Spring Data 想统一数据操作的“野心”,即想提供关于所有 Data 方面的操作。目前 Reactive 主要有 Cassandra、MongoDB、Redis 的实现。
  • RxJava2/3CrudRepository 这条线是为了支持 RxJava 2/3 做的标准响应式编程的接口。
  • CoroutineCrudRepository 这条继承关系链是为了支持 Kotlin 语法而实现的。
  • CrudRepository 这条继承关系链正是本课时我要详细介绍的 JPA 相关的操作接口,你也可以把我的这种方法应用到另外 3 种继承关系链里面学习。

点击 Show Diagram 显示层次结构图,其中 JpaSpecificationExecutor 也是继承了借口后出现的,也是需要学习的!

image-20201219195652297

需要掌握和使用到的类如下所示:

7 大 Repository 接口:

  • Repository(org.springframework.data.repository),没有暴露任何方法;
  • CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
  • PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
  • QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
  • JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
  • JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;
  • QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装。

2 大 Repository 实现类:

  • SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类;
  • QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类。

关于其他的类,后面我也会通过不同方式的讲解

# 测试 Entity—User

@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    private String name;
    private String email;
}
1
2
3
4
5
6
7
8
9
10
11
12

# Repository 接口示例—方法名称规则查询 🔥

利用 UserRepository 继承 Repository 来实现对 User 的两个查询方法(方法名称规则查询)

public interface UserRepository extends Repository<User,Long> {
	//根据名称进行查询用户列表
	List<User> findByName(String name);
	// 根据用户的邮箱和名称查询
	List<User> findByEmailAndName(String email, String name);
}
1
2
3
4
5
6

由于 Repository 接口里面没有任何方法,所以此 UserRepository 对外只有两个可用方法

# CrudRepository 接口示例 🔥

通过 IDEA 工具,看下 CrudRepository 为我们提供的方法有哪些

image-20201219200638254

  • <S extends T> S save(S entity); 保存/更新实体方法

    Entity 中若 id 为 null,则 persist,否则执行 merge(源码中会进行 id 是否为 null 等判断。有的说根据id 查询了?乐观锁)

    如果当传递的参数里面没有 ID,则直接 insert;若当传递的参数里面有 ID,则会触发 select 查询。此方法会去看一下数据库里面是否存在此记录,若存在,则 update,否则 insert

    注意更新时实体类中当前为null的字段也会被更新为null,可以配置更改该策略,DynamicUpdate 策略

    参数和返回结果可以是实体的子类

    public <S extends T> S save(S entity) {
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  • saveAll(Iterable<S> entities) : 批量保存/更新,原理和 save方法相同,我们去看实现的话,就是 for 循环调用上面的 save 方法。

    那这就很扯淡了,当然可以设置一下spring.jpa.properties.hibernate.jdbc.batch_size=200,就会批量操作了!

  • void deleteById(ID id); 根据主键删除,查看源码会发现,其是先根据主键查询出来再进行删除,没查询到直接抛异常(需要处理该异常,但是一般删除操作不会产生并发);(实际使用时无需手动查询!)

    public void deleteById(ID id) {
        Assert.notNull(id, ID_MUST_NOT_BE_NULL);
        // 没查询到直接抛异常
        delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
            String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
    }
    
    1
    2
    3
    4
    5
    6
  • void delete(T entity) 根据 Entity(其实是主键) 进行删除(底层和 deleteById 一样);

  • void deleteAll(Iterable<? extends T> entities) 批量删除,还是for循环调用 delete(T entity)。

    那这就很扯淡了,当然可以使用 JpaRepository 的批量删除!

  • void deleteAll() 删除所有;原理可以通过刚才的类关系查看,CrudRepository 的实现方法如下:

    //SimpleJpaRepository里面的deleteALL方法
    // 利用 for 循环调用 delete 方法进行删除操作
    public void deleteAll() {
       for (T element : findAll()) {
          delete(element);
       }
    }
    
    1
    2
    3
    4
    5
    6
    7
  • boolean existsById(ID id) 根据主键判断实体是否存在;

  • Optional<T> findById(ID id); 根据主键查询实体,返回 JDK 1.8 的 Optional,这可以避免 null exception;

  • Iterable<T> findAllById(Iterable ids); 根据主键列表查询实体列表;

  • Iterable<T> findAll(); 查询实体的所有列表;

  • count(): long 查询总数返回 long 类型;

上面这些方法是 CrudRepository 对外暴露的常见的 Crud 接口,我们在对数据库进行 Crud 的时候就会运用到,如下所示

public interface UserRepository extends CrudRepository<User,Long> {
}
1
2

# PagingAndSortingRepository 接口示例 🔥

主要用于分页查询和排序查询。源码如下:

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
	Iterable<T> findAll(Sort sort); (1)
	Page<T> findAll(Pageable pageable); (2)
}
1
2
3
4
5
  • 第一个方法 findAll 参数是 Sort,是根据排序参数,实现不同的排序规则获取所有的对象的集合
  • 🔥 第二个方法 findAll 参数是 Pageable,是根据分页和排序进行查询,并用 Page 对返回结果进行封装。而 Pageable 对象包含 Page 和 Sort 对象

使用方式如下:

public interface UserRepository extends PagingAndSortingRepository<User,Long> {
}
1
2
/**
 * 验证排序和分页查询方法,Pageable的默认实现类:PageRequest
 * @return
 */
@GetMapping(path = "/page")
@ResponseBody
public Page<User> getAllUserByPage() {
   return userRepository.findAll(
         PageRequest.of(1, 20,Sort.by(new Sort.Order(Sort.Direction.ASC,"name"))));
}
/**
 * 排序查询方法,使用Sort对象
 * @return
 */
@GetMapping(path = "/sort")
@ResponseBody
public Iterable<User> getAllUsersWithSort() {
   return userRepository.findAll(Sort.by(new Sort.Order(Sort.Direction.ASC,"name")));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# JpaRepository 接口示例 🔥

到这里可以进入到分水岭了,上面的那些都是 Spring Data 为了兼容 NoSQL 而进行的一些抽象封装,而从 JpaRepository 开始是对关系型数据库进行抽象封装。从类图可以看出来它继承 PagingAndSortingRepository 类,也就继承了其所有方法,并且其实现类也是 SimpleJpaRepository。从类图上还可以看出 JpaRepository 继承和拥有了 QueryByExampleExecutor 的相关方法,我们先来看一下 JpaRepository 有哪些方法。

image-20201219224246112

涉及 QueryByExample 的部分我们在 11 课时“JpaRepository 如何自定义”再详细介绍,

JpaRepository 里面重点新增了批量删除,优化了批量删除的性能,类似于之前 SQL 的 batch 操作,并不是像上面的 deleteAll 来 for 循环删除。

其中 flush() 和 saveAndFlush() 提供了手动刷新 session,把对象的值立即更新到数据库里面的机制。JPA 是 由 Hibernate 实现的,所以有 session 一级缓存的机制,当调用 save() 方法的时候,数据库里面是不会立即变化的,其原理我将在 21 课时“Persistence Context 所表达的核心概念是什么”再详细讲解

使用方式如下:

public interface UserRepository extends JpaRepository<User,Long> {
}
1
2

# 具体实现类 SimpleJpaRepository 🔥

PagingAndSortingRepository 和 CrudRepository 都是 Spring Data Common 的标准接口,那么实现类是什么呢?如果我们采用 JPA,那对应的实现类就是 Spring Data JPA 的 jar 包里面的 SimpleJpaRepository。如果是其他 NoSQL的 实现如 MongoDB,那实现就在 Spring Data MongoDB 的 jar 里面的 MongoRepositoryImpl。JpaRepository 的实现类也是 SimpleJpaRepository

关系数据库的所有 Repository 接口的实现类就是 SimpleJpaRepository,如果有些业务场景需要进行扩展了,可以继续继承此类,如 QueryDsl 的扩展(虽然不推荐使用了,但我们可以参考它的做法,自定义自己的 SimpleJpaRepository),如果能将此类里面的实现方法看透了,基本上 JPA 中的 API 就能掌握大部分内容。

UserRepository 的实现类是 Spring 启动的时候,利用 Java 动态代理机制帮我们生成的实现类,而真正的实现类就是 SimpleJpaRepository。

image-20201219225302748

image-20201220000423131

它是在什么时候被动态代理生成的?在 RepositoryFactorySupport 设置一个断点,启动的时候,在我们的断点处就会发现 UserRepository 的接口会被 JdkDynamicAopProxy 动态代理成 SimpleJapRepository 的实现类(底层还是 InvocationHandler 的 invoke 方法;非接口的动态代理会使用 Cglib)。注意每一个 Repository 的子类,都会通过这里的动态代理生成实现类

image-20201219225948290

部分源码如下:

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";

    private final JpaEntityInformation<T, ?> entityInformation;

    private final EntityManager em;

    private final PersistenceProvider provider;

    private @Nullable CrudMethodMetadata metadata;

    // ......
  
    @Transactional
    public void deleteAllInBatch() {
        em.createQuery(getDeleteAllQueryString()).executeUpdate();
    }
    //......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

通过此类的源码,我们可以挺清晰地看出 SimpleJpaRepository 的实现机制,是通过 EntityManger 进行实体的操作(就是和JPA一样),而 JpaEntityInforMation 里面存在实体的相关信息和 Crud 方法的元数据等。

# 启发 🔥

  • 上面的 7 个大 Repository 接口,在使用的时候可以根据实际场景继承不同的接口,从而选择暴露不同的 Spring Data Common 给我们提供的已有接口。这其实利用了 Java 语言的 interface 特性。
  • 利用源码也可以很好地理解一下 Spring 中动态代理的作用,可以利用这种思想,在改善 MyBatis 的时候使用。

# @Entity 中的 JPA 注解 🔥

# Entity 实体是什么?

  • 实体是直接进行数据库持久化操作的领域对象(即一个简单的 POJO,可以按照业务领域划分),必须通过 @Entity 注解进行标示。
  • 实体必须有一个 public 或者 protected 的无参数构造方法。
  • 持久化映射的注解可以标示在 Entity 的字段 field 上,也可以将持久化注解运用在 Entity 里面的 get/set 方法上,通常我们是放在 get 方法中。即 Entity 里面的注解生效只有两种方式:将注解写在字段上或者将注解写在方法上(JPA 里面称 Property)。但是需要注意的是,在同一个 Entity 里面只能有一种方式生效,也就是说,注解要么全部写在 field 上面,要么就全部写在 Property 上面
  • 只要是在 @Entity 的实体里面被注解标注的字段,都会被映射到数据库中,除了使用 @Transient 注解的字段之外。
  • 实体里面必须要有一个主键,主键标示的字段可以是单个字段,也可以是复合主键字段。

JPA 协议的官方地址:https://download.oracle.com/otn-pub/jcp/persistence-2_2-mrel-spec/JavaPersistence.pdf

# 详细的注解都有哪些?

利用 IEDA 工具,打开 @Entity 所在的包,就可以看到 JPA 里面支持的注解有哪些。如下所示;

img

这里只提及一些最常见的,包括 @Entity、@Table、@Access、@Id、@GeneratedValue、@Enumerated、@Basic、@Column、@Transient、@Lob、@Temporal 等。

# @Entity

用于定义对象将会成为被 JPA 管理的实体,必填,将字段映射到指定的数据库表中,使用起来很简单,直接用在实体类上面即可,通过源码表达的语法如下:

@Target(TYPE) //表示此注解只能用在class上面
public @interface Entity {
   //可选,默认是实体类的名字,整个应用里面全局唯一。
   String name() default "";
}
1
2
3
4
5

# @Table

用于指定数据库的表名,表示此实体对应的数据库里面的表名,非必填,默认表名和 entity 名字一样。

@Target(TYPE) //一样只能用在类上面
public @interface Table {
   //表的名字,可选。如果不填写,系统认为好实体的名字一样为表名。
   String name() default "";
   //此表所在schema,可选
   String schema() default "";
   //唯一性约束,在创建表的时候有用,表创建之后后面就不需要了。
   UniqueConstraint[] uniqueConstraints() default { };
   //索引,在创建表的时候使用,表创建之后后面就不需要了。
   Index[] indexes() default {};
}
1
2
3
4
5
6
7
8
9
10
11

# @Access

用于指定 entity 里面的注解是写在字段上面,还是 get/set 方法上面生效,非必填。在默认不填写的情况下,当实体里面的第一个注解出现在字段上或者 get/set 方法上面,就以第一次出现的方式为准;也就是说,一个实体里面的注解既有用在 field 上面,又有用在 properties 上面的时候,看下面的代码你就会明白。

@Id
private Long id;
@Column(length = 20, nullable = false)
public String getUserName(){
    return userName;
}
1
2
3
4
5
6

那么由于 @Id 是实体里面第一个出现的注解,并且作用在字段上面,所以所有写在 get/set 方法上面的注解就会失效。而 @Access 可以干预默认值,指定是在 fileds 上面生效还是在 properties 上面生效。我们通过源码看下语法:

@Target( { TYPE, METHOD, FIELD })//表示此注解可以运用在class上(那么这个时候就可以指定此实体的默认注解生效策略了),也可以用在方法上或者字段上(表示可以独立设置某一个字段或者方法的生效策略);
@Retention(RUNTIME)
public @interface Access {
//指定是字段上面生效还是方法上面生效
    AccessType value();
}
public enum AccessType {
    FIELD,
    PROPERTY
}
1
2
3
4
5
6
7
8
9
10

# @Id

定义属性为数据库的主键,一个实体里面必须有一个主键,但不一定是这个注解,可以和 @GeneratedValue 配合使用或成对出现。

# @GeneratedValue

主键生成策略,如下所示:

public @interface GeneratedValue {
    //Id的生成策略
    GenerationType strategy() default AUTO;
    //通过Sequences生成Id,常见的是Orcale数据库ID生成规则,这个时候需要配合@SequenceGenerator使用
    String generator() default "";
}
1
2
3
4
5
6

其中,GenerationType 一共有以下四个值:

public enum GenerationType {
    //通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
    TABLE,
    //通过序列产生主键,通过 @SequenceGenerator 注解指定序列名, MySql 不支持这种方式;
    SEQUENCE,
    //采用数据库ID自增长, 一般用于mysql数据库
    IDENTITY,
//JPA 自动选择合适的策略,是默认选项;
    AUTO
}
1
2
3
4
5
6
7
8
9
10

# @Enumerated(没啥用)

这个注解很不好用,因为它对 enum 提供了下标和 name 两种方式,用法直接映射在 enum 枚举类型的字段上。请看下面源码。

@Target({METHOD, FIELD}) //作用在方法和字段上
public @interface Enumerated {
//枚举映射的类型,默认是ORDINAL(即枚举字段的下标)。
    EnumType value() default ORDINAL;
}
public enum EnumType {
    //映射枚举字段的下标
    ORDINAL,
    //映射枚举的Name
    STRING
}
1
2
3
4
5
6
7
8
9
10
11

再来看一个 User 里面关于性别枚举的例子,你就会知道 @Enumerated 在这里没什么作用了,如下所示:

//有一个枚举类,用户的性别
public enum Gender {
    MAIL("男性"), FMAIL("女性");
    private String value;
    private Gender(String value) {
        this.value = value;
    }
}
//实体类@Enumerated的写法如下
@Entity
@Table(name = "tb_user")
public class User implements Serializable {
    @Enumerated(EnumType.STRING)
    @Column(name = "user_gender")
    private Gender gender;
    //.......................
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这时候插入两条数据,数据库里面的值会变成 MAIL/FMAIL,而不是“男性” / 女性。

经验分享: 如果我们用 @Enumerated(EnumType.ORDINAL),这时候数据库里面的值是 0、1。但是实际工作中,不建议用数字下标,因为枚举里面的属性值是会不断新增的,如果新增一个,位置变化了就惨了。并且 0、1、2 这种下标在数据库里面看着非常痛苦,时间长了就会一点也看不懂了。

# @Basic

表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为 @Basic。也就是说默认所有的字段肯定是和数据库进行映射的,并且默认为 Eager 类型。

public @interface Basic {
    //可选,EAGER(默认):立即加载;LAZY:延迟加载。(LAZY主要应用在大字段上面)
    FetchType fetch() default EAGER;
    //可选。这个字段是否可以为null,默认是true。
    boolean optional() default true;
}
1
2
3
4
5
6

# @Transient

表示该属性并非一个到数据库表的字段的映射,表示非持久化属性。JPA 映射数据库的时候忽略它,与 @Basic 有相反的作用。也就是每个字段上面 @Transient 和 @Basic 必须二选一,而什么都不指定的话,默认是 @Basic。

# @Column

定义该属性对应数据库中的列名。

public @interface Column {
    //数据库中的表的列名;可选,如果不填写认为字段名和实体属性名一样。
    String name() default "";
    //是否唯一。默认flase,可选。
    boolean unique() default false;
    //数据字段是否允许空。可选,默认true。
    boolean nullable() default true;
    //执行insert操作的时候是否包含此字段,默认,true,可选。
    boolean insertable() default true;
    //执行update的时候是否包含此字段,默认,true,可选。
    boolean updatable() default true;
    //表示该字段在数据库中的实际类型。
    String columnDefinition() default "";
   //数据库字段的长度,可选,默认255
    int length() default 255;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# @Temporal

用来设置 Date 类型的属性映射到对应精度的字段,存在以下三种情况:

  • @Temporal(TemporalType.DATE)映射为日期 // date (只有日期)
  • @Temporal(TemporalType.TIME)映射为日期 // time (只有时间)
  • @Temporal(TemporalType.TIMESTAMP)映射为日期 // date time (日期+时间)

# 完整的例子

@Entity
@Table(name = "user_topic")
@Access(AccessType.FIELD)
@Data
public class UserTopic {
   @Id
   @Column(name = "id", nullable = false)
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;
   @Column(name = "title", nullable = true, length = 200)
   private String title;
   @Basic
   @Column(name = "create_user_id", nullable = true)
   private Integer createUserId;
   @Basic(fetch = FetchType.LAZY)
   @Column(name = "content", nullable = true, length = -1)
   @Lob
   private String content;
   @Basic(fetch = FetchType.LAZY)
   @Column(name = "image", nullable = true)
   @Lob
   private byte[] image;
   @Basic
   @Column(name = "create_time", nullable = true)
   @Temporal(TemporalType.TIMESTAMP)
   private Date createTime;
   @Basic
   @Column(name = "create_date", nullable = true)
   @Temporal(TemporalType.DATE)
   private Date createDate;
   @Enumerated(EnumType.STRING)
   @Column(name = "topic_type")
   private Type type;
   @Transient
   private String transientSimple;
   //非数据库映射字段,业务类型的字段
   public String getTransientSimple() {
      return title + "auto:jack" + type;
   }
   //有一个枚举类,主题的类型
   public enum Type {
      EN("英文"), CN("中文");
      private final String des;
      Type(String des) {
         this.des = des;
      }
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

其实这里面的很多注解都可以省略,直接使用默认的就可以。如 @Basic、@Column 名字有一定的映射策略(我们在第 17 课时讲 DataSource 的时候会详细讲解映射策略),所以可以省略。此外,@Access 也可以省略,我们只要在这些类里面保持一致就可以了。学会利用工具去生成 Entity 类,将会节省很多时间。

首先,打开 Persistence 视图,点击 Generate Persistence Mapping,接着点击选中数据源,然后,选择表和字段,并点击 OK。

但是需要注意的是,在生产环境中我们要把外键关联关系关闭,不然会出现意想不到的 ERROR,毕竟生产环境不同开发环境,我们可以通过在开发环境生成的表导出 DDL 到生产执行。我经常会利用生成 DDL 来做测试和写案例, 这样省去了创建表的时间,只需要关注我的代码就行了。

# 联合主键

可以通过 javax.persistence.EmbeddedId 和 javax.persistence.IdClass 两个注解实现联合主键的效果。

# 通过@Id 和 @IdClass 做到联合主键

我们先看一下怎么通过 @IdClass 做到联合主键。

第一步:新建一个 UserInfoID 类里面是联合主键。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoID implements Serializable {
   private String name,telephone;
}
1
2
3
4
5
6
7

第二步:再新建一个 UserInfo 的实体,采用 @IdClass 引用联合主键类。

@Entity
@Data
@Builder
@IdClass(UserInfoID.class)
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
   private Integer ages;
   @Id
   private String name;
   @Id
   private String telephone;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

第三步:新增一个 UserInfoReposito 类来做 CRUD 操作。

public interface UserInfoRepository extends JpaRepository<UserInfo,UserInfoID> {
}
1
2

第四步:写一个测试用例,测试一下。

@DataJpaTest
public class UserInfoRepositoryTest {
   @Autowired
   private UserInfoRepository userInfoRepository;
   @Test
   public void testIdClass() {
   userInfoRepository.save(UserInfo.builder().ages(1).name("jack").telephone("123456789").build());
      Optional<UserInfo> userInfo = userInfoRepository.findById(UserInfoID.builder().name("jack").telephone("123456789").build());
      System.out.println(userInfo.get());
   }
}
1
2
3
4
5
6
7
8
9
10
11
Hibernate: create table user_info (name varchar(255) not null, telephone varchar(255) not null, ages integer, primary key (name, telephone))
Hibernate: select userinfo0_.name as name1_3_0_, userinfo0_.telephone as telephon2_3_0_, userinfo0_.ages as ages3_3_0_ from user_info userinfo0_ where userinfo0_.name=? and userinfo0_.telephone=?
UserInfo(ages=1, name=jack, telephone=123456789)
1
2
3

通过上面的例子我们可以发现,我们的表的主键是 primary key (name, telephone),而 Entity 里面不再是一个 @Id 字段了。那么我来介绍另外一个注解 @Embeddable,也能做到这一点。

# @Embeddable 与 @EmbeddedId 注解使用

第一步:在我们上面例子中的 UserInfoID 里面添加 @Embeddable 注解。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public class UserInfoID implements Serializable {
   private String name,telephone;
}
1
2
3
4
5
6
7
8

第二步:改一下我们刚才的 User 对象,删除 @IdClass,添加 @EmbeddedId 注解,如下:

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
   private Integer ages;
   @EmbeddedId
   private UserInfoID userInfoID;
   @Column(unique = true)
   private String uniqueNumber;
}
1
2
3
4
5
6
7
8
9
10
11
12

第三步:UserInfoRepository 不变,我们直接修改一下测试用例。

@Test
public void testIdClass() {
  userInfoRepository.save(UserInfo.builder().ages(1).userInfoID(UserInfoID.builder().name("jack").telephone("123456789").build()).build());
   Optional<UserInfo> userInfo = userInfoRepository.findById(UserInfoID.builder().name("jack").telephone("123456789").build());
   System.out.println(userInfo.get());
}
1
2
3
4
5
6

运行完之后,你可以得到相同的结果。那么 @IdClass 和 @EmbeddedId 的区别是什么?有以下两个方面:

  1. 如上面测试用例,在使用的时候,Embedded 用的是对象,而 IdClass 用的是具体的某一个字段;

  2. 二者的JPQL 也会不一样:

    用 @IdClass JPQL 的写法:SELECT u.name FROM UserInfo u

    用 @EmbeddedId 的 JPQL 的写法:select u.userInfoId.name FROM UserInfo u

联合主键还有需要注意的就是,它与唯一性索引约束的区别是写法不同,如上面所讲,唯一性索引的写法如下:

@Column(unique = true)
private String uniqueNumber;
1
2

到这里,联合主键我们讲完了,那么在遇到联合主键的时候,利用 @IdClass、@EmbeddedId,你就可以应对联合主键了。

此外,Java 是面向对象的,肯定会用到多态的使用场景,那么场景都有哪些?公共父类又该如何写?我们来学习一下。

# 实体之间的继承关系如何实现?🔥

在 Java 面向对象的语言环境中,@Entity 之间的关系多种多样,而根据 JPA 的规范,我们大致可以将其分为以下几种:

  1. 纯粹的继承,和表没关系,对象之间的字段共享。利用注解 @MappedSuperclass,协议规定父类不能是 @Entity。
  2. 单表多态问题,同一张 Table,表示了不同的对象,通过一个字段来进行区分。利用@Inheritance(strategy = InheritanceType.SINGLE_TABLE)注解完成,只有父类有 @Table。
  3. 多表多态,每一个子类一张表,父类的表拥有所有公用字段。通过@Inheritance(strategy = InheritanceType.JOINED)注解完成,父类和子类都是表,有公用的字段在父表里面。
  4. Object 的继承,数据库里面每一张表是分开的,相互独立不受影响。通过@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)注解完成,父类(可以是一张表,也可以不是)和子类都是表,相互之间没有关系。

# @MappedSuperclass 🔥

其中,第一种 @MappedSuperclass,我们暂时不多介绍,在第 12 课时讲解“JPA 的审计功能”时,再做详细介绍,我们先看一下第二种SINGLE_TABLE。

# @Inheritance(strategy = InheritanceType.SINGLE_TABLE)

父类实体对象与各个子实体对象共用一张表,通过一个字段的不同值代表不同的对象,我们看一个例子。

我们抽象一个 Book 对象,如下所示:

@Entity(name="book")
@Data
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="color", discriminatorType = DiscriminatorType.STRING)
public class Book {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String title;
}
1
2
3
4
5
6
7
8
9
10

再新建一个 BlueBook 对象,作为 Book 的子对象。

@Entity
@Data
@EqualsAndHashCode(callSuper=false)
@DiscriminatorValue("blue")
public class BlueBook extends Book{
   private String blueMark;
}

1
2
3
4
5
6
7
8

再新建一个 RedBook 对象,作为 Book 的另一子对象。

//红皮书
@Entity
@DiscriminatorValue("red")
@Data
@EqualsAndHashCode(callSuper=false)
public class RedBook extends Book {
   private String redMark;
}
1
2
3
4
5
6
7
8

这时,我们一共新建了三个 Entity 对象,其实都是指 book 这一张表,通过 book 表里面的 color 字段来区分红书还是绿书。我们继续做一下测试看看结果。

我们再新建一个 RedBookRepositor 类,操作一下 RedBook 会看到如下结果:

public interface RedBookRepository extends JpaRepository<RedBook,Long>{
}
1
2

然后再新建一个测试用例。

@DataJpaTest
public class RedBookRepositoryTest {
   @Autowired
   private RedBookRepository redBookRepository;
   @Test
   public void testRedBook() {
      RedBook redBook = new RedBook();
      redBook.setTitle("redbook");
      redBook.setRedMark("redmark");
      redBook.setId(1L);
      redBookRepository.saveAndFlush(redBook);
      RedBook r = redBookRepository.findById(1L).get();
    System.out.println(r.getId()+":"+r.getTitle()+":"+r.getRedMark());
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最后看一下执行结果。

Hibernate: create table book (color varchar(31) not null, id bigint not null, title varchar(255), blue_mark varchar(255), red_mark varchar(255), primary key (id))
1

你会发现,我们只创建了一张表,insert 了一条数据,但是我们发现 color 字段默认给的是 red。

Hibernate: insert into book (title, red_mark, color, id) values (?, ?, 'red', ?)
1

那么再看一下打印结果。

1:redbook:redmark
1

结果完全和预期的一样,这说明了 RedBook、BlueBook、Book,都是一张表,通过字段 color 的值不一样,来区分不同的实体。 那么接下来我们看一下 InheritanceType.JOINED,它的每个实体都是独立的表。

# @Inheritance(strategy = InheritanceType.JOINED)

在这种映射策略里面,继承结构中的每一个实体(entity)类都会映射到数据库里一个单独的表中。也就是说,每个实体(entity)都会被映射到数据库中,一个实体(entity)类对应数据库中的一个表。

其中根实体(root entity)对应的表中定义了主键(primary key),所有的子类对应的数据库表都要共同使用 Book 里面的 @ID 这个主键。

首先,我们改一下上面的三个实体,测试一下InheritanceType.JOINED,改动如下:

@Entity(name="book")
@Data
@Inheritance(strategy = InheritanceType.JOINED)
public class Book {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String title;
}
1
2
3
4
5
6
7
8
9

其次,我们 Book 父类、改变 Inheritance 策略、删除 DiscriminatorColumn,你会看到如下结果。

@Entity
@Data
@EqualsAndHashCode(callSuper=false)
@PrimaryKeyJoinColumn(name = "book_id", referencedColumnName = "id")
public class BlueBook extends Book{
   private String blueMark;
}


@Entity
@PrimaryKeyJoinColumn(name = "book_id", referencedColumnName = "id")
@Data
@EqualsAndHashCode(callSuper=false)
public class RedBook extends Book {
   private String redMark;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

然后,BlueBook和RedBook也删除DiscriminatorColumn,新增@PrimaryKeyJoinColumn(name = "book_id", referencedColumnName = "id"),和 book 父类共用一个主键值,而 RedBookRepository 和测试用例不变,我们执行看一下结果。

Hibernate: create table blue_book (blue_mark varchar(255), book_id bigint not null, primary key (book_id))
Hibernate: create table book (id bigint not null, title varchar(255), primary key (id))
Hibernate: create table red_book (red_mark varchar(255), book_id bigint not null, primary key (book_id))
Hibernate: alter table blue_book add constraint FK9uuwgq7a924vtnys1rgiyrlk7 foreign key (book_id) references book
Hibernate: alter table red_book add constraint FKk8rvl61bjy9lgsr9nhxn5soq5 foreign key (book_id) references book
1
2
3
4
5

上述代码可以看到,我们一共创建了三张表,并且新增了两个外键约束;而我们 save 的时候也生成了两个 insert 语句,如下:

Hibernate: insert into book (title, id) values (?, ?)
Hibernate: insert into red_book (red_mark, book_id) values (?, ?)
1
2

而打印结果依然不变。

1:redbook:redmark
1

这就是 InheritanceType.JOINED 的例子,这个方法和上面的 InheritanceType.SINGLE_TABLE 区别在于表的数量和关系不一样,这是表设计的另一种方式。

# @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 🔥

我们在使用 @MappedSuperClass 主键的时候,如果不指定 @Inhertance,默认就是此种TABLE_PER_CLASS模式。当然了,我们也显示指定,要求继承基类的都是一张表,而父类不是表,是 java 对象的抽象类。我们看一个例子。

首先,还是改一下上面的三个实体。

@Entity(name="book")
@Data
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Book {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String title;
}
1
2
3
4
5
6
7
8
9

其次,Book 表采用 TABLE_PER_CLASS 策略,其子实体类都代表各自的表,实体代码如下:

@Entity
@Data
@EqualsAndHashCode(callSuper=false)
public class RedBook extends Book {
   private String redMark;
}


@Entity
@Data
@EqualsAndHashCode(callSuper=false)
public class BlueBook extends Book{
   private String blueMark;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这时,从 RedBook 和 BlueBook 里面去掉 PrimaryKeyJoinColumn,而 RedBookRepository 和测试用例不变,我们执行看一下结果。

Hibernate: create table blue_book (id bigint not null, title varchar(255), blue_mark varchar(255), primary key (id))
Hibernate: create table book (id bigint not null, title varchar(255), primary key (id))
Hibernate: create table red_book (id bigint not null, title varchar(255), red_mark varchar(255), primary key (id))
1
2
3

这里可以看到,我们还是创建了三张表,但三张表什么关系也没有。而 insert 语句也只有一条,如下:

Hibernate: insert into red_book (title, red_mark, id) values (?, ?, ?)
1

打印结果还是不变。

1:redbook:redmark
1

这个方法与上面两个相比较,语义更加清晰,是比较常用的一种做法。 以上就是实体之间继承关系的实现方法,可以在涉及 java 多态的时候加以应用,不过要注意区分三种方式所表达的表的意思,再加以运用。

# 关于继承关系的经验之谈—@Inheritance 被淘汰 🔥

从我的个人经验来看,@Inheritance 的这种使用方式会逐渐被淘汰,因为这样的表的设计很复杂,本应该在业务层面做的事情(多态),而在 datasoure 的表级别做了。所以在 JPA 中使用这个的时候你就会想:“这么复杂的东西,我直接用 Mybatis 算了。”我想告诉你,其实它们是一样的,只是我们使用的思路不对。

那么为什么行业内都不建议使用了,还要介绍这么详细呢?因为,如果你遇到的是老一点的项目,如果不是用 Java 语言写的,不一定有面向对象的思想。这个时候如果让你迁移成 Java 怎么办?如果你可以想到这种用法,就不至于束手无措。

此外,在互联网项目中,一旦有关表的业务对象过多了之后,就可以拆表拆库了,这个时候我们要想到我们的@Table 注解指定表名和 schema。

关于上面提到的方法中,最常用的是第一种 @MappedSuperclass,这个我们将在第 12 课时“JPA 的审计功能解决了哪些问题?”中详细介绍,到时候你可以体验一下它的不同之处。

# 总结

Entity 里面常用的基本注解我们就介绍到这里,因为注解太多没办法一一介绍,你可以掌握一下学习方法。先通过源码把大致注解看一下,有哪些不熟悉的可以看看源码里面的注释,再阅读 JPA 官方协议,还可以写一个测试用例试,跑一下看看 sql 输出和日志,这样很快就可以知道结果了。

# 附. Spring MVC 测试

在 /src/test/resources 中创建 JpaApplication.http 文件

### 新增(注意请求空行!)
POST /api/v1/user HTTP/1.1
Host: 127.0.0.1:10000
Content-Type: application/json
Cache-Control: no-cache

{"name":"jack","email":"123@126.com"}


### 查询(我也不直到为啥这个必须加 ip + port
GET http://127.0.0.1:10000/api/v1/users?size=3&page=0
1
2
3
4
5
6
7
8
9
10
11

# 未整理 🔥

  • save对null,DynamicUpdate
  • 控制台怎么打印出的binding parameter:logging.level.org.hibernate.type.descriptor.sql=TRACE
编辑
上次更新: 2021/06/21, 15:45:42
JPA基础
基础操作

← JPA基础 基础操作→

最近更新
01
线程生命周期
07-06
02
线程安全理论
06-24
03
并发简史
06-24
更多文章>
Theme by Vdoing | Copyright © 2019-2021 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×