--- title: Spring 之 JPA date: 2019-02-18 14:33:55 order: 04 categories: - Java - 框架 - Spring - Spring数据 tags: - Java - 框架 - Spring - SpringBoot - JPA permalink: /pages/ffe362a4/ --- # Spring 之 JPA JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。 - 简化数据持久化代码的开发 - 为 Java 社区屏蔽不同持久化 API 的差异 ## 快速入门 (1)在 pom.xml 中引入依赖 ```xml org.springframework.boot spring-boot-starter-data-jpa ``` (2)设置启动注解 ```java // 【可选】指定扫描的 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)配置 ```properties # 数据库连接 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)定义实体 ```java 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 ```java 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 findUserById(@PathVariable("id") Long id); /** * 根据用户名查找用户 *

* 示例:http://localhost:8080/user/search/findByName?name=lisi * * @param name 用户名 * @return {@link User} */ User findUserByName(@Param("name") String name); /** * 根据邮箱查找用户 *

* 示例:http://localhost:8080/user/search/findByEmail?email=xxx@163.com * * @param email 邮箱 * @return {@link User} */ @Query("from User u where u.email=:email") List findByEmail(@Param("email") String email); /** * 根据用户名删除用户 * * @param name 用户名 */ @Transactional(rollbackFor = Exception.class) void deleteByName(@Param("name") String name); } ``` (6)测试 ```java @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 optional = repository.findById(user.getId()); assertThat(optional).isNotNull(); assertThat(optional.isPresent()).isTrue(); } @Test public void batchInsert() { List 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 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 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 list = repository.findAll(); assertThat(list).isEmpty(); } @Test public void findAllInPage() { List 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 page = repository.findAll(pageRequest); assertThat(page).isNotNull(); assertThat(page.isEmpty()).isFalse(); assertThat(page.getTotalElements()).isEqualTo(4); assertThat(page.getTotalPages()).isEqualTo(2); List 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` 中定义了以下几种可供选择的策略: ```java 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` 明确指定,它也可以设置一些属性 ```java @Column(length = 10, nullable = false, unique = true) ``` ```java @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 findAll();` - 返回所有实体 - `List findAllById(Iterable var1);` - 返回指定 id 的所有实体 - `T getOne(ID var1);` - 根据 id 返回对应的实体,如果未找到,则返回空。 - `List findAll(Sort var1);` - 返回所有实体,按照指定顺序返回。 - `Page findAll(Pageable var1);` - 返回实体列表,实体的 offset 和 limit 通过 pageable 来指定 ### 方法名字方式查询方式 Spring Data 通过查询的方法名和参数名来自动构造一个 JPA QQL 查询。 ```java public interface UserRepository extends JpaRepository { 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` 示例: ```java // 根据名字查询,且按照名字升序 List findByLastnameOrderByFirstnameAsc(String name); // 根据名字查询,且使用翻页查询 Page findByLastname(String lastname, Pageable pageable); // 查询满足条件的前10个用户 List findFirst10ByLastname(String lastname, Sort sort); // 使用And联合查询 List findByFirstnameAndLastname(String firstname, String lastname); // 使用Or查询 List 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 ages)` | `… where x.age in ?1` | | `NotIn` | `findByAgeNotIn(Collection 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。 其中操作针对的是对象名和对象属性名,而非数据库中的表名和字段名。 ```java @Query("select u form User u where u.name=?1 and u.depantment.id=?2"); public User findUser(String name, Integer departmentId); ``` ```java @Query("form User u where u.name=?1 and u.depantment.id=?2"); public User findUser(String name, Integer departmentId); ``` 如果使用 SQL 而不是 JPSQL,可以使用 `nativeQuery` 属性,设置为 true。 ```java @Query(value="select * from user where name=?1 and department_id=?2", nativeQuery=true) public User nativeQuery(String name, Integer departmentId); ``` 无论 JPQL,还是 SQL,都支持"命名参数": ```java @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[]` 数组代替,比如分组统计每个部分的用户数 ```java @Query(value="select department_id,count(*) from user group by department_id", nativeQuery=true) public List queryUserCount() ``` 这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 `String` 和 `BigInteger` 类型 查询时可以使用 `Pageable` 和 `Sort` 来完成翻页和排序。 ```java @Query("select u from User u where department.id=?1") public Page QueryUsers(Integer departmentId, Pageable page); ``` `@Query` 还允许 SQL 更新、删除语句,此时必须搭配 `@Modifying` 使用,比如: ```java @Modifying @Query("update User u set u.name= ?1 where u.id= ?2") int updateName(String name, Integer id); ``` ### 动态 SQL 方式查询 可参考:[SpringDataJpa 中的复杂查询和动态查询,多表查询](https://juejin.cn/post/6844904160807092237) ### Example 方式查询 允许根据实体创建一个 Example 对象,Spring Data 通过 Example 对象来构造 JPQL。但是使用不灵活条件是 AND,不能使用 or,时间的大于小于,between 等。 继承 `JpaRepository` ```java List findAll(Example var1); List findAll(Example var1, Sort var2); ``` ```java public List getByExample(String name) { Department dept = new Department(); dept.setId(1); User user = new User(); user.setName(name); user.setDepartment(dept); Example example = Example.of(user); List list = userDao.findAll(example); return list } ``` 以上代码首先创建了 User 对象,设置 查询条件,名称为参数 name,部门 id 为 1,通过 `Example.of` 构造了此查询。 大部分查询并非完全匹配查询,ExampleMatcher 提供了更多的条件指定.比如以 xxx 开头的所有用户,则可以使用以下代码构造 ```java ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("xxx", GenericPropertyMatchers.startsWith().ignoreCase()); Example example = Example.of(user, matcher); ``` ### 排序 Sort Sort 对象用来指定排序,最简单的 Sort 对象构造可以传入一个属性名列表(不是数据库列名,是属性名)。默认采用升序排序。 ```java 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 getContent();` - 返回此次查询的结果集 ## 核心 API ![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230123160810.png) ## 参考资料 - [Spring 官网](https://spring.io/) - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) - [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html)