Spring Data JPA
简介
Spring Data JPA 旨在通过减少实际需要的工作量来显着改进数据访问层的实现。
常见使用方式
Repository 中的返回对象及分页与排序参数
在查询大量数据时,Spring 提供了多种的返回对象与输入参数,可以在不同情况下进行采用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository;
@Repository public interface UserRepository extends JpaRepository<User,Long> { Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
Window<User> findTop10ByLastname(String lastname, ScrollPosition position, Sort sort);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
}
|
自动获取更新时间创建时间等内容
首先需要按照如下样例创建模型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate;
import javax.persistence.Entity; import javax.persistence.Id; import java.util.Date;
enum TypeEnum { A, B }
@Getter @Setter @ToString @RequiredArgsConstructor @Entity @EntityListeners(AuditingEntityListener.class) public class Test {
@Id private String id; @Enumerated(EnumType.STRING) private TypeEnum type = TypeEnum.A;
@CreatedBy private String createdBy;
@LastModifiedBy private String lastModifiedBy;
@CreatedDate private Date createdDate;
@LastModifiedDate private Date updatedDate;
@Transient private String cache;
}
|
创建如下的操作员获取类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component;
import java.util.Optional;
@Component public class AuditorConfig implements AuditorAware<String> {
@NotNull @Override public Optional<String> getCurrentAuditor() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof Jwt) { return Optional.ofNullable(((Jwt) principal).getSubject()); } else { return Optional.empty(); } }
}
|
注:此样例为 SpringSecurity 启用 JWT 之后获取用户名的方式。
开启编辑审计功能(在启动类中加入下面的注解即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication @EnableJpaAuditing public class TestApplication {
public static void main(String[] args) { SpringApplication.run(Test.class, args); }
}
|
表名或列名大写
在配置中填入如下内容即可:
1 2 3 4 5
| spring: jpa: hibernate: naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
自定义返回分页对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable;
import java.util.ArrayList; import java.util.List;
class T { }
public class Test { public void test() { long total = 1L; List<T> contentList = new ArrayList<>(); Pageable pageable = PageRequest.of(0, 10); Page<T> result = new PageImpl<>(contentList, pageable, total); } }
|
使用大写的表名和列名
新增如下配置即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.springframework.context.annotation.Configuration;
@Configuration public class CustomNamingConf extends CamelCaseToUnderscoresNamingStrategy {
@Override public Identifier toPhysicalCatalogName(Identifier logicalName, JdbcEnvironment context) { return logicalName; }
@Override public Identifier toPhysicalSchemaName(Identifier logicalName, JdbcEnvironment context) { return logicalName; }
@Override public Identifier toPhysicalTableName(Identifier logicalName, JdbcEnvironment context) { return logicalName; }
@Override public Identifier toPhysicalSequenceName(Identifier logicalName, JdbcEnvironment context) { return logicalName; }
@Override public Identifier toPhysicalColumnName(Identifier logicalName, JdbcEnvironment context) { return logicalName; }
@Override protected boolean isCaseInsensitive(JdbcEnvironment jdbcEnvironment) { return false; }
}
|
使用自定义的 SQL 文件
在初始化时可以采用自定义的 SQL 文件:
1 2 3 4 5 6
| spring: sql: init: mode: always schema-locations: schema.sql data-locations: data.sql
|
使用 JPA 中的关系
使用如下模型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Entity @NamedEntityGraph(name = "Item.characteristics", attributeNodes = @NamedAttributeNode("characteristics") ) public class Item {
@Id private Long id; private String name; @OneToMany(mappedBy = "item") private List<Characteristic> characteristics = new ArrayList<>();
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Entity public class Characteristic {
@Id private Long id; private String type; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private Item item;
}
|
然后在 Repository 中声明启用相关图即可:
1 2 3 4 5
| public interface ItemRepository extends JpaRepository<Item, Long> {
@EntityGraph(value = "Item.characteristics") Item findByName(String name); }
|
动态构建查询
基本查询
首先需要在 Repository 中额外引入 JpaSpecificationExecutor
1 2 3
| @Repository public interface TestRepository extends JpaRepository<Test, String>, JpaSpecificationExecutor<Test> { }
|
然后即可在查询中进行如下操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service public class Test { @Resource private TestRepository testRepository;
public void test() { testRepository.findAll((root, criteriaQuery, cb) -> { List<Predicate> predicate = new ArrayList<>(); Path<String> testPath = root.get("test"); predicate.add(cb.like(testPath, "test")); Predicate[] pre = new Predicate[predicate.size()]; criteriaQuery.where(predicate.toArray(pre)); return criteriaQuery.getRestriction(); }); } }
|
连表查询
如果需要联表查询则可使用如下的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Data;
@Data @Entity public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String title;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import jakarta.persistence.*; import lombok.Data;
import java.util.List;
@Data @Entity public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String firstName;
private String lastName;
@OneToMany(cascade = CascadeType.ALL) private List<Book> books;
}
|
1 2 3
| @Repository public interface AuthorsRepository extends JpaRepository<Author, Long>, JpaSpecificationExecutor<Author> { }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import jakarta.persistence.criteria.Predicate; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.List;
@Service public class TestService { public static Specification<Author> hasBookWithTitle(String bookTitle) { return (root, query, criteriaBuilder) -> { Join<Book, Author> authorsBook = root.join("books"); return criteriaBuilder.equal(authorsBook.get("title"), bookTitle); }; } }
|
QueryDSL
注:和 Specification 类似,但是对 GraphQL 更友好。
引入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <scope>provided</scope> </dependency> </dependencies>
|
安装插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <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>
|
编写模型,加上 @Table
与 @Entity
注解,然后使用 mvn compile
即可在对应目录找到查询类。
引入依赖:
1 2 3 4 5 6 7 8
| dependencies { implementation("com.querydsl:querydsl-core:5.0.0") implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta") annotationProcessor( "com.querydsl:querydsl-apt:5.0.0:jakarta", "jakarta.persistence:jakarta.persistence-api:3.1.0" ) }
|
编写模型,加上 @Table
与 @Entity
注解,然后使用 ./gradlew compileJava
即可在对应目录找到查询类。
如果准备与 SpringDataJpa 一起使用则可以依照如下方式编写 Repository :
1 2 3 4 5 6 7
| import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository;
@Repository public interface AuthorRepository extends JpaRepository<Author, Long>, QuerydslPredicateExecutor<Author> { }
|
然后按如下方式进行查询即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.querydsl.core.types.Predicate; import jakarta.annotation.Resource; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.stereotype.Controller;
@Controller public class TestController {
@Resource private AuthorRepository authorRepository;
@QueryMapping Iterable<Author> authors(@Argument String name) { if (name != null) { QAuthor author = QAuthor.author; Predicate predicate = author.name.eq(name); return authorRepository.findAll(predicate); } else { return authorRepository.findAll(); } } }
|
如果使用 QueryDSL 原始的查询方式则可以按照如下方式编写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class QueryDSLConfig { @Bean public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) { return new JPAQueryFactory(entityManager); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import com.querydsl.core.types.Predicate; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.Resource; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.stereotype.Controller;
@Controller public class TestController {
@Resource private JPAQueryFactory queryFactory;
@QueryMapping Iterable<Author> queryAuthor(@Argument String name, @Argument String publisher) { QAuthor author = QAuthor.author; QBook book = QBook.book; return queryFactory.selectFrom(author).leftJoin(author.books, book).fetchJoin().where(author.name.like(name).and(book.publisher.eq(publisher))).fetch(); }
}
|
分页和排序功能如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.*; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.stereotype.Controller;
@Slf4j @Controller public class TestController {
@Resource private JPAQueryFactory queryFactory;
@QueryMapping Page<Author> pageAuthor(@Argument String name, @Argument String publisher) { Pageable pageable = PageRequest.of(0, 2); Sort sort = Sort.by(Sort.Direction.DESC, "id"); QAuthor author = QAuthor.author; QBook book = QBook.book; List<Long> count = queryFactory.select(author.id.countDistinct()).from(author).leftJoin(book).on(book.author.id.eq(author.id)).where(author.name.like(name).and(book.publisher.eq(publisher))).fetch(); PathBuilder<Author> authorPathBuilder = new PathBuilder<>(Author.class, author.getMetadata().getName()); JPAQuery<Author> query = queryFactory.selectFrom(author).leftJoin(author.books, book).fetchJoin().where(author.name.like(name).and(book.publisher.eq(publisher))); sort.get().forEach(order -> query.orderBy(QueryDSLUtil.toOrderSpecifier(order, authorPathBuilder))); query.offset(pageable.getOffset()); query.limit(pageable.getPageSize()); return new PageImpl<>(query.fetch(), pageable, count.get(0)); } }
|
注:此处由于检索逻辑复杂,所以 QueryDSL 在内存中做了数据合并,并不能依照正常的方式完成数据分页,在使用时需要尤其注意。
单元测试
在编写单元测试时需要加上 @DataJpaTest
注解,此注解会使用内存数据库进行测试,而不会产生多余的测试数据。
样例如下:
1 2 3 4 5 6 7 8
| import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
@DataJdbcTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class DataTest { }
|
还可以在测试时指定独立的配置环境:
1 2 3 4 5 6
| import org.springframework.test.context.ActiveProfiles;
@ActiveProfiles("dev") public class DataTest {
}
|
或是直接使用测试容器:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers public class DataTest {
@Container @ServiceConnection static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16.0"); }
|