HHH-18714 add support for inheritance in NamedEntityGraph annotation

This commit is contained in:
Lansana DIOMANDE 2025-01-02 18:37:53 +01:00 committed by Gavin King
parent 36d6e46eb3
commit 4251dd8612
3 changed files with 574 additions and 34 deletions

View File

@ -20,6 +20,7 @@
import java.util.stream.Collectors;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.boot.model.NamedEntityGraphDefinition;
import org.hibernate.boot.query.NamedQueryDefinition;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
@ -73,7 +74,6 @@
import static org.hibernate.metamodel.internal.InjectionHelper.injectTypedQueryReference;
/**
*
* @author Steve Ebersole
*/
public class JpaMetamodelImpl implements JpaMetamodelImplementor, Serializable {
@ -106,7 +106,7 @@ private ImportInfo(String importedName, Class<T> loadedClass) {
private final Map<Class<?>, String> entityProxyInterfaceMap = new HashMap<>();
private final Map<String, ImportInfo<?>> nameToImportMap = new ConcurrentHashMap<>();
private final Map<String,Object> knownInvalidnameToImportMap = new ConcurrentHashMap<>();
private final Map<String, Object> knownInvalidnameToImportMap = new ConcurrentHashMap<>();
public JpaMetamodelImpl(
@ -143,13 +143,14 @@ public JpaCompliance getJpaCompliance() {
public <X> ManagedDomainType<X> managedType(String typeName) {
final ManagedDomainType<X> managedType = findManagedType( typeName );
if ( managedType == null ) {
throw new IllegalArgumentException("Not a managed type: " + typeName);
throw new IllegalArgumentException( "Not a managed type: " + typeName );
}
return managedType;
}
@Override
@Nullable public EntityDomainType<?> findEntityType(@Nullable String entityName) {
@Nullable
public EntityDomainType<?> findEntityType(@Nullable String entityName) {
if ( entityName == null ) {
return null;
}
@ -162,18 +163,19 @@ public EntityDomainType<?> entity(String entityName) {
final EntityDomainType<?> entityType = findEntityType( entityName );
if ( entityType == null ) {
// per JPA
throw new IllegalArgumentException("Not an entity: " + entityName);
throw new IllegalArgumentException( "Not an entity: " + entityName );
}
return entityType;
}
@Override
@Nullable public EmbeddableDomainType<?> findEmbeddableType(@Nullable String embeddableName) {
@Nullable
public EmbeddableDomainType<?> findEmbeddableType(@Nullable String embeddableName) {
if ( embeddableName == null ) {
return null;
}
final ManagedDomainType<?> managedType = managedTypeByName.get( embeddableName );
if ( !( managedType instanceof EmbeddableDomainType<?> embeddableDomainType) ) {
if ( !( managedType instanceof EmbeddableDomainType<?> embeddableDomainType ) ) {
return null;
}
return embeddableDomainType;
@ -183,7 +185,7 @@ public EntityDomainType<?> entity(String entityName) {
public EmbeddableDomainType<?> embeddable(String embeddableName) {
final EmbeddableDomainType<?> embeddableType = findEmbeddableType( embeddableName );
if ( embeddableType == null ) {
throw new IllegalArgumentException("Not an embeddable: " + embeddableName);
throw new IllegalArgumentException( "Not an embeddable: " + embeddableName );
}
return embeddableType;
}
@ -226,7 +228,8 @@ public <X> EntityDomainType<X> resolveHqlEntityReference(String entityName) {
}
@Override
@Nullable public <X> ManagedDomainType<X> findManagedType(Class<X> cls) {
@Nullable
public <X> ManagedDomainType<X> findManagedType(Class<X> cls) {
//noinspection unchecked
return (ManagedDomainType<X>) managedTypeByClass.get( cls );
}
@ -242,7 +245,8 @@ public <X> ManagedDomainType<X> managedType(Class<X> cls) {
}
@Override
@Nullable public <X> EntityDomainType<X> findEntityType(Class<X> cls) {
@Nullable
public <X> EntityDomainType<X> findEntityType(Class<X> cls) {
final ManagedType<?> type = managedTypeByClass.get( cls );
if ( !( type instanceof EntityDomainType<?> ) ) {
return null;
@ -281,7 +285,7 @@ public <X> EmbeddableDomainType<X> embeddable(Class<X> cls) {
private Collection<ManagedDomainType<?>> getAllManagedTypes() {
// should never happen
return switch (jpaMetaModelPopulationSetting) {
return switch ( jpaMetaModelPopulationSetting ) {
case IGNORE_UNSUPPORTED -> managedTypeByClass.values();
case ENABLED -> managedTypeByName.values();
case DISABLED -> emptySet();
@ -311,7 +315,7 @@ public Set<EmbeddableType<?>> getEmbeddables() {
@Override
public @Nullable Set<String> getEnumTypesForValue(String enumValue) {
return allowedEnumLiteralsToEnumTypeNames.get( enumValue);
return allowedEnumLiteralsToEnumTypeNames.get( enumValue );
}
@Override
@ -388,7 +392,8 @@ public <T> void addNamedEntityGraph(String graphName, RootGraphImplementor<T> en
}
}
@Override @SuppressWarnings("unchecked")
@Override
@SuppressWarnings("unchecked")
public <T> RootGraphImplementor<T> findEntityGraphByName(String name) {
return (RootGraphImplementor<T>) entityGraphMap.get( name );
}
@ -494,17 +499,48 @@ private void applyNamedEntityGraphs(Collection<NamedEntityGraphDefinition> named
);
}
final NamedEntityGraph namedEntityGraph = definition.getAnnotation();
final RootGraphImpl<?> entityGraph =
createRootGraph( definition.getRegisteredName(), entityType,
namedEntityGraph.includeAllAttributes() );
if ( namedEntityGraph.attributeNodes() != null ) {
applyNamedAttributeNodes( namedEntityGraph.attributeNodes(), namedEntityGraph, entityGraph );
}
createEntityGraph(
namedEntityGraph,
definition.getRegisteredName(),
entityType,
namedEntityGraph.includeAllAttributes()
);
entityGraphMap.put( definition.getRegisteredName(), entityGraph );
}
}
private <T> RootGraphImpl<T> createEntityGraph(
NamedEntityGraph namedEntityGraph,
String registeredName,
EntityDomainType<T> entityType,
boolean includeAllAttributes) {
final RootGraphImpl<T> entityGraph =
createRootGraph( registeredName, entityType, includeAllAttributes );
if ( namedEntityGraph.subclassSubgraphs() != null ) {
for ( var subclassSubgraph : namedEntityGraph.subclassSubgraphs() ) {
GraphImplementor<?> subgraph = (GraphImplementor<?>) entityGraph.addTreatedSubgraph(
(Class) subclassSubgraph.type() );
applyNamedAttributeNodes(
subclassSubgraph.attributeNodes(),
namedEntityGraph,
subgraph
);
}
}
if ( namedEntityGraph.attributeNodes() != null ) {
applyNamedAttributeNodes( namedEntityGraph.attributeNodes(), namedEntityGraph, entityGraph );
}
return entityGraph;
}
private static <T> RootGraphImpl<T> createRootGraph(
String name, EntityDomainType<T> entityType, boolean includeAllAttributes) {
final RootGraphImpl<T> entityGraph = new RootGraphImpl<>( name, entityType );
@ -521,31 +557,44 @@ private void applyNamedAttributeNodes(
NamedEntityGraph namedEntityGraph,
GraphImplementor<?> graphNode) {
for ( NamedAttributeNode namedAttributeNode : namedAttributeNodes ) {
final AttributeNodeImplementor<?> attributeNode =
graphNode.findOrCreateAttributeNode( namedAttributeNode.value() );
final String value = namedAttributeNode.value();
final AttributeNodeImplementor<?> attributeNode = (AttributeNodeImplementor<?>) graphNode.addAttributeNode(
value );
if ( isNotEmpty( namedAttributeNode.subgraph() ) ) {
applyNamedSubgraphs(
namedEntityGraph,
namedAttributeNode.subgraph(),
attributeNode.makeSubGraph()
attributeNode,
false
);
}
if ( isNotEmpty( namedAttributeNode.keySubgraph() ) ) {
applyNamedSubgraphs(
namedEntityGraph,
namedAttributeNode.keySubgraph(),
attributeNode.makeKeySubGraph()
attributeNode,
true
);
}
}
}
private void applyNamedSubgraphs(
@SuppressWarnings({ "unchecked", "rawtypes" })
private <T> void applyNamedSubgraphs(
NamedEntityGraph namedEntityGraph,
String subgraphName,
SubGraphImplementor<?> subgraph) {
AttributeNodeImplementor<T> attributeNode, Boolean isKeySubGraph) {
for ( NamedSubgraph namedSubgraph : namedEntityGraph.subgraphs() ) {
if ( subgraphName.equals( namedSubgraph.name() ) ) {
final var isDefaultSubgraphType = namedSubgraph.type().equals( void.class );
final Class subGraphType = isDefaultSubgraphType ? null : namedSubgraph.type();
final SubGraphImplementor<?> subgraph = makeAttributeNodeSubgraph(
attributeNode, isKeySubGraph,
subGraphType
);
applyNamedAttributeNodes(
namedSubgraph.attributeNodes(),
namedEntityGraph,
@ -555,6 +604,21 @@ private void applyNamedSubgraphs(
}
}
private static <T> SubGraphImplementor<?> makeAttributeNodeSubgraph(
AttributeNodeImplementor<T> attributeNode,
Boolean isKeySubGraph,
Class<T> subGraphType) {
if ( isKeySubGraph ) {
return subGraphType != null ? attributeNode.makeKeySubGraph( subGraphType )
: attributeNode.makeKeySubGraph();
}
return subGraphType != null ?
attributeNode.makeSubGraph( subGraphType ) :
attributeNode.makeSubGraph();
}
private <X> Class<X> resolveRequestedClass(String entityName) {
try {
return getServiceRegistry().requireService( ClassLoaderService.class )
@ -631,7 +695,10 @@ public <T> EntityDomainType<T> resolveEntityReference(Class<T> javaType) {
}
}
throw new EntityTypeException( "Could not resolve entity class '" + javaType.getName() + "'", javaType.getName() );
throw new EntityTypeException(
"Could not resolve entity class '" + javaType.getName() + "'",
javaType.getName()
);
}
@Override
@ -716,11 +783,30 @@ public void processJpa(
private void populateStaticMetamodel(MetadataImplementor bootMetamodel, MetadataContext context) {
bootMetamodel.visitNamedHqlQueryDefinitions( definition
-> injectTypedQueryReference( definition, namedQueryMetamodelClass( definition, context ) ) );
-> injectTypedQueryReference(
definition,
namedQueryMetamodelClass(
definition,
context
)
) );
bootMetamodel.visitNamedNativeQueryDefinitions( definition
-> injectTypedQueryReference( definition, namedQueryMetamodelClass( definition, context ) ) );
bootMetamodel.getNamedEntityGraphs().values().forEach(definition
-> injectEntityGraph( definition, graphMetamodelClass( definition, context ), this ) );
-> injectTypedQueryReference(
definition,
namedQueryMetamodelClass(
definition,
context
)
) );
bootMetamodel.getNamedEntityGraphs().values().forEach( definition
-> injectEntityGraph(
definition,
graphMetamodelClass(
definition,
context
),
this
) );
}
private Class<?> namedQueryMetamodelClass(NamedQueryDefinition<?> definition, MetadataContext context) {
@ -764,9 +850,7 @@ private <T> EntityDomainType<T> locateOrBuildEntityType(
MetadataContext context,
final TypeConfiguration typeConfiguration) {
@SuppressWarnings("unchecked")
final EntityDomainType<T> entityType =
(EntityDomainType<T>)
context.locateEntityType( persistentClass );
final EntityDomainType<T> entityType = (EntityDomainType<T>) context.locateEntityType( persistentClass );
return entityType == null
? buildEntityType( persistentClass, context, typeConfiguration )
: entityType;
@ -818,8 +902,7 @@ private <T> MappedSuperclassDomainType<T> locateOrBuildMappedSuperclassType(
TypeConfiguration typeConfiguration) {
@SuppressWarnings("unchecked")
final MappedSuperclassDomainType<T> mappedSuperclassType =
(MappedSuperclassDomainType<T>)
context.locateMappedSuperclassType( mappedSuperclass );
(MappedSuperclassDomainType<T>) context.locateMappedSuperclassType( mappedSuperclass );
return mappedSuperclassType == null
? buildMappedSuperclassType( mappedSuperclass, context, typeConfiguration )
: mappedSuperclassType;

View File

@ -0,0 +1,250 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.entitygraph;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hibernate.Hibernate;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Lansana DIOMANDE
*/
@DomainModel(
annotatedClasses = {
EntityGraphWithInheritanceTest.Person.class,
EntityGraphWithInheritanceTest.Student.class,
EntityGraphWithInheritanceTest.Teacher.class,
EntityGraphWithInheritanceTest.School.class,
EntityGraphWithInheritanceTest.Course.class,
EntityGraphWithInheritanceTest.FreeCourse.class,
EntityGraphWithInheritanceTest.PayingCourse.class
}
)
@SessionFactory
@JiraKey(value = "HHH-18714")
public class EntityGraphWithInheritanceTest {
private static Long STUDENT_ID = 1L;
@BeforeAll
public void setup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
var frenchTeacher = new Teacher();
frenchTeacher.firstname = "John 2";
frenchTeacher.lastname = "DOE";
frenchTeacher.id = 2L;
var frenchCourse = new PayingCourse();
frenchCourse.id = 1L;
frenchCourse.name = "French";
frenchCourse.moneyReceiver = frenchTeacher;
var student = new Student();
student.id = STUDENT_ID;
student.firstname = "John";
student.lastname = "Doe";
var school = new School();
school.name = "Fake";
school.id = 1l;
student.school = school;
student.courses = new ArrayList<>() {{
add( frenchCourse );
}};
session.persist( student );
} );
}
@AfterAll
public void clean(SessionFactoryScope scope) {
scope.getSessionFactory().getSchemaManager().truncateMappedObjects();
}
@Test
public void testWithoutGraph(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
var person = session.find(
Person.class,
STUDENT_ID
);
session.detach( person );
assertThat( person ).isInstanceOf( Student.class );
var student = (Student) person;
assertThat( Hibernate.isInitialized( student.courses ) ).isFalse();
assertThat( Hibernate.isInitialized( student.school ) ).isFalse();
}
);
}
@Test
public void testWhenGraphHasSubclassSubgraph(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
var graph = session.createEntityGraph( Person.class );
var studentSubGraph = graph.addTreatedSubgraph( Student.class );
studentSubGraph.addAttributeNode( "school" );
studentSubGraph.addAttributeNode( "courses" );
var person = session.find(
Person.class,
STUDENT_ID,
Collections.singletonMap( GraphSemantic.FETCH.getJakartaHintName(), graph )
);
session.detach( person );
assertThat( person ).isInstanceOf( Student.class );
var student = (Student) person;
assertThat( Hibernate.isInitialized( student.school ) ).isTrue();
assertThat( Hibernate.isInitialized( student.courses ) ).isTrue();
assertThat( student.courses ).allSatisfy( course -> {
assertThat( course ).isInstanceOf( PayingCourse.class );
var payingCourse = (PayingCourse) course;
assertThat( Hibernate.isInitialized( payingCourse.moneyReceiver ) ).isFalse();
} );
}
);
}
@Test
public void testWhenGraphHasSubclassSubgraphLinkedWithSubgraphThatHasInheritance(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
var graph = session.createEntityGraph( Person.class );
var studentSubGraph = graph.addTreatedSubgraph( Student.class );
studentSubGraph.addAttributeNode( "school" );
studentSubGraph.addAttributeNode( "courses" );
var payingCourseSubgraph = studentSubGraph
.addSubgraph( "courses", PayingCourse.class );
payingCourseSubgraph.addSubgraph( "moneyReceiver" );
var person = session.find(
Person.class,
STUDENT_ID,
Collections.singletonMap( GraphSemantic.FETCH.getJakartaHintName(), graph )
);
session.detach( person );
assertThat( person ).isInstanceOf( Student.class );
var student = (Student) person;
assertThat( Hibernate.isInitialized( student.school ) ).isTrue();
assertThat( Hibernate.isInitialized( student.courses ) ).isTrue();
assertThat( student.courses ).allSatisfy( course -> {
assertThat( course ).isInstanceOf( PayingCourse.class );
var payingCourse = (PayingCourse) course;
assertThat( Hibernate.isInitialized( payingCourse.moneyReceiver ) ).isTrue();
} );
}
);
}
@Entity(name = "Person")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "person_type", discriminatorType = DiscriminatorType.STRING)
public static abstract class Person {
@Id
Long id;
String firstname;
String lastname;
}
@Entity(name = "Student")
@DiscriminatorValue("student")
public static class Student extends Person {
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
School school;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<Course> courses;
}
@Entity(name = "Teacher")
@DiscriminatorValue("teacher")
public static class Teacher extends Person {
}
@Entity(name = "School")
public static class School {
@Id
Long id;
String name;
}
@Entity(name = "Course")
@DiscriminatorColumn(name = "course_type", discriminatorType = DiscriminatorType.STRING)
public static abstract class Course {
@Id
Long id;
String name;
}
@Entity(name = "FreeCourse")
public static class FreeCourse extends Course {
}
@Entity(name = "PayingCourse")
@DiscriminatorValue("paying")
public static class PayingCourse extends Course {
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
Teacher moneyReceiver;
}
}

View File

@ -0,0 +1,207 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.entitygraph.named.multiple;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hibernate.Hibernate;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.NamedSubgraph;
import jakarta.persistence.OneToMany;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Lansana DIOMANDE
*/
@DomainModel(
annotatedClasses = {
NamedEntityGraphsWithInheritanceTest.Person.class,
NamedEntityGraphsWithInheritanceTest.Student.class,
NamedEntityGraphsWithInheritanceTest.Teacher.class,
NamedEntityGraphsWithInheritanceTest.School.class,
NamedEntityGraphsWithInheritanceTest.Course.class,
NamedEntityGraphsWithInheritanceTest.FreeCourse.class,
NamedEntityGraphsWithInheritanceTest.PayingCourse.class
}
)
@SessionFactory
@JiraKey(value = "HHH-18714")
public class NamedEntityGraphsWithInheritanceTest {
private static Long STUDENT_ID = 1L;
@BeforeAll
public void setup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
var frenchTeacher = new Teacher();
frenchTeacher.firstname = "John 2";
frenchTeacher.lastname = "DOE";
frenchTeacher.id = 2L;
var frenchCourse = new PayingCourse();
frenchCourse.id = 1L;
frenchCourse.name = "French";
frenchCourse.moneyReceiver = frenchTeacher;
var student = new Student();
student.id = STUDENT_ID;
student.firstname = "John";
student.lastname = "Doe";
var school = new School();
school.name = "Fake";
school.id = 1l;
student.school = school;
student.courses = new ArrayList<>() {{
add( frenchCourse );
}};
session.persist( student );
} );
}
@AfterAll
public void clean(SessionFactoryScope scope) {
scope.getSessionFactory().getSchemaManager().truncateMappedObjects();
}
@Test
public void testWhenGraphHasSubclassSubgraphLinkedWithSubgraphThatHasInheritance(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
var graph = session.getEntityGraph(
"graph_having_subclasssubgraph_linked_with_subgraph_that_has_inheritance" );
var person = session.find(
Person.class,
STUDENT_ID,
Collections.singletonMap( GraphSemantic.FETCH.getJakartaHintName(), graph )
);
session.detach( person );
assertThat( person ).isInstanceOf( Student.class );
var student = (Student) person;
assertThat( Hibernate.isInitialized( student.school ) ).isTrue();
assertThat( Hibernate.isInitialized( student.courses ) ).isTrue();
assertThat( student.courses ).allSatisfy( course -> {
assertThat( course ).isInstanceOf( PayingCourse.class );
var payingCourse = (PayingCourse) course;
assertThat( Hibernate.isInitialized( payingCourse.moneyReceiver ) ).isTrue();
} );
}
);
}
@NamedEntityGraph(
name = "graph_having_subclasssubgraph_linked_with_subgraph_that_has_inheritance",
subgraphs = {
@NamedSubgraph(name = "course_sub_graph", type = PayingCourse.class, attributeNodes = {
@NamedAttributeNode("moneyReceiver")
})
},
subclassSubgraphs = {
@NamedSubgraph(
name = "notUsed",
type = Student.class,
attributeNodes = {
@NamedAttributeNode(value = "school"),
@NamedAttributeNode(value = "courses", subgraph = "course_sub_graph")
}
)
}
)
@Entity(name = "Person")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "person_type", discriminatorType = DiscriminatorType.STRING)
public static abstract class Person {
@Id
Long id;
String firstname;
String lastname;
}
@Entity(name = "Student")
@DiscriminatorValue("student")
public static class Student extends Person {
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
School school;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
List<Course> courses;
}
@Entity(name = "Teacher")
@DiscriminatorValue("teacher")
public static class Teacher extends Person {
}
@Entity(name = "School")
public static class School {
@Id
Long id;
String name;
}
@Entity(name = "Course")
@DiscriminatorColumn(name = "course_type", discriminatorType = DiscriminatorType.STRING)
public static abstract class Course {
@Id
Long id;
String name;
}
@Entity(name = "FreeCourse")
public static class FreeCourse extends Course {
}
@Entity(name = "PayingCourse")
@DiscriminatorValue("paying")
public static class PayingCourse extends Course {
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
Teacher moneyReceiver;
}
}