springData-JPA学习笔记

182 阅读6分钟

JPA

jpa是一套spring date的规范,源于orm思想

orm思想:每个实体类对应一张数据库表,操作实体类就相当于操作数据库,将实体类与数据库表映射

orm思想衍生了hibernate框架,是对orm思想的实现,但是除了hibernate框架之外,还有很多实现了orm思想的框架,重复学习会消耗大量的时间,所以出现了jpa,jpa是对orm思想框架的一个整合,定义了一个规范,降低了学习成本。

jpa环境搭建

  • 导入jpa的启动器依赖
 		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
  • 在配置文件中配置jpa的相关参数
## jpa相关配置
## jpa连接的数据库类型
spring.jpa.database=MYSQL
## jpa的建表策略
spring.jpa.hibernate.ddl-auto=create-drop
## 格式化输出
spring.jackson.serialization.indent_output=true
## 命名策略,此策略表示:表名,字段为小写,当有大写字母的时候会转换为分隔符号“_”
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
## 格式化日志中的sql语句
spring.jpa.properties.hibernate.format_sql=true
## 设置打印的日志级别的debug
logging.level.org.hibernate.SQL=debug
## 在日志中显示sql语句
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace

jpa的核心接口

Repository<T,ID>

提供了基于方法命名的查询方式(顶层接口)

public interface BookRepository extends JpaRepository<Book, Integer>, JpaSpecificationExecutor<SecurityProperties.User> {//这个jpaRepository接口继承了CrudRepository接口
    Book findByIdAndBookName(Integer id,String name);//这个方法会根据id和bookName在Book所对应的表中进行查询
}

方法命名规范:

  • 方法名以findBy开头
  • 后面跟上实体类的类名(可省略)
  • 后面跟上进行查询的条件
  • 多条件之间用And连接

实现了上述规范之后,jpa会根据参数的顺序进行条件映射,从而实现查询方法

提供了基于@Query注解的查询与更新

@Query("from Book where bookName=?")
    List<Book> queryMyBook(String bookName);

此注解中value值默认是写HQL语句,根据对象查询,如果要写sql语句,则需要加上==nativeQuery=true==属性,表明此value是原生sql语句

@Query("update Book set name=? where id=?")
@Modifying//表明这是一个更新操作,和squry一起使用实现update
void updateBook(String name,Integer id);

CrudRepository接口

此接口继承了JpaRepository<T,ID>接口,主要完成一些crud操作。

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S var1);//如果表中有数据,则执行update,没有则执行insert
    <S extends T> Iterable<S> saveAll(Iterable<S> var1);
    Optional<T> findById(ID var1);
    boolean existsById(ID var1);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> var1);
    long count();
    void deleteById(ID var1);
    void delete(T var1);
    void deleteAll(Iterable<? extends T> var1);
    void deleteAll();
}

这里的增删改方法都加入了事务管理


PagingAndSortingRepository接口<T,ID>

此接口继承了CrudRepository接口,主要添加了一些分页和排序的操作

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort var1);//排序
    Page<T> findAll(Pageable var1);//分页
}

此接口中的两个方法只能对所有的数据进行排序和分页,没有定义带条件的排序和分页方法

  • 排序方法

Direction:各种排序的枚举类

Order对象:排序规则

//order 定义了排序规则
Order order=new Order(Direction.Desc,"id");//根据id降序

Sort对象:封装了排序规则的对象

Sort sort=new Sort(order..);//可以添加多个排序规则
Class obj=(Class)**RepositoryPagingAndSorting.findAll(sort);//返回的是Iterable<T>类型,需要强转
  • 分页方法

Pageable:封装了分页的参数,当前页和每页显示的条数,当前页从0开始

Pageable pageable=new PageRequest(0,2);//PageRequest来指定两个参数
Page<Class> page = **RepositoryPagingAndSorting.findAll(pageable);

JpaRepository<T,ID>接口

此方法继承了PagingAndSortingRepository<T, ID>接口,主要对父类接口中方法的返回值进行了适配,另外还添加了一些新的抽象方法

例如:

//这是JpaRepository的findAll方法,返回值是list
@Override
List<T> findAll(Sort sort);

//这是PagingAndSortingRepository的findAll方法,返回值是Iterable<T>
Iterable<T> findAll(Sort var1);

JpaSpecificationExecutor接口

此接口提供了多条件查询,并且在条件查询中加入分页和排序

==注意==:此接口就是一个顶层接口,没有任何父接口

public interface JpaSpecificationExecutor<T> {
    Optional<T> findOne(@Nullable Specification<T> spec);
	List<T> findAll(@Nullable Specification<T> spec);
	Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
	List<T> findAll(@Nullable Specification<T> spec, Sort sort);
	long count(@Nullable Specification<T> spec);
}

此接口的所有方法都接收Specification类型的参数

Specification接口中定义了复杂查询的连接方法和查询条件的构建方法

public interface Specification<T> extends Serializable {
    static <T> Specification<T> not(@Nullable Specification<T> spec){}
    static <T> Specification<T> where(@Nullable Specification<T> spec) {}
	default Specification<T> and(@Nullable Specification<T> other){}
	default Specification<T> or(@Nullable Specification<T> other) {}
	Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
  • Specification的前四个方法的操作都是对整个where表达式的操作,效果等同于将表达式作为一个整体来执行方法
  • 上面四个方法都是条件的连接方法,最后一个toPredicate方法是对查询条件的构建

Root:存放实体类的对象

CriteriaQuery<?>:查询语句的构建

CriteriaBuilder:查询条件表达式的构建,包含各种聚合函数


表之间的关系映射

表与表之间可能存在三种关系:一对一,一对多,多对多

在jpa中表与表之间的外键关系不是由数据库维护的,而是由jpa通过索引来维护的

表之间的绑定有单向绑定和双向绑定,单向绑定可以从主表去操作从表,而双向绑定可以实现互相操作

一对多关系

  • 双向一对多:

    • 主表

    • public class Book {
          @OneToMany(mappedBy = "book",cascade = CascadeType.PERSIST)
          private List<Author> authorList = new ArrayList<>();
      
    • 从表

    • public class Author {
      	@ManyToOne
          @JoinColumn(name = "bookId")
          private Book book;
      

@JoinColumn注解写在外键端,用于指定外键的名称,和@ManyToOne连用

  • 一对多关联添加

    • 测试代码

    • @Test
          public void testSave(){
              //建author
              Author author=new Author();
              author.setAuthorName("张三");
      
              //建book
              Book book = new Book();
              book.setBookDesc("good!");
              book.setBookName("head first");
              book.setCreatedTime(new Date());
      
              //建立关系
              book.getAuthorList().add(author);
              author.setBook(book);
              bookRepository.save(book);
          }
      

      这里由于是添加book,级联添加author,所以要在book端加上cascade属性,并且设置为CascadeType.PERSIST

      • ALL:等同于cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH},意为所有的操作都为级联
      • PERSIST:将Insert操作设置为级联
      • MERGE:将update操作设置为级联
      • REFRESH:当两个update并发进行时,后执行的那个需要刷新获取最新数据之后才能执行update操作
      • REMOVE:将delete操作设置为级联
      • DETACH:删除关联,当有这个权限,就会撤销所有的外键关联,就可以删除有外键的实体

多对多关系

  • 多对多

    • 表1

    • public class Book {
          @ManyToMany(cascade = CascadeType.PERSIST)
          @JoinTable(name = "t_book_author",joinColumns = @JoinColumn(name = "book_id"),inverseJoinColumns = @JoinColumn(name = "author_id"))
          @ApiModelProperty("此书的作者")
          private List<Author> authorList = new ArrayList<>();
      
    • 表2

    • public class Author {
          @ManyToMany(mappedBy = "authorList")
          @ApiModelProperty("此作者所写的书")
          private List<Book> bookList;
      

在多对多关系中,需要用到@JoinTable注解,来指定中间表的名称,以及其中两个外键的名称

  • name::会根据这个属性名创建一个中间表
  • joinColumns:这个属性中使用@JoinColumn来指定本id在中间表所对应的外键名称
  • InverseJoinColumns:这个属性中使用@JoinColumn来指定表2的id在中间表所对应的外键名称

在另一张表中加上@ManyToMany注解并且指定关系的维护方mappedBy

  • 测试

    • //创建book
      Book book = new Book();
      book.setBookName("mybook2");
      book.setBookDesc("very good22");
      
      //创建author
      Author author1=new Author();
      author1.setAuthorName("jack2");
      
      Author author2=new Author();
      author2.setAuthorName("rose2");
      
      //设置关联
      book.getAuthorList().add(author1);
      book.getAuthorList().add(author2);
      
      author1.getBookList().add(book);
      author2.getBookList().add(book);
      
      //级联保存
      bookRepository.save(book);
      
  • ==踩坑日记:==

    ​ 今天在jpa的多表关系中,级联的添加操作都很顺利,直到级联的查询操作时,先是遇到了==LazyInitializationExceptions==的==no session==异常,此异常是由于在junit环境下缺乏对事务的控制,在需要使用获取session时session已经关闭,从而造成nosession异常

    ​ 通过在方法上加上@Transactional注解解决此异常

    ​ 之后再进行查询测试,出现了栈溢出异常,以为是多表关系定义错了,最后发现是Lombok的@ToString的问题(其实也不是他的问题),主要是由于忽略了这个toString()方法,由于懒加载是在使用到此实体类对象的时候触发加载条件,但是由于toString并没有使用到实体类,所以不会触发延迟加载的条件,会在控制台死循环调用查询方法,直到栈溢出(不只是toString方法会使用到实体类,hashCode,Equals等方法都会触发死循环查询,所以尽量少用双向关联)

    • 问题:由于Lombok的@Data注解默认生成的toString方法是包含所有的成员属性,而我们的多表关系默认是延迟加载的,而且主表的toString方法并不会触发从表的关联查询,所以会一直循环调用toString方法,直到栈溢出。

    • 解决:覆盖toString方法,不显示关联表的实体类集合

    • toString方法覆写

      • public class Author {
            private Integer id;
            private String authorName;
            private List<Book> bookList=new ArrayList<>();//toString中屏蔽此成员
            
            public String toString() {
                    return "Author{" +
                            "id=" + id +
                            ", authorName='" + authorName + '\'' +
                            '}';
            }
        }
        
        public class Book {
            private Integer id;
            private String bookName;
            private String bookDesc;
            private Date createdTime;
            private List<Author> authorList = new ArrayList<>();//toString中屏蔽此成员
            
            public String toString() {
                    return "Book{" +
                            "id=" + id +
                            ", bookName='" + bookName + '\'' +
                            ", bookDesc='" + bookDesc + '\'' +
                            ", createdTime=" + createdTime +
                            '}';
            }
        

    这样就能正常实现查询操作