1 Spring Data JPA

1.1 简介

1.1.1 JPA

JPAJava Persistence API)即java持久化API,它的出现主要是为了简化持久层开发以及整合ORM技术,结束Hibernate、TopLink、JDO等ORM框架各自为营的局面。JPA是在吸收现有ORM框架的基础上发展而来,易于使用,伸缩性强。

JPA诞生的缘由是为了整合第三方ORM框架,建立一种标准的方式,是JDK为了实现ORM的天下归一,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate是一支很大的部队,使用很广泛,也很方便,能力也很强,同时Hibernate也是和JPA整合的比较良好,我们可以认为JPA是标准,事实上也是,JPA几乎都是接口,实现都是Hibernate在做,宏观上面看,在JPA的统一之下Hibernate很良好的运行

总的来说,JPA包括以下三方面的技术:

  • ORM映射元数据:支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系
  • API:操作实体对象来执行CRUD操作
  • 查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合

1.1.2 Spring Data JPA

Spring Data JPA的基本介绍:
Spring Data JPASpring Data家族的一部分,可以轻松实现基于JPA的存储库。此模块处理对基于JPA的数据访问层的增强支持。它使构建使用数据访问技术的Spring驱动应用程序变得更加容易。底层是使用hibernate实现

在相当长的一段时间内,实现应用程序的数据访问层一直很麻烦。必须编写太多样板代码来执行简单查询以及执行分页和审计。Spring Data JPA旨在减少实际需要的工作量来显著改善数据访问层的实现

1.2 配置文件

pom依赖
SpringBoot整合Spring Data JPA
导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

数据库配置:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/chapter05?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
#表示Jpa对应的数据库是mysql
spring.jpa.show-sql=true
#项目启动时根据实体类更新数据库中的表
spring.jpa.hibernate.ddl-auto=update

其中:spring.jpa.hibernate.ddl-auto,可选参数:

  • create:每次运行程序时,都会重新创建表,故而数据会丢失
  • create-drop:每次运行程序时会先创建表结构,然后程序结束时清空表
  • update:每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用)
  • validate:运行程序会校验数据于数据库的字段类型是否相同,字段不同会报错
  • none:禁用DDL处理

1.3 操作使用JPA

1.3.1 实体类相关

创建实体类:

import lombok.Data;
import javax.persistence.*;
 
@Data
@Entity
@Table(name = "t_book")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name = "book_name")
    private String name;
    @Column(name = "book_author")
    private String author;
    private Float price;
    @Transient
    private String description;
}

注解解释:

  • @Entity:注解表示该类是一个实体类,在项目启动时会根据该类自动生成一张表,表的名称即@Entity注解中name的值,如果不配置name,默认表名为类名
    指定实体名称(表名):
    • 没有指定name属性且没有使用@Table,命名为类名生成
    • 指定name属性且没有使用@Table,命名为name属性value
    • 指定name属性且使用了@Table指定name,命名以@Tablenamevalue
  • @Id:所有的实体类都要有的主键,@Id注解表示该属性是一个主键
  • @GeneratedValue:注解表示主键自动生成,strategy则表示主键的生成策略
    JPA自带的几种主键生成策略:
    • TABLE:使用一个特定的数据库表格来保存主键
    • SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。这个值要于generator一起使用,generator指定生成主键的生成器
    • IDENTITY:主键由数据库自动生成(主要支持自动增长的数据库,如mysql)
    • AUTO:主键由程序控制,也是GenerationType的默认值,mysql不支持,会报错:test.hibernate_sequence不存在
  • @Column:默认情况下,生成的表中字段的名称是实体类中属性的名称,通过@Column注解可以定制生成的字段的属性,name表示该属性对应的数据表中字段的名称,nullable表示该字段非空
  • @Transient:注解表示在生成数据库的表时,该属性被忽略,即不生成对应的字段

1.3.2 Dao层

1.3.2.1 基本示例

创建Dao接口,继承JpaRepository,代码如下:


import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Component;
 
import java.util.List;
 
@Component
public interface BookDao extends JpaRepository<User,Integer> {
    //查询以某个字符开始的所有书
    List<Book> getBooksByAuthorStartingWith(String author);
    //查询单价大于某个值的所有书
    List<Book> getBooksByPriceGreaterThan(Float price);
    @Query(value = "select * from t_book where id=(select max(id) from t_book)",nativeQuery = true)
    Book getMaxIdBook();
    @Query("select b from t_book b where b.id>:id and b.author=:author")
    List<Book> getBookByIdAndAuthor(@Param("author") String author,@Param("id") Integer id);
    @Query("select b from t_book b where b.id<?2 and b.name like %?1%")
    List<Book> getBookByIdAndName(String name,Integer id);
}

解释:

  • 自定义Dao继承自JpaRepositiory<T,ID>,需要两个参数T指当前需要映射得实体,ID指当前映射得实体中的主键类型。JpaRepositiory中提供了一些基本的数据操作方法,有基本的增删改查、分页查询、排序查询等
  • Spring Data JPA中,只要方法的定义符合既定规范,Spring Data就能分析出开发者意图,从而避免开发者定义SQL
1.3.2.2 @Query注解

@Query注解使用起来很简单,默认的属性是value,就是当前写的SQL语句,有时会用到nativeQuery属性,这个属性是用来标记当前的SQL是本地SQL,还是符合JPA语法规范的SQL
这里需要解释一下本地SQL和JPA语法规范的SQL区别。

  • 本地SQL:根据实际使用的数据库类型写的SQL,这种SQL中使用到的一些语法格式不能被JPA解析以及可能不兼容其他数据库,这种SQL称为本地SQL,此时需要将nativeQuery属性设置为true,否则会报错。
  • JPA语法规范的SQL:往往这种SQL本身是不适用于任何数据库的,需要JPA将这种SQL转换成真正当前数据库所需要的SQL语法格式。

注意JPA很好的一个特性就是用JPA语法规范写的SQL,会根据当前系统使用的数据库类型改变生成的SQL语法,兼容数据库类型的切换,如之前使用的是MySQL,现在换成Oracle,由于不同类型的数据库,SQL语法会有区别,如果使用的是mybatis,就需要手动去改SQL兼容Oracle,而JPA就不用,无缝对接。
说明:很大的时候使用JPA感觉都是为了兼容后期可能会有数据库切换的问题,所以在使用JPA的时候,不要去使用本地SQL,这就违背了使用JPA的初衷,让nativeQuery属性保持默认值即可

1.3.2.3 SQL传参

根据这个例子再引出一些常用的东西,代码如下:

//示例1
@Query("select t from Device t where t.deviceSn=:deviceSn and t.deleteFlag=1")
Device findExistDevice(@Param("deviceSn") String deviceSn);
//示例2
@Query("select t from Device t where t.deviceSn=:deviceSn and t.deviceType =:deviceType and t.deleteFlag=1")
Device findExistDevice(@Param("deviceSn") String deviceSn,@Param("deviceType")Integer deviceType);
//示例3
@Query("select t from Device t where t.deviceSn=?1 and t.deviceType = ?2 and t.deleteFlag=1")
Device findDevice(String deviceSn,Integer deviceType);

SQL上使用占位符的两种方式:

  • 使用:后加变量的名称,需要使用@Param注解来指定变量名
  • 使用?后加方法参数的位置。就需要注意参数的位置。

SQL语句中直接用实体类代表表名,因为在实体类中使用了@Table注解,将该实体类和表进行了关联

1.3.2.4 @Modifying注解

相信在正常的项目开发中都会涉及到修改数据信息的操作,如逻辑删除、封号、解封、修改用户名、头像等等。在使用JPA的时候,如果@Query涉及到update就必须同时加上@Modifying注解,注明当前方法是修改操作。

如下代码:

@Modifying
@Query("update Device t set t.userName =:userName where t.id =:userId")
User updateUserName(@Param("userId") Long userId,@Param("userName") String userName);

当在测试类中直接调用时,需要需要在测试类中添加注解@Transactional,但是@Transactional@Test一起连用会导致事务自动回滚,这时候需要指定事务不回滚@Rollback(false)

@Test
@Transactional
@Rollback(false)
public void updateById() {
    dao11.updateById("张三",1);
}

1.3.3 Service层

创建BookService,代码如下:

import com.example.demo.dao.BookDao;
import com.example.demo.domain.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
 
import java.util.List;
 
@Service
public class BookService {
    @Autowired
    BookDao bookDao;
 
    //save方法由JpaRepository接口提供
    public void addBook(Book book){
        bookDao.save(book);
    }
 
    //分页查询
    public Page<Book> getBookByPage(Pageable pageable){
        return bookDao.findAll(pageable);
    }
 
    public List<Book> getBooksByAuthorStartingWith(String author){
        return bookDao.getBooksByAuthorStartingWith(author);
    }
 
    public List<Book> getBooksByPriceGreaterThan(Float price){
        return bookDao.getBooksByPriceGreaterThan(price);
    }
 
    public Book getMaxIdBook(){
        return bookDao.getMaxIdBook();
    }
 
    public List<Book> getBookByIdAndName(String name,Integer id){
        return bookDao.getBookByIdAndName(name,id);
    }
 
    public List<Book> getBookByIdAndAuthor(String author,Integer id){
        return bookDao.getBookByIdAndAuthor(author,id);
    }
}

1.3.4 Controller层

创建BookContrller,实现对数据的测试,代码如下:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.List;
 
@RestController
public class BookController {
    @Autowired
    BookService bookService;
 
    @GetMapping(value = "/findAll")
    public void findAll(){
        PageRequest pageRequest = PageRequest.of(2,3);
        Page<Book> page = bookService.getBookByPage(pageRequest);
        System.out.println("总页数:"+page.getTotalPages());
        System.out.println("总记录数:"+page.getTotalElements());
        System.out.println("查询结果:"+page.getContent());
        //从0开始记,所以加上1
        System.out.println("当前页数:"+(page.getNumber()+1));
        System.out.println("当前记录数:"+page.getNumberOfElements());
        System.out.println("每页记录数:"+page.getSize());
    }
 
    @GetMapping(value = "search")
    public void search(){
        List<Book> bs1 = bookService.getBookByIdAndAuthor("鲁迅",7);
        List<Book> bs2 = bookService.getBooksByAuthorStartingWith("吴");
        List<Book> bs3 = bookService.getBookByIdAndName("西",8);
        List<Book> bs4 = bookService.getBooksByPriceGreaterThan(30F);
 
        Book b = bookService.getMaxIdBook();
        System.out.println("bs1:"+bs1);
        System.out.println("bs2:"+bs2);
        System.out.println("bs3:"+bs3);
        System.out.println("bs4:"+bs4);
        System.out.println("b:"+b);
    }
 
    @GetMapping(value = "/save")
    public void save(){
        Book book = new Book();
        book.setAuthor("鲁迅");
        book.setName("呐喊");
        book.setPrice(23F);
        bookService.addBook(book);
    }
}

代码解释:

在findAll接口中,首先通过调用PageRequest中的of方法构造PageRequest对象。of方法接收两个参数:第一个参数是页数,从0开始计数,第二个参数是每页显示的条数

1.4 Spring Data JPA提供的核心接口

1.4.1 Repository接口

Repository<T, ID>:其中T是要查询的表,ID是主键类型

1.4.1.1 方法名称命名方式

方法名称命名查询方式,hibername会根据方法名生成对应的sql语句
注意:方法名称必须要遵循驼峰命名规则

import cn.jzh.entity.User;
import org.springframework.data.repository.Repository;

import java.util.List;

/**
 * Repository接口方法名称命名查询
 */
public interface UserRepository extends Repository<User,Integer> {

    List<User> findByName (String name);

    List<User> findByNameOrAge(String name, Integer age);

    List<User> findByNameLike(String name);
}
1.4.1.2 注解方式

@Query注解查询方式,用这样的方式就不局限于方法名(hibernate根据方法名生成sql)

@Query("from User u where u.name = ?1")
List<User> queryByUseHQL (String name);

 @Modifying
@Query("update User set name =?1 where id =?2")
void updateById(String name, Integer id);

测试

 @Test
    public void queryByUse() {
        List<User> list = dao11.queryByUseHQL("哈哈");
        System.out.println(JSON.toJSON(list));
    }
    @Test
    @Transactional
    @Rollback(false)
    public void updateById() {
        dao11.updateById("张三",1);

    }

当在测试类中直接调用时,需要需要在测试类中添加注解@Transactional,但是@Transactional@Test一起连用会导致事务自动回滚,这时候需要指定事务不回滚@Rollback(false)

1.4.2 CrudRepository接口

CrudRepository<T, ID>:其中T是要查询的表,ID是主键类型
CrudRepository接口,主要是完成一些增删改查的操作,封装了好多接口可以直接使用,当然依旧可以使用注解方式
注意CrudRepository接口继承了Repository接口

public interface UserCrudRepository extends CrudRepository<User,Integer> {
}

测试

 @Test
    public void testCrudAdd(){
        User u = new User();
        u.setName("张无忌");
        u.setAge(20);
        u.setAddress("冰火岛");
        u.setTime(new Date());
        User save = userCrudRepository.save(u);
        System.out.println(save);
    }

1.4.3 PagingAndSortingRepository接口

PagingAndSortingRepository<T, ID>:其中T是要查询的表,ID是主键类型
PagingAndSortingRepository主要是提供了分页与排序的操作
注意PagingAndSortingRepository接口继承了CrudRepository接口

import org.springframework.data.repository.PagingAndSortingRepository;

public interface UserPagingAndSortingRepository  extends PagingAndSortingRepository<User,Integer> {
}

测试使用

//排序操作
@Test
public void pageAndSort(){
    Sort.Order order = new Sort.Order(Sort.Direction.DESC,"id");
    Sort sort = Sort.by(order);
    List<User> all = (List<User>)dao.findAll(sort);
    System.out.println(JSON.toJSON(all));
}
//分页操作
@Test
public void page(){
    PageRequest pageParam = PageRequest.of(0, 1);
    Page<User> page = dao.findAll(pageParam);

    System.out.println("总页数:"+page.getTotalPages());
    System.out.println("总记录数:"+page.getTotalElements());
    System.out.println("查询结果:"+page.getContent());
    //从0开始记,所以加上1
    System.out.println("当前页数:"+(page.getNumber()+1));
    System.out.println("当前记录数:"+page.getNumberOfElements());
    System.out.println("每页记录数:"+page.getSize());
}

1.4.4 JpaRepository接口

该接口继承了PagingAndSortingRepository接口,是开发中最常用的接口
对继承的父接口中的方法的返回值进行适配
具体使用方法参考上面例子

1.4.5 JpaSpecificationExecutor接口

JpaSpecificationExecutor<T>,其中T是要查询的表
JpaSpecificationExecutor接口主要是提供了多条件查询的支持,并且可以在查询中添加分页与排序。
注意JpaSpecificationExecutor是单独存在,完全独立的,一般是和上面接口联合使用

public interface UserJPASpecificationExecutor 
	extends JpaRepository<User, Integer>,JpaSpecificationExecutor<User> {}

测试使用

 @Autowired
    private UserJPASpecificationExecutor executor;
    @Test
    public void jpsExecutorPage1(){

        Specification<User> spec = new Specification<User>(){

            /**
             * 封装了查询条件
             * root:查询对象的属性封装
             * query:封装了要执行的查询中的各个部分信息:select from  order by 等,询哪些字段,排序是什么(主要是把多个查询的条件连系起来)
             * criteriaBuilder:查询条件的构造器,定义不同的查询条件
             * 字段之间是什么关系,如何生成一个查询条件,每一个查询条件都是什么方式
 *                      主要判断关系(和这个字段是相等,大于,小于like等)
             */
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                /**
                 * 参数一:查询属性
                 * 参数二:条件的值
                 */
                List<Predicate> list = new ArrayList<>();
                list.add(cb.equal(root.get("name"), "张三"));

                Predicate[] arr = new Predicate[list.size()];
                Predicate[] predicates = list.toArray(arr);
                return cb.and(predicates);

				或者这样操作
				return cb.and(cb.equal(root.get("name"), "张三"),cb.equal(root.get("name"), "张三"));
            }
        };

        List<User> list = executor.findAll(spec);
        System.out.println(JSON.toJSON(list));

    }

更多推荐