浅入浅出 Spring Data Jpa

前言

目前在做的项目使用的是 Spring Data Jpa,以前都是使用 Mybatis ,前段时间研究了 JPA 的使用。

目前公司项目的架构许多的技术点是我没有实践过的,所以我这段时间在学习这些东西,从 2021-5-15 开始博客的更新尽量保证一周一篇。

下周更新 GraphQL 。

本文例子全部在 github.com/zhangpanqin… ,数据库使用的是内存数据库H2。

JPA

认识JPA

JPAJava Persistence API 的简称,定义了 Java 对象与数据库表的映射关系,以及定义运行时期怎么 CRUD 的接口规范。

Hibernate 提供了 JPA 的实现。除此之外还有别的实现,比如 Open Jpa 等等。

Spring Data 为数据访问提供了一个熟悉且一致的,基于Spring的编程模型,同时仍保留基础数据存储的特殊特征。

  • Spring Data JPA 用于操作关系型数据库

  • Spring Data MongoDB 用于操作 MongoDB

  • Spring Data Elasticsearch 用于操作 Es

  • Spring Data Redis 用于操作 Redis

Spring Data JPA 底层的 JPA 实现采用的是 Hibernate ,也可以说是封装了 Hibernate,提供了 Spring 统一的编程模型。

统一的编程模型是指:下面这段代码,可以操作 JPA,ES,Redis 等等,只是 Person 上的注解不一样。

又可以通过更换 CrudRepository 接口,提供更细粒度的不同数据库的数据控制。

public interface PersonRepository extends CrudRepository<Person, Long> {

  List<Person> findByLastname(String lastname);

  List<Person> findByFirstnameLike(String firstname);
}
复制代码

JPA 常用注解介绍

使用 JPA 的时候不要使用数据库的外键,一是影响性能,二是不利于更换数据库。

不要使用 Hibernate 生成表结构,使用 flyway 组件,通过 SQL 来控制数据库表、索引,字段管理,flyway 灵活性更强

@Data
@Entity
@Table(name = "sys_user")
public class SysUserEntity extends BaseEntity {

    private String nickname;

    private Integer age;

    /**
     * name 定义的是关联表字段,referencedColumnName 是当前表中的主键字段
     */
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
    private List<SysBlogEntity> sysBlogEntities;
}
复制代码

@Entity

标记该类是一个 Entity ,被 JPA 管理

@Table

指定 Entity 与数据库中的那个表映射

@JoinColumn

指定了两个关联表之间使用哪两个字段关联

@Column

指定了Entity 字段与表的那个字段关联

@Id

指定主键字段

@GeneratedValue

指定主键的生成策略,下文详细介绍

@Transient

忽略字段与表字段的映射关系

@OneToMany

一对多关系指定,当前 Entity 与另一个 Entity 的映射关系。

/**
  * name 定义的是关联表字段,referencedColumnName 是当前表中的主键字段
  */
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
private List<SysBlogEntity> sysBlogEntities;
复制代码

@ManyToOne

参考 OneToMany

@ManyToMany

/**
  * @JoinTable 指定中间表,及中间表中的字段映射
  * @JoinColumn(name = ROLE_ID,referencedColumnName = ID) 指定了中间表的字段(name) 和另一个表那个字段关联(referencedColumnName)
  */
@ManyToMany
@JoinTable(name = SYS_USER_ROLE, joinColumns = {@JoinColumn(name = USER_ID, referencedColumnName = ID)},
           inverseJoinColumns = {@JoinColumn(name = ROLE_ID,referencedColumnName = ID)})
private List<SysRoleEntity> sysRoleEntities;
复制代码

@OneToOne

@OneToOne(optional=false)
@JoinColumn(name="CUSTREC_ID", unique=true, nullable=false, updatable=false)
private CustomerRecord customerRecord;
复制代码

@Query

可以写 SQL 操作数据库

public interface SysBlogRepository extends JpaRepository<SysBlogEntity,Long> {
    @Query(nativeQuery = true,value = "select  * from sys_blog where user_id = :userId")
    List<SysBlogEntity> findByUserId(Long userId);

    @Query(nativeQuery = true ,value = "select  * from sys_blog where title = :#{#sysBlogDTO.title}")
    List<SysBlogEntity> findByTitle(@Param("sysBlogDTO") SysBlogDTO sysBlogDTO);
}
复制代码

主键生成策略

/**
 * strategy 取值 
 * 
 * AUTO 由程序控制,默认策略。Oracle 默认是 SEQUENCE,Mysql默认是 IDENTITY
 *
 * IDENTITY: 主键自增长,需要在表中定义表字段自增长,Mysql ,PostgreSQL,SQL Server 可以采用)
 *
 * SEQUENCE:使用序列作为主键 ,Oracle、PostgreSQL、DB2 可以使用
 * @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emailSeq")
 * @SequenceGenerator(initialValue = 1, name = "emailSeq", sequenceName = "EMAIL_SEQUENCE")
 * private long id; 
 * 然后再数据库创建一个序列 create sequence EMAIL_SEQUENCE;
 * 当不在 SequenceGenerator 指定 sequenceName ,默认使用 Hibernate 提供的序列名称为 hibernate_sequence
 * 
 * TABLE 一般不适用这一个
 */
public @interface GeneratedValue {
    GenerationType strategy() default AUTO;
    String generator() default "";
}
复制代码

主键生成策略例子

@Data
@Table
@Entity
public class KeyGeneratorEntity {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid",strategy = "uuid")
    private String id;

    private String username;
}

@SpringBootTest
class KeyGeneratorRepositoryTest {

    @Autowired
    private KeyGeneratorRepository keyGeneratorRepository;

    @Test
    public void run(){
        final KeyGeneratorEntity keyGeneratorEntity = new KeyGeneratorEntity();
        keyGeneratorEntity.setUsername(LocalDateTime.now().toString());
        keyGeneratorRepository.save(keyGeneratorEntity);

        final List<KeyGeneratorEntity> all = keyGeneratorRepository.findAll();
        // [KeyGeneratorEntity(id=ff808081796f260c01796f2616aa0000, username=2021-05-15T16:30:37.722)]
        System.out.println(all);
    }
}
复制代码

hibernate 提供了以下主键生成策略

@GeneratedValue(strategy = GenerationType.SEQUENCE) 使用的是 SequenceStyleGenerator.class 控制主键生成。

@GenericGenerator(name = "system-uuid",strategy = "uuid") ,使用的是 UUIDHexGenerator.class

public class DefaultIdentifierGeneratorFactory		implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {	private ConcurrentHashMap<String, Class> generatorStrategyToClassNameMap = new ConcurrentHashMap<String, Class>();	@SuppressWarnings("deprecation")	public DefaultIdentifierGeneratorFactory() {		register( "uuid2", UUIDGenerator.class );		register( "guid", GUIDGenerator.class );			// can be done with UUIDGenerator + strategy		register( "uuid", UUIDHexGenerator.class );			// "deprecated" for new use		register( "uuid.hex", UUIDHexGenerator.class ); 	// uuid.hex is deprecated		register( "assigned", Assigned.class );		register( "identity", IdentityGenerator.class );		register( "select", SelectGenerator.class );		register( "sequence", SequenceStyleGenerator.class );		register( "seqhilo", SequenceHiLoGenerator.class );		register( "increment", IncrementGenerator.class );		register( "foreign", ForeignGenerator.class );		register( "sequence-identity", SequenceIdentityGenerator.class );		register( "enhanced-sequence", SequenceStyleGenerator.class );		register( "enhanced-table", TableGenerator.class );	}	public void register(String strategy, Class generatorClass) {		LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );		final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );		if ( previous != null ) {			LOG.debugf( "    - overriding [%s]", previous.getName() );		}	}}
复制代码

Lazy 需要在一个事务内执行

public class Order1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.LAZY)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    private List<OrderItem> orderItemList;}//    @Transactional(readOnly = true)public List<Order1> listOrder(){    System.out.println("---------------------开始查询---------------------");    final List<Order1> all = orderRepository.findAll();    System.out.println("---------------------开始懒加载---------------------");    System.out.println(JSON.toJSONString(all));    return all;}
复制代码

当数据需要懒加载的时候,JPA不会查询 Lazy 的数据,只有在使用的时候才会查询,但是使用的时候需要和原来的查询在同一个事务中,不然会抛出以下异常

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mflyyou.jpa.n1.Order1.orderItemList, could not initialize proxy - no Session
复制代码

由于没有开启事务,orderRepository.findAll() 执行之后这个查询事务就关闭了,所以获取 Order1.orderItemList 的时候报错。

当添加事务注解 @Transactional ,整个方法在一个事务内执行,就不会报错了。

N+1 问题

@Entity@Table(name = "order1")@Datapublic class Order1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.LAZY)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    private List<OrderItem> orderItemList;}@Transactional(readOnly = true)public Order1 findOne(Long id){    System.out.println("---------------------开始查询---------------------");    final Optional<Order1> byId = orderRepository.findById(id);    System.out.println("---------------------开始懒加载---------------------");    System.out.println(JSON.toJSONString(byId.get()));    return byId.get();}
复制代码

当查询 Order1 的时候,实际上不会查询 orderItemList ,当使用 orderItemList 的时候在查询一次。

Order1 有 N 个关联属性的时候,就会查询 N 次来获取对应的数据。

当数据都处于 FetchType.LAZY 获取数据,就会产生懒加载问题

---------------------开始查询------------------------------------------开始查询---------------------Hibernate:     select        order1x0_.id as id1_2_0_,        order1x0_.description as descript2_2_0_     from        order1 order1x0_     where        order1x0_.id=?---------------------开始懒加载---------------------Hibernate:     select        orderiteml0_.order_id as order_id3_3_0_,        orderiteml0_.id as id1_3_0_,        orderiteml0_.id as id1_3_1_,        orderiteml0_.name as name2_3_1_,        orderiteml0_.order_id as order_id3_3_1_,        orderiteml0_.price as price4_3_1_     from        order_item orderiteml0_     where
复制代码
{    "description": "测试2021-05-15T17:35:50.349",    "id": 1,    "orderItemList": [        {            "id": 2,            "name": "ceshi2021-05-15T17:35:50.423",            "orderId": 1,            "price": 10        },        {            "id": 3,            "name": "ceshi2021-05-15T17:35:50.423",            "orderId": 1,            "price": 10        },        {            "id": 4,            "name": "ceshi2021-05-15T17:35:50.423",            "orderId": 1,            "price": 10        },        {            "id": 5,            "name": "ceshi2021-05-15T17:35:50.423",            "orderId": 1,            "price": 10        },        {            "id": 6,            "name": "ceshi2021-05-15T17:35:50.423",            "orderId": 1,            "price": 10        }    ]}
复制代码

解决办法呢,可以使用 @OneToMany(fetch = FetchType.EAGER)

查询一次获取了全部数据

Hibernate:     select        order1x0_.id as id1_2_0_,        order1x0_.description as descript2_2_0_,        orderiteml1_.order_id as order_id3_3_1_,        orderiteml1_.id as id1_3_1_,        orderiteml1_.id as id1_3_2_,        orderiteml1_.name as name2_3_2_,        orderiteml1_.order_id as order_id3_3_2_,        orderiteml1_.price as price4_3_2_     from        order1 order1x0_     left outer join        order_item orderiteml1_             on order1x0_.id=orderiteml1_.order_id     where        order1x0_.id=?
复制代码

当出现 orderItemList 和 orderItemList2 的时候,@OneToMany(fetch = FetchType.EAGER) 会报错

public class Order1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    private List<OrderItem> orderItemList;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))    private List<OrderItem2> orderItemList2;}
复制代码

@NamedEntityGraph@EntityGraph 可以解决 N+1 问题。又可以解决级联查询的时候,查询哪些成员变量,不查询哪些成员变量。让我们可以根据业务有更高的自由度查询数据。

EntityGraph

@NamedEntityGraph 定义查询的时候查询哪些数据,@EntityGraph 用于标记 Repository 使用哪个 NamedEntityGraph

image-20210516132930802

@Entity@Table(name = "order1")@Data@NamedEntityGraph(name = "searchOrderGraphItem",        attributeNodes = {                @NamedAttributeNode(value = "orderGraphItemList", subgraph = "OrderGraphItem_productGraphs"),        },        subgraphs = {                @NamedSubgraph(name = "OrderGraphItem_productGraphs", attributeNodes = {                        @NamedAttributeNode(value = "productGraphs")                })        })public class OrderGraph1 {    @Id    @GeneratedValue    private Long id;    private String description;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))    private Set<OrderGraphItem> orderGraphItemList;    @OneToMany(fetch = FetchType.EAGER)    @JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))    private Set<OrderGraphItem2> orderGraphItemList2;}public interface OrderGraphRepository extends JpaRepository<OrderGraph1, Long> {    @EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)    OrderGraph1 findByIdEquals(Long id);}
复制代码
@Testpublic void findById() {    final OrderGraph1 orderGraph1s = orderGraphService.findById(1L);    assertThat(orderGraph1s, notNullValue());}
复制代码

@EntityGraph 中指定的的 type 可以取值 FETCHLOAD

  • FETCH 对于 NamedEntityGraph 定义的 attributeNodes 使用eager,未声明的使用 lazy
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)OrderGraph1 findByIdEquals(Long id);
复制代码

只会查询出 OrderGraph1 对应表中的字段和 orderGraphItemList。orderGraphItemList2 当用的时候才会查询。

  • LOAD 对于 NamedEntityGraph 定义的 attributeNodes 使用eager,未声明的属性使用属性配置的 FetchType
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.LOAD)OrderGraph1 findByIdEquals(Long id);
复制代码

这个会查询出 OrderGraph1 对应表中的字段和 orderGraphItemList。orderGraphItemList2 属性由于配置的是 FetchType.EAGER ,也会直接查出来。

orderGraphItemList2 属性如果配置的是 FetchType.Lazy ,orderGraphItemList2 使用的时候才会被查出来

审计功能

一般表中都会有,主键 id ,创建时间 ,更新事件,谁创建,谁更新,再加上乐观锁。

实现 AuditorAware,填充用户 id。

@Data@MappedSuperclass@EntityListeners(AuditingEntityListener.class)public abstract class BaseEntity {    @Id    private Long id;    /**     * 创建时间     */    @CreatedDate    @Column(name = "create_date", updatable = false)    private Instant createDate;    /**     * 修改时间     */    @LastModifiedDate    @Column(name = "update_date")    private Instant updateDate;    /**     * 被谁创建     */    @CreatedBy    @Column(name = "create_by", updatable = false)    private Integer createBy;    /**     * 被谁修改     */    @LastModifiedBy    @Column(name = "update_by")    private Integer updateBy;    /**     * 乐观锁     */    @Version    @Column(name = "version")    private Long version = 0L;}@Componentpublic class MyAuditorAware implements AuditorAware<Integer> {    /**     * 获取当前登录的 id     */    @Override    public Optional<Integer> getCurrentAuditor() {        // 在请求头获取登录标识,再查询用户主键 id        return Optional.ofNullable(100);    }}
复制代码

乐观锁,更新的version 必须等于数据库中的版本,否则更新会抛出异常。也可以使用 Spring-retry 捕获 ObjectOptimisticLockingFailureException 重试更新。

@Data@Entity@Table(name = "sys_user")public class SysUserEntity extends BaseEntity {    private String nickname;    private Integer age;}
复制代码
@SpringBootTestclass JpaStudyApplicationTests {    private static final Long USER_ID_EQUALS_1 = 1L;    private static final Long USER_ID_EQUALS_2 = 2L;    private static final Long USER_ID_EQUALS_3 = 3L;    @Resource    private SysUserRepository sysUserRepository;    private SysUserEntity saveSysUserEntity;    private SysUserEntity saveSysUserEntity2;    private SysUserEntity saveSysUserEntity3;    @BeforeEach    public void beforeEach() {        saveSysUserEntity = new SysUserEntity();        saveSysUserEntity.setAge(10);        saveSysUserEntity.setId(USER_ID_EQUALS_1);        saveSysUserEntity.setNickname("测试");        saveSysUserEntity.setVersion(10L);        saveSysUserEntity2 = new SysUserEntity();        saveSysUserEntity2.setAge(10);        saveSysUserEntity2.setId(USER_ID_EQUALS_2);        saveSysUserEntity2.setNickname("测试");        saveSysUserEntity2.setVersion(10L);        saveSysUserEntity3 = new SysUserEntity();        saveSysUserEntity3.setAge(10);        saveSysUserEntity3.setId(USER_ID_EQUALS_3);        saveSysUserEntity3.setNickname("测试");        saveSysUserEntity3.setVersion(10L);    }    @Test    public void should_update_error() {        sysUserRepository.save(saveSysUserEntity);        final Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_1);        assertThat(byId.isPresent(), is(Boolean.TRUE));        final SysUserEntity sysUserEntity = byId.get();        // 设置版本 2 也报错//        sysUserEntity.setVersion(2L);        sysUserEntity.setVersion(12L);        sysUserEntity.setNickname("乐观锁更新" + LocalDateTime.now().toString());        final Executable executable = () -> sysUserRepository.save(sysUserEntity);        assertThrows(ObjectOptimisticLockingFailureException.class, executable);    }    @Test    public void should_update_success() {        sysUserRepository.save(saveSysUserEntity2);        Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_2);        assertThat(byId.isPresent(), is(Boolean.TRUE));        SysUserEntity sysUserEntity = new SysUserEntity();        sysUserEntity.setId(USER_ID_EQUALS_2);        sysUserEntity.setVersion(10L);        sysUserEntity.setNickname("乐观锁更新" + LocalDateTime.now().toString());        SysUserEntity save = sysUserRepository.save(sysUserEntity);        assertThat(save.getVersion(), is(11L));    }}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享