前言
? JPA (Java Persistence API),最初于 2006 年 5 月 11 日发布,是一个为 Java 开发人员提供ORM的Java 规范,用于管理 Java 应用程序中的关系数据
? JPA 是规范,Hibernate是实现。在springboot-data-jpa中,底层使用了 Hibernate 的 JPA 技术实现
一、基本使用
下面演示在Springboot中如何使用Jpa,pom如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
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 BaseRepository<T,I> extends JpaRepository<T,I>, JpaSpecificationExecutor<T>, QuerydslPredicateExecutor<T> {
}
RoleRepository.java
public interface RoleRepository extends BaseRepository<Role, Integer> {
List<Role> findRolesByNameStartingWith(String name);
}
UserRepository.java
public interface UserRepository extends BaseRepository<User, Integer> {
}
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<Role> 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<User> 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<Role> 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<Role> 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<User> 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<User> 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<User> 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<User> users = jpaQueryFactory.selectFrom(qUser).where(qUser.username.eq("张三")).fetch();
assertThat(users.size()).isEqualTo(1);
List<User> 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<User> all = userRepository.findAll(p);
List<User> userList2 = Lists.newArrayList(all);
assertThat(userList2.size()).isEqualTo(2);
}
@Test
void testRoleQuery() {
List<Role> 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种类型,具体可以查看官方文档 |
三、主要接口
?interface?
Repository
?interface?
CrudRepository
?interface?
PagingAndSortingRepository
?interface?
JpaRepository
?interface?
QueryByExampleExecutor
?interface?
JpaRepositoryImplementation
?interface?
JpaSpecificationExecutor
SimpleJpaRepository
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<Age> ages) | … where x.age in ?1 | NotIn | findByAgeNotIn(Collection<Age> 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<Role> 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<User> 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());
List<User> allUser = userRepository.findAll(Example.of(user, exampleMatcher));
logger.info("users:{}", allUser);
-
Querydsl查询 pom.xml 引入依赖 加入插件,用于生成查询实例Q类,具体使用可以查看官方文档 <dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
</dependencies>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
实体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;
List<User> users = jpaQueryFactory.selectFrom(qUser).where(qUser.username.eq("张三")).fetch();
logger.info("{}", users);
List<User> 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<User> 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
-
在测试方法上使用@Transactional回滚数据失败,原因是之前添加的方言是org.hibernate.dialect.MySQL5Dialect ,jpa自动生成的表示MyISAM表,不支持事务和外键,需要使用InnoDB引擎,解决方法,删除原来的表,修改dialect如上,重新执行代码
参考
|