接(4) - Database 系列.html
Java Persistence API,能够理解就是 Java 一个持久化标准或规范,Spring Data JPA 是对它的实现。而且提供多个 JPA 厂商适配,如 Hibernate、Apache 的 OpenJpa、Eclipse的EclipseLink等。java
spring-boot-starter-data-jpa 默认使用的是 Hibernate 实现。mysql
<!-- more -->spring
直接引入依赖:sql
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
开启 SQL 调试:数据库
spring.jpa.database=mysql spring.jpa.show-sql=true
在 SpringBoot + Spring Data Jpa 中,不须要额外的配置什么,只须要编写实体类(Entity)与数据访问接口(Repository)就能开箱即用,Spring Data JPA 能基于接口中的方法规范命名自动的帮你生成实现(根据方法命名生成实现,是否是很牛逼?)app
Spring Data JPA 还默认提供了几个经常使用的Repository接口:函数
Repository: 仅仅是一个标识,没有任何方法,方便 Spring 自动扫描识别spring-boot
CrudRepository: 继承 Repository,实现了一组 CRUD 相关的方法测试
PagingAndSortingRepository: 继承 CrudRepository,实现了一组分页排序相关的方法
JpaRepository: 继承 PagingAndSortingRepository,实现一组JPA规范相关的方法
推荐教程:Spring Data JPA实战入门训练 https://course.tianmaying.com...
根据 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<User, Long>, 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<T> 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<User> 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<User> users = userRepository.findAll(pageable); return this.responseData(users); }
看起来,这种方式更优雅一些。
Spring Data JPA 的关联关系定义上,感受并非很灵活,姿式也比较难找。
视频教程:http://www.jikexueyuan.com/co...
一对一的关系,拿 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 来查询某个角色的列表可能性很小。
附表表含外键的关联:其实和主表含外键的关联彻底相反,关联的定义也是相反的。
单向关联,直接在 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<Order>,而且通常设置为 LAZY
@OneToMany(fetch = FetchType.LAZY) @JoinColumn(name="user_id") private List<Order> orders;
测试一下:
User user = userService.findOne(userId); if (user != null) { // LAZY 的缘故,在 getOrders 才会触发获取操做 List<Order> orders = user.getOrders(); return this.responseData(orders); }
再看看反向关联,也就是 @ManyToOne,稍做调整
User 实体类 @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") private List<Order> 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<Product> products;
这样就定义了单向关联,双向关联相似在 Product 实体配置:
@ManyToMany(mappedBy = "products", fetch = FetchType.LAZY) @JsonIgnore private List<Order> 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 参数能够设置级联的相关东西。
通过一番研究,这部分暂时我还没搞明白正确姿式,玩不转。
通常关键表会记录建立、更新时间,知足基本审计需求,之前我喜欢使用 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()); } }
缘由大概是,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 在 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 的字段。