SpringData框架
1、什么是SpringData
SpringData是Spring的一个子项目,它存在的目的是用于简化持久层(数据库访问层)开发,既支持关系型数据库也支持NoSQL数据库的操作。其主要的目标是数据库的访问变得更加便捷。
SpringData支持的非关系型数据库:
-
MongoDB(文档数据库)
-
Neo4j(图形数据库)
-
Redis(键/值存储)
-
HBase
-
ES(搜索库)
SpringData支持的关系型数据库:MySQL、DB2、Oracle等都支持:
-
JDBC:sun公司推出原生的Java操作数据库的规范(JDBC接口编程)
-
JPA:JPA是sun公司给出持久层(ORM层)一种编程规范(不同的ORM框架对JPA都进行具体的实现:Hibernate)。
SpringData的官网: https://spring.io/projects/spring-data
SpringData JPA:致力于减少数据访问层(DAO、Repository)的开发量,作为开发者主要需要做的唯一的事情,根据SpringData JPA定义的接口来书写相关的DAO层的子接口即可,其他的CRUD操作等全部都交给SpringData JPA完成。
2、SpringData JPA快速入门
2.1、搭建SpringData JPA的开发环境
说明环境:使用SpringBoot构建基础环境,使用Maven作为基础的管理。然后搭建一套完成的web环境(SpringMVC、Spring、SpringData、SpringBoot)、使用开发工具是Idea。
2.1.1、使用maven创建项目
2.1.2、导入pom依赖和编写配置
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qubo.springdata</groupId>
<artifactId>springdata-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 引入SpringBoot父工程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<!-- 配置一些jdk、编码等信息 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<!--引入需要的依赖-->
<dependencies>
<!-- 引入springmvc,spring等依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 导入springdata jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
</project>
yml配置文件:
# 服务器的端口号
server:
port: 80
# springdatajap 和数据源的配置
spring:
jpa: # springdata jpa相关的配置
generate-ddl: false
show-sql: true
hibernate:
ddl-auto: none
datasource:
url: jdbc:mysql://127.0.0.1:3306/springdata
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
# 日志
logging:
level:
root: info
org.hibernate.*: debug
编写核心启动类:
package com.qubo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringDataJPAStart {
public static void main(String[] args) {
SpringApplication.run(SpringDataJPAStart.class,args);
}
}
2.2、SpringData JPA的开发步骤
使用SpringDataJPA进行持久层开发的时候一般有4个步骤:
-
配置Spring整合JPA
-
在Spring配置文件中需要配置SpringData
-
定义持久层的接口,需要继承SpringData中提供的Repository接口(一般继承它的子接口JPARepository)
-
如果继承的接口中的方法不够用,可以在自己定义的接口中按照一定的规则去书写方法操作数据库
友情提示:
由于使用SpringBoot构建的项目环境,因此第一步和第二步基本省略了,其实改成注解方式。
2.3、Lombok插件
开发中需要大量书写pojo实体类,虽然IDE能够直接生成get、set、constructor、equals、hashCode、toString等等方法,但是还是比较麻烦,代码比较多,看起来pojo类很臃肿。
使用lombok插件,能够快速的生成get、set、constructor、equals、hashCode、toString等等方法。并且实体类中代码很简洁。
说明:这个插件是帮助我们去在编译的时候,给生成的class文件中添加上述的方法。而不是说真正没有这些方法。源码中没有,但是class文件(字节码)中存在。
注意:需要导入lombok的依赖包的同时,还要给IDE(eclipse、idea)安装插件。
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
2.4、快速完成查询操作
创建表:
CREATE TABLE tb_user(
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
PASSWORD VARCHAR(50),
age INT ,
salary DOUBLE
)
改在核心启动类:
@SpringBootApplication
// 配置持久层接口所在的包
@EnableJpaRepositories(basePackages = "com.qubo.repository")
// 配置pojo所在的包
@EntityScan("com.qubo.pojo")
public class SpringDataJPAStart {
public static void main(String[] args) {
SpringApplication.run(SpringDataJPAStart.class,args);
}
}
编写Controller类:
package com.qubo.controller;
import com.qubo.pojo.User;
import com.qubo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("users")
public List<User> findUsers(){
return this.userService.findUsers();
}
}
编写servcie类:
package com.qubo.service;
import com.qubo.pojo.User;
import com.qubo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> findUsers() {
return this.userRepository.findAll();
}
}
编写repository接口:
package com.qubo.repository;
import com.qubo.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* 在使用SpringDataJPA自定义的接口需要继承Repository接口或者其子接口
* Repository : 它只是一个接口中没有提供方法
* JpaRepository : 它提供CRUD的方法,以及分页,排序等方法
* 接口中泛型:
* 第一个:实体类类型
* 第二个:主键的类型
*/
public interface UserRepository extends JpaRepository<User,Integer> {
}
测试结果:
@Data
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 有参数构造
@ToString
@EqualsAndHashCode
@Getter
@Setter
public class User {
private String username;
private String password;
private Integer age;
private Double salary;
}
3、关于SpringData JPA接口介绍
3.1、Repository接口
@Indexed
public interface Repository<T, ID> {
}
Repository接口是SpringData 的一个核心接口,但是它中不提供任何的方法,类似于Java中标记型接口概念。如果我们编程的时候直接继承这个接口,那么就需要自己在自定义的接口中添加CRUD等方法。
SpringData 提供了一些定义方法的规范,这样可以省略书写SQL过程:
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is , Equals | findByFirstname ,findByFirstnameIs ,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull , Null | findByAge(Is)Null | … where x.age is null |
IsNotNull , NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended % ) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended % ) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in % ) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
package com.qubo.repository;
import com.qubo.pojo.User;
import org.springframework.data.repository.Repository;
import java.util.List;
public interface UserRepository2 extends Repository<User, Integer> {
// select * from tb_user where username = ? and password = ?
public List<User> findByUsernameAndPassword(String username , String password);
}
3.2、CrudRepository接口
CrudRepository:它是Repository接口的子接口,其中主要提供简单的增删改查操作的方法。
public interface CrudRepository<T, ID> extends Repository<T, ID>
CrudRepository细节:
在进行根据主键操作的时候,我们需要给pojo指定主键和主键的策略
3.3、PagingAndSortingRepository接口
PagingAndSortingRepository:它主要在CrudRepository基础上提供分页和排序功能。
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort); // 排序
Page<T> findAll(Pageable pageable); // 分页
}
准备分页对象:
@Data
public class PageResult<T> {
// 设置总页数
private Integer total;
private Integer pages;
private List<T> datas;
}
service中的逻辑代码:
public PageResult<User> getPage(Integer page, Integer size) {
// 设置分页参数
Pageable pageable = PageRequest.of(page , size);
// 执行查询
Page<User> users = this.userRepository.findAll(pageable);
// 需要将数据构建到自定义的分页结果对象中
PageResult<User> pageResult = new PageResult<>();
// 封装分页页面所需的数据
pageResult.setDatas(users.getContent());
// 封装总页数
pageResult.setPages(users.getTotalPages());
// 封装总记录数
pageResult.setTotal(users.getTotalElements());
return pageResult;
}
controller的代码:
@RequestMapping("page")
public PageResult<User> getPage(
@RequestParam(name="page" ,required = true , defaultValue = "0")Integer page,
@RequestParam(name="size" ,required = true , defaultValue = "2")Integer size ){
return this.userService.getPage(page , size);
}
3.4、JpaRepository接口
JpaRepository : 它是我们书写代码的时候真正需要继承的持久层的接口。JpaRepository接口将SpringData JPA提供的所有的常用的功能都继承到了
3.5、JpaSpecificationExecutor接口
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来封装查询的条件。由于Specification又是一个接口,因此经常配合内部类或者使用Lambda表达式完成条件的封装
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
public List<User> findNames(String name) {
// 构建查询的条件 , 使用匿名内部类
Specification spec = new Specification(){
/*
在使用Specification接口构建查询条件的时候,需要复写toPredicate方法
但是toPredicate方法上有三个参数:
Root root:与数据库表对应的实体类对象,就是pojo
CriteriaQuery query:获取Root中的属性名,添加额外的一些查询条件
CriteriaBuilder criteriaBuilder:用来构建出查询条件对象
*/
public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) {
// SELECT * FROM tb_user WHERE username LIKE '%唐%'
Path username = root.get("username"); // 获取到属性名,对应将来就是列名
return criteriaBuilder.like(username , "%"+name+"%");
}
};
// List<User> users = this.userRepository.findAll(spec);
// 上的写法完成可以使用Lambda表达式代替
List<User> users = this.userRepository.findAll( (root , query ,criteriaBuilder )-> criteriaBuilder.like(root.get("username"),"%"+name+"%") );
return users;
}
3.5、总结
使用SpringData JPA的时候,我们需要在持久层书写接口,然后去继承JPARepository接口。这时我们的接口就已经具备了对数据库表的基本的操作了。
比如:增删改查,分页,排序等等。
但是不具备编写一些复杂的条件查询,于是一般建议自定义的接口同时还要去继承JpaSpecificationExecutor 。
public interface UserRepository extends JpaRepository<User,Integer> , JpaSpecificationExecutor<User>
3.6、@Query注解
有时使用SpringData JPA的时候,已经提供的哪些方法无法满足我们的需求,只能自己手写sql来完成查询操作。这时需要就需要在自定义的方法上使用@Query注解来书写sql语句。
3.6.1、数字占位符
// 自定义的方法,不要和接口中已经存在的方法重名,也不要和已经提供的方法命名规范重合
//@Query("select * from tb_user where id = ?")
@Query("select u from User u where id = ?1")
public User findUserById(Integer id);
说明:
-
在接口中自定义的方法上使用@Query注解来实现自定义sql语句,在sql语句中参数使用?数字,数字从1往后排。要求数字和方法上的参数一一对应
-
SQL中表名的位置不能书写表名,需要书写的是表对应的POJO的名字。并且需要给起别名
-
select后面千万不要使用 * 号,使用别名代替
3.6.2、使用参数占位符
上面使用的数字占位符,参数多了非常容易搞错。可以使用参数占位符代替。但是需要在方法上使用@Param注解声明参数占位符名称。
@Query("select u from User u where u.username = :username and u.password = :password")
public User findUserByNameAndPwd(String username , String password);
注意:
参数占位符需要还是用:号,后面跟着参数的名称。
有时我们希望真正书写的是SQL语句,而不是上面这种写法。其实严格意义上来讲不算合法的SQL。
@Query(value="select * from tb_user where id = ?1 and age = ?2" , nativeQuery = true)
public User findUserByIdAndAge(Integer id , Integer age);
3.7、@Modifying注解
使用SpringData JPA的时候对数据库进行查询之外,还有增删改的操作。一般对数据库的增删改的操作,都会配合事务一起操作。
如果需要自定义SQL,还是增删改的操作,需要两个注解一起使用。
@Modifying
@Query("update User set username=:name where id =:id")
public Integer updateUserName(String name , Integer id);
说明:@Modifying 注解来说明是对数据库进行非查询操作
public Integer updateUserName(String username , Integer id){
return this.userRepository.updateUserName(username , id);
}
执行程序发生下面的异常:
上面的异常是因为SpringData JPA规定,在对数据库进行增删改的时候,必须放在事务中。
@Transactional
public Integer updateUserName(String username , Integer id){
return this.userRepository.updateUserName(username , id);
}
4、多表的操作
多表的操作:一对一、一对多、多对多
4.1、一对一查询操作
假设一对一的场景:用户和住址,一般一个用户只能拥有一个地址(户籍归属地)。
CREATE TABLE tb_addr(
id INT PRIMARY KEY AUTO_INCREMENT,
address VARCHAR(100),
uid INT
)
SpringData JPA的一对一查询:提供@oneToOne注解,标注一对一的关系。同时还需要使用@JoinColumn注解声明关联关系。
package com.qubo.pojo;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name="tb_addr")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String address;
//private Integer uid;
// 在一对一中,需要在某个pojo中添加另外一个实体的引用
@OneToOne
@JoinColumn(name="uid" , referencedColumnName = "id")
private User user;
}
说明:
-
@OneToOne : 来指定当前pojo中的属性(实体引用)和另外类之间是一个一对一关系
-
@JoinColumn(name="uid" , referencedColumnName = "id") : 一对一中的主外键关系。name属性书写的是外键列名,referencedColumnName属性的是外键引用的主键的列名。
还需要编写关于tb_addr 表查询的Repository接口:
public interface AddressRepository extends JpaRepository<Address , Integer> {
}
关于service类:
@Service
public class AddressService {
@Autowired
private AddressRepository addressRepository;
public Address findAddrById(Integer id){
Address address = this.addressRepository.findById(id).get();
return address;
}
}
4.2、一对多的查询操作
假设一对多业务场景:一个用户可以编著多本书(作者和读书)。通过User对应的多个Book。
CREATE TABLE tb_book(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(50),
price DOUBLE,
pages INT,
uid INT
)
最终的查询目标:通过用户的id查询用户的数据,同时还要找到对应的著作信息
一对多:需要在一的一方pojo中添加集合,关联多的一方的数据。
// 多的一方的实体类
@Data
@Entity
@Table(name = "tb_book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private Double price;
private Integer uid;
}
@Data
@Entity
@Table(name="tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
private Integer age;
private Double salary;
// 添加对应的多的一方的引用
@OneToMany
@JoinColumn(name="uid",referencedColumnName = "id")
private List<Book> books;
}
编写Repository接口
public interface BookRepository extends JpaRepository<Book , Integer> {
// 需要提供一个方法,可以通过外键查询book数据
public Book findByUid(Integer uid);
}
4.3、多对多查询
假设业务场景:订单和商品关系,一个订单中可以拥有多个商品,某类商品可以被添加到不同的订单中。
CREATE TABLE tb_order(
id INT PRIMARY KEY AUTO_INCREMENT,
uid INT,
order_number VARCHAR(100)
);
CREATE TABLE tb_item(
id INT PRIMARY KEY AUTO_INCREMENT,
item_name VARCHAR(100),
item_price DOUBLE,
item_detail VARCHAR(200)
)
CREATE TABLE tb_order_detail(
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT,
item_id INT,
total_price DOUBLE
)
@Entity
@Data
@Table(name="tb_order")
public class Order {
private Integer id;
private Integer uid;
private String orderNumber;
@ManyToMany // 声明是多对多关系
@JoinTable(
name="tb_order_detail" , // 多对多的中间表的名称
joinColumns = { @JoinColumn( name="order_id" ,referencedColumnName = "id")},
inverseJoinColumns = { @JoinColumn( name="item_id" , referencedColumnName = "id") } )
private List<Item> items;
}
@Entity
@Table(name = "tb_item")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Getter@Setter
private Integer id;
@Getter@Setter
private String itemName;
@Getter@Setter
private Double itemPrice;
@Getter@Setter
private String itemDetail;
@ManyToMany(targetEntity = Order.class , mappedBy = "items")
private List<Order> orders;
}