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();
- 返回此次查询的结果集