首页 > 代码库 > Spring Boot教程35——Spring Data JPA

Spring Boot教程35——Spring Data JPA

Hibernate是数据访问解决技术的绝对霸主。JPA是由Hibernate主导的一个基于O/R映射的标准规范。O/R映射即将领域模型类和数据库的表进行映射,通过程序操作对象而实现表数据操作的能力,让数据访问操作无须关注数据库相关的技术。
Spring Data JPA介绍

1.定义数据访问层

使用Spring Data JPA建立数据访问层十分简单,只需定义一个继承JpaRepository的接口即可:

public interface PersonRepository extends JpaRepository<Person,Long>{
    //定义数据访问操作的方法
}

继承JpaRepository接口意味着我们默认已经有了下面的数据访问操作方法:

@NoRepositoryBean
public interface JpaRepository<T,ID extends Serializable> extends PagingAndSortingRepository<T,ID>{
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAll(Iteralbe<ID> ids);
    <S extends T> List<S> save(Iterable<S> entities);
    void flush();
    <S extends T> S saveAndFlush(S entity);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
}

2.配置使用Spring Data JPA

在Spring环境中,使用Spring Data JPA可通过@EnableJpaRepositories注解来开启Spring Data JPA的支持。@EnableJpaRepositories接收的value参数用来扫描数据访问层所在包下的数据访问的接口定义。

@Configuration
@EnableJpaRepositories("net.quickcodes.repos")
public class JpaConfiguration{
    @Bean
    public EntityManagerFactory entityManagerFactory(){
        //...
    }
    //还需要配置DataSource、PlatformTransactionManager等相关必须bean
}

3.定义查询方法

在讲解查询方法前,假设我们有一张数据表叫PERSON,有ID(Number)、NAME(Varchar2)、AGE(Number)、ADDRESS(Varchar2)这几个字段,对应的实体类叫Person,分别有id(Long),name(String),age(Integer),address(String)。下面我们以这个简单的实体查询为演示。

1>.根据属性名查询

Spring Data JPA支持通过定义在Repository接口中的方法名来定义查询,而方法名是根据实体类的属性名来确定的。

常规查询
根据属性名来定义查询方法

public interface PersonRepository extends JpaRepository<Person,Long>{
    //通过名字相等查询,参数为name。相当于JPQL:select p from Person p where p.name=?1
    List<Person> findByName(String name);
    
    //通过名字like查询,参数为name。相当于JPQL:select p from Person p where p.name like ?1
    List<Person> findByNameLike(String name);
    
    //通过名字和地址查询,参数为name和address。相当于JPQL:select p from Person p where p.name = ?1 and p.address=?2
    List<Person> findByNameAndAddress(String name,String address);
    
}

从代码可以看出,这里使用了findBy、Like、And这样的关键字。其中findBy可以用find、read、readBy、query、queryBy、get、getBy来代替。

与Like和and这类关键字类似的关键字还有:

关键字 示例 同功能JPQL
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 findByAgeIsNull where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull where x.age not null
Like findFirstnameLike where x.firstname like ?1
NotLike findFirstnameNotLike where x.firstname not like ?1
StartingWith findFirstnameStartingWith where x.firstname like ?1(参数后加%)
EndingWith findFirstnameEndingWith where x.firstname like ?1(参数前加%)
Containing findFirstnameContaining where x.firstname like ?1(参数前后加%)
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)

限制结果数量
限制结果数量可以使用top和first关键字来实现。

public interface PersonRepository extends JpaRepository<Person,Long>{
    //获得符合条件的前10条数据
    List<Person> findFirst10ByName(String name);
    
    //获得符合条件的前30条数据
    List<Person> findTop30ByName(String name);
    
}

2>.使用JPA的NamedQuery查询

Spring Data JPA支持用JPA的NameQuery来定义查询方法,即一个名称映射一个查询语句,定义如下:

@Entity
@NamedQuery(name="Person.findByName",query="select p from Person p where p.name=?1")
public class Person{
}

使用如下语句:

public interface PersonRepository extends JpaRepository<Person,Long>{
    //这时我们使用的是NamedQuery里定义的查询语句,而不是根据方法名称查询
    List<Person> findByName(String name);
}

3>.使用@Query查询

使用参数索引
Spring Data JPA还支持用@Query注解在接口的方法上实现查询

public interface PersonRepository extends JpaRepository<Person,Long>{
    @Query("select p from Person p where p.address=?1")//使用参数的索引号?1来查询
    List<Person> findByAddress(String address);
}

使用命名参数
在语句里用名称来匹配查询参数

public interface PersonRepository extends JpaRepository<Person,Long>{
    @Query("select p from Person p where p.address= :address")//:address
    List<Person> findByAddress(@Param("address") String address);//@Param("address") 
}

更新查询
Spring Data JPA支持@Modifying和@Query注解组合来实现更新查询

public interface PersonRepository extends JpaRepository<Person,Long>{
    @Modifying
    @Transactional
    @Query("update Person p set p.name=?1")
    int setName(String name);//返回值int代表更新语句影响的行数
}

4>.Specification

JPA提供了基于准则查询的方式,即Criteria查询。而Spring Data JPA提供了一个Specification(规范)接口让我们可以更方便地构造准则查询,Specification接口定义了一个toPredicate方法用来构造查询条件。
定义
我们的接口类必须实现JpaSpecificationExecutor接口

public interface PersonRepository extends JpaRepository<Person,Long>,JpaSpecificationExecutor<Person>{
}

然后需要定义Criterial查询

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.jpa.domain.Specification;

import net.quickcodes.domain.Person;

public class CustomerSpecs {

    public static Specification<Person> personFromHefei() { //1
        return new Specification<Person>() {

            @Override
            public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.equal(root.get("address"),"合肥");
            }

        };

    }
    }

我们使用Root来获得需要查询的属性,通过CriteriaBuilder构造查询条件,本例是查出所有来自合肥的人。
注意:CriteriaBuilder、CriteriaQuery、Predicate、Root都是来自JPA接口。

CriteriaBuilder包含的条件构造有:exists、and、or、not、conjunction、disjunction、isTrue、isFalse、isNull、isNotNull、equal、notEqual、greaterThan、greaterThanOrEqualTo、lessThan、lessThanOrEqualTo、between等,详细请查看CriteriaBuilder的API。

使用
静态导入:
import static net.quickcodes.specs.CustomerSpecs.*;
注入personRepository的Bean后:
List people=personRepository.findAll(personFromHefei());

5>.排序与分页

Spring Data JPA充分考虑了在实际开发中所必需的排序和分页的场景,为我们提供了Sort类以及Page接口和Pageable接口。
定义

package net.quickcodes.repos;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import net.quickcodes.domain.Person;

public interface PersonRepository extends JpaRepository<Person,Long>{
    List<Person> findByName(String name,Sort sort);
    Page<Person> findByName(String name,Pageable pageable);
}

使用排序
List person = personRepository.findByName("xx",new Sort(Direction.ASC,"age"));

使用分页
Page people2 = personRepository.findByName("xx",new PageRequest(0,10));

其中Page接口可以获得当前页面的记录、总页数、总记录数、是否有上一页或下一页等。

Spring Boot的支持

1.JDBC的自动配置

spring-boot-starter-data-jpa依赖于spring-boot-starter-jdbc,而Spring Boot对JDBC做了一些自动配置。源码在org.springframework.boot.autoconfigure.jdbc下。
从源码分析可以看出,我们通过“spring.datasource”为前缀的属性自动配置dataSource;

Spring Boot自动开启了注解事务的支持@EnableTransactionManagement,还配置了一个jdbcTemplate。

Spring Boot还提供了一个初始化数据的功能:放置在类路径下的schema.sql文件会自动用来初始化表结构;放置在类路径下的data.sql文件会自动用来填充表数据。

2.对JPA的自动配置

Spring Boot对JPA的自动配置放置在org.springframework.boot.autoconfigure.orm.jpa下

从HibernateJpaAutoconfiguration可以看出,Spring Boot默认的JPA的实现者是Hibernate;HibernateJpaAutoconfiguration依赖于DataSourceAutoConfiguration。

从JpaProperties的源码可看出,配置JPA可以使用spring.jpa为前缀的属性在application。properties中配置。

从JpaBaseConfiguration的源码中可看出,Spring Boot为我们配置了transactionManager、jpaVendorAdapter、entityManagerFactory等Bean。JpaBaseConfiguration还有一个getPackagesToScan方法,可以自动扫描注解有@Entity的实体类。

在Web项目中我们经常会遇到在控制器或页面访问数据的时候出现会话连接已关闭的错误,这时候我们会配置一个OpenEntityManage(Session) In View这个过滤器。令人惊喜的是,Spring Boot为我们自动配置了OpenEntityManagerInViewInterceptor这个Bean,并注册到Spring MVC的拦截器中。

3.对Spring Data JPA的自动配置

而Spring Boot对Spring Data JPA的自动配置放置在org.springframework.boot.antoconfigure.data.jpa下。从源码可以看出,JpaRepositoriesAutoConfiguration是依赖于HibernateJpaAutoConfiguration配置的,且Spring Boot自动开启了对Spring Data JPA的支持,即我们无须在配置类显式声明@EnableJpaRepositories。

4.Spring Boot下的Spring Data JPA

通过上述分析可知,我们在Spring Boot下使用Spring Data JPA,在项目的Maven依赖里添加spring-boot-starter-data-jpa,然后只需定义DataSource、实体类和数据访问层,并在需要使用数据访问的地方注入数据访问层的Bean即可,无须任何额外配置。

实战

1.安装Oracle XE

因大部分Java程序员在实际开发中一般使用的是Oracle,所以此处选择用Oracle XE作为开发测试数据库。Oracle XE是Oracle公司提供的免费开发测试用途的数据库,可自由使用,功能与使用与Oracle完全一致,但数据大小限制为4G。

1>.Docker安装

docker run -d -p 9090:8080 -p 1521:1521 wnameless/oracle-xe-11g
将容器中的Oracle XE管理界面的8080端口映射为本机的9090端口,将Oracle XE的1521端口映射为本机的1521端口。

本容器提供如下的安装信息:
hostname:localhost
端口:1521
SID:XE
username:system/sys
password:oracle

管理界面访问:
url:http://localhost:9090/apex
workspace:internal
username:admin
password:oracle

登录管理界面后新增一个用户:用户名boot,密码boot,用于测试。

2.新建Spirng Boot项目

搭建Spring Boot项目,依赖选择JPA和Web。

因为我们使用的是Oracle XE数据库,所以需要使用Oracle的JDBC驱动,而Maven中心库没有Oracle JDBC的驱动下载,因此我们需要通过Maven命令,自己打包Oracle的JDBC驱动到本地库。
在Oracle官网下载ojdbc14.jar,通过控制台命令将ojdbc14.jar安装到本地库:
mvn install:install-file -DgroupId=com.oracle "-DartifactId=ojdbc14" "-Dversion=11.4" "-Dpackaging=jar" "-Dfile=E:\ojdbc14.jar"

在pom.xml中添加如下依赖:

<dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc14</artifactId>
            <version>11.4</version>
        </dependency>

添加google guava依赖,它包含大量Java常用工具类:

<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>

新建src/main/resources/data.sql,内容为向表格添加一些数据,数据插入完成后请删除或对此文件改名称。

insert into person(id,name,age,address) values(hibernate_sequence.nextval,'Manon',32,'深圳');
insert into person(id,name,age,address) values(hibernate_sequence.nextval,'xx',31,'北京');
insert into person(id,name,age,address) values(hibernate_sequence.nextval,'yy',30,'上海');
insert into person(id,name,age,address) values(hibernate_sequence.nextval,'zz',29,'南京');
insert into person(id,name,age,address) values(hibernate_sequence.nextval,'aa',28,'武汉');
insert into person(id,name,age,address) values(hibernate_sequence.nextval,'bb',27,'合肥');    

3.配置基本属性

在application.properties里配置数据源和jpa的相关属性。

spring.datasource.driverClassName=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc\:oracle\:thin\:@localhost\:1521\:xe
spring.datasource.username=system
spring.datasource.password=oracle

#1
spring.jpa.hibernate.ddl-auto=update
#2
spring.jpa.show-sql=true


spring.jackson.serialization.indent_output=true

上面第一段配置数据源,第二段是用来配置jpa。

hibernate提供了根据实体类自动维护数据库表结构的功能,可通过spring.jpa.hibernate.ddl-auto来配置,有下列可选项:
create:启动时删除上次生成的表,并根据实体类生成表,表中数据会被清空;
create-drop:启动时根据实体类生成表,sessionFactory关闭时表会被删除;
update:启动时会根据实体类生成表,当实体类属性变动的时候,表结构也会更新,在初期开发阶段使用此选项;
validate:启动时验证实体类与数据表是否一致,在我们数据结构稳定时采用此选项;
none:不采取任何措施。

spring.jpa.show-sql用来设置hibernate操作的时候在控制台显示其真实的sql语句。

spring.jackson.serialization.indent_output让控制器输出的json字符串格式更美观。

4.定义映射实体类

package net.quickcodes.jpademo.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQuery;

@Entity //@Entity注解指明这是一个和数据库表映射的实体类
@NamedQuery(name = "Person.withNameAndAddressNamedQuery",
        query = "select p from Person p where p.name=?1 and address=?2")
public class Person {
    @Id //@Id注解指明这个属性映射为数据库的主键
    @GeneratedValue //@GeneratedValue注解默认使用主键生成方式为自增,hibernate会为我们自动生成一个名为HIBERNARE_SEQUENCE的序列
    private Long id;

    private String name;

    private Integer age;

    private String address;



    public Person() {
        super();
    }
    public Person(Long id, String name, Integer age, String address) {
        super();
        this.id = id;
        this.name = name;
        this.age = age;
        this.address = address;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    
}

5.定义数据访问接口

package net.quickcodes.jpademo.dao;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import net.quickcodes.jpademo.domain.Person;



public interface PersonRepository extends JpaRepository<Person, Long> {
    List<Person> findByAddress(String name);

    Person findByNameAndAddress(String name,String address);

    //使用@Query查询,参数按照名称绑定
    @Query("select p from Person p where p.name=:name and p.address=:address")
    Person withNameAndAddressQuery(@Param("name")String name,@Param("address")String address);

    //使用NamedQuery查询,请注意我们在实体类中做的@NamedQuery的定义
    Person withNameAndAddressNamedQuery(String name,String address);

}

6.控制器层

在本例中没有复杂的业务逻辑,我们将PersonRepository注入到控制器中,以简化演示。

package net.quickcodes.jpademo.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import net.quickcodes.jpademo.domain.Person;
import net.quickcodes.jpademo.dao.PersonRepository;

@RestController
public class DataController {
    //1 Spring Data JPA已自动为你注册bean,所以可自动注入
    @Autowired
    PersonRepository personRepository;

    /**
     * 保存
     * save支持批量保存:<S extends T> Iterable<S> save(Iterable<S> entities);
     *
     * 删除:
     * 删除支持使用id,对象以,批量删除及删除全部:
     * void delete(ID id);
     * void delete(T entity);
     * void delete(Iterable<? extends T> entities);
     * void deleteAll();
     *
     */
    @RequestMapping("/save")
    public Person save(String name,String address,Integer age){

        Person p = personRepository.save(new Person(null, name, age, address));

        return p;

    }



    /**
     * 测试findByAddress
     */
    @RequestMapping("/q1")
    public List<Person> q1(String address){

        List<Person> people = personRepository.findByAddress(address);

        return people;

    }

    /**
     * 测试findByNameAndAddress
     */
    @RequestMapping("/q2")
    public Person q2(String name,String address){

        Person people = personRepository.findByNameAndAddress(name, address);

        return people;

    }

    /**
     * 测试withNameAndAddressQuery
     */
    @RequestMapping("/q3")
    public Person q3(String name,String address){

        Person p = personRepository.withNameAndAddressQuery(name, address);

        return p;

    }

    /**
     * 测试withNameAndAddressNamedQuery
     */
    @RequestMapping("/q4")
    public Person q4(String name,String address){

        Person p = personRepository.withNameAndAddressNamedQuery(name, address);

        return p;

    }

    /**
     * 测试排序
     */
    @RequestMapping("/sort")
    public List<Person> sort(){

        List<Person> people = personRepository.findAll(new Sort(Direction.ASC,"age"));

        return people;

    }

    /**
     * 测试分页
     */
    @RequestMapping("/page")
    public Page<Person> page(){

        Page<Person> pagePeople = personRepository.findAll(new PageRequest(1, 2));

        return pagePeople;

    }


    @RequestMapping("/auto")
    public Page<Person> auto(Person person){

        Page<Person> pagePeople = personRepository.findByAuto(person, new PageRequest(0, 10));

        return pagePeople;

    }
}

7.访问地址测试运行效果

http://localhost:8080/save?name=dd&address=上海&age=25
http://localhost:8080/q1?address=深圳
http://localhost:8080/q2?address=深圳&name=Manon
http://localhost:8080/q3?address=深圳&name=Manon
http://localhost:8080/q4?address=深圳&name=Manon
http://localhost:8080/sort
http://localhost:8080/page

Spring Boot教程35——Spring Data JPA