Spring 之 JPA

Spring 之 JPA

JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。

  • 简化数据持久化代码的开发
  • 为 Java 社区屏蔽不同持久化 API 的差异

快速入门

(1)在 pom.xml 中引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

(2)设置启动注解

1
2
3
4
5
6
7
8
9
10
11
12
// 【可选】指定扫描的 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)配置

1
2
3
4
5
6
7
8
9
# 数据库连接
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)定义实体

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
49
50
51
52
53
54
55
56
57
58
59
60
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

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

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)测试

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@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 中定义了以下几种可供选择的策略:

1
2
3
4
5
6
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 明确指定,它也可以设置一些属性

1
@Column(length = 10, nullable = false, unique = true)
1
2
@Column(columnDefinition = "INT(3)")
private int age;

@Column 支持的参数:

  • unique 属性表示该字段是否为唯一标识,默认为 false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用 @Table 标记中的 @UniqueConstraint
  • nullable 属性表示该字段是否可以为 null 值,默认为 true。
  • insertable 属性表示在使用 INSERT 插入数据时,是否需要插入该字段的值。
  • updatable 属性表示在使用 UPDATE 更新数据时,是否需要更新该字段的值。insertableupdatable 属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。
  • 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 查询。

1
2
3
public interface UserRepository extends JpaRepository<User, Integer> {
public User findByName(String name);
}

方法名和参数名要遵守一定的规则,Spring Data JPA 才能自动转换为 JPQL:

  • 方法名通常包含多个实体属性用于查询,属性之间可以使用 ANDOR 连接,也支持 BetweenLessThanGreaterThanLike

  • 方法名可以以 findBygetByqueryBy 开头;

  • 查询结果可以排序,方法名包含 OrderBy+属性+ASC(DESC);

  • 可以通过 TopFirst 来限定查询的结果集;

  • 一些特殊的参数可以出现在参数列表里,比如 PageeableSort

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 根据名字查询,且按照名字升序
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。

其中操作针对的是对象名和对象属性名,而非数据库中的表名和字段名。

1
2
@Query("select u form User u where u.name=?1 and u.depantment.id=?2");
public User findUser(String name, Integer departmentId);
1
2
@Query("form User u where u.name=?1 and u.depantment.id=?2");
public User findUser(String name, Integer departmentId);

如果使用 SQL 而不是 JPSQL,可以使用 nativeQuery 属性,设置为 true。

1
2
@Query(value="select * from user where name=?1 and department_id=?2", nativeQuery=true)
public User nativeQuery(String name, Integer departmentId);

无论 JPQL,还是 SQL,都支持”命名参数”:

1
2
@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[] 数组代替,比如分组统计每个部分的用户数

1
2
@Query(value="select department_id,count(*) from user group by department_id", nativeQuery=true)
public List<Object[]> queryUserCount()

这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 StringBigInteger 类型

查询时可以使用 PageableSort 来完成翻页和排序。

1
2
@Query("select u from User u where department.id=?1")
public Page<User> QueryUsers(Integer departmentId, Pageable page);

@Query 还允许 SQL 更新、删除语句,此时必须搭配 @Modifying 使用,比如:

1
2
3
@Modifying
@Query("update User u set u.name= ?1 where u.id= ?2")
int updateName(String name, Integer id);

动态 SQL 方式查询

可参考:SpringDataJpa 中的复杂查询和动态查询,多表查询

Example 方式查询

允许根据实体创建一个 Example 对象,Spring Data 通过 Example 对象来构造 JPQL。但是使用不灵活条件是 AND,不能使用 or,时间的大于小于,between 等。

继承 JpaRepository

1
2
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
1
2
3
4
5
6
7
8
9
10
11
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 开头的所有用户,则可以使用以下代码构造

1
2
3
ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("xxx",
GenericPropertyMatchers.startsWith().ignoreCase());
Example<User> example = Example.of(user, matcher);

排序 Sort

Sort 对象用来指定排序,最简单的 Sort 对象构造可以传入一个属性名列表(不是数据库列名,是属性名)。默认采用升序排序。

1
2
3
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.ASCDirection.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

参考资料