1 Spring Data JPA
1.1 简介
1.1.1 JPA
JPA
(Java 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 JPA
是Spring 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
,命名以@Table
的name
的value
- 没有指定
-
@Id
:所有的实体类都要有的主键,@Id
注解表示该属性是一个主键 -
@GneeratedValue
:注解表示主键自动生成,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));
}
网友评论