JPA (Java Persistence API),最初于 2006 年 5 月 11 日发布,是一个为 Java 开发人员提供ORM的Java 规范,用于管理 Java 应用程序中的关系数据
JPA 是规范,Hibernate是实现。在springboot-data-jpa中,底层使用了 Hibernate 的 JPA 技术实现
一、基本使用下面演示在Springboot中如何使用Jpa,pom如下
org.springframework.boot spring-boot-starter-parent 2.6.7 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime org.projectlombok lombok true com.google.guava guava 30.0-jre com.querydsl querydsl-jpa provided com.querydsl querydsl-apt provided org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok com.mysema.maven apt-maven-plugin 1.1.3 process target/generated-sources/java com.querydsl.apt.jpa.JPAAnnotationProcessor
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password:
url: jdbc:mysql://localhost:3306/test01?characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL57Dialect
enable_lazy_load_no_trans: true
spring.jpa.hibernate.ddl-auto有以下4个选项
-
create 启动时删数据库中的表,然后创建,退出时不删除数据表
-
create-drop 启动时删数据库中的表,然后创建,退出时删除数据表 如果表不存在报错
-
update 启动时表格式不一致则更新表,原有数据保留
-
validate 启动时对表结构进行校验 如果不一致则报错
BaseRepository.java
@NoRepositoryBean public interface BaseRepositoryextends JpaRepository , JpaSpecificationExecutor , QuerydslPredicateExecutor { }
RoleRepository.java
public interface RoleRepository extends BaseRepository{ List findRolesByNameStartingWith(String name); }
UserRepository.java
public interface UserRepository extends BaseRepository{ }
User.java
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
private String salt;
private LocalDate birthdate;
private LocalDateTime lastLoginTime;
@Builder.Default
@ToString.Exclude
@ManyToMany
@JoinTable(name = "user_role",
joinColumns = { @JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = { @JoinColumn(name = "role_id", referencedColumnName = "id")})
private Set roles = Sets.newHashSet();
}
Role.java
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private static final Logger logger = LoggerFactory.getLogger(Role.class);
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private LocalDateTime createTime;
@Builder.Default
@ToString.Exclude
@ManyToMany(mappedBy = "roles")
private Set users = Sets.newHashSet();
}
测试文件
@SpringBootTest
@Transactional
class DemojpaApplicationTests {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private JPAQueryFactory jpaQueryFactory;
private static final Logger logger = LoggerFactory.getLogger(DemojpaApplicationTests.class);
@BeforeEach
void setUp() {
User user1 = User.builder().username("赵立").password("qiwjqieq1w31").salt("avewe32")
.birthdate(LocalDate.of(2000, 1, 1))
.lastLoginTime(LocalDateTime.of(2022, 2, 2, 2, 2, 2)).build();
User user2 = User.builder().username("张三").password("VEBCEDVqew").salt("vwew233dv")
.birthdate(LocalDate.of(2001, 2, 2))
.lastLoginTime(LocalDateTime.of(2022, 1, 1, 1, 1, 1)).build();
Role role1 = Role.builder().name("管理员").createTime(LocalDateTime.now()).build();
Role role2 = Role.builder().name("会计组").createTime(LocalDateTime.now()).build();
Role role3 = Role.builder().name("开发组").createTime(LocalDateTime.now()).build();
Set roles = Sets.newHashSet(role1, role2, role3);
roleRepository.saveAll(roles);
user1.setRoles(roles);
user2.setRoles(Sets.newHashSet(role2, role3));
userRepository.saveAll(Lists.newArrayList(user1, user2));
}
@Test
void testUserPageQuery() {
Page rolePages = roleRepository.findAll(PageRequest.of(0, 2));
logger.info("nums:{}, pages:{}, list:{}", rolePages.getTotalElements(), rolePages.getTotalPages(), rolePages.get().collect(Collectors.toList()));
assertThat(rolePages.getTotalElements()).isEqualTo(3);
assertThat(rolePages.getTotalPages()).isEqualTo(2);
assertThat(rolePages.getContent().size()).isEqualTo(2);
}
@Test
void testCriteriaSimpleQuery() {
Specification specification = (root, query, criteriaBuilder) -> {
Predicate namePred = criteriaBuilder.like(root.get("username").as(String.class),"%赵%");
Predicate datePred = criteriaBuilder.between(root.get("lastLoginTime").as(LocalDateTime.class), LocalDateTime.of(2020, 2, 2, 2, 2, 2), LocalDateTime.now());
return criteriaBuilder.or(namePred, datePred);
};
List all = userRepository.findAll(specification);
logger.info("名字带赵或者上次登录时间在2020-2-2 2:2:2到现在的用户:{}", all);
assertThat(all.size()).isEqualTo(2);
}
@Test
void testCriteriaJoinQuery() {
Specification specification = (root, query, criteriaBuilder) -> {
Join role = root.join("roles", JoinType.LEFT);
Predicate p1 = criteriaBuilder.equal(role.get("name"), "管理员");
Predicate p2 = criteriaBuilder.equal(root.get("username"), "张三");
return criteriaBuilder.and(p1, p2);
};
List all = userRepository.findAll(specification);
logger.info("名字叫张三的管理员:{}", all);
assertThat(all.size()).isEqualTo(0);
}
@Test
void testExample() {
User user = User.builder().username("赵").build();
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
.withMatcher("username", matcher -> matcher.contains());
List allUser = userRepository.findAll(Example.of(user, exampleMatcher));
logger.info("users:{}", allUser);
assertThat(allUser.size()).isEqualTo(1);
}
@Test
void testQueryDsl() {
QUser qUser = QUser.user;
QRole qRole = QRole.role;
List users = jpaQueryFactory.selectFrom(qUser).where(qUser.username.eq("张三")).fetch();
assertThat(users.size()).isEqualTo(1);
List userList = jpaQueryFactory.selectFrom(qUser).leftJoin(qUser.roles, qRole).where(qRole.name.eq("管理员")).fetch();
assertThat(userList.size()).isEqualTo(1);
com.querydsl.core.types.Predicate p = qUser.birthdate.between(LocalDate.of(1996, 1, 1), LocalDate.of(2002, 1, 1))
.or(qUser.username.contains("赵"));
Iterable all = userRepository.findAll(p);
List userList2 = Lists.newArrayList(all);
assertThat(userList2.size()).isEqualTo(2);
}
@Test
void testRoleQuery() {
List groups = roleRepository.findRolesByNameContaining("组");
assertThat(groups.size()).isEqualTo(2);
}
}
二、常用注解
以下注解都位于javax.persistence包下,详细说明可以查看官方源码
| 注解名 | Target | 含义 |
|---|---|---|
| @Id | METHOD, FIELD | 指定数据库的主键,每个实体必须有 |
| @IdClass | TYPE | 指定该类的联合主键实体 |
| @Enity | TYPE | 指定该类是被jpa管理的实体 |
| @Table | TYPE | 指定数据库的表名 |
| @Column | METHOD, FIELD | 指定该属性对应数据库列名 |
| @Transient | METHOD, FIELD | 指定该属性并非是实体映射到数据库的字段 |
| @GeneratedValue | METHOD, FIELD | 指定主键生成策略 |
| @Basic | METHOD, FIELD | 指定该属性是实体映射到数据库的字段,默认属性都加@Basic |
| @Temporal | METHOD, FIELD | 指定时间(java.util.Data,java.util.Calendar)属性的精度 |
| @Enumerated | METHOD, FIELD | 指定映射枚举字段,默认下标 |
| @Lob | METHOD, FIELD | 指定该属性是一个大对象 |
| @OneToOne | METHOD, FIELD | 指定该属性和另一个实体有1对1的关联 |
| @OneToMany | METHOD, FIELD | 指定该属性和另一个实体有1对多的关联 |
| @ManyToMany | METHOD, FIELD | 指定该属性和另一个实体有多对1的关联 |
| @ManyToOne | METHOD, FIELD | 指定该属性和另一个实体有多对多的关联 |
| @MappedSuperclass | TYPE | 指定该类不是一个完整的实体类,不会映射到数据库表中,即不会创建它对应的表,但是其属性都将映射到其子类的数据库字段中 |
| @JoinColumn | METHOD, FIELD | 定义实体的外键关联字段,通常和OneToOne、ManyToOne、OneToMany一起使用 |
| @OrderBy | METHOD, FIELD | 定义查询时排序规则 |
| @JoinTable | METHOD, FIELD | 定义多对多关联关系时的关联表 |
| @NamedEntityGraph | TYPE | 使用该注解可以提高查询效率,解决n+1问题,可以和org.springframework.data.jpa.repository.EntityGraph配合使用 |
| @PreUpdate ,@PrePersist,@PreRemove | METHOD | 在update,persist,remove前调用该方法 |
| @PostLoad,@PostPersist,@PostRemove,@PostUpdate | METHOD | 在save,persist,remove,update后调用该方法 |
jpa注解
| 注解名 | Target | 含义 |
|---|---|---|
| @Query | METHOD, ANNOTATION_TYPE | 自定义query方法,默认使用hql, 原生sql需要使用nativeQuery=true |
| @Modifying | METHOD, ANNOTATION_TYPE | 使用@Query进行非查询时,需要加上这个 |
| @Lock | METHOD, ANNOTATION_TYPE | 为当前方法执行加锁,锁有6种类型,具体可以查看官方文档 |
spring-data-jpa相比mybatis而言,强大的一点就是不用编写sql语句,仅通过特定的规则构造方法名,就能从数据库查询特定的数据,相应的缺点就是只能单表查询。规则如下所示
| 关键字 | 例子 | JPQL 代码片段 |
|---|---|---|
| Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
| And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
| Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
| Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
| Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
| LessThan | findByAgeLessThan | … where x.age < ?1 |
| LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
| GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
| After | findByStartDateAfter | … where x.startDate > ?1 |
| Before | findByStartDateBefore | … where x.startDate < ?1 |
| IsNull, Null | findByAge(Is)Null | … where x.age is null |
| IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
| Like | findByFirstnameLike | … where x.firstname like ?1 |
| NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
| StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
| EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
| Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
| OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
| Not | findByLastnameNot | … where x.lastname <> ?1 |
| In | findByAgeIn(Collection ages) | … where x.age in ?1 |
| NotIn | findByAgeNotIn(Collection ages) | … where x.age not in ?1 |
| True | findByActiveTrue() | … where x.active = true |
| False | findByActiveFalse() | … where x.active = false |
| IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
-
分页
Repository自带的方法就包含分页,所以只要构造参数传递过去就可以。下面使用PageRequest构造
Page
rolePage = roleRepository.findAll(PageRequest.of(0, 2)); logger.info("nums:{}, pages:{}, list:{}", rolePage.getTotalElements(), rolePage.getTotalPages(), rolePage.get().collect(Collectors.toList())); -
Criteria查询
在jpa中,通过Specification这个接口实现Criteria查询
-
单表多条件查询
Specification
specification = (root, query, criteriaBuilder) -> { Predicate namePred = criteriaBuilder.like(root.get("username").as(String.class),"%赵%"); Predicate datePred = criteriaBuilder.between(root.get("lastLoginTime").as(LocalDateTime.class), LocalDateTime.of(2020, 2, 2, 2, 2, 2), LocalDateTime.now()); return criteriaBuilder.or(namePred, datePred); }; logger.info("名字带赵或者上次登录时间在2020-2-2 2:2:2到现在的用户:{}", userRepository.findAll(specification)); -
多表连接查询
Specification specification = (root, query, criteriaBuilder) -> { Join role = root.join("roles", JoinType.LEFT); Predicate p1 = criteriaBuilder.equal(role.get("name"), "管理员"); Predicate p2 = criteriaBuilder.equal(root.get("username"), "张三"); return criteriaBuilder.and(p1, p2); }; List all = userRepository.findAll(specification); logger.info("名字叫张三的管理员:{}", all);
-
-
Example查询
Query by Example (QBE) 是一种简单、用户友好的查询技术。一般用来对字符串进行精确的查询,不能对时间、数字进行大于小于等查询。它的优点在于可以忽略某些属性、能够对基本类型处理、大小写处理等。
User user = User.builder().username("赵").build(); ExampleMatcher exampleMatcher = ExampleMatcher.matching() .withMatcher("username", matcher -> matcher.contains()); ListallUser = userRepository.findAll(Example.of(user, exampleMatcher)); logger.info("users:{}", allUser); -
Querydsl查询
pom.xml 引入依赖 加入插件,用于生成查询实例Q类,具体使用可以查看官方文档
com.querydsl querydsl-jpa com.querydsl querydsl-apt com.mysema.maven apt-maven-plugin 1.1.3 generate-sources process target/generated-sources/java com.querydsl.apt.jpa.JPAAnnotationProcessor 实体Bean配置了@Entity被检测到之后,就会在target的子目录中自动生成一个Q+实体名称的类,这个类对我们使用QueryDSL非常重要,正是因为它,我们才使得QueryDSL能够构建类型安全的查询
如果没有生成,需要手动mvn compile
@Bean public JPAQueryFactory jpaQueryFactory(EntityManager entityManager){ return new JPAQueryFactory(entityManager); }@Test void testQueryDsl() { QUser qUser = QUser.user; QRole qRole = QRole.role; Listusers = jpaQueryFactory.selectFrom(qUser).where(qUser.username.eq("张三")).fetch(); logger.info("{}", users); List userList = jpaQueryFactory.selectFrom(qUser).leftJoin(qUser.roles, qRole).where(qRole.name.eq("管理员")).fetch(); logger.info("{}", userList); com.querydsl.core.types.Predicate p = qUser.birthdate.between(LocalDate.of(2000, 1, 1), LocalDate.of(2022, 1, 1)).or(qUser.username.contains("赵")); Iterable all = userRepository.findAll(p); logger.info("{}", Lists.newArrayList(all)); }
-
出现org.hibernate.tool.schema.spi.CommandAcceptanceException:Error executing DDL via JDBC Statement错误
需要加上方言
spring: jpa: properties: hibernate: dialect: org.hibernate.dialect.MySQL57Dialect # 之前使用的是MySQL5Dialect,会造成下面的问题,需要改成这个 -
在测试方法上使用@Transactional回滚数据失败,原因是之前添加的方言是org.hibernate.dialect.MySQL5Dialect,jpa自动生成的表示MyISAM表,不支持事务和外键,需要使用InnoDB引擎,解决方法,删除原来的表,修改dialect如上,重新执行代码
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
- https://hibernate.org/orm/documentation/5.5/
- https://docs.oracle.com/javaee/6/tutorial/doc/bnbpz.html
- https://www.jianshu.com/p/69dcb1b85bbb



