JPA学习笔记(二)

上一次只是说了保存数据的方法,下面就把其他的一些 CRUD 操作补充完整,以及其他的一些细节,有些知识是和上一篇有关联的,但是上一篇感觉写的有点多于是就放在了这里
虽然这一篇也非常的多….

基本使用

在很多情况和 hibernate 是差不多的,毕竟作者是主导者,套路都一样
保存数据在上一篇已经说了,主要就是那个 persist 方法,Hibernate 中也是一样的,以前可能是 save 方法,persist 这个命名更规范,作用其实是一样的

获取数据

在 hibernate 中获取数据是 get/load 简单说分别对应的是正常获取和延迟加载(lazy),在 JPA 中也是类似的,对应的通过 find/getReference 获取;由于它们使用了 Java 泛型方法,无需任何显示的类型转换即可获得特定类型的实体对象。

1
2
3
4
5
6
7
8
9
10
11
12
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpaDemo");
EntityManager em = factory.createEntityManager();
// 查询其实无需开启事务
em.getTransaction().begin();

Employee emp = em.find(Employee.class, 1);
// 相同的,返回的也是代理对象,延迟加载
Employee emp = em.getReference(Employee.class, 1);

em.getTransaction().commit();
em.close();
factory.close();

与 hibernate 一样,如果使用延迟加载,在 EntityManager 关闭后是无法再向数据库查询数据的,EntityManager 内部其实也是在操纵 session 对象
然后下面就当复习了,find 如果找不到会返回 null;getReference 找不到会抛异常

PS:获取的时候不使用事务也是可以的

更新数据

继续复习,因为开启了事务,所以修改实体后在最后事务提交的时候会将变化更新到数据库,所以不需要再保存
总结下就是只要满足两点,就会在事务提交时自动更新:1.和事务关联,2.处于托管状态

1
2
Employee emp = em.find(Employee.class, 1);
emp.setName("new Name");

可以参考下后面的 JPA 中的对象状态

删除数据

删除也很简单,就是调用 EntityManager 的 remove 方法。

1
2
Employee emp = em.find(Employee.class, 1);
em.remove(emp);

对象状态

在 JPA 中对象有四种状态,分别是:新建、托管、游离、删除 ;在 hibernate 中好像是三种来,其实是差不多的,所以也就不多解释了,可以参考 Hibernate 笔记里的内容

  • 新建
    当进行 new 对象时,这个对象处于新建状态
  • 托管
    首先事务要开启,然后比如通过 find 来获取实体,那么这个实体就处于托管状态
    这个对象发生改变 EntityManager 会感知到,并且在事务提交的时候会自动更新到数据库
  • 游离
    当调用 entity.clear();等方法时此时,bean 将变成游离状态(此方法就是把实体管理器中的所有实体变成游离状态)
    游离状态修改实体不会再同步到数据库中
    如果此时调用 entity.merge(person); 将会把游离状态的实体变化的数据同步到数据库
  • 删除
    就是认为删除的对象或是垃圾回收掉的对象

JPQL语句

和 HQL 语句非常的相似,它们最大的特点就是采用的面向对象的方式,所以语句中出现的应该都是属性名而不是字段,那就先来看一段简单的 JPQL 使用:

1
2
3
4
5
// createQuery 中还可以传入第二个参数,这样就不用强转了,当然不写也是可以执行的
TypedQuery<Employee> query =
em.createQuery("SELECT e FROM Employee e",
Employee.class);
List<Employee> emps = query.getResultList();

如果是使用 HQL 的话那就是直接 FROM Employee 就可以了,当然如果使用的 ORM 是 hibernate,那么其实也是可以这样写的,但是为了规范,最好还是这样写:SELECT e FROM Employee e 这里的 e 就是起的别名啦
其他的也都差不多,如果后面填充参数所使用的占位符,索引的话用 ?,key 的话用 : ,补充一点的是如果想从指定的索引开始可以这样写:?1 这样就是从1开始的

Hibernate 中获取单一结果(只查一条)是使用 query.uniqueResult 方法;在 JPA 中是使用 getSingleResult 方法,其实也就只是方法名不一样

查询可以不开启事务,增删改就需要开启事务了

如果是执行增删改的语句,那就得执行 query 的 executeUpdate 方法了,和 hibernate 是一样的

其他的多数语句是一样的,遇到特殊的再回来补充吧…..(ORM 如果用的是 hibernate,写 HQL 也是可以跑的嘛)

关于create

EntityManager 以 create 开头的有三个(用于创建相关查询语句),分别是 createQuery、createNamedQuery、createNativeQuery

  • createQuery
    用于执行编写的 jpql 语句,不能用于执行 SQL 语句
    它有两个重载:
    Query createQuery(java.lang.String qlString)
    <T> TypedQuery<T> createQuery(java.lang.String qlString,java.lang.Class<T> resultClass)
    执行的结果是一样的(返回值略有不同),不同的是前者指定了返回的结果集的类型,后者没有制定,当然这两种查询得到的结果集是一样的,但是建议在使用的时候制定结果集

  • createNamedQuery
    用于传入命名查询的名称创建查询,如:createNamedQuery("findAllUser");
    使用 @NamedQuery 注解在实体类中定义命名查询:
    @NamedQuery(name="findAllUser",query="SELECT u FROM User u")
    如果要定义多条命名查询,可以使用这种形式:
    @NamedQueries({@NamedQuery(xx),...})

  • createNativeQuery
    此方法用于执行 SQL 语句,不能用于执行 jpql 语句,一般用于复杂的 sql 语句
    此方法有三个重载

    1
    2
    3
    Query createNativeQuery(java.lang.String sqlString,java.lang.Class resultClass);
    Query createNativeQuery(java.lang.String sqlString);
    Query createNativeQuery(java.lang.String sqlString,java.lang.String resultSetMapping);

    前面两个方法中一个指定了结果集一个没有制定,但是其执行结果是不一样的
    在返回的结果中由于没有指定结果集的类型,返回结果中只有结果,没有结果对应的字段名。
    也就是说指定结果集意味着原生查询的结果集中的栏将完全匹配实体的 O/R 映射。
    第三个方法使用一个字符串来指定结果集(主要应用于复杂的查询,返回的结果不能被实体一一映射),和第一个使用类来指定有所不同。

关系模型

结合上一篇的注解看,哎╮(╯▽╰)╭又是分开的,太长了写不下了
首先记住第一定律:只要注解后面是以 Many 结尾的(比如:@OneToMany)默认统一是延迟加载(lazy),其他的一般是立即加载

关于级联

JPA 中的提供的级联有四种,下面会用到

  • CascadeType.REFRESH
    级联刷新
    当你刚开始获取到了这条记录,那么在你处理业务过程中,这条记录被另一个业务程序修改了(数据库这条记录被修改了),那么你获取的这条数据就不是最新的数据,那你就要调用实体管理器里面的 refresh 方法来刷新实体。
    使用其他获取是获取的缓存的数据,所以…..注意
    一般配置为:cascade={CascadeType.REFRESH},fetch=FetchType.LAZY,不会影响系统的正常运作。
  • CascadeType.PERSIST
    级联持久化(级联保存)
    比如保存 order 的时候也保存 orderItem,如果在数据库里已经存在与需要保存的 orderItem 相同的 id 记录,则级联保存出错。
    也就是说相当于是 insert 操作
  • CascadeType.MERGE
    级联更新
    比如当对象 Order 处于游离状态时,对对象 Order 里面的属性作修改,也修改了 Order 里面的 orderItems。
  • CascadeType.REMOVE
    级联删除
    就是删除的时候都删了….不是很推荐,加上逻辑判断更好
  • CascadeType.ALL
    具备上面的全部

还有一些需要注意的是:

级联的配置是生效于对应的方法的,比如上面的四个就依次对应实体管理器的 refreshpersistmergeremove
所以说,使用 JPQL 语句的时候级联是不会生效的

一对多/多对一

大体套路是不变的:多的一方为关系维护端,负责外键记录的更新,关系被维护端没有权利更新外键记录
当属性中出现 mappedBy 时,一般就是关系被维护端了,值为是由谁维护的(维护端的属性名)

在一对多的“一”的一方,可以使用注解 @OneToMany 来标识多的一方的集合,以及设置相应的级联(级联是可以设置多个的,用括号套)
下面是一个简单的用户和订单的栗子,对于关系被维护端的用户类:

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
@Entity
@Table(name = "users")
public class User {
private Integer id;
private String name;
private Set<Order> orders = new HashSet<>();

@Id
public Integer getId() {
return id;
}

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

public String getName() {
return name;
}

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

@OneToMany(cascade = CascadeType.ALL,mappedBy = "user")
//拥有mappedBy注解的实体类为关系被维护端
public Set<Order> getOrders() {
return orders;
}

public void setOrders(Set<Order> orders) {
this.orders = orders;
}

// 只为方便测试添加
public void addItem(Order order) {
order.setUser(this);
this.orders.add(order);
}
}

然后是多的一方,为关系的维护端 Order:

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
@Entity
@Table(name = "orders")
public class Order {
private Integer id;
private String orderName;
private User user;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId() {
return id;
}

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

public String getOrderName() {
return orderName;
}

public void setOrderName(String orderName) {
this.orderName = orderName;
}

// 通过 optional 设置为必填项
@ManyToOne(cascade = CascadeType.ALL,optional = false)
@JoinColumn(name = "user_id")
public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}
}

一个简单的测试类,其实只需要执行完 createEntityManagerFactory 就会自动建表了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void save() {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpaDemo");
EntityManager em = factory.createEntityManager();
em.getTransaction().begin();

User user = new User();
user.setName("佳芷");
user.setId(123);
Order order1 = new Order();
order1.setOrderName("测试项1");
Order order2 = new Order();
order2.setOrderName("测试项2");
user.addItem(order1);
user.addItem(order2);

em.persist(user);
em.getTransaction().commit();
em.close();
factory.close();
}

需要说下的是,如果设置的一对多/多对一关系是单向的,那么不加 mappedBy 也是可以的,在双向关系中是必须的,否则会引发数据一致性的问题
什么是单向映射?
简单地说就是可以从关联的一方去查询另一方,却不能反向查询;以上面的栗子,在 user 中的 set 集合使用 @OneToMany (不用 mappedBy),在 order 中去掉 user 的属性,什么也不用加,这就是单向的;只能通过 user 获取 order ,不能通过 order 获取 user
想深入了解的话可以去:https://www.ibm.com/developerworks/cn/java/j-lo-jparelated/index.html

一对一

在一对一中和一对多其实也差不多,都需要设置关系维护端和被维护端,只不过因为是一对一,所以这两个角色是可以互换的,设谁都可以
在被维护端记得加 mappedBy 属性,在维护端记得使用 @JoinColumn 注解指定外键约束的那一列的名字,然后因为是一对一,如果在关系维护端使用了 (非空),那么关系被维护端就没必要在设了
具体的代码就不贴了,和上面很类似,一对一也区分单向和双向,如果是单向的话,只需要在一方加入 @OneToOne @JoinColumn(name="PSPACE_ID") ;在另一方什么都不用了。
如果是双向的,那么就需要在被维护方加 @OneToOne(mappedBy="department") 注解;在关系维护方加 注解 @OneToOne @JoinColumn(name="PSPACE_ID")

多对多

会了上面的两种,这种其实也差不多了,会比上面的复杂一点,关于实体的定义就不全部贴了,主要贴下不太一样的东西,以学生和老师为例,这是多对多的关系,假设规定学生的一方为关系的维护端,那么老师就是关系被维护端,学生有权修改和更新中间表的数据,老师则不可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class Student {
private Integer id;
private String name;
private Set<Teacher> teachers = new HashSet<>();

// inverseJoinColumns 即关系被维护端的外键列名
@ManyToMany(cascade = CascadeType.REFRESH)
@JoinTable(name = "student_teacher",inverseJoinColumns = @JoinColumn(name = "teacher_id"),
joinColumns = @JoinColumn(name = "student_id"))
public Set<Teacher> getTeachers() {
return teachers;
}
....
}

然后是被维护端的老师,也是省略了一部分:

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
@Entity
public class Teacher {
private Integer id;
private String name;
private Set<Student> students = new HashSet<>();

// 在多对多的关系中,一般不会设置级联删除,太危险
@ManyToMany(cascade = CascadeType.REFRESH,mappedBy = "teachers")
public Set<Student> getStudents() {
return students;
}

// 相同的 ID 即认为是相同的对象
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Teacher teacher = (Teacher) o;
return id != null ? id.equals(teacher.id) : teacher.id == null;
}

@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}

然后是测试类,大部分的管理其实只要操作关系维护端就可以了,因为它才是有权利更新中间表的

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
 @Test
public void save() throws Exception {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpaDemo");
EntityManager em = factory.createEntityManager();
em.getTransaction().begin();

Student student = new Student("佳芷");
Teacher teacher = new Teacher("冰封承諾");

em.persist(student);
em.persist(teacher);
em.getTransaction().commit();
em.close();
factory.close();
}

// 建立学生与老师之间的关系,也就是在中间表中添加记录
// 只有关系维护端才有权利
@Test
public void buildBind() throws Exception {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpaDemo");
EntityManager em = factory.createEntityManager();
em.getTransaction().begin();

Student student = em.find(Student.class, 1);
student.getTeachers().add(em.getReference(Teacher.class, 1));
// 解除关系的话
// student.getTeachers().remove(em.getReference(Teacher.class, 1));

em.getTransaction().commit();
em.close();
factory.close();
}

// 删除学生,会自动删除相关的“关系”
// 如果是删除老师,没有权利删除关系,所以要先解除关系再删除
@Test
public void delStudent() throws Exception {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpaDemo");
EntityManager em = factory.createEntityManager();
em.getTransaction().begin();

Student student = em.find(Student.class, 1);

em.remove(student);
em.getTransaction().commit();
em.close();
factory.close();
}

联合主键

所谓主键就是可以唯一确定该行数据,由此可以知道,当一个字段不能决定该行的值时,就要考虑采用多个字段作为主键。
比如,对于学校来说,班号可以决定班级,但是决定不了班级里的某个人,表示班级里的某个人就需要用班号+该学生在该班内的编号.这就可以说是联合(复合)主键了
首先要定义一个联合主键的类,也就是里面的属性都是主键,并且这个类有几个规定:首先要实现序列化的接口,然后要重写 hashCode 和 equals,最后标上 @Embeddable 注解就可以了

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
@Embeddable
public class StuPK implements Serializable {
private Integer classId;
private Integer stuId;

// 为了下面测试便于初始数据
public StuPK() {}
public StuPK(Integer classId, Integer stuId) {
this.classId = classId;
this.stuId = stuId;
}

public Integer getClassId() {
return classId;
}

public void setClassId(Integer classId) {
this.classId = classId;
}

public Integer getStuId() {
return stuId;
}

public void setStuId(Integer stuId) {
this.stuId = stuId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StuPK stuPK = (StuPK) o;
if (classId != null ? !classId.equals(stuPK.classId) : stuPK.classId != null) return false;
return stuId != null ? stuId.equals(stuPK.stuId) : stuPK.stuId == null;
}

@Override
public int hashCode() {
int result = classId != null ? classId.hashCode() : 0;
result = 31 * result + (stuId != null ? stuId.hashCode() : 0);
return result;
}
}

下面就是具体的持久化类了,引用 StuPK 作为主键,StuPK 类里的属性在这里是可以直接用的,也就是说最后会保存到数据库的表中

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
@Entity
public class Stu {
private StuPK id;
private String name;

public Stu() {}
public Stu(String name,Integer classId, Integer stuId) {
this.name = name;
id = new StuPK(classId, stuId);
}

@EmbeddedId
public StuPK getId() {
return id;
}

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

@Column(length = 10)
public String getName() {
return name;
}

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

测试类就不写了,没什么好写的和上面是一样的,主要是执行后看看数据库对应的表结构对不对

主键生成策略

这个算是个补充内容吧,本来是应该写在上一篇中的,但是那篇看了下有点长,就补充在这吧,自带的四种就不说了,上一篇中已经说的很详细了,下面主要说的是使用 hibernate 的情况下如何使用 hibernate 提供的其他注解生成策略,通过 Hibernate 的 @GenericGenerator 实现。首先看下用法吧(以 uuid 为例):

1
2
3
4
5
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name="system-uuid", strategy="uuid")
@Column(name="uuid", length=32)
private String uuid;

@GeneratedValue(generator = "system-uuid") :用 generator 属性指定要使用的策略生成器。
@GenericGenerator(name = "system-uuid", strategy = "uuid") :声明一个策略通用生成器,策略 strategy 为 “uuid”。
uuid 可以说是最通用的,适用于所有数据库。

persistence-unit中的类集合

一个 persistence unit 将固定数量的一组类映射到关系数据库。缺省情况下,如果你没有在 persistence.xml 中指定任何元数据,persistence provider 就会对包含该 persistence.xml 的 JAR 文件进行扫描,从根目录开始搜寻任何标注有 @javax.persistence.Entity 注解的类,并将这些类添加到由 persistence unit 管理的类集合中。
此外,你还可以通过 <jar-file> 元素指定额外的 JAR 文件,以供 persistence provider 搜索。该元素的值不能使用绝对路径,只能是一个以包含 persistence.xml 的JAR文件为基准的相对路径。比如:<jar-file>../lib/customer.jar</jar-file>
JAR 文件的自动扫描在 Java EE 环境下是保证可以正常执行的,但在 Java SE 应用程序中却无法做到可移植。
理论上,要决定必须搜索哪些 JAR 文件也许是不太可能的。不过,现实中这不是问题。参与 EJB 3.0 专家组的主要厂商都非正式地宣称过,它们会毫无疑问的在 Java SE 中支持这一特性。无论是否使用自动 JAR 文件扫描,你都可以用 <class> 元素显式的列出 persistence unit 中的类集合。

通常情况下,你无需指定 <class><jar-file><mapping-file> 元素。但是有一种情形你可能需要使用上述元素,即当你需要在两个或多个 persistence unit 中映射同一个类时。

参考

https://wizardforcel.gitbooks.io/tutorialspoint-java/jpa/629.html
https://www.w3cschool.cn/java/jpa-entitymanager.html
实体管理器和实体管理器工厂
JPA中的级联
JPQL大全
stence-Unit中的类集合

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~