文章目录
- 1. JPA
- 1.1 JAP 规范
- 1.2 Spring Data
- 2. [Spring Data Jpa](https://spring.io/projects/spring-data-jpa#overview)使用
- 2.1 环境搭建
- 2.2 类创建
- 2.3 测试
- 2.3.1 save
- 2.3.2 findALl
- 2.3.3 findById
- 2.3.4 delete
- 3. JpaRepository接口
- 3.1 基本原理
- 3.2 自定义查询
- 3.3 复杂查询
- 3.3.1 自定义SQL
- 3.3.2 分页查询
- 3.3.3 多表查询
- 4. 参考
1. JPA
1.1 JAP 规范
JPA是Java Persistence API的简称,是一套Sun官方提出的Java持久化规范。其设计目标主要是为了简化现有的持久化开发工作和整合ORM技术,它为Java开发人员提供了一种ORM工具来管理Java应用中的关系数据。 简而言之,JPA提供了使用面向对象的方式操作数据库的功能。JPA充分吸收了现有Hibernate,TopLink,JDO等ORM框架的优势,具有易于使用、伸缩性强等优点。
1.2 Spring Data
Spring Data JPA是Spring基于Spring Data框架对于JPA规范的一套具体实现方案,使用Spring Data JPA可以极大地简化JPA 的写法,几乎可以在不写具体实现的情况下完成对数据库的操作,并且除了基础的CRUD操作外,Spring Data JPA还提供了诸如分页和排序等常用功能的实现方案。合理的使用Spring Data JPA可以极大的提高我们的日常开发效率和有效的降低项目开发成本。
Spring Data 项目的目的是为了简化构建基于Spring 框架应用的数据访问技术,包括非关系数据库、Map-Reduce 框架、云数据服务等等;另外也包含对关系数据库的访问支持。
Spring Data 包含多个子项目:
- Spring Data Commons
- Spring Data JPA
- Spring Data KeyValue
- Spring Data LDAP
- Spring Data MongoDB
- Spring Data Gemfire
- Spring Data REST
- Spring Data Redis
- Spring Data for Apache Cassandra
- Spring Data for Apache Solr
- Spring Data Couchbase (community module)
- Spring Data Elasticsearch (community module)
- Spring Data Neo4j (community module)
Spring Data提供了统一的API来对数据访问层进行操作,只要基于Spring Data Commons项目。Spring Data Commons让我们在使用关系型或者非关系型数据访问技术时都基于Spring提供的统一标准,标准包含了CRUD(创建、获取、更新、删除)、查询、排序和分页的相关操作。
Spring Data提供了统一的Repository接口,我们不需要自己编写具体实现的Dao接口,只需要继承提供的Repository接口,就可以使用其中的方法来操作数据库。相关的接口有:
-
Repository<T, ID extends Serializable>
:统一接口 -
RevisionRepository<T, ID extends Serializable, N extends Number & Comparable<N>>
:基于乐观锁机制 -
CrudRepository<T, ID extends Serializable>
:基本CRUD操作 -
PagingAndSortingRepository<T, ID extends Serializable>
:基本CRUD及分页
一般选择继承JpaRepository接口即可。同时它还提供了数据访问的模板类xxxTemplate,如MongoTemplate、RedisTemplate等。
2. Spring Data Jpa使用
2.1 环境搭建
下面通过一个例子来介绍如何在Spring Boot中使用Jpa进行数据库相关的操作。首先需要在pom.xml中导入相关的依赖
<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>
为了后面方便进行测试,这里同时导入swagger相关的依赖:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
导入依赖后,还需要在配置文件中配置数据源和Jpa相关的依赖:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
其中 ddl-auto用于自动创建表, show-sql用于在测试过程中自动打印SQL语句。
2.2 类创建
环境创建好之后,我们使用面向对象的思想通过Jpa在数据库中自动的根据实体类生成表。假设此时的Account表定义如下:
@Entity
@Table(name = "t_account")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Account {
@Getter
@Setter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Getter
@Setter
@Column
private String name;
@Getter
@Setter
@Column
private Float money;
@Getter
@Setter
@Column
private String email;
}
其中:
- @Entity:必选,表示该类对应数据库中的一张表
- @Table(name = “AUTH_USER”) :可选,声明了数据库实体对应的表信息。包括表名称、索引信息等。这里声明这个实体类对应的表名是 t_account。如果没有指定,则表名和实体的名称保持一致
- @Id:声明主键
- @GeneratedValue:声明主键生成策略
JPA自带的几种主键生成策略:
- TABLE: 使用一个特定的数据库表格来保存主键
- SEQUENCE: 根据底层数据库的序列来生成主键,条件是数据库支持序列。这个值要与generator一起使用,generator 指定生成主键使用的生成器(可能是orcale中自己编写的序列)
- IDENTITY: 主键由数据库自动生成(主要是支持自动增长的数据库,如mysql)
- AUTO: 主键由程序控制,也是GenerationType的默认值
- @Column:声明实体属性的表字段的定义,默认的实体每个属性都对应了表的一个字段,字段的名称默认和属性名称保持一致(并不一定相等),字段的类型根据实体属性类型自动推断
接着编写数据持久化层,这里需要继承JpaRepository接口
public interface AccountRepository extends JpaRepository<Account, Integer> {}
其中JpaRepository的泛型的第一个元素为对应的实体类,第二个为主键的数据类型。
然后编写service层接口,其中的方法包含对Account的增删改查:
public interface AccountService {
public Object save(Account account);
public void delete(Integer id);
public List<Account> findAll();
public Account findById(Integer id);
public long count();
}
接口对应的实现类为:
@Service
public class AccountImpl implements AccountService {
@Autowired
private AccountRepository accountRepository;
@Override
public Object save(Account account) {
accountRepository.save(account);
return null;
}
@Override
public void delete(Integer id) {
accountRepository.deleteById(id);
}
@Override
public List<Account> findAll() {
return accountRepository.findAll();
}
@Override
public Account findById(Integer id) {
Optional<Account> account = accountRepository.findById(id);
Account a = Account.builder().id(10).name("Amy").money(2000.0f).email("Amy@163.com").build();
Account account1 = account.orElse(a);
return account1;
}
@Override
public long count() {
return accountRepository.count();
}
}
最后编写表现层的controller
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("/findall")
public List<Account> findAll(){
return accountService.findAll();
}
@GetMapping("/{id}")
public Account findById(@PathVariable Integer id){
Account account = accountService.findById(id);
return account;
}
@GetMapping("/delete/{id}")
public void delete(@PathVariable Integer id){
accountService.delete(id);
}
@PostMapping("/save")
public Object save(@RequestBody Account account) {
return accountService.save(account);
}
}
最后为了使用swagger进行测试,还需要编写相关的配置类:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("SpringBoot API Doc")
.description("This is a restful api document of Spring Boot.")
.version("1.0")
.build();
}
}
2.3 测试
通过http://localhost:8080/swagger-ui.html
在浏览器中发送请求,
2.3.1 save
执行测试,往数据表中写入三条数据:
执行之后查看数据库,发现数据保存成功
mysql> select * from t_account;
+----+------------------+-------+----------+
| id | email | money | name |
+----+------------------+-------+----------+
| 1 | ball@163.com | 1000 | ball |
| 2 | Forlogen@163.com | 1000 | Forlogen |
| 3 | kobe@163.com | 1000 | kobe |
+----+------------------+-------+----------+
3 rows in set (0.00 sec)
2.3.2 findALl
同样的测试findAll方法也可以证明前面的数据保存成功
2.3.3 findById
执行根据ID查询的测试,如果输入的id是数据表中存在的,那么自然会返回对应的记录
而如果输入的id并不存在,那么结果为service层实现类中设定的默认值
2.3.4 delete
执行删除测试
执行完毕后,查询数据库中的记录,可以看到删除成功。
mysql> select * from t_account;
+----+------------------+-------+----------+
| id | email | money | name |
+----+------------------+-------+----------+
| 2 | Forlogen@163.com | 1000 | Forlogen |
| 3 | kobe@163.com | 1000 | kobe |
+----+------------------+-------+----------+
2 rows in set (0.00 sec)
3. JpaRepository接口
3.1 基本原理
在前面的使用中,我们并没有在持久层接口中定义任何方法,但是在业务层可以使用很多的方法,这就是Spring Data Jpa的一个优势。在持久层中继承的接口JpaRepository源码如下所示:
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
@Override
List<T> findAll();
@Override
List<T> findAll(Sort sort);
@Override
List<T> findAllById(Iterable<ID> ids);
@Override
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
T getOne(ID id);
@Override
<S extends T> List<S> findAll(Example<S> example);
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
其中:
findAll()
:查询所有,此时按照默认的排序方式输出结果findAll(Sort sort)
:可以传入Sort对象,指定结果排序的方式,例如传入Sort sort = Sort.by(Sort.Direction.DESC, "money")
,表示按照money属性值的大小降序排列。例如修改t_account表中的记录如下所示:
mysql> select * from t_account;
+----+------------------+-------+----------+
| id | email | money | name |
+----+------------------+-------+----------+
| 2 | Forlogen@163.com | 1000 | Forlogen |
| 3 | kobe@163.com | 500 | kobe |
| 4 | James@163.com | 2500 | James |
+----+------------------+-------+----------+
3 rows in set (0.00 sec)
如果使用findAll()
,页面输出为:
[{"id":2,"name":"Forlogen","money":1000.0,"email":"Forlogen@163.com"},{"id":3,"name":"kobe","money":500.0,"email":"kobe@163.com"},{"id":4,"name":"James","money":2500.0,"email":"James@163.com"}]
此时结果是按照表中的顺序排列。如果使用findAll(Sort sort)
,业务层和表现层代码为:
@Override
public List<Account> findAll(Sort sort) {
return accountRepository.findAll(sort);
}
@GetMapping("/findallsort")
public List<Account> findALlSort(){
Sort sort = Sort.by(Sort.Direction.DESC, "money");
return accountService.findAll(sort);
}
执行测试,此时的输出为:
[{"id":4,"name":"James","money":2500.0,"email":"James@163.com"},{"id":2,"name":"Forlogen","money":1000.0,"email":"Forlogen@163.com"},{"id":3,"name":"kobe","money":500.0,"email":"kobe@163.com"}]
可以看到,结果是按照money属性的值降序排列。
getOne(ID id)
:根据给定的id获取对应的结果,这里的返回类型为泛型T。因此,当给定的id在表中存在时,返回对应的记录;如果id不存在,那么会抛出空指针异常findAllById(Iterable<ID> ids)
:如果为了避免抛出空指针异常,更好的方式就是使用这种方法。它的返回类型为Optional,它是Java提供的一个用于解决空指针异常的容器。因此,可以为查询结果指定默认值来避免抛出异常,例如:
@Override
public Account findById(Integer id) {
Optional<Account> account = accountRepository.findById(id);
Account a = Account.builder().id(10).name("Amy").money(2000.0f).email("Amy@163.com").build();
Account account1 = account.orElse(a);
return account1;
}
- 更多的方法可参考JpaRepository接口的实现类SimpleJpaRepository
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
// 实现方法
}
3.2 自定义查询
自定义的简单查询就是根据方法名来自动生成 SQL,主要的语法是findXXBy
,readAXXBy
,queryXXBy
,countXXBy
, getXXBy
、deleteByxx
、countByxx
后面跟属性名称,同时方法名的定义中还可以使用And、Or等关键字,基本上 SQL 体系中的关键词都可以使用,例如:LIKE
、 IgnoreCase
、 OrderBy
。
具体的关键字,使用方法和生产成SQL如下表所示:
Keyword | Sample | JPQL snippet |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | 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 | findByAgeIsNull | … 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 age) | … where x.age not in ?1 |
TRUE | findByActiveTrue() | … where x.active = true |
FALSE | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
例如,在上面的例子中添加
Float countAccountByMoney(Float money);
public interface AccountService {
Float countAccountByMoney(Float money);
}
@Override
public Float countAccountByMoney(Float money) {
return accountRepository.countAccountByMoney(money);
}
@GetMapping("/money/{money}")
public Float countByMoney(@PathVariable Float money){
return accountService.countAccountByMoney(money);
}
执行测试,可以获取到money属性的数量,也就是表中的记录条数。<img
同样可以自定义查询方法,例如根据name进行查询,代码定义如下:
Account findByName(String name);
@Override
public Account findByName(String name){
return accountRepository.findByName(name);
}
@GetMapping("/name/{name}")
public Account findByName(@PathVariable String name){
return accountService.findByName(name);
}
执行测试也可以获取到相应的结果
其他的方法按照Jpa的规范命名即可,这里不再多做演示。
3.3 复杂查询
3.3.1 自定义SQL
虽然经常使用的SQL语句都可以根据Spring Data Jpa的方法名定义的规范来实现,但Jpa也完美支持用户自定义的SQL语句。在 SQL 的查询方法上面使用@Query
注解,如涉及到删除和修改在需要加上@Modifying
;也可以根据需要添加 @Transactional
对事物的支持,查询超时的设置等。
例如,在上面的例子中统计所有用户money的总和,可以定义如下的方法:
@Transactional
@Query("select sum(a.money) from Account a")
Float sumOfMoney();
业务层和表现层的定义为:
@Override
public Float sumOfMoney() {
return accountRepository.sumOfMoney();
}
@GetMapping("/sum")
public Float sum(){
return accountService.sumOfMoney();
}
执行测试,由于此时数据表中只有两条记录,每条记录的money值为1000。因此,最后统计的结果输出2000。
在
@Query("select sum(a.money) from Account a")
中查询使用的是Account,而数据库中表的名字确实t_account。这是因为Jpa操作的是实体类,而不是数据库中的表和字段。如果想使用原生的SQL语句怎么办?首先看一下@Query注解的源码:@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @QueryAnnotation @Documented public @interface Query { String value() default ""; String countQuery() default ""; String countProjection() default ""; boolean nativeQuery() default false; String name() default ""; String countName() default ""; }
定义中存在一个布尔类型的nativeQuery,顾名思义,当取值为true时,表示可以使用原生的SQL直接操作表。因此,使用
@Query(value = "select sum(a.money) from t_account a", nativeQuery=true)
和适用上面的SQL语句作用是相同的。
3.3.2 分页查询
在进行分页查询前,首先往表中多添加几条数据,添加后查询结果如下所示:
mysql> select * from t_account;
+----+------------------+--------+----------+
| id | email | money | name |
+----+------------------+--------+----------+
| 2 | Forlogen@163.com | 1000 | Forlogen |
| 3 | kobe@163.com | 500 | kobe |
| 4 | James@163.com | 2500 | James |
| 5 | Amy@qq.com | 200 | Amy |
| 6 | Bill@qq.com | 100000 | Bill |
+----+------------------+--------+----------+
5 rows in set (0.00 sec)
Spring Data Jpa可以在方法参数中直接传入Pageable
对象来完成动态分页查询,通常pageable会是方法的最后一个参数。在JpaRepository接口中定义了分页查询相关的findAll()
,因此可以在业务层中直接使用。
@Override
public Page<Account> listAccount(Pageable pageable) {
return accountRepository.findAll(pageable);
}
方法的参数为Pageable对象。最后在表现层添加相应的方法
@GetMapping("/list")
public Page<Account> listBlog(@PageableDefault(size = 3, sort = {"money"}, direction = Sort.Direction.DESC) Pageable pageable){
Page<Account> accounts = accountService.listAccount(pageable);
return accounts;
}
其中@PageableDefault用于定义Pageable对象的相关属性:
- size:每页的记录条数
- sort:结果排序的属性依据
- direction:结果排序的方向
执行测试,页面输出结果如下所示:
{
"content": [
{
"id": 6,
"name": "Bill",
"money": 100000.0,
"email": "Bill@qq.com"
},
{
"id": 4,
"name": "James",
"money": 2500.0,
"email": "James@163.com"
},
{
"id": 2,
"name": "Forlogen",
"money": 1000.0,
"email": "Forlogen@163.com"
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"pageSize": 3,
"pageNumber": 0,
"unpaged": false,
"paged": true
},
"totalPages": 2,
"totalElements": 5,
"last": false,
"number": 0,
"size": 3,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 3,
"first": true,
"empty": false
}
从结果中可以看出,此时结果按照指定的方式进行排序输出。而且pageSize
大小为3,因此totalPages
结果为2。
除了简单的分页查询外,还可以根据指定的条件进行查询,例如在业务层定义根据name进行模糊查询:
@Override
public Page<Account> listAccount(String name, Pageable pageable) {
return accountRepository.findByQuery(name,pageable);
}
持久层定义findByQuery()
,如下所示:
@Query("select a from Account a where a.name like %?1%")
Page<Account> findByQuery(String name, Pageable pageable);
其中?1
表示占位符,这里就是参数列表中的name。最后编写表现层代码:
@GetMapping("/list1")
public Page<Account> listBlog1(@PageableDefault(size = 3, sort = {"money"}, direction = Sort.Direction.DESC) Pageable pageable) {
Page<Account> accounts = accountService.listAccount("o", pageable);
return accounts;
}
执行测试,页面输出结果如下所示:
{
"content": [
{
"id": 2,
"name": "Forlogen",
"money": 1000.0,
"email": "Forlogen@163.com"
},
{
"id": 3,
"name": "kobe",
"money": 500.0,
"email": "kobe@163.com"
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"pageSize": 3,
"pageNumber": 0,
"unpaged": false,
"paged": true
},
"totalPages": 1,
"totalElements": 2,
"last": true,
"number": 0,
"size": 3,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 2,
"first": true,
"empty": false
}
从结果中可以看出,name中包含o的结果有两个,并且结果已经按money值的大小倒序输出。由于此时只有两条记录,因此totalPages的值是1。
更加复杂的分页查询,有兴趣的可以自己建表尝试,这里不再多做演示。
前面使用
?1
占位符来进行参数的绑定。当参数很多时,更好的方式是使用**@Param**注解来指定参数的具体名称并在查询绑定。例如,上面持久层的findByQuery()
可写作如下形式,两者效果是相同的。@Query("select a from Account a where a.name like %:name%") Page<Account> findByQuery(@Param("name") String name, Pageable pageable) }
3.3.3 多表查询
多表查询可以查看Spring官方案例Spring Data Book Examples,后续会在具体的项目中再详细介绍,这里就不再写啦[偷个懒应该没人看见~]
4. 参考
spring data jpa 详解
SpringBoot学习笔记九:Spring Data Jpa的使用