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 + '}'; }
-
这样就能正常实现查询操作
-