概览
在前面的文章中,我介绍访问数据的几种方式,使用Spring JDBC和使用Mybatis框架,相比原生这两种方式大大简化了数据的操作过程,今天我将会介绍另外一种方式,使用JPA来访问数据库。
什么JPA
全称Java Persistence API,可以通过注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持久化到数据库中。
为我们提供了:
1)ORM映射元数据:JPA支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中;
如:@Entity、@Table、@Column、@Transient等注解。
2)JPA 的API:用来操作实体对象,执行CRUD操作,框架在后台替我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来。
如:entityManager.merge(T t);
3)JPQL查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
如:from Student s where s.name = ?
但是:
JPA仅仅是一种规范,也就是说JPA仅仅定义了一些接口,而接口是需要实现才能工作的。所以底层需要某种实现,而Hibernate就是实现了JPA接口的ORM框架。
什么是Spring Data JPA
Spirng Data Jpa是Spring提供的一套简化JPA开发的框架,按照约定好的【方法命名规则】写dao层接口,就可以在不写接口实现的情况下,实现对数据库的访问和操作。同时提供了很多除了CRUD之外的功能,如分页、排序、复杂查询等等。
Spring Data JPA 可以理解为 JPA 规范的再次封装抽象,底层还是使用了 Hibernate 的 JPA 技术实现。
Spring Data JPA、Hibernate、JPA三者之间的关系如下:

简单入门
实例代码对应的仓库地址:github.com/dragon8844/…
我们使用spring-boot-starter-data-jpa来实现对 Spring Data JPA的自动化配置,同时使用单元测试来演示单表的CRUD操作,废话不说,现在我们就开始
引入依赖
在pom.xml的文件中引入相关依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 实现对 Spring Data JPA 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
添加配置
在resources目录下创建应用的配置文件application.yml,添加如下配置内容:
spring:
# datasource 数据源配置内容
datasource:
url: jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
# JPA 配置内容,对应 JpaProperties 类
jpa:
show-sql: true # 打印 SQL 。生产环境,建议关闭
# Hibernate 配置内容,对应 HibernateProperties 类
hibernate:
ddl-auto: none
-
datasource配置项,配置 datasource 数据源配置内容。 -
jpa配置项,配置 Spring Data JPA 配置内容,对应 org.springframework.boot.autoconfigure.orm.jpa.JpaProperties.java 类。 -
hibernate配置项,配置 Hibernate 配置内容,对应 org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties.java类。-
ddl-auto 配置项,设置 Hibernate DDL 处理策略。一共有 none、create、create-drop、update、validate 五个选项。
create :每次加载 hibernate 时都会删除上一次的生成的表,然后根据你的 model 类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
create-drop :每次加载 hibernate 时根据 model 类生成表,但是 sessionFactory 一关闭,表就自动删除。
update :最常用的属性,第一次加载 hibernate 时根据 model 类会自动建立起表的结构(前提是先建立好数据库),以后加载 hibernate 时根据 model 类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
validate :每次加载 hibernate 时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。
-
编写代码
-
编写实体类
@Entity @Table(name = "user") @Data public class User { /** * 主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY, // strategy 设置使用数据库主键自增策略; generator = "JDBC") private Integer id; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 创建时间 */ private Date createTime; /** * 是否删除 */ private Integer deleted; } -
实体类对应的DDL语句:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名', `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `deleted` tinyint(1) DEFAULT NULL COMMENT '是否删除 0-未删除;1-删除', PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; -
编写Repository类,com.dragon.springdatajpa.dao包下创建接口User01Repository
public interface User01Repository extends JpaRepository<User,Integer> { }继承
org.springframework.data.repository.CrudRepository接口,第一个泛型设置对应的实体是 User ,第二个泛型设置对应的主键类型是 Integer 。因为实现了 CrudRepository 接口,Spring Data JPA 会自动生成对应的 CRUD 的代码。
单元测试
创建 User01RepositoryTest 测试类,我们来测试一下简单的 User01Repository 的每个操作。代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class User01RepositoryTest {
@Resource
User01Repository user01Repository;
@Test
public void testInsert(){
User user = new User();
user.setDeleted(0);
user.setUsername("张三");
user.setCreateTime(new Date());
user.setPassword("123456");
user01Repository.save(user);
}
@Test
public void testSelectById(){
Optional<User> userOptional = user01Repository.findById(13);
if(userOptional.isPresent()){
User user = userOptional.get();
log.info("user: {}", user.getId());
}
}
@Test
public void testUpdateById(){
Optional<User> userOptional = user01Repository.findById(13);
if(userOptional.isPresent()){
User user = userOptional.get();
user.setUsername("李四");
user = user01Repository.save(user);
log.info("user: {}", user.getUsername());
}
}
@Test
public void testDeleteBy(){
user01Repository.deleteById(13);
}
}
分页操作
Spring Data 提供 org.springframework.data.repository.PagingAndSortingRepository 接口,继承 CrudRepository 接口,在 CRUD 操作的基础上,额外提供分页和排序的操作。代码如下:
// PagingAndSortingRepository.java
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort); // 排序操作
Page<T> findAll(Pageable pageable); // 分页操作
}
编写Repository接口
在 com.dragon.springdatajpa.dao 包路径下,创建 UserRepository02 接口。代码如下:
public interface User02Repository extends PagingAndSortingRepository<User,Integer> {
}
实现 PagingAndSortingRepository 接口,第一个泛型设置对应的实体是 User ,第二个泛型设置对应的主键类型是 Integer 。
单元测试
创建 UserRepository02Test 测试类,我们来测试一下简单的 UserRepository02 的每个操作。代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class User02RepositoryTest {
@Resource
User02Repository user02Repository;
@Test
//排序
public void testList(){
Sort sort = Sort.by("id").descending();
Iterable<User> iterable = user02Repository.findAll(sort);
iterable.forEach(System.out :: println);
}
@Test
//分页
public void testPage(){
Sort sort = Sort.by("id").descending();
PageRequest pageRequest = PageRequest.of(0,10,sort);
Page<User> page = user02Repository.findAll(pageRequest);
log.info("page: {}", page.getTotalElements());
log.info("content:{}", page.getContent());
}
}
基于方法名查询
在 Spring Data 中,支持根据方法名作生成对应的查询(WHERE)条件,进一步进化我们使用 JPA ,具体是方法名以 findBy、existsBy、countBy、deleteBy 开头,后面跟具体的条件。具体的规则,在 《Spring Data JPA —— Query Creation》 文档中,已经详细提供。如下:
| 关键字 | 方法示例 | 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, Null | findByAge(Is)Null | … 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 ages) | … 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) |
编写Repository接口
public interface User03Repository extends PagingAndSortingRepository<User,Integer> {
/**
* 通过username查询
*@paramusername
*@return
*/
User findByUsername(String username);
/**
* 分页查询指定时间后的用户
*@paramcreateTime
*@parampageable
*@return
*/
Page<User> findByCreateTimeAfter(Date createTime, Pageable pageable);
}
如果方法中涉及到分页操作,需要使用到 Pageable 参数,需要作为方法的最后一个参数。
单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class User03RepositoryTest {
@Resource
User03Repository user03Repository;
@Test
public void testFindByUsername(){
User user = user03Repository.findByUsername("张三");
log.info("user:{}", user.getUsername());
}
@Test
public void findByCreateTimeAfter(){
Sort sort = Sort.by("id").descending();
PageRequest pageRequest = PageRequest.of(0,10,sort);
Page<User> page = user03Repository.findByCreateTimeAfter(new Date(), pageRequest);
log.info("page:{}",page.getTotalElements());
}
}
基于注解查询
Spring Data JPA 提供了非常强大基于方法名查询的机制,可以满足绝大多数业务场景下的 CRUD 操作,但是可能特殊情况下,我们仍然需要使用自定义的 SQL 操作数据库,我们可以使用在方法上添加 org.springframework.data.jpa.repository.@Query 注解来实现。
如果是更新或删除的 SQL 操作,需要额外在方法上添加 org.springframework.data.jpa.repository.@Modifying 注解。
编写Repository接口
/**
* 基于注解查询
*
* @author LiLong
* @date 2020/12/24
*/
public interface User04Repository extends PagingAndSortingRepository<User,Integer> {
/**
* 使用 @Query 自定义了一个 SQL 操作,并且参数使用占位符(?) + 参数位置的形式
* @param username
* @return
*/
@Query("SELECT u FROM User u WHERE u.username = ?1")
User findByUsername01(String username);
/**
* 使用占位符(:) + 参数名字(需要使用 @Param 声明)的形式。
* @param username
* @return
*/
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername02(@Param("username") String username);
/**
* 增加了 nativeQuery = true ,表示在 @Query 自定义的是原生SQL
* @param username
* @return
*/
@Query(value = "SELECT * FROM user u WHERE u.username = :username", nativeQuery = true)
User findByUsername03(@Param("username") String username);
/**
* 定义了更新操作,需要加上 @Modifying 注解
* @param id
* @param username
* @return
*/
@Query("UPDATE User u SET u.username = :username WHERE u.id = :id")
@Modifying
Integer updateUsernameById(Integer id, String username);
}
单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class User04RepositoryTest {
@Resource
User04Repository user04Repository;
@Test
public void testFindByUsername01(){
User user = user04Repository.findByUsername01("张三");
log.info("user:{}", user.getUsername());
}
@Test
public void testFindByUsername02(){
User user = user04Repository.findByUsername02("张三");
log.info("user:{}", user.getUsername());
}
@Test
public void testFindByUsername03(){
User user = user04Repository.findByUsername03("张三");
log.info("user:{}", user.getUsername());
}
@Test
@Transactional
public void testUpdateUsernameById(){
Integer count = user04Repository.updateUsernameById(14,"修改");
log.info("count:{}", count);
}
}
小结
各位朋友,Spring Data JPA的基础入门教程就已经写完了,来我们回顾下,首先我们介绍了什么是JPA、什么是Spring Data JPA,接着我们基于Spring Data JPA写了一个简单入门的例子,这里我们使用了spring-boot-starter-data-jpa,来完成Spring Data JPA的自动装配,在配置文件中,添加JPA的配置,接下我们编写了实体类和Repository类,实体类中我们使用JPA的注解,Repository类中我们继承了JpaRepository,就实现了对单表的CRUD操作。实际开发中,使用比较多还有分页操作以及基于方法名的查询,基于方法名的查询机制确实很强大,大部分的业务场景都可以处理。有些特殊的使用场景需要使用到原生的SQL,我们也可以用基于注解的机制来查询。
最后说一句
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。
此外,关注公众号:黑色的灯塔,专注Java后端技术分享,涵盖Spring,Spring Boot,SpringCloud,Docker,Kubernetes中间件等技术。