资讯专栏INFORMATION COLUMN

Spring Boot QuickStart (5) - Spring Data JPA

sutaking / 3294人阅读

摘要:关联关系的关联关系定义上,感觉并不是很灵活,姿势也比较难找。如,定义在关联关系上的参数可以设置级联的相关东西。因为序列化会涉及到实体类关联对象的获取,会触发所有的关联关系。

接(4) - Database 系列.

Java Persistence API,可以理解就是 Java 一个持久化标准或规范,Spring Data JPA 是对它的实现。并且提供多个 JPA 厂商适配,如 Hibernate、Apache 的 OpenJpa、Eclipse的EclipseLink等。

spring-boot-starter-data-jpa 默认使用的是 Hibernate 实现。

直接引入依赖:


    org.springframework.boot
    spring-boot-starter-data-jpa

开启 SQL 调试:

spring.jpa.database=mysql
spring.jpa.show-sql=true

在 SpringBoot + Spring Data Jpa 中,不需要额外的配置什么,只需要编写实体类(Entity)与数据访问接口(Repository)就能开箱即用,Spring Data JPA 能基于接口中的方法规范命名自动的帮你生成实现(根据方法命名生成实现,是不是很牛逼?)

Spring Data JPA 还默认提供了几个常用的Repository接口:

Repository: 仅仅是一个标识,没有任何方法,方便 Spring 自动扫描识别

CrudRepository: 继承 Repository,实现了一组 CRUD 相关的方法

PagingAndSortingRepository: 继承 CrudRepository,实现了一组分页排序相关的方法

JpaRepository: 继承 PagingAndSortingRepository,实现一组JPA规范相关的方法

推荐教程:Spring Data JPA实战入门训练 https://course.tianmaying.com...

Entity 实体和 Respository 接口

根据 user 表结构,我们定义好 User 实体类与 UserRespository 接口类。

这里,还自定义了一个 @Query 接口,为了体验下自定义查询。因为使用了 lombok,所以实体类看起来很干净。

User.java

@Data
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true, updatable = false)
    @JsonProperty(value = "email")
    private String username;

    @Column(nullable = false)
    @JsonIgnore
    private String password;

    @Column(nullable = false)
    @JsonIgnore
    private String salt;

    @Column(nullable = true)
    private Date birthday;

    @Column(nullable = false)
    private String sex;

    @Column(nullable = true)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Timestamp access;

    @Column(nullable = true)
    @JsonFormat(pattern="HH:mm:ss")
    private Time accessTime;

    @Column(nullable = false)
    private Integer state;

    @Column(nullable = false, insertable = false, updatable = false)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Timestamp created;

    @Column(nullable = false, insertable = false, updatable = false)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Timestamp updated;
}

@Data 是 lombok 的注解,自动生成Getter,Setter,toString,构造函数等

@Entity 注解这是个实体类

@Table 注解表相关,如别名等

@Id 注解主键,@GeneratedValue 表示自动生成

@DynamicUpdate,@DynamicInsert 注解可以动态的生成insert、update 语句,默认会生成全部的update

@Column 标识一些字段特性,字段别名,是否允许为空,是否唯一,是否进行插入和更新(比如由MySQL自动维护)

@Transient 标识该字段并非数据库字段映射

@JsonProperty 定义 Spring JSON 别名,@JsonIgnore 定义 JSON 时忽略该字段,@JsonFormat 定义 JSON 时进行格式化操作

UserRepository.java

public interface UserRepository extends JpaRepository, UserCustomRepository {
    User findByUsername(String username);

    @Transactional
    @Modifying
    @Query("UPDATE User SET state = ?2 WHERE id = ?1 ")
    Integer saveState(Long id, Integer state);
}

@Transactional 用来标识事务,一般修改、删除会用到, @Modifying 标识这是个修改、删除的Query

@Param 标注在参数上,可用于标识参数式绑定(不使用 ?1 而使用 :param)

好了,接下来我们就可以进行单表的增、删、改、查分页排序操作了:

@Autowired
private UserRepository userRepository;
    
User user = new User();
userRepository.save(user); // 插入或保存
userRepository.saveFlush(user); // 保存并刷新
userRepository.exists(1) // 主键查询是否存在
userRepository.findOne(1); // 主键查询单条
userRepository.delete(1); // 主键删除
userRepository.findByUsername("a@b.com"); // 查询单条
userRepository.findAll(pageable); // 带排序和分页的查询列表
userRepository.saveState(1, 0); // 更新单个字段

通常,exist(),delete()之类的方法,我们可能直接会操作 UserRepository,但是一般情况下,在 UserRepository 上面还会提供一个 UserService 来进行一系列的操作(比如数据校验,逻辑判断之类)

分页和排序

PagingAndSortingRepository 和 JpaRepository 接口都具有分页和排序的功能。因为后者继承自前者。比如下面这个方法:

Page findAll(Pageable var1);

Pageable 是Spring Data库中定义的一个接口,该接口是所有分页相关信息的一个抽象,通过该接口,我们可以得到和分页相关所有信息(例如pageNumber、pageSize等),这样,Jpa就能够通过pageable参数来组装一个带分页信息的SQL语句。

Page 类也是Spring Data提供的一个接口,该接口表示一部分数据的集合以及其相关的下一部分数据、数据总数等相关信息,通过该接口,我们可以得到数据的总体信息(数据总数、总页数...)以及当前数据的信息(当前数据的集合、当前页数等)

Pageable只是一个抽象的接口。可以通过两种途径生成 Pageable 对象:

通过参数,自己接收参数,自己构造生成 Pageable 对象

@RequestMapping(value = "", method = RequestMethod.GET)
public Object page(@RequestParam(name = "page", required = false) Integer page,
                  @RequestParam(name="size", required = false) Integer size) {
                  
   Sort sort = new Sort(Sort.Direction.DESC, "id");
   Pageable pageable = new PageRequest(page, size, sort);

   Page users = userRepository.findAll(pageable);

   return this.responseData(users);
}

这种方式你可以灵活的定义传参。

通过 @PageableDefault 注解,会把参数自动注入成 Pageable 对象,默认是三个参数值:

page=,第几页,从0开始,默认为第0页
size=,每一页的大小
sort=,排序相关的信息,例如sort=firstname&sort=lastname,desc

@RequestMapping(value = "/search", method = RequestMethod.GET)
public Object search(@PageableDefault(size = 3, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
   Page users = userRepository.findAll(pageable);

   return this.responseData(users);
}

看起来,这种方式更优雅一些。

关联关系

Spring Data JPA 的关联关系定义上,感觉并不是很灵活,姿势也比较难找。

视频教程:http://www.jikexueyuan.com/co...

OneToOne 一对一

一对一的关系,拿 user,user_detail 来说,一般应用起来,有以下几种情况:

主键直接关联:user(id, xx);user_detail(id, xx) 或 user(id, xx),user_detail(user_id, xx) 其中 id, userid 为主键

主表含外键的关联:user(id, role_id, xx);role(id, xx) 。 其中 id 为自增主键

附表含外键的关联:user(id, xx);user_detail(id, user_id, xx) 。其中 id 为自增主键

主表含外键的关联:用户->角色是一对一,而角色->用户是多对一,而大部分情况,我们是通过 user 表来查询某个角色的列表,而通过 role 来查询某个角色的列表可能性很小。

附表表含外键的关联:其实和主表含外键的关联完全相反,关联的定义也是相反的。

主键ID关联

单向关联,直接在 User 上定义 @OneToOne 与 @PrimaryKeyJoinColumn 即可完成

@Entity
@Data
public class User {
...
     @OneToOne
    @PrimaryKeyJoinColumn
    private UserDetail detail;
...
}

// 获取的user,会包含detail属性
User user = userRepository.findOne(userId);

双向关联,除了要定义 User 的 @OneToOne,还需要定义 UserDetail 的 @OneToOne,用 mappedBy 指示 User 表的属性名。

@Entity
@Data
public class UserDetail {
...
    @OneToOne(mappedBy = "detail")
    private User user;
...
}

出问题了,双向关联,涉及到一个循环引用无限递归的问题,这个问题会发生在 toString、 JSON 转换上。可能这只是个基础问题,但对于我这个入门汉,抓瞎了好长时间。

解决办法:

分别给User、UserDetail的关联属性加上:@JsonManagedReference、@JsonBackReference注解,解决 JSON 问题

给 UserDetail 实体类加上 @ToString(exclude = "user") 注解,解决 toString 的问题。

所以 UserDetail 最终造型应该是这样的:

@Entity
@Data
@ToString(exclude = "user")
public class UserDetail {
...

    @OneToOne(mappedBy = "detail")
    @JsonBackReference
    private User user;
}

// 现在可以进行双向查询了
User user1 = userRepository.findOne(userId);
userDetail userdetail = userDetailRepository.findOne(userId);
User user2 = userdetail.getUser();

@PrimaryKeyJoinColumn 注解主要用于主键关联,注意实体属性需要使用 @Id 的为主键,假如现在是:user(id, xx),user_detail(user_id, xx) 这种情况。则需要在 User 类上自定义它的属性:

// User
@OneToOne
@PrimaryKeyJoinColumn(referencedColumnName = "user_id")
@JsonManagedReference
private UserDetail detail;
主表含外键

使用 @JoinColumn 注解即可完成,默认使用的外键是(属性名+下划线+id)。关联附表的主键 id。

可以通过 name=,referencedColumnName= 属性重新自定义。

@Entity
@Data
public class User {
...
    // 属性名为role,所以 @JoinColumn 会默认外键是 role_id
    @OneToOne
    @JoinColumn
    @JsonManagedReference
    private Role role;
...
}

对于 user->role 的表关联需求,我们不需要定义 OneToOne 反向关系,并且 role->user 本来是个一对多关系。

附表含外键

这种情况一般也会经常出现,它可以保证每个表都有一个自增主键的id

因为外键在附表上,所以需要反过来,在 User 上定义 mapped。

如果是双向关联,同样需要加上忽略 toString(),JSON 的注解

@Entity
@Data
public class User {
...
    @OneToOne(mappedBy = "user")
    @JsonManagedReference
    private UserDetail detail;
...
}

@Entity
@Data
@ToString(exclude = "user")
public class UserDetail {
...
    @OneToOne
    @JoinColumn
    @JsonBackReference
    private User user;
...
}

User user1 = userRepository.findOne(userId);

// 给 UserDetail 定义一个独立的 findByUserId 接口,这样可以通过操作 UserDetail 反向获取到 user 的数据
userDetail userdetail = userDetailRepository.findByUserId(userId);
User user2 = userdetail.getUser();

实际上,在上面的例子里面,考虑实际的场景,几乎不需要定义 OneToOne 的反向关联(伪需求),这样就不用解决循环引用的问题了。这里只是意淫,不是吗?

现在有个问题出现了,这种情况下(附表含外键),我如何定义 User->UserDetail 的单向关系呢?

关联关系:一对多

接着上面的例子,Role -> User 实际上是个一对多的关系。但我们一般不会这么做。直接通过 User 就可以查询嘛。所以这里演示另一个例子。

User->Order 是一对多,Order->User 是多对一,定义 Order 实体,注意@Table 注解,因为 order 是 MySQL 关键词(此处中枪)

@Entity
@Data
@Table(name = "`order`")
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;
}

然后在 User 中定义 @OneToMany,因为是一对多,所以返回的是List,并且一般设置为 LAZY

@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name="user_id")
private List orders;

测试一下:

User user = userService.findOne(userId);
if (user != null) {
     // LAZY 的缘故,在 getOrders 才会触发获取操作
     List orders = user.getOrders();
     return this.responseData(orders);
}

再看看反向关联,也就是 @ManyToOne,稍作调整

User 实体类
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private List orders;

Order 实体类
@ManyToOne
private User user;

再测试一下:

Order order = orderRepository.findOne(orderId);
if (order != null) {
    User user = order.getUser();
    return this.responseData(user);
}
总结

想想实际场景,我们不太需要定义 User->Order 这种关联,因为用户可能有很多订单,这个量是无可预测的。这时候这种关联查询,不能分页,没有意义(也可能是我姿势不对)。

如果是有限的 XToMany 关联,是有意义的。比如配置管理。一个应用拥有有限的多项配置?

Order->User 这种关联是有意义的。拿到一个 order_id 去反查用户信息。

关联关系:多对多

Order <-> Product 是多对多的关系,关联表是 order_product,

Order 实体配置 @ManyToMany 属性,不需要定义 OrderProduct 实体类,

// @JoinTable 实际可以省略,因为使用的是默认配置
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
   name = "order_product",
   joinColumns = @JoinColumn(name = "order_id"),
   inverseJoinColumns = @JoinColumn(name = "product_id"))
@JsonManagedReference
private List products;

这样就定义了单向关联,双向关联类似在 Product 实体配置:

@ManyToMany(mappedBy = "products", fetch = FetchType.LAZY)
@JsonIgnore
private List orders;

好了,这样就OK了,实际按照上面的解释,Product -> Order 是不太有意义的。

属性参数

@OneToOne的属性:

cascade 属性表示级联操作策略,有 CascadeType.ALL 等值。

fetch 属性表示实体的加载方式,有 FetchType.LAZY 和 FetchType.EAGER 两种取值,默认值为 EAGER

拿OneToOne来说,如果是 EAGER 方式,那么会产生一个连接查询,如果是 LAZY 方式,则是两个查询。并且第二个查询会在用的时候才会触发(仅仅.getXXX是不够的)。

级联

在未定义级联的情况下,我们通常需要手动插入。

如 user(id, xx),user_detail(id, user_id, xx)

User user = new User();
userRepository.save(user);

UserDetail userDetail = new UserDetail();
userDetail.setUserId(user.getId());
userDetailRepository.save(userDetail);

定义在关联关系上的 cascade 参数可以设置级联的相关东西。

经过一番研究,这部分暂时我还没搞明白正确姿势,玩不转。

复杂的查询 问题总结 数据库默认值字段,插入后不会自动返回默认值。Entity not return default value after insert

一般关键表会记录创建、更新时间,满足基本审计需求,以前我喜欢使用 MySQL 默认值特性,这样应用层就可以不用管他们了,如:

`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

在实体中,我们要忽略插入和更新对他们的操作。

@Column(nullable = false, insertable = false, updatable = false)
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Timestamp created;

@Column(nullable = false, insertable = false, updatable = false)
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Timestamp updated;

看起来不错哦,工作正常,但是:

Spring Data Jpa 在 save() 完成以后,对于这种数据库默认插入的值,拿不到回写的数据啊,无论我尝试网上的方法使用 saveAndFlush() 还是手动 flush() 都是扯淡。

这个坑,我踩了好久,到现在,依然不知道这种情况怎么解决。

临时解决方案:

抛弃数据库默认值特性,在实体类借助 @PrePersist、@PreUpdate 手动实现,如果有多个表,遵循同一规范,可以搞个基类,虽然不太爽,但是能正常工作。

@MappedSuperclass
@Getter
@Setter
public class BaseEntity {
    @Column(nullable = false, updatable = false)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Timestamp created;

    @Column(nullable = false)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Timestamp updated;

    @PrePersist
    public void basePrePersist() {
       long timestamp = new java.util.Date().getTime();
       created = new Timestamp(timestamp);
       updated = new Timestamp(timestamp);
    }
    
    @PreUpdate
    public void basePreUpdate() {
       updated = new Timestamp(new java.util.Date().getTime());
    }
}
OneToOne 关联关系,指定 FetchType.LAZY,JSON 时会出错,得不到数据

原因大概是,JSON序列化的时候,数据还没有fetch到,出错信息如下:

Could not write JSON: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer

解决方法:

application.properties 增加配置项:

spring.jackson.serialization.fail-on-empty-beans=false

然而你会发现最终的 JSON 多出来两个key,分别是handler、hibernateLazyInitializer

所以还需要在实体类上增加注解:

@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})

搞定!

关于循环引用和 Lazy Fetch

接上个问题,这里要思考一个问题场景:

Lazy Fetch 在 JSON 序列化实体类时失效

我们定义了一个 User 实体类,这个实体类有几个关联,比如 OneToOne和 OneToMany,并且设置了Lazy,我们执行了 findOne 查询并返回结果,在 RestController 的时候,会默认执行 Jackson 的序列化JSON 操作。

因为序列化会涉及到实体类关联对象的获取,会触发所有的关联关系。生成一大堆的查询 SQL, 这样 LAZY 就失去意义了啊,比如我只想要 User 单表的基本信息怎么办?

stackoverflow 可以搜到了好多类似问题,我目前还没找到正确的姿势。

可以想象的是,不应当将实体类直接返回给客户端,应该再定义一个返回数据的DTO,将实体类的数据复制到DTO,然后返回并JSON。然而这样好蛋疼,随便一个项目你至少需要定义实体类,输入参数的DTO,输出参数的DTO。

问题暂放这里。

循环引用

我们虽然通过 @JsonBackReference 和 JsonManagedReference 来解决。但是有时候,对于两个 OneToOne 实体,我们都需要 JSON 序列化怎么办?如 User 与 UserDetail

另一个办法,给实体类加上 @JsonIdentityInfo:

@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")

这样好处是显而易见的,只需要在实体类上注解一下即可。

猜测原理是引入了id,检测了主键是否一致,决定是否引用下去。如 User->UserDetail->User。

所以他还会多一次查询,并且关联数据上会多一个关联关系的 id 的字段。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67271.html

相关文章

  • Spring Boot QuickStart (1)

    摘要:开始介绍简化了基于的应用开发,你只需要就能创建一个独立的,产品级别的应用。该包含很多搭建,快速运行项目所需的依赖,并提供一致的,可管理传递性的依赖集。日志级别通过标识开启控制台级别日志记录,也可以在中指定日志级别配置示例 开始 介绍 Spring Boot 简化了基于 Spring 的应用开发,你只需要 run 就能创建一个独立的,产品级别的 Spring 应用。 Spring 平台...

    klinson 评论0 收藏0
  • Spring Boot QuickStart (4) - Database

    摘要:本文只是引子,后续更新到独立章节。尤其是,这也是现在号称流行的组合。幸亏现在看起来不主流了。增删改查多条件组合查询分页,排序等多表关联。而每个类写上构造函数,,实在是蛋疼。 本文只是引子,后续更新到独立章节。 环境:Spring Boot 1.5.4 到了操作数据库的环节,以 MySQL 为基准,体验一下数据库的相关操作,先让我纠结一下,至少有以下四种姿势。 JDBC。原生的 JD...

    FrozenMap 评论0 收藏0
  • Spring Boot [组件学习-Spring Data JPA]

    摘要:与的关系是什么是官方提出的持久化规范。它为开发人员提供了一种对象关联映射工具来管理应用中的关系数据。他的出现主要是为了简化现有的持久化开发工作和整合技术,结束现在,,等框架各自为营的局面。定义了在对数据库中的对象处理查询和事务运行时的的。 导读: 在上篇文章中对Spring MVC常用的一些注解做了简要的说明,在这篇文章中主要对Spring Data JPA 做一个简要的说明,并附有一...

    andong777 评论0 收藏0
  • 猫头鹰的深夜翻译:为什么要使用Spring Boot

    摘要:初次使用的人往往会困惑,不知道该使用哪种方法。目前来说,团队推荐使用基于的方法来提供更高的灵活性。配置,从而在应用启动时执行脚本来初始化数据库。目前为止我们没有任何消息需要配置,所以只在文件夹中创建一个空的文件。将配置为,它包含的上下文。 前言 spring是一个用于创建web和企业应用的一个很流行的框架。和别的只关注于一点的框架不同,Spring框架通过投资并组合项目提供了大量的功能...

    Jaden 评论0 收藏0
  • 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》

    摘要:下一代服务端开发下一代服务端开发第部门快速开始第章快速开始环境准备,,快速上手实现一个第章企业级服务开发从到语言的缺点发展历程的缺点为什么是产生的背景解决了哪些问题为什么是的发展历程容器的配置地狱是什么从到下一代企业级服务开发在移动开发领域 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》 Kotlin + Spring Boot : 下一代 Java...

    springDevBird 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<