浅谈JPA

一、关于JPA

 JPA全称Java Persistence API,是Sun官方在JDK5.0后提出的Java持久化规范(JSR 338),这些类存在于java.persistence包中。JPA的出现主要是为了简化持久层开发以及整合ORM实体映射技术,结束Hibernate、TopLink、ORM各自为营的局面。


 JPA是吸收现有ORM架构的基础再发展,它易于使用,伸缩性强。总的来说,其具有以下三个特点:

  • ORM映射元数据
    支持xml和注解两种形式,元数据描述对象和表之间的映射关系。
  • API
    操作实体对象来进行数据库CRUD。
  • 查询语言
    通过面向对象,而非面向数据库的查询语言查询数据,降低与数据库的耦合。

    二、Spring Data JPA

     官方网址:https://spring.io/projects/spring-data-jpa

 官方给出的说明中总结几点:

  1. Spring Data JPA是开源项目Spring Data中的一员。
  2. 可以轻松的实现基于JPA的存储库。
  3. 让你Spring项目中操作数据库更简便。
  4. 不用再书写太多样板化的代码。
  5. 可以基于一定规则给你自动提供实现。

    三、JPA、Hibernate、Spring Data JPA三者之间的关系

     JPA是一种规范,内部是由接口和抽象类组成的。而Hibernate是一套成熟的ORM框架,且实现了JPA规范,理论上来说也可以成其为JPA的一种实现方式。Spring Data JPA是Spring中提供的一套基于JPA规范封装的更高级的框架,它的基础实现还是使用Hibernate。

    四、Spring Data JPA在Java中的实现

    1.1 创建并配置SpringBoot项目

    本例直接在官网上创建和下载,创建SpringBoot项目可以参考《从零到一搭建一个SpringBoot2.0项目

 查看mysql-connector-java版本,如果用的6.0以上,数据库驱动类就要用com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver,并且给数据库连接就爱上serverTimezone参数,否则就会报错。

 mysql配置名称在SpringBoot2.x和1.x之间还有区别,所以配置的时候先看org.springframework.boot.autoconfigure.jdbc.DataSourceProperties类:

 根据名称来对应,比如驱动类的配置就需要spring.datasource.driverClassName。

 mysql连接配置(在application.properties中配置):

1
2
3
4
5
# mysql连接配置
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&nullCatalogMeansCurrent=true&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=root

 同样JPA的配置名称也可以在JpaProperties中找,完整路径:org.springframework.boot.autoconfigure.orm.jpa.JpaProperties

 jpa配置:

1
2
3
4
# Spring Data JPA配置
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

 hbm2ddl.auto的四种选项:

  • create
    每次运行该程序,没有表格会新建表格,表内有数据会清空;
  • create-drop
    每次程序结束的时候会清空表;
  • update
    每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新;
  • validate
    运行程序会校验数据与数据库的字段类型是否相同,不同会报错;

 完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
server.port=8080

# mysql连接配置
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&nullCatalogMeansCurrent=true&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=root

# Spring Data JPA配置
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

1.2 简单接口调用

 项目结构:

 新建学生实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.yl.jpa.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "student")
public class Student {

@Id
@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
@GeneratedValue(generator = "uuidGenerator")
private String id;

@Column
private String name;

@Column
private Integer age;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

}
  • @Entity
    表示其是ORM实体;
  • @Table(name = “student”)
    表示对应表的名称为student;
  • @Id
    表示本字段为主键;
  • @GenericGenerator(name = “uuidGenerator”, strategy = “uuid”)
    表示主键生成器,命名为uuidGenerator,策略为uuid;
  • @GeneratedValue(generator = “uuidGenerator”)
    自动生成值,生成规则为上面定义的名称;
  • @column
    表示是表中的一个字段,名称会根据驼峰自动映射。如果该实体中某字段不需要称为表中一个字段,需要在字段上加@Transient注解。

 这种形式表示主键自动递增。

 数据访问层:

1
2
3
4
5
6
7
8
9
10
11
package com.yl.jpa.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.yl.jpa.entity.Student;

@Repository
public interface StudentDao extends JpaRepository<Student, String> {

}

 需要加上@Repository注解,继承JpaRepository,Studnet为其实体类,String为主键类型。

 继承类之后,就可以调用默认方法了,如:

 业务层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.yl.jpa.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.yl.jpa.dao.StudentDao;
import com.yl.jpa.entity.Student;
import com.yl.jpa.service.IStudentService;

@Service
public class StudentServiceImpl implements IStudentService {

@Autowired
private StudentDao studentDao;

@Override
public void save(Student student) {
studentDao.save(student);
}

}

 业务接口:

1
2
3
4
5
6
7
8
package com.yl.jpa.service;

import com.yl.jpa.entity.Student;

public interface IStudentService {

void save(Student student);
}

 控制层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.yl.jpa.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.yl.jpa.entity.Student;
import com.yl.jpa.service.IStudentService;

@RestController
@RequestMapping("/student")
public class StudentController {

@Autowired
private IStudentService studentService;

@PostMapping("/save")
public String doSave(@RequestBody Student student) {
studentService.save(student);
return "success";
}
}

 启动项目,项目启动成功后,可以看到自动运行了创建表语句:


 使用postman进行接口测试:

 控制台中打印insert语句:


 查看新增的记录:

1.3 查询

1.3.1 名称匹配查询

 在Spring Data JPA中,只要你根据规则写了repository方法,就不用自己写sql语句,JPA会自动生成对应的sql语句。
| 关键词 | 示例 | 生成语句 |
| —————- | —————————– | ——————————— |
| And | findByNameAndAge | where name = ?1 and age = ?2 |
| Or | findByNameOrAge | where name = ?1 or age = ?2 |
| Is,Equals | findByNameIs,findByNameEquals | where name = ?1 |
| Between | findByAgeBetween | where age between ?1 and ?2 |
| LessThan | findByAgeLessThan | where age < ?1 |
| LessThanEqual | findByAgeLessThanEqual | where age <= ?1 |
| GreaterThan | findByAgeGreaterThan | where age > ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | where age >= ?1 |
| After | findByCreateDateAfter | where create_date > ?1 |
| Before | findByCreateDateBefore | where create_date < ?1 |
| IsNull | findByAgeIsNull | where age is null |
| IsNotNull | findByAgeIsNotNull | where age is not null |
| Like | findByNameLike | where name like %?1% |
| NotLike | findByNameNotLike | where name not like %?1% |
| OrderBy | findByNameOrderByAgeDesc | where name = ?1 order by age desc |
| In | findByNameIn | where name in (?1) |
| NotIn | findByNameNotIn | where name not in (?1) |
| … | | |

1.3.2 限制查询

关键词 示例 生成语句
Top10 findTop10ByName where name = ?1 limit 10
First findFirstByName where name =? limit 1

1.3.3 自定义查询

 使用@Query可以实现自定义查询

1
2
3
4
5
6
7
8
9
10
11
@Query("select s from Student s where name = :name")
List<Student> queryByName(@Param("name") String name);

@Query("select s from Student s where name = ?1")
List<Student> queryByName2(@Param("name") String name);

@Query(value = "select * from student where age = ?1", nativeQuery = true)
List<Student> queryByAge(@Param("age") Integer age);

@Query(value = "select * from student where age = :age", nativeQuery = true)
List<Student> queryByAge2(@Param("age") Integer age);

 这四种方式都可以实现查询,但是需要注意,nativeQuery=true表示是原生sql查询,不能写入java实体,比如Student 。

1.3.4 分页查询

 可以使用PageRequest封装页码分页大小,排序数据,并传入findAll方法:

1
2
3
4
@Override
public Page<Student> findPage(PageRequest pageRequest) {
return studentDao.findAll(pageRequest);
}
1
2
3
4
5
6
7
@GetMapping("page")
public Page<Student> doGetPage(HttpServletRequest request){
Integer no = Integer.valueOf(request.getParameter("no"));
Integer size = Integer.valueOf(request.getParameter("size"));
Sort sort = Sort.by(Direction.DESC, "age");
return studentService.findPage(PageRequest.of(no, size, sort));
}

1.4 修改删除

1
2
3
4
@Transactional
@Modifying
@Query("delete from Student where name = ?1")
int deleteByName(String name);

 需要在方法上添加@Modifying注解,且在调用方法内加@Transactional注解,否则报错。

1.5 关联操作

1.5.1 多对多

 修改学生实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.yl.jpa.entity;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "student")
public class Student {

@Id
@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
@GeneratedValue(generator = "uuidGenerator")
private String id;

@Column
private String name;

@Column
private Integer age;

@ManyToMany(targetEntity = Course.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "student_course", joinColumns = {@JoinColumn(name = "student_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "course_id", referencedColumnName = "id")})
private List<Course> courseList;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public List<Course> getCourseList() {
return courseList;
}

public void setCourseList(List<Course> courseList) {
this.courseList = courseList;
}


}

 新建课程实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.yl.jpa.entity;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "course")
public class Course {

@Id
@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
@GeneratedValue(generator = "uuidGenerator")
private String id;

@Column
private String courseName;

@ManyToMany(targetEntity = Student.class, fetch = FetchType.LAZY)
@JoinTable(name = "student_course", joinColumns = {@JoinColumn(name = "course_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "student_id", referencedColumnName = "id")})
private List<Student> studentList;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getCourseName() {
return courseName;
}

public void setCourseName(String courseName) {
this.courseName = courseName;
}

}

 学生和课程是多对多,所以需要设置@ManyToMany。

 joinColumns表示当前对象的配置,其中course_id为表student_course中的字段,对应着本对象中的id值。

 inverseJoinColumns表示对方对象的配置,其中student_id为表student_course中的字段,对应着对方对象中的id值。
重启项目,会发现自动创建了student_course表:


 保存学生实体:

 其中的课程以及关联表也会新增:




 查询学生实体:

 关联信息也被查询出来。

1.5.2 一对多

 新建Address类,学生对地址是一对多的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.yl.jpa.entity;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "address")
public class Address {

@Id
@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
@GeneratedValue(generator = "uuidGenerator")
private String id;

private String address;

@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "student_id")
private Student student;

public Student getStudent() {
return student;
}

public void setStudent(Student student) {
this.student = student;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

}
  • @JoinColumn(name = “student_id”)

    表示在Address表中增加一列,列名叫做student_id,关联student的id。

    1.5.3 多对一

     地址对学生是多对一的关系,所以修改Student类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.yl.jpa.entity;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "student")
public class Student {

@Id
@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
@GeneratedValue(generator = "uuidGenerator")
private String id;

@Column
private String name;

@Column
private Integer age;

@ManyToMany(targetEntity = Course.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "student_course", joinColumns = {@JoinColumn(name = "student_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "course_id", referencedColumnName = "id")})
private List<Course> courseList;

@OneToMany(mappedBy = "student")
private List<Address> addressList;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public List<Course> getCourseList() {
return courseList;
}

public void setCourseList(List<Course> courseList) {
this.courseList = courseList;
}

public List<Address> getAddressList() {
return addressList;
}

public void setAddressList(List<Address> addressList) {
this.addressList = addressList;
}

}

  • @OneToMany(mappedBy = “student”)

    表示关系由student方维护。

    1.6 对象状态

     Spring Data JPA中对象的状态如下:

  • 瞬时状态(new/transient):没有主键,不与持久化上下文关联,即 new 出的对象(但不能指定id的值,若指定则是游离态而非瞬时态)

  • 托管状态(persistent):使用EntityManager进行find或者persist操作返回的对象即处于托管状态,此时该对象已经处于持久化上下文中(被EntityManager监控),任何对该实体的修改都会在提交事务时同步到数据库中。

  • 游离状态(detached):有主键,但是没有跟持久化上下文关联的实体对象。

  • 删除状态 (deleted):当调用EntityManger对实体进行remove后,该实体对象就处于删除状态。其本质也就是一个瞬时状态的对象。

 需要注意的是,在一个事务方法中,即使你是在调用save方法之后对实体重新赋值,那么新赋的值也会被修改到数据库里。因为此时是在一个事务中,且对象还是托管状态。
如:

1
2
3
4
5
6
@Transactional
@Override
public void save(Student student) {
studentDao.save(student);
student.setAge(100);
}

 save之后对age赋值。使用postman测试:



 得出的结果是100,这显然不是我们预期的结果。

 我们可以用BeanUtils.copyProperties克隆一份对象进行操作。

1
2
3
4
5
6
7
8
9
@Transactional
@Override
public void save(Student student) {
studentDao.saveAndFlush(student);

Student student2 = new Student();
BeanUtils.copyProperties(student, student2);
student2.setAge(100);
}
查看评论