title | date | order | categories | tags | permalink | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Spring 之 JPA |
2019-02-18 06:33:55 -0800 |
4 |
|
|
/pages/a03d7b/ |
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/[email protected]
*
* @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, "北京", "[email protected]");
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, "北京", "[email protected]"));
users.add(new User("李四", 19, "上海", "[email protected]"));
users.add(new User("王五", 18, "南京", "[email protected]"));
users.add(new User("赵六", 20, "武汉", "[email protected]"));
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, "北京", "[email protected]"));
users.add(new User("李四", 19, "上海", "[email protected]"));
users.add(new User("王五", 18, "南京", "[email protected]"));
users.add(new User("赵六", 20, "武汉", "[email protected]"));
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, "北京", "[email protected]"));
users.add(new User("李四", 19, "上海", "[email protected]"));
users.add(new User("王五", 18, "南京", "[email protected]"));
users.add(new User("赵六", 20, "武汉", "[email protected]"));
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, "北京", "[email protected]");
oldUser.setName("张三丰");
repository.save(oldUser);
User newUser = repository.findUserByName("张三丰");
assertThat(newUser).isNotNull();
}
}
当多个实体有共同的属性字段,比如说 id,则可以把它提炼出一个父类,并且加上 @MappedSuperclass
,则实体基类就可以继承了。
当实体名和表名不一致时,可以通过 @Table(name="CUSTOMERS")
的形式来明确指定一个表名。
@Id 注解用于声明一个实体类的属性映射为数据库的主键。
@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)。
当你的 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
表示一对一关系
@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
允许在方法上使用 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);
可参考:SpringDataJpa 中的复杂查询和动态查询,多表查询
允许根据实体创建一个 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 = 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)
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();
- 返回此次查询的结果集