Merge pull request #3154 from ebean-orm/feature/3134-idClass-delete

#3134 - String cannot be cast to class io.ebean.bean.EntityBean" when deleting entities that use cascade = [CascadeType.ALL] with composite keys
This commit is contained in:
Rob Bygrave
2023-08-10 21:15:09 +12:00
committed by GitHub
10 changed files with 249 additions and 29 deletions
@@ -212,7 +212,6 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
private final EntityBean prototypeEntityBean;
private final IdBinder idBinder;
private final String idSelect;
private String idBinderInLHSSql;
private String idBinderIdSql;
private String deleteByIdSql;
@@ -355,23 +354,11 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
propertiesIndex[i] = propMap.get(ebi.property(i));
}
}
idSelect = initIdSelect();
}
String initIdSelect() {
if (idProperty != null && !idProperty.name().equals("_idClass")) {
return idProperty.name();
} else if (entityType == EntityType.EMBEDDED) {
return null;
} else {
StringJoiner sj = new StringJoiner(",");
for (BeanProperty prop : propertiesNonMany) {
if (prop.isImportedPrimaryKey()) {
sj.add(prop.name());
}
}
return sj.toString().intern();
}
public String idSelect() {
if (idBinder == null) throw new UnsupportedOperationException();
return idBinder.idSelect();
}
public boolean isJacksonCorePresent() {
@@ -2450,9 +2437,13 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
BeanProperty _findBeanProperty(String propName) {
BeanProperty prop = propMap.get(propName);
if (prop == null && inheritInfo != null) {
// search in sub types...
return inheritInfo.findSubTypeProperty(propName);
if (prop == null) {
if ("_idClass".equals(propName)) {
return idProperty;
} else if (inheritInfo != null) {
// search in sub types...
return inheritInfo.findSubTypeProperty(propName);
}
}
return prop;
}
@@ -3057,10 +3048,6 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
return idProperty;
}
public String idSelect() {
return idSelect;
}
/**
* Return true if this bean should be inserted rather than updated.
*
@@ -23,11 +23,6 @@ abstract class BeanDescriptorElement<T> extends BeanDescriptor<T> {
this.elementHelp = elementHelp;
}
@Override
String initIdSelect() {
return null;
}
private String shortName(String name) {
int pos = name.lastIndexOf('.');
if (pos > 1) {
@@ -26,6 +26,8 @@ public interface IdBinder {
*/
void initialise();
String idSelect();
/**
* Return true if this is a compound key and must use expanded and or form.
*/
@@ -45,6 +45,11 @@ public final class IdBinderEmbedded implements IdBinder {
this.idInValueSql = idInExpandedForm ? idInExpanded() : idInCompressed();
}
@Override
public String idSelect() {
return embIdProperty.name();
}
@Override
public boolean isIdInExpandedForm() {
return idInExpandedForm;
@@ -2,11 +2,11 @@ package io.ebeaninternal.server.deploy.id;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.server.bind.DataBind;
import io.ebeaninternal.server.core.DefaultSqlUpdate;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.DbReadContext;
import io.ebeaninternal.server.deploy.DbSqlContext;
import io.ebeaninternal.server.bind.DataBind;
import java.io.DataInput;
import java.io.DataOutput;
@@ -27,6 +27,11 @@ final class IdBinderEmpty implements IdBinder {
public void initialise() {
}
@Override
public String idSelect() {
return "";
}
@Override
public boolean isIdInExpandedForm() {
return false;
@@ -45,6 +45,11 @@ public final class IdBinderSimple implements IdBinder {
// do nothing
}
@Override
public String idSelect() {
return idProperty.name();
}
@Override
public boolean isIdInExpandedForm() {
return false;
@@ -0,0 +1,53 @@
package org.tests.cache.embeddedid;
import javax.persistence.*;
import java.util.List;
@Entity
@IdClass(ConceptId.class)
@SuppressWarnings("unused")
public class Concept {
@Id
private String id;
@Id
private String networkId;
private String label;
@OneToMany(mappedBy = "from", cascade = {CascadeType.ALL})
private List<Connection> outgoingConnections;
@OneToMany(mappedBy = "to", cascade = {CascadeType.ALL})
private List<Connection> incomingConnections;
public Concept(String networkId, String id, String label) {
this.networkId = networkId;
this.id = id;
this.label = label;
}
public String id() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String networkId() {
return networkId;
}
public void setNetworkId(String networkId) {
this.networkId = networkId;
}
public String label() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}
@@ -0,0 +1,31 @@
package org.tests.cache.embeddedid;
import javax.persistence.Embeddable;
@Embeddable
public class ConceptId {
private final String networkId;
private final String id;
public ConceptId(String networkId, String id) {
this.networkId = networkId;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConceptId conceptId = (ConceptId) o;
if (!id.equals(conceptId.id)) return false;
return networkId.equals(conceptId.networkId);
}
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + networkId.hashCode();
return result;
}
}
@@ -0,0 +1,87 @@
package org.tests.cache.embeddedid;
import javax.persistence.*;
@Entity
@IdClass(ConceptId.class)
@SuppressWarnings("unused")
public class Connection {
@Id
private String id;
@Id
private String networkId;
private String label;
@ManyToOne(optional = false)
@JoinColumns({
@JoinColumn(name = "from_conc", referencedColumnName = "id", nullable = false),
@JoinColumn(
name = "network_id", referencedColumnName = "network_id",
nullable = false, insertable = false, updatable = false
)
})
private Concept from;
@ManyToOne(optional = false)
@JoinColumns({
@JoinColumn(name = "to_conc", referencedColumnName = "id", nullable = false),
@JoinColumn(
name = "network_id", referencedColumnName = "network_id",
nullable = false, insertable = false, updatable = false
)
})
private Concept to;
public Connection(
String networkId, String id, String label,
Concept from, Concept to
) {
this.networkId = networkId;
this.id = id;
this.label = label;
this.from = from;
this.to = to;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getNetworkId() {
return networkId;
}
public void setNetworkId(String networkId) {
this.networkId = networkId;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Concept from() {
return from;
}
public void setFrom(Concept from) {
this.from = from;
}
public Concept to() {
return to;
}
public void setTo(Concept to) {
this.to = to;
}
}
@@ -0,0 +1,50 @@
package org.tests.cache.embeddedid;
import io.ebean.DB;
import io.ebean.test.LoggedSql;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
class TestCompositeForeignKey extends BaseTestCase {
@Test
void createConnectionWithCompositeForeignKey() {
String networkId = "test-network";
Concept concept1 = new Concept(networkId, "concept1", "concept 1");
Concept concept2 = new Concept(networkId, "concept2", "concept 2");
DB.saveAll(concept1, concept2);
String connectionId = "test-connection";
Connection connection = new Connection(
networkId, connectionId, "test", concept1, concept2
);
DB.save(connection);
Connection reloaded = DB.find(Connection.class, new ConceptId(networkId, connectionId));
Objects.requireNonNull(reloaded);
LoggedSql.start();
// this breaks if we uncomment the OneToMany relationships with cascade = ALL
DB.createQuery(Concept.class).delete();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(4);
assertThat(sql.get(0)).contains("select t0.network_id, t0.id from concept t0");
if (isSqlServer() || isDb2() || isNuoDb()) {
assertThat(sql.get(1)).contains("delete from connection where ");
assertThat(sql.get(2)).contains("delete from connection where ");
assertThat(sql.get(3)).contains("delete from concept where ");
} else {
assertThat(sql.get(1)).contains("delete from connection where (network_id,from_conc) in");
assertThat(sql.get(2)).contains("delete from connection where (network_id,to_conc) in");
assertThat(sql.get(3)).contains("delete from concept where (network_id,id) in");
}
}
}