SpringBoot 2.x系列:Spring Data JPA

1,759 阅读8分钟

默认文件1618197992068.png

概览

在前面的文章中,我介绍访问数据的几种方式,使用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三者之间的关系如下:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7fa8511d60044359908bc956d3259b6f~tplv-k3u1fbpfcp-zoom-1.image

简单入门

实例代码对应的仓库地址: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
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
IsNull, NullfindByAge(Is)Null… 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 ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… 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中间件等技术。