spring data简介
spring data是spring的一个子项目,是spring家族成员之一,旨在简化数据访问层的开发,给操作数据库(关系数据库sql、非关系数据库nosql)提供了一系列组件,常用的比如
- spring data jpa:整合关系数据库
- spring data redis:整合redis
- spring data mongodb:整合mongodb
- spring data elasticsearch:整合es
spring data jpa简介
- Java Persistence API,spring data jpa是spring data系列组件之一,旨在简化对关系数据库的操作,可作为 mybatis、hibernate 外的另一种选择。
- 包含了 hibernate 的很多功能,比如实体类生成表的正向工程,比如 @Entity、@Table、@Id、@GeneratedValue、@ManyToMany、@JoinTable 等注解。
- 封装了dao层的通用接口,提供了许多通用方法,可通过方法命名约定不写单表操作的简单sql,简化了数据访问层的开发,大大提高了开发效率。
- 提供的关联查询、级联操作了解即可,不推荐使用,因为容易踩坑、难以维护。
官方文档:https://docs.spring.io/spring-data/jpa/reference/index.html
github:https://github.com/spring-projects/spring-data-jpa
springboot整合jpa
依赖
创建项目时勾选 SQL -> Spring Data JPA、数据库驱动
<!-- 已包含 hibernate-core -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
jpa 的依赖中已包含 hibernate 的核心依赖。
yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# createDatabaseIfNotExist=true 数据库不存在时自动创建
url: jdbc:mysql://localhost:3306/mall?serverTimezone=Asia/Shanghai&allowMultiQueries=true&createDatabaseIfNotExist=true
username: root
password: password
jpa:
#hibernate正向工程,根据实体类生成对应的数据表
hibernate:
ddl-auto: update
#打印sql的日志
show-sql: true
spring.jpa.hibernate.ddl-auto 可用的值
- none:默认值,应用启动时不自动进行数据表DDL操作。
- validate:应用启动时自动检查数据表定义与java实体类是否匹配,不匹配时直接启动失败。
- create:应用启动时自动先删除再重建数据表。
- create-drop:应用启动时自动先删除再重建数据表,应用停止(正常停止)时自动删除数据表。
- update(推荐):根据java实体类更新对应的数据表定义,为了数据安全、避免误操作,只会在现有的数据表结构上进行新增(列、约束、默认值等),不会删除已有的列、约束。
实体类
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@Entity //标识为实体类
@Table(name = "t_user") //可指定表名,默认取类名的snake写法作为表名,eg. GoodsInfo => goods_info
public class User implements Serializable {
@Id //标识主键字段
@GeneratedValue //指定主键生成策略,strategy默认为AUTO 自动根据数据库类型选择合适的主键生成策略
private Long id;
@Column(unique = true, nullable = false) //unique是加唯一索引约束,默认false;nullable是允许空值,默认true
private String username;
@Column(name = "name") //指定数据表列名,未指定时默认取变量名的snake写法作为列名,eg. realName=> real_name
private String realName;
@Column(length = 11) //String类型字段可以使用 length 指定字符串最大长度,默认255,会自动根据length选择varchar、text等合适的数据库字段类型
private String tel;
private String password;
@Column(precision = 15, scale = 2) //decimal类型必须指定位数,precision是总位数,scale是小数位数
private BigDecimal balance;
@Column(columnDefinition = "VARCHAR(50) DEFAULT 'normal'") //可以自行指定列定义,columnDefinition 会被拼接到DDL中
private String status;
private Date createTime;
private Date updateTime;
}
@GeneratedValue 的 strategy 属性指定主键生成策略
- IDENTITY:使用数据库的主键自增,会给列定义加 auto_increment,只适用于支持主键自增的数据库,Oracle 不支持此策略。
- TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
- SEQUENCE:通过序列产生主键,通过 @SequenceGenerator 注解指定序列名,mysql 不支持这种方式。
- AUTO:默认值,自动根据数据库类型选择合适的主键生成策略,mysql 是选择主键自增,但不会给列定义添加 auto_increment。
DAO层
继承 JpaRepository 接口,JpaRepository 提供了通用的 crud 功能,可以自行补充所需方法
// 会自动作为bean放到spring容器中,不需要手动加 @Repository 之类的注解。泛型执行实体类、主键对应的java类型
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
Optional<User> findByTel(String tel);
User findByIdAndStatus(Long id, String status);
User findByUsernameOrTel(String username, String tel);
List<User> findByRealNameNull();
List<User> findByRealNameNotNull();
List<User> findByIdBetween(Long minId, Long maxId);
List<User> findByIdLessThan(Long maxId);
List<User> findByIdLessThanEqual(Long maxId);
List<User> findByIdIn(List<Long> idList);
List<User> findByStatusNotIn(List<String> statusList);
}
方法命名需遵守jpa规范:https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html。
常用方法
// 新增或更新,持久化到数据库后会回填入参、返回值中的字段
// 暂时将实体对象保存到持久化上下文中(Persistence Context),在提交事务或显式调用flush()时才持久化到数据库
User save = userRepository.save(user);
List<User> saveAll = userRepository.saveAll(userList);
userRepository.deleteAll();
userRepository.deleteById(id);
userRepository.deleteAllById(idList);
Example<User> example = Example.of(User.builder().username("xxx").build());
boolean existsById = userRepository.existsById(id);
boolean exists = userRepository.exists(example);
long count = userRepository.count();
long count = userRepository.count(example );
Optional<User> byId = userRepository.findById(id);
List<User> allById = userRepository.findAllById(idList);
// 有多条匹配的记录时会抛出异常
Optional<User> xxx = userRepository.findOne(example);
List<User> all = userRepository.findAll();
List<User> all = userRepository.findAll(example);
// 排序。未指定排序方向时,默认为 Direction.ASC 升序
Sort sort = Sort.by("id");
Sort sort = Sort.by("id").descending();
Sort sort = Sort.by("id", "createTime");
Sort sort = Sort.by(Sort.Direction.DESC, "id", "createTime");
Sort sort = Sort.by(Sort.Order.by("id"), Sort.Order.by("createTime"));
Sort sort = Sort.by(Sort.Order.desc("id"), Sort.Order.asc("createTime"));
List<User> all = userRepository.findAll(sort);
List<User> all = userRepository.findAll(example, sort);
// 分页。page从0开始,0即第一页,未指定page时默认为0
Pageable pageable = Pageable.ofSize(20).withPage(0);
// 也可以使用子类 PageRequest,同时支持分页、排序
PageRequest pageRequest = PageRequest.of(0, 20);
PageRequest pageRequest = PageRequest.ofSize(20);
PageRequest pageRequest = PageRequest.of(0, 20, sort);
Page<User> all = userRepository.findAll(pageable);
Page<User> all = userRepository.findAll(example, pageable);
自定义sql
// 使用的是java实体类、实体类的字段名
@Query("select user from User user where user.tel = :tel and user.status = :status")
User findByTelAndStatus(@Param("tel") String tel, @Param("status") String status);
// select可以选取整个实体或单个字段,不能 select user.username, user.tel 这样选择多个字段,也不能使用通配符 *
@Query("select user.id from User user where user.tel = :tel and user.status = :status")
Long findIdByTelAndStatus(@Param("tel") String tel, @Param("status") String status);
// insert、update、delete 语句还需要加 @Modifying;调用 @Modifying 修饰的DAO层方法时,必须要在方法上加事务 @Transactional
@Modifying
@Query("update User user set user.username = :username, user.tel = :tel where user.id = :id")
void updateUsernameAndTelById(@Param("id") Long id, @Param("username") String username, @Param("tel") String tel);
参数绑定
jpa有2种参数绑定方式
- 基于位置的参数绑定:使用?指定参数位置,?1 即引用形参表的第一个参数,?2 即引用第二个参数。增删参数个数、调整参数顺序时可能搞忘维护sql中的?顺序,容易留坑,不推荐使用此种方式。
- 基于注解的参数绑定(推荐):使用jpa的 @Param 注解绑定参数名,sql中用冒号引用参数
@Query("select user from User user where user.username = ?1 and user.tel = ?2")
User findByUsernameAndStatus(String name, String tel);
@Query("select user from User user where user.username = :username and user.status = :status")
User findByUsernameAndStatus(@Param("username") String username, @Param("status") String status);
@Query("select user from User user where user.username = :username and user.status = :status")
User findByUsernameAndStatus(@Param("username") String x1, @Param("status") String x2);
// 约定方法名的方法,jpa默认使用基于位置的参数绑定
User findByUsernameAndStatus(String x1, String x2);
// @Param 需要搭配自定义sql使用,单独使用无效
@Query("select user from User user where user.username = :username and user.status = :status")
User findByUsernameAndStatus(@Param("username") String x1, @Param("status") String x2);
// spring4开始支持jdk8的参数名自动发现,可以省略jpa的@Param注解,直接引用参数名
// 不建议这样做,可能留坑,尽量用@Param显式绑定参数名
@Query("select user from User user where user.username = :username and user.status = :status")
User findByUsernameAndStatus(String username, String status);
// 此句方法中的 @Param 无效,实际使用的是基于位置的参数绑定
User findByUsernameAndStatus(@Param("username") String x1, @Param("status") String x2);
关联关系的表达
生成表时,jpa会自动把 @JoinColumn#name 指定的数据表字段 设置为外键;查询时,jpa会自动根据实体类的关联关系获取成员变量对应的实体。
一对一
eg. 一个用户对应一张会员卡,一张会员卡也只属于一个用户
@Entity
@Data
public class User implements Serializable {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
private String tel;
private String address;
@OneToOne
@JoinColumn(name = "card_id") //指定该实体类对应的外键列名
private Card card;
}
@Data
@Entity
public class Card implements Serializable {
@Id
@GeneratedValue
private Integer id;
private BigDecimal money;
@OneToOne
@JoinColumn(name = "user_id") //此处可不要@JoinColumn
@JsonIgnore //放弃维护此字段
private User user;
}
不使用@JsonIgnore会根据循环引用查询嵌套结果,使用@JsonIgnore后,该字段不会关联到对应的表,相当于一方放弃了关联关系。
此处User可以查到对应的Card,Card不能查到对应的User。
一对多
eg. 一个用户有多个订单,这些订单都属于同一个用户
@Entity
@Data
public class User implements Serializable {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
private String tel;
private String address;
@OneToMany(mappedBy = "user") //mappedBy指定Order中的当前实体(User)对应的成员变量
private List<Order> orderList;
}
@Data
@Entity
@Table(name = "tb_order") //order和sql的排序关键字order冲突,不能使用order作为表名
public class Order implements Serializable {
@Id
@GeneratedValue
private Integer id;
@ManyToOne
@JoinColumn(name = "user_id") //在多的一方指定外键列名
@JsonIgnore //放弃维护此字段
private User user;
}
哪方是一就加@OneToMany,哪方是多就加@ManyToOne,To前面的单词代表当前实体类。
此处User可以查到对应的Order,Order不能查到所属的User。
多对多
eg. 一个学生有多个老师,一个老师也有多个学生
@Entity
@Data
public class Student implements Serializable {
@Id
@GeneratedValue
private Integer id;
@ManyToMany // @JoinTable指定中间表,name指定表名,joinColumns指定中间表中当前实体类对应的字段,inverseJoinColumns指定中间表中对方对应的字段
@JoinTable(name = "teacher_student", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "teacher_id"))
private List<Teacher> teacherList;
}
@Data
@Entity
public class Teacher implements Serializable {
@Id
@GeneratedValue
private Integer id;
@ManyToMany
@JoinTable(name = "teacher_student",joinColumns =@JoinColumn(name = "teacher_id"),inverseJoinColumns = @JoinColumn(name = "student_id"))
@JsonIgnore //放弃维护此字段
private List<Student> studentList;
}
此处,学生可以查到自己的老师,老师不能查到自己的学生。
常见问题
1、关联查询使用懒加载时报错 LazyInitializationException - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xxx.xxx.xxx, could not initialize proxy - no Session
原因:执行懒加载之前事务就提交了,数据库 session 已经关闭,导致执行懒加载时没有可用的数据库session。
解决方案:在 yml 中添加以下配置
#允许在无事务的情况下进行懒加载
spring.jpa.properties.hibernate.enable_lazy_load_no_trans: true