jpa vue管理系统
介绍
JPA将实体状态转换转换为数据库DML语句。 由于对实体图进行操作很常见,因此JPA允许我们将实体状态更改从父级传播到子级。
通过CascadeType映射配置此行为。
JPA与Hibernate级联类型
Hibernate支持所有JPA级联类型和一些其他旧式级联样式。 下表列出了JPA级联类型与其等效的Hibernate本机API之间的关联:
JPA EntityManager操作 | JPA CascadeType | Hibernate本机会话操作 | Hibernate原生CascadeType | 事件监听器 |
---|---|---|---|---|
分离(实体) | 分离 | 逐出(实体) | 分离或 |
默认驱逐事件侦听器 |
合并(实体) | 合并 | 合并(实体) | 合并 | 默认合并事件监听器 |
坚持(实体) | 坚持 | 坚持(实体) | 坚持 | 默认的持久事件监听器 |
刷新(实体) | 刷新 | 刷新(实体) | 刷新 | 默认刷新事件监听器 |
移除(实体) | 去掉 | 删除(实体) | 删除或删除 | 默认删除事件监听器 |
saveOrUpdate(实体) | SAVE_UPDATE | 默认的保存或更新事件监听器 | ||
复制(实体,复制模式) | 复制 | 默认复制事件监听器 | ||
锁(实体,lockModeType) | buildLockRequest(实体,lockOptions) | 锁 | 默认锁定事件监听器 | |
以上所有EntityManager方法 | 所有 | 以上所有的Hibernate Session方法 | 所有 |
从该表可以得出以下结论:
- 在JPA EntityManager或Hibernate Session上调用persist , merge或refresh没有什么区别。
- JPA的remove和detach调用被委托给Hibernate Delete和逐出本机操作。
- 只有Hibernate支持复制和saveOrUpdate 。 尽管复制对于某些非常特定的情况很有用(当确切的实体状态需要在两个不同的数据源之间进行镜像时),但持久合并合并始终是比本机saveOrUpdate操作更好的替代方法。将持久对象用于TRANSIENT实体,并为已分离实体进行合并。saveOrUpdate的缺点(当将分离的实体快照传递给已经管理该实体的Session时)导致了合并操作的前身:现已不存在的saveOrUpdateCopy操作。
- JPA锁定方法与Hibernate锁定请求方法具有相同的行为。
- JPA CascadeType.ALL不仅适用于EntityManager状态更改操作,而且还适用于所有Hibernate CascadeTypes 。因此,如果将您的关联映射到CascadeType.ALL ,您仍然可以级联Hibernate特定事件。 例如,即使JPA没有定义LOCK CascadeType ,您也可以级联JPA锁定操作(尽管它的行为就像重新连接,而不是实际的锁定请求传播)。
级联最佳做法
级联仅对父级-子级关联有意义(父级实体状态转换级联到其子级实体)。 从孩子级联到父级不是很有用,通常,这是映射代码的味道。
接下来,我将采取分析所有JPA家长的级联行为-子关联。
一对一
最常见的一对一双向关联如下所示:
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@OneToOne(mappedBy = "post",
cascade = CascadeType.ALL, orphanRemoval = true)
private PostDetails details;
public Long getId() {
return id;
}
public PostDetails getDetails() {
return details;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void addDetails(PostDetails details) {
this.details = details;
details.setPost(this);
}
public void removeDetails() {
if (details != null) {
details.setPost(null);
}
this.details = null;
}
}
@Entity
public class PostDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "created_on")
@Temporal(TemporalType.TIMESTAMP)
private Date createdOn = new Date();
private boolean visible;
@OneToOne
@PrimaryKeyJoinColumn
private Post post;
public Long getId() {
return id;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public void setPost(Post post) {
this.post = post;
}
}
Post实体扮演Parent角色,而PostDetails是Child 。
双向关联应始终在两侧更新,因此父级侧应包含addChild和removeChild组合。 这些方法确保我们始终同步关联的双方,以避免对象或关系数据损坏问题。
在这种特殊情况下,删除CascadeType.ALL和孤立的孤岛是有意义的,因为PostDetails生命周期绑定到其Post Parent实体的生命周期。
进行一对一的持久化操作
CascadeType.PERSIST与CascadeType.ALL配置一起提供,因此我们只需要持久化Post实体,并且关联的PostDetails实体也可以持久化:
Post post = new Post();
post.setName("Hibernate Master Class");
PostDetails details = new PostDetails();
post.addDetails(details);
session.persist(post);
生成以下输出:
INSERT INTO post(id, NAME)
VALUES (DEFAULT, Hibernate Master Class'')
insert into PostDetails (id, created_on, visible)
values (default, '2015-03-03 10:17:19.14', false)
级联一对一合并操作
CascadeType.MERGE继承自CascadeType.ALL设置,因此我们仅需合并Post实体,并且关联的PostDetails也将合并:
Post post = newPost();
post.setName("Hibernate Master Class Training Material");
post.getDetails().setVisible(true);
doInTransaction(session -> {
session.merge(post);
});
合并操作将生成以下输出:
SELECT onetooneca0_.id AS id1_3_1_,
onetooneca0_.NAME AS name2_3_1_,
onetooneca1_.id AS id1_4_0_,
onetooneca1_.created_on AS created_2_4_0_,
onetooneca1_.visible AS visible3_4_0_
FROM post onetooneca0_
LEFT OUTER JOIN postdetails onetooneca1_
ON onetooneca0_.id = onetooneca1_.id
WHERE onetooneca0_.id = 1
UPDATE postdetails SET
created_on = '2015-03-03 10:20:53.874', visible = true
WHERE id = 1
UPDATE post SET
NAME = 'Hibernate Master Class Training Material'
WHERE id = 1
级联一对一删除操作
CascadeType.REMOVE也是从CascadeType.ALL配置继承的,因此Post实体删除也会触发PostDetails实体删除:
Post post = newPost();
doInTransaction(session -> {
session.delete(post);
});
生成以下输出:
delete from PostDetails where id = 1
delete from Post where id = 1
一对一删除孤立级联操作
如果一个孩子实体从母公司分离,儿童外键设置为NULL。 如果我们也要删除“子行”,则必须使用孤立删除支持。
doInTransaction(session -> {
Post post = (Post) session.get(Post.class, 1L);
post.removeDetails();
});
除去孤儿将生成以下输出:
SELECT onetooneca0_.id AS id1_3_0_,
onetooneca0_.NAME AS name2_3_0_,
onetooneca1_.id AS id1_4_1_,
onetooneca1_.created_on AS created_2_4_1_,
onetooneca1_.visible AS visible3_4_1_
FROM post onetooneca0_
LEFT OUTER JOIN postdetails onetooneca1_
ON onetooneca0_.id = onetooneca1_.id
WHERE onetooneca0_.id = 1
delete from PostDetails where id = 1
单向一对一关联
大多数情况下,父实体是反方(如的mappedBy),儿童controling通过它的外键关联。 但是级联不限于双向关联,我们还可以将其用于单向关系:
@Entity
public class Commit {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String comment;
@OneToOne(cascade = CascadeType.ALL)
@JoinTable(
name = "Branch_Merge_Commit",
joinColumns = @JoinColumn(
name = "commit_id",
referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(
name = "branch_merge_id",
referencedColumnName = "id")
)
private BranchMerge branchMerge;
public Commit() {
}
public Commit(String comment) {
this.comment = comment;
}
public Long getId() {
return id;
}
public void addBranchMerge(
String fromBranch, String toBranch) {
this.branchMerge = new BranchMerge(
fromBranch, toBranch);
}
public void removeBranchMerge() {
this.branchMerge = null;
}
}
@Entity
public class BranchMerge {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String fromBranch;
private String toBranch;
public BranchMerge() {
}
public BranchMerge(
String fromBranch, String toBranch) {
this.fromBranch = fromBranch;
this.toBranch = toBranch;
}
public Long getId() {
return id;
}
}
层叠在于传播父实体状态过渡到一个或多个儿童的实体,它可用于单向和双向关联。
一对多
最常见的父-子关联由一到多和多到一的关系,其中级联是只对一个一对多侧有用:
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL,
mappedBy = "post", orphanRemoval = true)
private List comments = new ArrayList<>();
public void setName(String name) {
this.name = name;
}
public List getComments() {
return comments;
}
public void addComment(Comment comment) {
comments.add(comment);
comment.setPost(this);
}
public void removeComment(Comment comment) {
comment.setPost(null);
this.comments.remove(comment);
}
}
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
private Post post;
private String review;
public void setPost(Post post) {
this.post = post;
}
public String getReview() {
return review;
}
public void setReview(String review) {
this.review = review;
}
}
就像一对一的示例一样, CascadeType.ALL和孤立删除是合适的,因为Comment生命周期绑定到其Post Parent实体的生命周期。
级联一对多持久操作
我们只需要保留Post实体,所有相关的Comment实体也将保留:
Post post = new Post();
post.setName("Hibernate Master Class");
Comment comment1 = new Comment();
comment1.setReview("Good post!");
Comment comment2 = new Comment();
comment2.setReview("Nice post!");
post.addComment(comment1);
post.addComment(comment2);
session.persist(post);
持久操作将生成以下输出:
insert into Post (id, name)
values (default, 'Hibernate Master Class')
insert into Comment (id, post_id, review)
values (default, 1, 'Good post!')
insert into Comment (id, post_id, review)
values (default, 1, 'Nice post!')
级联一对多合并操作
合并Post实体也将合并所有Comment实体:
Post post = newPost();
post.setName("Hibernate Master Class Training Material");
post.getComments()
.stream()
.filter(comment -> comment.getReview().toLowerCase()
.contains("nice"))
.findAny()
.ifPresent(comment ->
comment.setReview("Keep up the good work!")
);
doInTransaction(session -> {
session.merge(post);
});
生成以下输出:
SELECT onetomanyc0_.id AS id1_1_1_,
onetomanyc0_.NAME AS name2_1_1_,
comments1_.post_id AS post_id3_1_3_,
comments1_.id AS id1_0_3_,
comments1_.id AS id1_0_0_,
comments1_.post_id AS post_id3_0_0_,
comments1_.review AS review2_0_0_
FROM post onetomanyc0_
LEFT OUTER JOIN comment comments1_
ON onetomanyc0_.id = comments1_.post_id
WHERE onetomanyc0_.id = 1
update Post set
name = 'Hibernate Master Class Training Material'
where id = 1
update Comment set
post_id = 1,
review='Keep up the good work!'
where id = 2
级联一对多删除操作
删除Post实体后,关联的Comment实体也将被删除:
Post post = newPost();
doInTransaction(session -> {
session.delete(post);
});
生成以下输出:
delete from Comment where id = 1
delete from Comment where id = 2
delete from Post where id = 1
一对多删除孤立级联操作
移除孤儿使我们可以在父实体不再引用子实体时将其删除:
newPost();
doInTransaction(session -> {
Post post = (Post) session.createQuery(
"select p " +
"from Post p " +
"join fetch p.comments " +
"where p.id = :id")
.setParameter("id", 1L)
.uniqueResult();
post.removeComment(post.getComments().get(0));
});
如我们在以下输出中看到的,评论已删除:
SELECT onetomanyc0_.id AS id1_1_0_,
comments1_.id AS id1_0_1_,
onetomanyc0_.NAME AS name2_1_0_,
comments1_.post_id AS post_id3_0_1_,
comments1_.review AS review2_0_1_,
comments1_.post_id AS post_id3_1_0__,
comments1_.id AS id1_0_0__
FROM post onetomanyc0_
INNER JOIN comment comments1_
ON onetomanyc0_.id = comments1_.post_id
WHERE onetomanyc0_.id = 1
delete from Comment where id = 1
多对多
多对多关系非常棘手,因为此关联的每一方都扮演“父母”和“孩子”角色。 不过,我们可以从希望传播实体状态更改的地方识别出一侧。
我们不应该默认使用CascadeType.ALL ,因为CascadeTpe.REMOVE最终可能会删除比我们期望的更多的内容(您很快就会发现):
@Entity
public class Author {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@Column(name = "full_name", nullable = false)
private String fullName;
@ManyToMany(mappedBy = "authors",
cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List books = new ArrayList<>();
private Author() {}
public Author(String fullName) {
this.fullName = fullName;
}
public Long getId() {
return id;
}
public void addBook(Book book) {
books.add(book);
book.authors.add(this);
}
public void removeBook(Book book) {
books.remove(book);
book.authors.remove(this);
}
public void remove() {
for(Book book : new ArrayList<>(books)) {
removeBook(book);
}
}
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@ManyToMany(cascade =
{CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "Book_Author",
joinColumns = {
@JoinColumn(
name = "book_id",
referencedColumnName = "id"
)
},
inverseJoinColumns = {
@JoinColumn(
name = "author_id",
referencedColumnName = "id"
)
}
)
private List authors = new ArrayList<>();
private Book() {}
public Book(String title) {
this.title = title;
}
}
级联多对多持久操作
坚持Author实体也将保留Books :
Author _John_Smith = new Author("John Smith");
Author _Michelle_Diangello =
new Author("Michelle Diangello");
Author _Mark_Armstrong =
new Author("Mark Armstrong");
Book _Day_Dreaming = new Book("Day Dreaming");
Book _Day_Dreaming_2nd =
new Book("Day Dreaming, Second Edition");
_John_Smith.addBook(_Day_Dreaming);
_Michelle_Diangello.addBook(_Day_Dreaming);
_John_Smith.addBook(_Day_Dreaming_2nd);
_Michelle_Diangello.addBook(_Day_Dreaming_2nd);
_Mark_Armstrong.addBook(_Day_Dreaming_2nd);
session.persist(_John_Smith);
session.persist(_Michelle_Diangello);
session.persist(_Mark_Armstrong);
Book和Book_Author行与Authors一起插入:
insert into Author (id, full_name)
values (default, 'John Smith')
insert into Book (id, title)
values (default, 'Day Dreaming')
insert into Author (id, full_name)
values (default, 'Michelle Diangello')
insert into Book (id, title)
values (default, 'Day Dreaming, Second Edition')
insert into Author (id, full_name)
values (default, 'Mark Armstrong')
insert into Book_Author (book_id, author_id) values (1, 1)
insert into Book_Author (book_id, author_id) values (1, 2)
insert into Book_Author (book_id, author_id) values (2, 1)
insert into Book_Author (book_id, author_id) values (2, 2)
insert into Book_Author (book_id, author_id) values (3, 1)
解除多对多关联的一侧
要删除Author ,我们需要取消关联属于可移动实体的所有Book_Author关系:
doInTransaction(session -> {
Author _Mark_Armstrong =
getByName(session, "Mark Armstrong");
_Mark_Armstrong.remove();
session.delete(_Mark_Armstrong);
});
该用例生成以下输出:
SELECT manytomany0_.id AS id1_0_0_,
manytomany2_.id AS id1_1_1_,
manytomany0_.full_name AS full_nam2_0_0_,
manytomany2_.title AS title2_1_1_,
books1_.author_id AS author_i2_0_0__,
books1_.book_id AS book_id1_2_0__
FROM author manytomany0_
INNER JOIN book_author books1_
ON manytomany0_.id = books1_.author_id
INNER JOIN book manytomany2_
ON books1_.book_id = manytomany2_.id
WHERE manytomany0_.full_name = 'Mark Armstrong'
SELECT books0_.author_id AS author_i2_0_0_,
books0_.book_id AS book_id1_2_0_,
manytomany1_.id AS id1_1_1_,
manytomany1_.title AS title2_1_1_
FROM book_author books0_
INNER JOIN book manytomany1_
ON books0_.book_id = manytomany1_.id
WHERE books0_.author_id = 2
delete from Book_Author where book_id = 2
insert into Book_Author (book_id, author_id) values (2, 1)
insert into Book_Author (book_id, author_id) values (2, 2)
delete from Author where id = 3
多对多关联会生成太多冗余SQL语句,并且经常很难调整它们。 接下来,我将演示多对多CascadeType.REMOVE隐藏的危险。
多对多CascadeType.REMOVE陷阱
多对多CascadeType.ALL是另一个代码异味,我在查看代码时经常碰到。 所述CascadeType.REMOVE使用CascadeType.ALL时自动继承,但实体去除不仅应用到链接表,但对关联的另一侧为好。
让我们将Author实体书籍多对多关联更改为使用CascadeType.ALL代替:
@ManyToMany(mappedBy = "authors",
cascade = CascadeType.ALL)
private List books = new ArrayList<>();
删除一位作者时:
doInTransaction(session -> {
Author _Mark_Armstrong =
getByName(session, "Mark Armstrong");
session.delete(_Mark_Armstrong);
Author _John_Smith =
getByName(session, "John Smith");
assertEquals(1, _John_Smith.books.size());
});
属于已删除作者的所有图书都将被删除,即使我们仍与已删除图书相关联的其他作者也是如此:
SELECT manytomany0_.id AS id1_0_,
manytomany0_.full_name AS full_nam2_0_
FROM author manytomany0_
WHERE manytomany0_.full_name = 'Mark Armstrong'
SELECT books0_.author_id AS author_i2_0_0_,
books0_.book_id AS book_id1_2_0_,
manytomany1_.id AS id1_1_1_,
manytomany1_.title AS title2_1_1_
FROM book_author books0_
INNER JOIN book manytomany1_ ON
books0_.book_id = manytomany1_.id
WHERE books0_.author_id = 3
delete from Book_Author where book_id=2
delete from Book where id=2
delete from Author where id=3
通常,此行为与业务逻辑期望不符,仅在首次删除实体时才发现。
如果我们也将CascadeType.ALL设置为Book实体,则可以进一步推动该问题:
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "Book_Author",
joinColumns = {
@JoinColumn(
name = "book_id",
referencedColumnName = "id"
)
},
inverseJoinColumns = {
@JoinColumn(
name = "author_id",
referencedColumnName = "id"
)
}
)
这次,不仅书籍被删除,作者也被删除:
doInTransaction(session -> {
Author _Mark_Armstrong =
getByName(session, "Mark Armstrong");
session.delete(_Mark_Armstrong);
Author _John_Smith =
getByName(session, "John Smith");
assertNull(_John_Smith);
});
作者的删除触发所有相关书籍的删除,这进一步触发所有相关的作者的删除。 这是非常危险的操作,会导致大规模实体删除,这很少是预期的行为。
SELECT manytomany0_.id AS id1_0_,
manytomany0_.full_name AS full_nam2_0_
FROM author manytomany0_
WHERE manytomany0_.full_name = 'Mark Armstrong'
SELECT books0_.author_id AS author_i2_0_0_,
books0_.book_id AS book_id1_2_0_,
manytomany1_.id AS id1_1_1_,
manytomany1_.title AS title2_1_1_
FROM book_author books0_
INNER JOIN book manytomany1_
ON books0_.book_id = manytomany1_.id
WHERE books0_.author_id = 3
SELECT authors0_.book_id AS book_id1_1_0_,
authors0_.author_id AS author_i2_2_0_,
manytomany1_.id AS id1_0_1_,
manytomany1_.full_name AS full_nam2_0_1_
FROM book_author authors0_
INNER JOIN author manytomany1_
ON authors0_.author_id = manytomany1_.id
WHERE authors0_.book_id = 2
SELECT books0_.author_id AS author_i2_0_0_,
books0_.book_id AS book_id1_2_0_,
manytomany1_.id AS id1_1_1_,
manytomany1_.title AS title2_1_1_
FROM book_author books0_
INNER JOIN book manytomany1_
ON books0_.book_id = manytomany1_.id
WHERE books0_.author_id = 1
SELECT authors0_.book_id AS book_id1_1_0_,
authors0_.author_id AS author_i2_2_0_,
manytomany1_.id AS id1_0_1_,
manytomany1_.full_name AS full_nam2_0_1_
FROM book_author authors0_
INNER JOIN author manytomany1_
ON authors0_.author_id = manytomany1_.id
WHERE authors0_.book_id = 1
SELECT books0_.author_id AS author_i2_0_0_,
books0_.book_id AS book_id1_2_0_,
manytomany1_.id AS id1_1_1_,
manytomany1_.title AS title2_1_1_
FROM book_author books0_
INNER JOIN book manytomany1_
ON books0_.book_id = manytomany1_.id
WHERE books0_.author_id = 2
delete from Book_Author where book_id=2
delete from Book_Author where book_id=1
delete from Author where id=2
delete from Book where id=1
delete from Author where id=1
delete from Book where id=2
delete from Author where id=3
这种用例在很多方面都是错误的。 大量不必要的SELECT语句,最终我们最终删除了所有作者及其所有书籍。 这就是为什么当您在多对多关联中发现CascadeType.ALL时,它应该引起您的注意。
当涉及到Hibernate映射时,您应该始终追求简单性。 Hibernate文档也证实了这一假设:
真正的多对多关联的实际测试案例很少。 大多数时候,您需要存储在“链接表”中的其他信息。 在这种情况下,最好将两个一对多关联用于中间链接类。 实际上,大多数关联是一对多和多对一的。 因此,在使用任何其他关联样式时,您应谨慎进行。
结论
级联是一个方便的ORM功能,但并非没有问题。 您应该仅从父级实体级联到子级,而不是相反。 您应该始终仅使用业务逻辑要求所要求的Casacde操作,而不应将CascadeType.ALL转换为默认的Parent-Child关联实体状态传播配置。
- 代码可在GitHub上获得。
翻译自: https://www.javacodegeeks.com/2015/03/a-beginners-guide-to-jpa-and-hibernate-cascade-types.html
jpa vue管理系统