JPA了解与使用
- JPA相关介绍
- JPA介绍
- Spring Data JPA介绍
- 快速上手
- 自定义查询(简单操作)
- 自定义查询(复杂操作)
- 分页查询
- 多表查询
- 级联映射
JPA相关介绍
JPA介绍
JPA(Java Persistence API)是 Sun 官方提出的 Java 持久化规范。它为 Java 开发人员提供 了一种对象 / 关联映射工具来管理 Java 应用中的关系数据。它的出现主要是为了简化现有的 持久化开发工作和整合 ORM 技术,结束现在 Hibernate、TopLink、JDO 等 ORM 框架各自为 营的局面。
注意: JPA 是一套规范,不是一套产品,那么像 Hibernate、TopLink、JDO 它们是一套产 品,如果说这些产品实现了这个 JPA 规范,那么我们就可以称他们为 JPA 的实现产品。
Spring Data JPA介绍
- Spring Data 是 Spring 的一个子项目,用于简化数据库访问,包括 NoSQL 非关系型数据库, 另外还包括对关系型数据库的访问支持。Spring Data 使我们可以快速简单地使用普通的数据访 问技术及新的数据访问技术,Spring Data 会让数据的访问变得更加方便。
- Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架, 可以让开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增、删、改、查等在 内的常用功能,且易于扩展,学习并使用 Spring Data JPA 可以极大提高开发效率。Spring Data JPA 其实就是 Spring 基于 Hibernate 之上构建的 JPA 使用解决方案,方便在 Spring Boot 项目中 使用 JPA 技术。
- Spring Data JPA 让我们解脱了 DAO 层的操作,基本上所有 CRUD 都可以依赖于它实现。
快速上手
- 创建项目
选择创建Spring Initializr项目,然后选择如下几个: - 导入依赖(选择了图中的几个之后就会自动生成在pom中,只需更改版本即可)
<!-- 导入该依赖可以 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql依赖,版本号为自己的mysql数据库版本 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
<!-- 导入该依赖后可使用lombok插件提供的注解和方法等 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 添加配置文件
# 数据源
spring.datasource.url=jdbc:mysql://localhost:3306/boot?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# mysql方言
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# 是否在控制台输出sql语句
spring.jpa.show-sql=true
# 格式化sql语句,让sql语句更加清晰易懂
spring.jpa.properties.hibernate.format_sql=true
- 创建实体类、DAO层和业务逻辑层等
实体类
注解解析:
- @Entity ( name = “数据库表名” )
必须要写! 用来标注一个数据库对应的实体,数据库中创 建的表名默认和类名一致。 其中name可以省略,因为使用这种方法的话可能导致出错!!! - @Table ( name=“数据库表名”,catalog="",schema="" )
可以不写!一般配合@Entity注解使用, 用来标注一个数据库对应的实体,数据库中创 建的表名默认和类名一致。只能标注在实体类上! - @Data
可以不写!为实体类提供了get和set等方法,不包括有参方法和无参方法! - @Id
必须要写! 定义了映射到数据库表的主键的属性 ,一个实体类只能有一个属性被映射为主键!!! - @ GeneratedValue ( strategy=GenerationType,generator="" )
可以不写!strategy: 表示主键 生成策略,有 AUTO、INDENTITY、SEQUENCE 和 TABLE 4 种,分别表示让ORM 框 架自动选择,这一章先只用到IDENTITY。generator: 表示主键生成器的名称。 - @Column ( name = “数据库字段名”, nullable = false,unique=false ,length=32 )
可以不写! 描述了 数据库表中该字段的详细定义,这对于根据 JPA 注解生成数据库表结构的工具。 name:表示 表示数据库表中该字段的名称,默认情形属性名称一致 ;
nullable : 表示该字段是否允许为 null,默认为 true;
unique: 表示该字段是否是唯一标识,默认为 false;
length: 表示该字段的大小,仅对 String 类型的字段有效;
DAO层
JPA中dao层可以继承 JpaRepository<T,ID> 类 和 JpaSpecificationExecutor 类,这两个类都提供了很多的常用方法,比如常用的增删改查还有分页等。
// Dao层
public interface UserRepository extends JpaRepository<实体类类型,主键类型>, JpaSpecificat
ionExecutor<实体类类型> {
// 还可自定义方法进行使用
}
如何打开类图: 在当前类的大括号内的区域右键单击,选择 Diagrams | Show Diagram 选项,即可打开类图!打开后效果如图所示:
通过上图我们发现 JpaRepository 继 承 PagingAndSortingRepository 和 QueryByExampleExecutor, PagingAndSortingRepository 类主要负责排序和分页内容;
QueryByExampleExecutor 提供了很多示例的查询方法;
因此,继承 JpaRepository 的 Repository 会自动拥有示例查询方法和排序、分页功能。
继续往上查看 我们发现 PagingAndSortingRepository 又继承了 CrudRepository 。
CrudRepository 内置了我们最常用的增、删、改、查的方法;
Service(业务逻辑层)
该层暂时只需创建方法然后调用dao层中的方法返回数据即可。
测试层(以下方法都为自带方法,自动生成SQL)
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentTests {
@Resource
private StudentDao studentDao;
//新增
@Test
public void testInsertStudent(){
Student student = new Student("xixi","1234","女",2);
//save:该方法有id值时为修改,没有id值时为新增,返回对象类型
Student row = studentDao.save(student);
if (row != null){
System.err.println("==============>新增成功!");
}else{
System.err.println("==============>新增失败!");
}
}
//删除
@Test
public void testDeleteStudent(){
try{
studentDao.deleteById(6); //该方法返回void类型
System.err.println("=============》删除成功!");
}catch (Exception e){
System.err.println("=============》删除失败!");
e.printStackTrace();
}
}
//修改
@Test
public void testUpdateStudent(){
Student student = new Student();
student.setStuId(4);
student.setStuName("hhh");
student.setStuPassword("1234");
student.setStuSex("男");
student.setStuGrade(1);
//save:该方法有id值时为修改,没有id值时为新增,返回对象类型
Student row = studentDao.save(student);
if(row != null){
System.err.println("=================>修改成功!");
}else {
System.err.println("=================>修改失败!");
}
}
//查询所有
@Test
public void testSelectStudent(){
List<Student> list = studentDao.findAll(); //该方法返回List集合类型
for (Student student:list){
System.err.println(student.toString());
}
}
}
自定义查询(简单操作)
Spring Data JPA 还可以根据接口方法名来实现数据库操作,主要的语法是 findXXBy、 readXXBy、queryXXBy、countXXBy、getXXBy 后面跟属性名称,利用这个功能仅需要在定义的 Repository 中添加对应的方法名即可,使用时 Spring Boot 会自动帮我们自动编写sql然后实现。
注: 这种方式多用于查询,不用于增删改!!!
示例:
// 根据id查询用户
public User findByUsrId(Integer usrId);
// 用户登录
public User findByUsrNameAndUsrPassword(String usrName,String usrPassword);
以上示例方法命名规范调用使用时会自动生成动态SQL语句进行实现!
自定义查询(复杂操作)
使用 Spring Data 大部分的查询都可以根据方法名定义的方式来实现,但是由于某些原因 必须使用自定义的 QL 来查询,Spring Data 也可以完美支持。在dao层方法上加入@Query注解来编写HQL或SQL进行自定义操作。
HQL示例(推荐使用):
// 根据id查询用户
@Query(value = "select u from User u where u.usrId=:usrId")
public User SelectUserByUsrId(@Param("usrId") Integer usrId);
//查询单独几列时需要在HQL语句中 new map 或 new User。比如:
// 1. 按用户名进行查询(使用new map可返回 List 类型或 Map 类型)
@Query(value = "select new map(u.usrId,u.usrName) from User u where u.usrName=:usrName")
public Map SelectByName(@Param("usrName") String usrName);
// 2. 按照id进行查询(使用new User需要在User实体类中创建对应的有参方法)
@Query(value = "select new User(u.usrId,u.usrName) from User u where u.usrId=:id")
public User SelectById(@Param("id") Long id);
/*
如涉及到删除和修改需要加上 @Modifying,也可以根据需要添加 @Transactional 对事务的支持、操作超时设置等。
*/
@Transactional(timeout = 10)
@Modifying
@Query("update User s set s.usrName=:name where s.usrId=id")
public int UpdateByUsrId(@Param("name") String Name,@Param("id") Long Id);
SQL示例(不推荐使用):
// 根据id删除用户
@Query(value = "delete from user where usr_id=:usrId",nativeQuery = true)
public User DeleteUserByUsrId(@Param("usrId") Integer usrId);
// 根据id查询用户
@Query(value = "select * from User where usr_id=:usrId",nativeQuery = true)
public User SelectUserByUsrId(@Param("usrId") Integer usrId);
/**
* 因为sql语句和以前的都是一样的,这里就不一一举例了
*/
注解解析:
@Query(value="",nativeQuery=true)注解:
- 赋值可以使用?1、?2…的写法(不推荐)或者使用别名(@Param(" ")的方式(推荐),nativeQuery等 于true表示这是原生的SQL。
- 编写SQL语句:可以使用 * 号。
select * from sys_user where usr_id=:id
select * from sys_user where usr_id=?1 - 编写HQL语句:不能使用 * 号,且必须要取别名(思想)!
select u from User u where u.usrName=:name
select u from User u where u.usrName=?1 - SQL是根据数据库表的表名和字段名进行操作,而HQL是实体类的类名和属性名来进行操作。
- 编写SQL语句时需要加上 nativeQuery = true !
分页查询
Spring Data JPA 已经帮我们内置了分页功能,在查询的方法中,需要传入参数 Pageable, 当查询中有多个参数的时候 Pageable 建议作为最后一个参数传入。
Pageable 是 Spring 封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则, Page 是 Spring 封装的分页对象,封装了总页数、分页数据等。返回对象除使用 Page 外,还可以使用 Slice 作为返回值。
Page 和 Slice 的区别:
- Page 接口继承自 Slice 接口,而 Slice 继承自 Iterable 接口。
- Page 接口扩展了 Slice 接口,添加了获取总页数和元素总数量的方法,因此,返 回 Page 接口时,必须执行两条 SQL,一条复杂查询分页数据,另一条负责统计数 据数量。
- 返回 Slice 结果时,查询的 SQL 只会有查询分页数据这一条,不统计数据数量。
- 用途不一样:Slice 不需要知道总页数、总数据量,只需要知道是否有下一页、上 一页,是否是首页、尾页等,比如前端滑动加载一页可用;而 Page 知道总页数、 总数据量,可以用于展示具体的页数信息,比如后台分页查询。
示例:
/**
* Dao层
* 分页查询:
* Spring Data JPA 已经帮我们内置了分页功能,在查询的方法中,需要传入参数 Pageable,
* 当查询中有多个参数的时候 Pageable 建议作为最后一个参数传入。
*/
public interface UserDao extends JpaRepository<User,Long> {
//分页查询
@Query("select u from User u where u.usrRoleId=:roleId")
public Page<User> findByUsrRoleId(@Param("roleId") Long roleId, Pageable pageable);
}
@Resource
private UserDao userDao;
@Test
public void testPage(){
int page = 0; //第几页,一般从第0页开始
int size = 2; //每页显示多少条数据
//数据排序(默认为ASC(升序)从小到大,DESC为(降序)从大到小),这里按照id进行降序操作
Sort sort = Sort.by(Sort.Direction.DESC, "usrId");
// 控制分页的辅助类,可以设置页码、每页的数据条数、排序等
Pageable pageable = PageRequest.of(page, size, sort);
Page<User> userPage = userDao.findByUsrRoleId(2L, pageable);
System.err.println("总记录数 --------> " + userPage.getTotalElements());
System.err.println("总页数 ----------> " + userPage.getTotalPages());
System.err.println("当前页数 --------> " + userPage.getNumber());
System.err.println("每页记录数 ------> " + userPage.getNumberOfElements());
//获得查询记录
System.err.println("当前记录: ------> ");
for (User user : userPage.getContent()) {
System.out.println("usr_name=" + user.getUsrName());
}
}
复杂操作:
- 我们可以通过 AND 或者 OR 等连接词来不断拼接属性来构建多条件查询,但如果参数大 于 6 个时,方法名就会变得非常的长,并且还不能解决动态多条件查询的场景。这种时候就需要给大家介绍另外一个利器JpaSpecificationExecutor 了。
- JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景。 Spring Data JPA 为我们提供了 JpaSpecificationExecutor 接口,只要简单实现 toPredicate 方法就可以实现复杂的查询。
- 在前面的那些方法都只需要使用JpaRepository<T, ID>即可使用对应方法,而当出现参数过大的情况下就可以使用JpaSpecificationExecutor 接口。
示例:
/**
* Dao层
*/
public interface UserDaoMapper extends JpaRepository<User,Long>,JpaSpecificationExecutor<User> {
//使用复杂查询实现分页,dao层无需写任何方法
}
省略业务逻辑层代码…
/**
* 测试类
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserTests {
@Resource
private UserDaoMapper userDaoMapper;
@Test //复杂操作;根据用户名进行模糊查询(使用提供的方法,自定义方法尚不知晓)
public void testSelectByUsrNameLike(){
//创建map对象,用于提供条件
Map map = new HashMap();
map.put("Name","lao");
List<User> list = userDaoMapper.findAll(new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
//创建一个集合用于存储条件
List<Predicate> predicates = new ArrayList<>();
//判断是否传入有值,生成动态SQL
if (map.get("Name") != null){
predicates.add(criteriaBuilder.like(root.get("usrName"),"%"+map.get("Name")+"%"));
}
//条件多的话可以进行使用if进行判断
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
});
//打印输出
for (User user : list){
System.err.println(user.toString());
}
}
}
多表查询
多表查询在 Spring Data JPA 中有两种实现方式,第一种是创建一个结果集的接口来接收多表联接查询后的结果,第二种是利用 JPA 的关联映射来实现。
1. 创建结果集接口实现
/**
* 此接口为结构集接口,写在dao层中
* 可以将需要的字段全部写在这里
*/
public interface UserAndRoleInfo {
public Long usrId();
public String usrName();
public String usrPassword();
public Long usrRoleId();
public Integer usrFlag();
//需要显示的其他表的字段(Role表的name)
public String roleName();
}
/**
* UserDao接口方法
*/
//多表联查(必须要给字段名取别名); 根据用户名进行模糊查询
@Query(value = "select new map(u.usrId as usrId,u.usrName as usrName,u.usrPassword as usrPassword,r.roleName as roleName) from User u,Role r where u.usrRoleId=r.roleId and u.usrName like :usrName")
public List<UserAndRoleInfo> InnerJoin(@Param("usrName") String usrName);
省略业务逻辑层代码…
/**
* 测试类
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserTests {
//多表联查;根据用户名进行模糊查询,输出id、name、pasword和rolename列
@Test
public void testInnerJoin() {
//返回接口类型则只能返回一条数据,而返回集合类型则可返回多条数据,模糊查询的值必须要加上%
List<UserAndRoleInfo> list = userService.getInnerJoin("%x%");
for (UserAndRoleInfo userAndRoleInfo : list) {
System.err.println("用户信息:" + userAndRoleInfo.toString());
}
}
}
2. 关系映射实现
注: 使用关系映射的方式实现多表联查会导致lombok提供的注解失效,所以在实体类层建议大家自行编写get和set方法等!
单向多对一关联(我这里重新创两个表来使用)
/**
* 实现单向多对一关联
* 实体类UserSeveralForOne
*/
@Entity
@Table(name = "sys_user")
public class UserSeveralForOne implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "usr_id")
private Long usrId;
@Column(name = "usr_name")
private String usrName;
@Column(name = "usr_password")
private String usrPassword;
/**
* @ManyToOne(targetEntity=实体类.class):映射一对多的关联关系;targetEntity表示关联实体类型
* @JoinColumn(name="字段名"):映射外键字段
*/
@ManyToOne(targetEntity = RoleSeveralForOne.class,fetch = FetchType.EAGER)
@JoinColumn(name = "usr_role_id")
private RoleSeveralForOne role;
@Column(name = "usr_flag")
private Integer usrFlag;
/**
* 省略get和ser方法
* 省略有参和无参方法
* 省略toString方法
*/
}
/**
* 实现单向多对一关联
* 实体类RoleSeveralForOne
*/
@Entity
@Table(name = "sys_role")
public class RoleSeveralForOne implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
@Column(name = "role_desc")
private String roleDesc;
@Column(name = "role_flag")
private Integer roleFlag;
/**
* 省略get和ser方法
* 省略有参和无参方法
* 省略toString方法
*/
}
/**
* Dao层方法,接口需要继承JpaRepository<T,ID>
*/
//单向多对一关联,根据roleId查询数据
@Query(value = "select new list(u.usrName,u.usrPassword,r.roleName) from UserSeveralForOne u,RoleSeveralForOne r where u.role=r.roleId and u.role=:roleId")
public List SelectByUsrRoleId(@Param("roleId") RoleSeveralForOne roleId);
省略业务逻辑层代码…
/**
* 测试类
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserSeveralForOneTests {
@Resource
private UserSeveralForOneDao userSeveralForOneDao;
//单项多对一关联查询
@Test
public void testSelectByUsrRoleId(){
RoleSeveralForOne role = new RoleSeveralForOne();
role.setRoleId(1L);
List<UserSeveralForOne> list = userSeveralForOneDao.SelectByUsrRoleId(role);
/**
* 这里直接打印,原因:如果使用for循环遍历输出则会报类型异常,因为这是两个实体类,类型不一至
*/
System.err.println(list.toString());
}
}
级联映射
/**
* 实体类RoleSeveralForOne
* 在该实体类中添加Set<T>
*/
@Entity
@Table(name = "sys_role")
public class RoleSeveralForOne implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
@Column(name = "role_desc")
private String roleDesc;
@Column(name = "role_flag")
private Integer roleFlag;
/**
* targetEntity 属性表示关联的实体类型
* fetch 属性表示加载策略,EAGER表示立即加载,LAZY表示延迟加载(默认)
* cascade 属性表示级联操作,PERSIST 表示级联持久化(保存)操作,REMOVE 级联删除,ALL 级联所有操作。
* 注:两个实体类的加载策略最好一致!
*/
@OneToMany(targetEntity = UserSeveralForOne.class,fetch = FetchType.EAGER,cascade = CascadeType.ALL,mappedBy = "role")
private Set<UserSeveralForOne> users = new HashSet<>();
/**
* 省略get和ser方法
* 省略有参和无参方法
* 省略toString方法
*/
}
这里为了简便,我们就直接使用Da接口继承的父类提供的方法进行对应操作。
- 新增
//级联新增
@Test
public void testInsert(){
RoleSeveralForOne role = new RoleSeveralForOne();
role.setRoleName("程序员");
role.setRoleDesc("java");
role.setRoleFlag(32);
UserSeveralForOne user = new UserSeveralForOne();
user.setUsrName("cxy");
user.setUsrPassword("abab");
user.setRole(role);
user.setUsrFlag(12);
//将要插入的值存进Set<T>接口中,要插入多条时继续使用add()添加即可
role.getUsers().add(user);
//调用save()方法,有id值是为修改,无id值时为新增
RoleSeveralForOne roleSeveralForOne = roleSeveralForOneDao.save(role);
if (roleSeveralForOne != null){
System.err.println("新增成功!");
}else {
System.err.println("新增失败!");
}
}
新增方法(无id值):S save(S var1);
- 删除
//级联删除
@Test
public void testDelete(){
try {
roleSeveralForOneDao.deleteById(4L);
System.err.println("删除成功!");
}catch (Exception e){
System.err.println("删除失败!");
}
}
删除方法(根据id删除):void deleteById(ID var1);
- 修改
//级联修改
@Test
public void testInsert(){
RoleSeveralForOne role = new RoleSeveralForOne();
role.setRoleId(5L);
role.setRoleName("java程序员");
role.setRoleDesc("javacxy");
role.setRoleFlag(24);
UserSeveralForOne user = new UserSeveralForOne();
user.setUsrId(23L); //usrId,根据这个id来进行修改
user.setUsrName("javaCxy");
user.setUsrPassword("1234");
user.setRole(role);
user.setUsrFlag(25);
//将要修改的值存进Set<T>中,要修改多条时继续使用add()添加即可
role.getUsers().add(user);
//调用save()方法,有id值是为修改,无id值时为新增
RoleSeveralForOne roleSeveralForOne = roleSeveralForOneDao.save(role);
if (roleSeveralForOne != null){
System.err.println("修改成功!");
}else {
System.err.println("修改失败!");
}
}
修改方法(有id值):S save(S var1);