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 @EntityScan("io.github.dunwu.springboot.data.jpa") @EnableJpaRepositories(basePackages = {"io.github.dunwu.springboot.data.jpa"}) @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 spring.jpa.show-sql = true 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) ; User findUserByName (@Param("name") String name) ; @Query("from User u where u.email=:email") List<User> findByEmail (@Param("email") String email) ; @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
更新数据时,是否需要更新该字段的值。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 查询。
1 2 3 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
示例:
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) ; List<User> findFirst10ByLastname (String lastname, Sort sort) ; List<Person> findByFirstnameAndLastname (String firstname, String lastname) ; List<Person> findDistinctPeopleByLastnameOrFirstname (String lastname, String firstname) ; 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()
这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 String
和 BigInteger
类型
查询时可以使用 Pageable
和 Sort
来完成翻页和排序。
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" );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
参考资料