文章目录

  • 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可以极大的提高我们的日常开发效率和有效的降低项目开发成本。

Java jpa适配达梦 jpa spring data jpa_Java 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及分页

Java jpa适配达梦 jpa spring data jpa_Java jpa适配达梦_02

一般选择继承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在浏览器中发送请求,

Java jpa适配达梦 jpa spring data jpa_ide_03

2.3.1 save

执行测试,往数据表中写入三条数据:

Java jpa适配达梦 jpa spring data jpa_JPA_04

执行之后查看数据库,发现数据保存成功

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方法也可以证明前面的数据保存成功

Java jpa适配达梦 jpa spring data jpa_JPA_05

2.3.3 findById

执行根据ID查询的测试,如果输入的id是数据表中存在的,那么自然会返回对应的记录

Java jpa适配达梦 jpa spring data jpa_ide_06

而如果输入的id并不存在,那么结果为service层实现类中设定的默认值

Java jpa适配达梦 jpa spring data jpa_JPA_07

2.3.4 delete

执行删除测试

Java jpa适配达梦 jpa spring data jpa_ide_08

执行完毕后,查询数据库中的记录,可以看到删除成功。

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;

}

浅析Java中的Optional

  • 更多的方法可参考JpaRepository接口的实现类SimpleJpaRepository
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    // 实现方法
}
3.2 自定义查询

自定义的简单查询就是根据方法名来自动生成 SQL,主要的语法是findXXBy,readAXXBy,queryXXBy,countXXBy, getXXBydeleteByxxcountByxx后面跟属性名称,同时方法名的定义中还可以使用And、Or等关键字,基本上 SQL 体系中的关键词都可以使用,例如:LIKEIgnoreCaseOrderBy

具体的关键字,使用方法和生产成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

Java jpa适配达梦 jpa spring data jpa_Java jpa适配达梦_09

同样可以自定义查询方法,例如根据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);
}

执行测试也可以获取到相应的结果

Java jpa适配达梦 jpa spring data jpa_Data_10

其他的方法按照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。

Java jpa适配达梦 jpa spring data jpa_JPA_11

@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 Boot 整合 JPA

Spring Data JPA 实现原理

spring data jpa 详解

SpringBoot学习笔记九:Spring Data Jpa的使用

Spring Boot与数据访问