跳至主要內容

Spring 之 JPA

钝悟...大约 11 分钟Java框架SpringSpring数据Java框架SpringSpringBootJPA

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 更新数据时,是否需要更新该字段的值。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 查询。

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

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

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

  • 方法名可以以 findBygetByqueryBy 开头;

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

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

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

示例:

// 根据名字查询,且按照名字升序
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);
KeywordSampleJPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is,EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = 1?
BetweenfindByStartDateBetween… where x.startDate between 1? and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… 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()

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

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

@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 中的复杂查询和动态查询,多表查询open in 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.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

参考资料

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7