 Spring 之 JPA
Spring 之 JPA
  # Spring 之 JPA
JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。
- 简化数据持久化代码的开发
- 为 Java 社区屏蔽不同持久化 API 的差异
# 快速入门
(1)在 pom.xml 中引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
(2)设置启动注解
// 【可选】指定扫描的 Entity 目录,如果不指定,会扫描全部目录
@EntityScan("io.github.dunwu.springboot.data.jpa")
// 【可选】指定扫描的 Repository 目录,如果不指定,会扫描全部目录
@EnableJpaRepositories(basePackages = {"io.github.dunwu.springboot.data.jpa"})
// 【可选】开启 JPA auditing 能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间等等
@EnableJpaAuditing
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
(3)配置
# 数据库连接
spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.username = root
spring.datasource.password = root
# 是否打印 JPA SQL 日志
spring.jpa.show-sql = true
# Hibernate的DDL策略
spring.jpa.hibernate.ddl-auto = create-drop
(4)定义实体
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Objects;
import javax.persistence.*;
@Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(unique = true)
    private String name;
    private Integer age;
    private String address;
    private String email;
    public User(String name, Integer age, String address, String email) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.email = email;
    }
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof User)) {
            return false;
        }
        User user = (User) o;
        if (id != null && id.equals(user.id)) {
            return true;
        }
        return name.equals(user.name);
    }
}
(5)定义 Repository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@RepositoryRestResource(collectionResourceRel = "user", path = "user")
public interface UserRepository extends JpaRepository<User, Long> {
    User findUserById(@PathVariable("id") Long id);
    /**
     * 根据用户名查找用户
     * <p>
     * 示例:http://localhost:8080/user/search/findByName?name=lisi
     *
     * @param name 用户名
     * @return {@link User}
     */
    User findUserByName(@Param("name") String name);
    /**
     * 根据邮箱查找用户
     * <p>
     * 示例:http://localhost:8080/user/search/findByEmail?email=xxx@163.com
     *
     * @param email 邮箱
     * @return {@link User}
     */
    @Query("from User u where u.email=:email")
    List<User> findByEmail(@Param("email") String email);
    /**
     * 根据用户名删除用户
     *
     * @param name 用户名
     */
    @Transactional(rollbackFor = Exception.class)
    void deleteByName(@Param("name") String name);
}
(6)测试
@Slf4j
@SpringBootTest(classes = { DataJpaApplication.class })
public class DataJpaTests {
    @Autowired
    private UserRepository repository;
    @BeforeEach
    public void before() {
        repository.deleteAll();
    }
    @Test
    public void insert() {
        User user = new User("张三", 18, "北京", "user1@163.com");
        repository.save(user);
        Optional<User> optional = repository.findById(user.getId());
        assertThat(optional).isNotNull();
        assertThat(optional.isPresent()).isTrue();
    }
    @Test
    public void batchInsert() {
        List<User> users = new ArrayList<>();
        users.add(new User("张三", 18, "北京", "user1@163.com"));
        users.add(new User("李四", 19, "上海", "user1@163.com"));
        users.add(new User("王五", 18, "南京", "user1@163.com"));
        users.add(new User("赵六", 20, "武汉", "user1@163.com"));
        repository.saveAll(users);
        long count = repository.count();
        assertThat(count).isEqualTo(4);
        List<User> list = repository.findAll();
        assertThat(list).isNotEmpty().hasSize(4);
        list.forEach(this::accept);
    }
    private void accept(User user) { log.info(user.toString()); }
    @Test
    public void delete() {
        List<User> users = new ArrayList<>();
        users.add(new User("张三", 18, "北京", "user1@163.com"));
        users.add(new User("李四", 19, "上海", "user1@163.com"));
        users.add(new User("王五", 18, "南京", "user1@163.com"));
        users.add(new User("赵六", 20, "武汉", "user1@163.com"));
        repository.saveAll(users);
        repository.deleteByName("张三");
        assertThat(repository.findUserByName("张三")).isNull();
        repository.deleteAll();
        List<User> list = repository.findAll();
        assertThat(list).isEmpty();
    }
    @Test
    public void findAllInPage() {
        List<User> users = new ArrayList<>();
        users.add(new User("张三", 18, "北京", "user1@163.com"));
        users.add(new User("李四", 19, "上海", "user1@163.com"));
        users.add(new User("王五", 18, "南京", "user1@163.com"));
        users.add(new User("赵六", 20, "武汉", "user1@163.com"));
        repository.saveAll(users);
        PageRequest pageRequest = PageRequest.of(1, 2);
        Page<User> page = repository.findAll(pageRequest);
        assertThat(page).isNotNull();
        assertThat(page.isEmpty()).isFalse();
        assertThat(page.getTotalElements()).isEqualTo(4);
        assertThat(page.getTotalPages()).isEqualTo(2);
        List<User> list = page.get().collect(Collectors.toList());
        System.out.println("user list: ");
        list.forEach(System.out::println);
    }
    @Test
    public void update() {
        User oldUser = new User("张三", 18, "北京", "user1@163.com");
        oldUser.setName("张三丰");
        repository.save(oldUser);
        User newUser = repository.findUserByName("张三丰");
        assertThat(newUser).isNotNull();
    }
}
# 常用 JPA 注解
# 实体
# @Entity
 # @MappedSuperclass
 当多个实体有共同的属性字段,比如说 id,则可以把它提炼出一个父类,并且加上 @MappedSuperclass,则实体基类就可以继承了。
# @Table
 当实体名和表名不一致时,可以通过 @Table(name="CUSTOMERS") 的形式来明确指定一个表名。
# 主键
# @Id
 @Id 注解用于声明一个实体类的属性映射为数据库的主键。
# @GeneratedValue
 @GeneratedValue 用于标注主键的生成策略,通过 strategy 属性指定。
默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment。
在 javax.persistence.GenerationType 中定义了以下几种可供选择的策略:
public enum GenerationType {
    TABLE,
    SEQUENCE,
    IDENTITY,
    AUTO
}
- IDENTITY:采用数据库 ID 自增长的方式来自增主键字段,Oracle 不支持这种方式;
- AUTO: JPA 自动选择合适的策略,是默认选项;
- SEQUENCE:通过序列产生主键,通过- @SequenceGenerator注解指定序列名,MySql 不支持这种方式
- TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
也就是如果你没有指定 strategy 属性,默认策略是 AUTO,JPA 会根据你使用的数据库来自动选择策略,比如说我使用的是 mysql 则,自动的主键策略就是 IDENTITY (auto increment)。
# 映射
# @Column
 当你的 entity 属性名和数据库中的字段名不一致,可以使用 @Column 明确指定,它也可以设置一些属性
@Column(length = 10, nullable = false, unique = true)
@Column(columnDefinition = "INT(3)")
private int age;
@Column 支持的参数:
- unique属性表示该字段是否为唯一标识,默认为 false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用- @Table标记中的- @UniqueConstraint。
- nullable属性表示该字段是否可以为- null值,默认为 true。
- insertable属性表示在使用- INSERT插入数据时,是否需要插入该字段的值。
- updatable属性表示在使用- UPDATE更新数据时,是否需要更新该字段的值。- insertable和- updatable属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。
- columnDefinition属性表示创建表时,该字段创建的 SQL 语句,一般用于通过 Entity 生成表定义时使用。
- table属性表示当映射多个表时,指定表的表中的字段。默认值为主表的表名。
- length属性表示字段的长度,当字段的类型为- varchar时,该属性才有效,默认为 255 个字符。
- precision属性和 scale 属性表示精度,当字段类型为- double时,- precision表示数值的总长度,- scale表示小数点所占的位数。
@JoinTable
@JoinColumn
# 关系
表关系映射(双向映射)
- @OneToOne:一对一关系
- @OneToMany:一对多
- @ManyToMany(不推荐使用,而是采用用中间对象,把多对多拆成两个对多一关系)
字段映射(单向映射):
- @Embedded、- @Embeddable嵌入式关系(单向映射)
- @ElementCollection集合一对多关系(单向映射)
# @OneToOne
 @OneToOne 表示一对一关系
# @OneToMany
 @OneToMany 表示一对多关系
@ManyToOne
@ManyToMany
OrderBy
# 查询
查询方式有:
- 方法名字方式查询 
- @Query注解方式查询
- 动态 SQL 方式查询 
- Example 方式查询 
JpaRepository 提供了如下表所述的内置查询
- List<T> findAll();- 返回所有实体
- List<T> findAllById(Iterable<ID> var1);- 返回指定 id 的所有实体
- T getOne(ID var1);- 根据 id 返回对应的实体,如果未找到,则返回空。
- List<T> findAll(Sort var1);- 返回所有实体,按照指定顺序返回。
- Page<T> findAll(Pageable var1);- 返回实体列表,实体的 offset 和 limit 通过 pageable 来指定
# 方法名字方式查询方式
Spring Data 通过查询的方法名和参数名来自动构造一个 JPA QQL 查询。
public interface UserRepository extends JpaRepository<User, Integer> {
    public User findByName(String name);
}
方法名和参数名要遵守一定的规则,Spring Data JPA 才能自动转换为 JPQL:
- 方法名通常包含多个实体属性用于查询,属性之间可以使用 - AND和- OR连接,也支持- Between、- LessThan、- GreaterThan、- Like;
- 方法名可以以 - findBy、- getBy、- queryBy开头;
- 查询结果可以排序,方法名包含 OrderBy+属性+ASC(DESC); 
- 可以通过 - Top、- First来限定查询的结果集;
- 一些特殊的参数可以出现在参数列表里,比如 - Pageeable、- Sort
示例:
// 根据名字查询,且按照名字升序
List<Person> findByLastnameOrderByFirstnameAsc(String name);
// 根据名字查询,且使用翻页查询
Page<User> findByLastname(String lastname, Pageable pageable);
// 查询满足条件的前10个用户
List<User> findFirst10ByLastname(String lastname, Sort sort);
// 使用And联合查询
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
// 使用Or查询
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
// 使用like查询,name 必须包含like中的%或者?
public User findByNameLike(String name);
| 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 | 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 | 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> 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) | 
# @Query 注解方式查询
注解 @Query 允许在方法上使用 JPQL。
其中操作针对的是对象名和对象属性名,而非数据库中的表名和字段名。
@Query("select u form User u where u.name=?1 and u.depantment.id=?2");
public User findUser(String name, Integer departmentId);
@Query("form User u where u.name=?1 and u.depantment.id=?2");
public User findUser(String name, Integer departmentId);
如果使用 SQL 而不是 JPSQL,可以使用 nativeQuery 属性,设置为 true。
@Query(value="select * from user where name=?1 and department_id=?2", nativeQuery=true)
public User nativeQuery(String name, Integer departmentId);
无论 JPQL,还是 SQL,都支持"命名参数":
@Query(value="select * from user where name=:name and department_id=:departmentId", nativeQuery=true)
public User nativeQuery2(String name, Integer departmentId);
如果 SQL 活着 JPQL 查询结果集并非 Entity,可以用 Object[] 数组代替,比如分组统计每个部分的用户数
@Query(value="select department_id,count(*) from user group by department_id", nativeQuery=true)
public List<Object[]> queryUserCount()
这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 String 和 BigInteger 类型
查询时可以使用 Pageable 和 Sort 来完成翻页和排序。
@Query("select u from User u where department.id=?1")
public Page<User> QueryUsers(Integer departmentId, Pageable page);
@Query 还允许 SQL 更新、删除语句,此时必须搭配 @Modifying 使用,比如:
@Modifying
@Query("update User u set u.name= ?1 where u.id= ?2")
int updateName(String name, Integer id);
# 动态 SQL 方式查询
可参考:SpringDataJpa 中的复杂查询和动态查询,多表查询 (opens new window)
# Example 方式查询
允许根据实体创建一个 Example 对象,Spring Data 通过 Example 对象来构造 JPQL。但是使用不灵活条件是 AND,不能使用 or,时间的大于小于,between 等。
继承 JpaRepository
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
public List<User> getByExample(String name) {
    Department dept = new Department();
    dept.setId(1);
    User user = new User();
    user.setName(name);
    user.setDepartment(dept);
    Example<User> example = Example.of(user);
    List<User> list = userDao.findAll(example);
    return list
}
以上代码首先创建了 User 对象,设置 查询条件,名称为参数 name,部门 id 为 1,通过 Example.of 构造了此查询。
大部分查询并非完全匹配查询,ExampleMatcher 提供了更多的条件指定.比如以 xxx 开头的所有用户,则可以使用以下代码构造
ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("xxx",
    GenericPropertyMatchers.startsWith().ignoreCase());
Example<User> example = Example.of(user, matcher);
# 排序 Sort
Sort 对象用来指定排序,最简单的 Sort 对象构造可以传入一个属性名列表(不是数据库列名,是属性名)。默认采用升序排序。
Sort sort = new Sort("id");
//Sort sort = new Sort(Direction.DESC, "id");
return userDao.findAll(sort);
Hibernate 根据 Sort 构造了排序条件,Sort("id") 表示按照 id 采用默认 升序进行排序
其他 Sort 的构造方法还包括以下主要的一些:
- public Sort(String... properties),按照指定的属性列表升序排序。
- public Sort(Sort.Direction direction, String... properties),按照指定属性列表排序,排序由 direction 指定,direction 是一个枚举类型,有- Direction.ASC和- Direction.DESC。
- public Sort(Sort.Order... orders),可以通过 Order 静态方法来创建- public static Sort.Order asc(String property)
- public static Sort.Order desc(String property)
 
# 分页 Page 和 Pageable
Pageable 接口用于构造翻页查询,PageRequest 是其实现类,可以通过提供的工厂方法创建 PageRequest:
注意我这边使用的是 sring boot 2.0.2 ,jpa 版本是 2.0.8,新版本与之前版本的操作方法有所不同。
- public static PageRequest of(int page, int size)
- public static PageRequest of(int page, int size, Sort sort)- 也可以在 PageRequest 中加入排序
- public static PageRequest of(int page, int size, Direction direction, String... properties),或者自定义排序规则
page 是从 0 开始,表示查询页,size 指每页的期望行数。
Spring Data 翻页查询总是返回 Page 对象,Page 对象提供了以下常用的方法
- int getTotalPages();,总的页数
- long getTotalElements();- 返回总数
- List<T> getContent();- 返回此次查询的结果集
# 核心 API

