百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

浅入浅出 Spring Data Jpa(深入浅出springboot)

lipiwang 2024-11-15 22:01 13 浏览 0 评论

前言

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

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

下周更新 GraphQL 。

本文例子全部在 https://github.com/zhangpanqin/jpa-study ,数据库使用的是内存数据库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")
@Data
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 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

@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);
}
@Test
public 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);

审计功能

一般表中都会有,主键 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;
 }
 
 @Component
 public 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;
 }
 @SpringBootTest
 class 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));
     }
 
 }

相关推荐

前端入门——css 网格轨道详细介绍

上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...

Islands Architecture(孤岛架构)在携程新版首页的实践

一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...

HTML中script标签中的那些属性

HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...

CSS 中各种居中你真的玩明白了么

页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...

CSS样式更改——列表、表格和轮廓

上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...

一文吃透 CSS Flex 布局

原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...

css实现多行文本的展开收起

背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...

css 垂直居中的几种实现方式

前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...

WordPress固定链接设置

WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...

面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿

前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...

3种CSS清除浮动的方法

今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...

2025 年 CSS 终于要支持强大的自定义函数了?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...

css3属性(transform)的一个css3动画小应用

闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...

CSS基础知识(七)CSS背景

一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...

CSS 水平居中方式二

<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...

取消回复欢迎 发表评论: