blob: 46171ac3693b654141aee79bed868fdcf85351d6 [file] [log] [blame]
/*
* Copyright (c) 2005, 2021 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2015 SAP. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// SAP - initial API and implementation
package org.eclipse.persistence.testing.tests.wdf.jpa1.entitymanager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import jakarta.persistence.EntityManager;
import org.eclipse.persistence.testing.framework.wdf.JPAEnvironment;
import org.eclipse.persistence.testing.framework.wdf.ToBeInvestigated;
import org.eclipse.persistence.testing.models.wdf.jpa1.node.CascadingNode;
import org.eclipse.persistence.testing.models.wdf.jpa1.node.CascadingNodeDescription;
import org.eclipse.persistence.testing.tests.wdf.jpa1.JPA1Base;
import org.junit.Test;
/*
* A managed entity instance becomes removed by invoking the remove method on it or by cascading the remove operation. The
* semantics of the remove operation, applied to an entity X are as follows:
* <ul>
* <li>If X is a new entity, it is ignored by the remove operation. However, the remove operation is cascaded to entities
* referenced by X, if the relationships from X to these other entities is annotated with the cascade=REMOVE or cascade=ALL
* annotation element value.</li>
* <li>If X is a managed entity, the remove operation causes it to become removed. The remove operation is cascaded to entities
* referenced by X, if the relationships from X to these other entities is annotated with the cascade=REMOVE or cascade=ALL
* annotation element value.</li>
* <li>If X is a detached entity, an IllegalArgumentException will be thrown by the remove operation (or the transaction commit
* will fail).</li>
* <li>If X is a removed entity, it is ignored by the remove operation.</li>
* <li>A removed entity X will be removed from the database at or before transaction commit or as a result of the flush
* operation.</li>
* </ul>
* After an entity has been removed, its state (except for generated state) will be that of the entity at the point at which the
* remove operation was called.
*/
public class TestCascadeRemove extends JPA1Base {
@Test
public void testSimpleCascadeNew() throws SQLException {
final JPAEnvironment env = getEnvironment();
final EntityManager em = env.getEntityManager();
env.evictAll(em);
try {
// one-to-many relationship
CascadingNode child = new CascadingNode(1, null);
env.beginTransaction(em);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
child = em.find(CascadingNode.class, child.getId());
CascadingNode parent = new CascadingNode(2, null);
parent.addChild(child);
verify(!em.contains(parent), "Parent not a new entity");
verify(em.contains(child), "Child not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyAbsenceFromNodeTable(child.getId());
// one-to-one relationship
CascadingNodeDescription description = new CascadingNodeDescription(3, null, "a simple node");
env.beginTransaction(em);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
description = em.find(CascadingNodeDescription.class, description.getId());
parent = new CascadingNode(4, null);
parent.setDescription(description);
description.setNode(parent);
verify(!em.contains(parent), "Parent not a new entity");
verify(em.contains(description), "Description not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyAbsenceFromDescriptionTable(description.getId());
} finally {
closeEntityManager(em);
}
}
@Test
public void testSimpleCascadeManaged() throws SQLException {
final JPAEnvironment env = getEnvironment();
final EntityManager em = env.getEntityManager();
try {
// Case 1: status FOR_INSERT
// one-to-many relationship
CascadingNode child = new CascadingNode(101, null);
env.beginTransaction(em);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
CascadingNode parent = new CascadingNode(102, null);
em.persist(parent);
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(em.contains(parent), "Parent not managed");
verify(em.contains(child), "Child not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyAbsenceFromNodeTable(child.getId());
// one-to-one relationship
CascadingNodeDescription description = new CascadingNodeDescription(103, null, "a simple node");
env.beginTransaction(em);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
parent = new CascadingNode(104, null);
em.persist(parent);
description = em.find(CascadingNodeDescription.class, description.getId());
parent.setDescription(description);
description.setNode(parent);
verify(em.contains(parent), "Parent not managed");
verify(em.contains(description), "Description not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyAbsenceFromDescriptionTable(description.getId());
// Case 2: status FOR_UPDATE
// one-to-many relationship
parent = new CascadingNode(105, null);
child = new CascadingNode(106, null);
env.beginTransaction(em);
em.persist(parent);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
parent = em.find(CascadingNode.class, parent.getId());
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(em.contains(parent), "Parent not managed");
verify(em.contains(child), "Child not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyAbsenceFromNodeTable(child.getId());
// one-to-one relationship
parent = new CascadingNode(107, null);
description = new CascadingNodeDescription(108, parent, "a simple node");
parent.setDescription(description);
env.beginTransaction(em);
em.persist(parent);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
parent = em.find(CascadingNode.class, parent.getId());
description = parent.getDescription();
verify(em.contains(parent), "Parent not managed");
verify(em.contains(description), "Description not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyAbsenceFromDescriptionTable(description.getId());
} finally {
closeEntityManager(em);
}
}
@Test
public void testSimpleCascadeDetached() throws SQLException {
final JPAEnvironment env = getEnvironment();
final EntityManager em = env.getEntityManager();
try {
// Case 1: detached because entity exists on db but is not known by persistence context
// one-to-many relationship
CascadingNode parent = new CascadingNode(201, null);
CascadingNode child = new CascadingNode(202, null);
env.beginTransaction(em);
em.persist(parent);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(!em.contains(parent), "Parent not detached");
verify(em.contains(child), "Child not managed");
boolean removeFailed = false;
boolean immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(child), "Child is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (!checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyExistenceInNodeTable(parent.getId());
verifyExistenceInNodeTable(child.getId());
// one-to-one relationship
parent = new CascadingNode(203, null);
CascadingNodeDescription description = new CascadingNodeDescription(204, null, "a simple node");
env.beginTransaction(em);
em.persist(parent);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
description = em.find(CascadingNodeDescription.class, description.getId());
parent.setDescription(description);
verify(!em.contains(parent), "Parent not detached");
verify(em.contains(description), "Description not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(description), "Description is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (!checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyExistenceInNodeTable(parent.getId());
verifyExistenceInDescriptionTable(description.getId());
// Case 2: detached because an object with same pk but different object identity is known by persistence context
// Case 2a: state of known object: FOR_INSERT
// one-to-many relationship
CascadingNode existing = new CascadingNode(205, null);
parent = new CascadingNode(existing.getId(), null);
child = new CascadingNode(206, null);
env.beginTransaction(em);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
em.persist(existing); // status FOR_INSERT
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(!em.contains(parent), "Parent not detached");
verify(em.contains(existing), "Existing not managed");
verify(em.contains(child), "Child not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(child), "Child is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
verify(em.contains(existing), "Previously managed entity no longer managed");
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (immediateException || !checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyAbsenceFromNodeTable(existing.getId());
verifyExistenceInNodeTable(child.getId());
// one-to-one relationship
existing = new CascadingNode(207, null);
parent = new CascadingNode(existing.getId(), null);
description = new CascadingNodeDescription(208, null, "some text");
env.beginTransaction(em);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
em.persist(existing); // status FOR_INSERT
description = em.find(CascadingNodeDescription.class, description.getId());
parent.setDescription(description);
verify(!em.contains(parent), "Parent not detached");
verify(em.contains(existing), "Existing not managed");
verify(em.contains(description), "Description not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(description), "Description is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
verify(em.contains(existing), "Previously managed entity no longer managed");
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (immediateException || !checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyAbsenceFromNodeTable(existing.getId());
verifyExistenceInNodeTable(child.getId());
// Case 2b: state of known object: FOR_UPADTE
// one-to-many relationship
existing = new CascadingNode(209, null);
parent = new CascadingNode(existing.getId(), null);
child = new CascadingNode(210, null);
env.beginTransaction(em);
em.persist(existing);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
existing = em.find(CascadingNode.class, existing.getId()); // state FOR_UPADTE
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(!em.contains(parent), "Parent not detached");
verify(em.contains(existing), "Existing not managed");
verify(em.contains(child), "Child not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(child), "Child is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
verify(em.contains(existing), "Previously managed entity no longer managed");
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (!checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyExistenceInNodeTable(existing.getId());
verifyExistenceInNodeTable(child.getId());
// one-to-one relationship
existing = new CascadingNode(211, null);
parent = new CascadingNode(existing.getId(), null);
description = new CascadingNodeDescription(212, null, "some text");
env.beginTransaction(em);
em.persist(existing);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
existing = em.find(CascadingNode.class, existing.getId()); // state FOR_UPADTE
description = em.find(CascadingNodeDescription.class, description.getId());
parent.setDescription(description);
verify(!em.contains(parent), "Parent not detached");
verify(em.contains(existing), "Existing not managed");
verify(em.contains(description), "Description not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(description), "Description is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
verify(em.contains(existing), "Previously managed entity no longer managed");
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (!checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyExistenceInNodeTable(existing.getId());
verifyExistenceInDescriptionTable(description.getId());
// Case 2c: state of known object: FOR_REMOVE
// one-to-many relationship
existing = new CascadingNode(213, null);
parent = new CascadingNode(existing.getId(), null);
child = new CascadingNode(214, null);
env.beginTransaction(em);
em.persist(existing);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
existing = em.find(CascadingNode.class, existing.getId());
em.remove(existing); // state FOR_REMOVE
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(!em.contains(parent), "Parent not detached");
verify(!em.contains(existing), "Existing not removed");
verify(em.contains(child), "Child not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(child), "Child is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (!checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyExistenceInNodeTable(existing.getId());
verifyExistenceInNodeTable(child.getId());
// one-to-one relationship
existing = new CascadingNode(215, null);
parent = new CascadingNode(existing.getId(), null);
description = new CascadingNodeDescription(216, null, "some text");
env.beginTransaction(em);
em.persist(existing);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
existing = em.find(CascadingNode.class, existing.getId());
em.remove(existing); // state FOR_REMOVE
description = em.find(CascadingNodeDescription.class, description.getId());
parent.setDescription(description);
verify(!em.contains(parent), "Parent not detached");
verify(!em.contains(existing), "Existing not removed");
verify(em.contains(description), "Description not managed");
removeFailed = false;
immediateException = false;
try {
em.remove(parent);
verify(!em.contains(parent), "Parent is managed");
verify(!em.contains(description), "Description is still managed");
} catch (IllegalArgumentException e) {
removeFailed = true;
immediateException = true;
}
if (!immediateException) {
try {
env.commitTransactionAndClear(em);
} catch (RuntimeException e) {
if (!checkForPersistenceException(e)) {
throw e;
}
removeFailed = true;
}
} else {
env.rollbackTransactionAndClear(em);
}
verify(removeFailed, "remove succeeded on a detached instance");
verifyExistenceInNodeTable(existing.getId());
verifyExistenceInDescriptionTable(description.getId());
} finally {
closeEntityManager(em);
}
}
/* If X is a removed entity, it is ignored by the remove operation. */
@Test
public void testSimpleCascadeRemoved() throws SQLException {
final JPAEnvironment env = getEnvironment();
final EntityManager em = env.getEntityManager();
try {
// one-to-many relationship
CascadingNode parent = new CascadingNode(301, null);
CascadingNode child = new CascadingNode(302, null);
env.beginTransaction(em);
em.persist(parent);
em.persist(child);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
parent = em.find(CascadingNode.class, parent.getId());
em.remove(parent);
child = em.find(CascadingNode.class, child.getId());
parent.addChild(child);
verify(!em.contains(parent), "Parent not removed");
verify(em.contains(child), "Child not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyExistenceInNodeTable(child.getId());
// one-to-one relationship
parent = new CascadingNode(303, null);
CascadingNodeDescription description = new CascadingNodeDescription(304, null, "description");
env.beginTransaction(em);
em.persist(parent);
em.persist(description);
env.commitTransactionAndClear(em);
env.beginTransaction(em);
parent = em.find(CascadingNode.class, parent.getId());
em.remove(parent);
description = em.find(CascadingNodeDescription.class, description.getId());
parent.setDescription(description);
verify(!em.contains(parent), "Parent not removed");
verify(em.contains(description), "Description not managed");
em.remove(parent);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(parent.getId());
verifyExistenceInDescriptionTable(description.getId());
} finally {
closeEntityManager(em);
}
}
@Test
@ToBeInvestigated
public void testCircularCascade() throws SQLException {
final JPAEnvironment env = getEnvironment();
final EntityManager em = env.getEntityManager();
try {
CascadingNode node1 = new CascadingNode(401, null);
CascadingNode node2 = new CascadingNode(402, node1);
node2.addChild(node1);
node1.setParent(node2);
env.beginTransaction(em);
em.persist(node1);
env.commitTransactionAndClear(em);
verifyExistenceInNodeTable(node1.getId());
verifyExistenceInNodeTable(node2.getId());
env.beginTransaction(em);
node1 = em.find(CascadingNode.class, node1.getId());
node2 = em.find(CascadingNode.class, node2.getId());
em.remove(node1);
env.commitTransactionAndClear(em);
verifyAbsenceFromNodeTable(node1.getId());
verifyAbsenceFromNodeTable(node2.getId());
} finally {
closeEntityManager(em);
}
}
private void verifyExistenceInNodeTable(int nodeId) throws SQLException {
verify(checkForExistenceInNodeTable(nodeId), "no node with id " + nodeId + " found using JDBC.");
}
private void verifyAbsenceFromNodeTable(int nodeId) throws SQLException {
verify(!checkForExistenceInNodeTable(nodeId), "node with id " + nodeId + " still found using JDBC.");
}
private boolean checkForExistenceInNodeTable(int nodeId) throws SQLException {
Connection conn = getEnvironment().getDataSource().getConnection();
try {
PreparedStatement stmt = conn.prepareStatement("select ID, PARENT from TMP_CASC_NODE where ID = ?");
try {
stmt.setInt(1, nodeId);
ResultSet rs = stmt.executeQuery();
try {
return rs.next();
} finally {
rs.close();
}
} finally {
stmt.close();
}
} finally {
conn.close();
}
}
private void verifyExistenceInDescriptionTable(int descId) throws SQLException {
verify(checkForExistenceInDescriptionTable(descId), "no description with id " + descId + " found using JDBC.");
}
private void verifyAbsenceFromDescriptionTable(int descId) throws SQLException {
verify(!checkForExistenceInDescriptionTable(descId), "description with id " + descId + " still found using JDBC.");
}
private boolean checkForExistenceInDescriptionTable(int descId) throws SQLException {
Connection conn = getEnvironment().getDataSource().getConnection();
try {
PreparedStatement stmt = conn.prepareStatement("select ID, DESC_TEXT from TMP_CASC_NODE_DESC where ID = ?");
try {
stmt.setInt(1, descId);
ResultSet rs = stmt.executeQuery();
try {
return rs.next();
} finally {
rs.close();
}
} finally {
stmt.close();
}
} finally {
conn.close();
}
}
}