How can you implement inheritance with a List (@OneToMany) typed as a subclass, using JPA annotations ?
For my problem description I have created a simple example that I am trying to make work...
Please do not suggest introducing a many-to-many-relationship because I have an existing database structure that are using foreign key relationships that I need to reuse,
so I need to figure out how to make code like this working without using downcasting.
Except from my main problem (i.e. that I below want to be able use "List<Student>" instead of "List<Person>" below) I would actually prefer to not having any java reference to School from the Person/Student classes, i.e. I would prefer a uni-directional dependency, but as far as I have understood this bidirectional relation in the java code is necessary.
First some small pieces (the full code including the annotations can be found further down) of the three persisted classes, to illustrate their relations:
Code:
public class Person {
public String getPersonName() {
...
public class Student extends Person {
public int getGraduationYear() {
...
public class School {
public List<Student> getStudents() { // this is what I want ! NOTE: This is the row that illustrates what I want to do !
// but I have only been able to succesfully implement this:
// public List<Person> getStudents() {
// but with this typing of the List, the client code will need to down cast
// Here in the School class I also might have another List, such as
// public List<Teacher> getTeachers() { // "Teacher extends Person" (also)
// and would not want to do downcasting to be able to use the subclass methods
// i.e. I want the correct typing in the List
Here are some table content examples, to make it easier to understand the table relations (class relations) for the three annotated classes using these tables:
Code:
TABLE "school"
SCHOOL_ID , SCHOOL_NAME
1 , 'Harvard'
2 , 'Stanford'
TABLE "person" (PERSON_TYPE is the discrimator column with S=Student)
PERSON_ID , PERSON_NAME , SCHOOL_ID , PERSON_TYPE
1 , 'Scott' , 1 , 'S'
2 , 'Bill' , 1 , 'S'
3 , 'Steve' , 2 , 'S'
4 , 'Larry' , 2 , 'S'
TABLE "student" (PERSON_ID is both primary key and foreign key)
PERSON_ID , GRADUATION_YEAR
1 , 1980
2 , 1985
3 , 1990
4 , 1995
(there might be other kind of persons belonging to the schools also, for example a teacher table wih columns such as employment_year but for simplicity only one subclass of person is used in this example)
Now I will first show some code that will work (with the "Student" list typed as the baseclass, i.e. as a "Person" list that need to be downcasted:
(then I will show the changes that I tried with to make the code work for the desired subclass typing of the list)
Code:
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("students");
EntityManager em = emf.createEntityManager();
School school = em.find(School.class, 1);
System.out.println(school.getSchoolName());
List<Person> students = school.getStudents(); // note that the basetype "Person" is used but I would like to be able to use "List<Student>"
for (Person person : students) {
Student student = (Student)person; // note the undesired downcasting !
System.out.println(student.getGraduationYear());
}
school = new School();
school.setSchoolName("MIT");
Student student = new Student();
student.setPersonName("Martin");
student.setGraduationYear(1960);
school.addStudent(student);
EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.persist(school);
transaction.commit();
}
package domain;
// the imports are not displayed here ...
@Entity
@Table(name="SCHOOL")
public class School {
public School() {
}
private Integer id;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "SCHOOL_ID")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
private String schoolName;
@Column(name = "SCHOOL_NAME")
public String getSchoolName() {
return this.schoolName;
}
public void setSchoolName(String schoolName) {
this.schoolName = schoolName;
}
private List<Person> students = new Vector<Person>(); // NOTE ! I WANT to use List<Student> instead
@OneToMany(cascade=CascadeType.ALL, mappedBy="school")
public List<Person> getStudents() { // NOTE ! I WANT to use List<Student> instead
return students;
}
public void setStudents(List<Person> students) { // NOTE ! I WANT to use List<Student> instead
this.students = students;
for (Person student : students) {
student.setSchool(this);
}
}
public void addStudent(Student student) {
this.students.add(student);
}
}
package domain;
// the imports are not displayed here ...
@Entity
@Table(name="PERSON")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="PERSON_TYPE", discriminatorType=DiscriminatorType.STRING)
public class Person {
public Person() {
this.setPersonType("P");
}
private Integer id;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "PERSON_ID")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
private String personName;
@Column(name = "PERSON_NAME")
public String getPersonName() {
return this.personName;
}
public void setPersonName(String personName) {
this.personName = personName;
}
private String personType;
@Column(name = "PERSON_TYPE")
public String getPersonType() {
return this.personType;
}
public void setPersonType(String personType) {
this.personType = personType;
}
private School school;
@ManyToOne
@JoinColumn(name="SCHOOL_ID")
public School getSchool() {
return school;
}
public void setSchool(School school) {
this.school = school;
}
}
package domain;
// the imports are not displayed here ...
@Entity
@Table(name="STUDENT")
@DiscriminatorValue("S") // S = Student
@Inheritance(strategy=InheritanceType.JOINED)
@PrimaryKeyJoinColumn(name="PERSON_ID")
public class Student extends Person {
public Student() {
super.setPersonType("S");
}
private int graduationYear;
@Column(name="GRADUATION_YEAR")
public int getGraduationYear() {
return graduationYear;
}
public void setGraduationYear(int employmentYear) {
this.graduationYear = employmentYear;
}
}
The above code works but I instead want the list of students to be typed correctly, i.e. with the actual type (Student) of the items in the list and not typed as the basetype (Person) to avoid the dirty downcasting.
I tried to modify the above code with "List<Student>" instead of "List<Person>" and I also moved the "School methods and field" to the subclass, i.e. these were the changes I tried:
(and if not moving down the School code, I get this exception: "org.hibernate.AnnotationException: mappedBy reference an unknown target entity property: domain.Student.school in domain.School.students")
Code:
...
public class School {
...
private List<Student> students = new Vector<Student>(); // this is the typing I want, i.e. typed as subclass !
@OneToMany(cascade=CascadeType.ALL, mappedBy="school")
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
...
...
public class Person {
...
// I pushed down the "School code" to the subclass
public class Student extends Person {
...
// this code was pushed down from the baseclass
private School school;
@ManyToOne
@JoinColumn(name="SCHOOL_ID")
public School getSchool() {
return school;
}
public void setSchool(School school) {
this.school = school;
}
// but it would actually be desired if I even could get rid of this School reference from Student/Person ...
...
Below is the complete code listing of the code that does not work, and at the end the stacktrace is shown:
Code:
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("students");
EntityManager em = emf.createEntityManager();
School school = em.find(School.class, 1);
System.out.println(school.getSchoolName());
List<Student> students = school.getStudents(); // this is the typing I want, i.e. typed as subclass !
for (Student student : students) {
System.out.println(student.getGraduationYear());
}
school = new School();
school.setSchoolName("MIT");
Student student = new Student();
student.setPersonName("Martin");
student.setGraduationYear(1960);
school.addStudent(student);
EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.persist(school);
transaction.commit();
}
package domain;
// the imports are not displayed here ...
@Entity
@Table(name="SCHOOL")
public class School {
public School() {
}
private Integer id;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "SCHOOL_ID")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
private String schoolName;
@Column(name = "SCHOOL_NAME")
public String getSchoolName() {
return this.schoolName;
}
public void setSchoolName(String schoolName) {
this.schoolName = schoolName;
}
private List<Student> students = new Vector<Student>(); // this is the typing I want, i.e. typed as subclass !
@OneToMany(cascade=CascadeType.ALL, mappedBy="school")
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
// if I use the rows below, the Exception will be: "Exception occurred inside setter of domain.School.students"
//for (Student student : students) {
// student.setSchool(this);
//}
// but without the commented rows above, the exception will instead be:
// "could not initialize a collection: [domain.School.students#1]"
}
public void addStudent(Student student) {
this.students.add(student);
}
}
package domain;
// the imports are not displayed here ...
@Entity
@Table(name="PERSON")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="PERSON_TYPE", discriminatorType=DiscriminatorType.STRING)
public class Person {
public Person() {
this.setPersonType("P");
}
private Integer id;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "PERSON_ID")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
private String personName;
@Column(name = "PERSON_NAME")
public String getPersonName() {
return this.personName;
}
public void setPersonName(String personName) {
this.personName = personName;
}
private String personType;
@Column(name = "PERSON_TYPE")
public String getPersonType() {
return this.personType;
}
public void setPersonType(String personType) {
this.personType = personType;
}
}
package domain;
// the imports are not displayed here ...
@Entity
@Table(name="STUDENT")
@DiscriminatorValue("S") // S = Student
@Inheritance(strategy=InheritanceType.JOINED)
@PrimaryKeyJoinColumn(name="PERSON_ID")
public class Student extends Person {
public Student() {
super.setPersonType("S");
}
private int graduationYear;
@Column(name="GRADUATION_YEAR")
public int getGraduationYear() {
return graduationYear;
}
public void setGraduationYear(int employmentYear) {
this.graduationYear = employmentYear;
}
private School school;
@ManyToOne
@JoinColumn(name="SCHOOL_ID")
public School getSchool() {
return school;
}
public void setSchool(School school) {
this.school = school;
}
}
This is the result when I execute the main method above:
Code:
Hibernate: select school0_.SCHOOL_ID as SCHOOL1_0_0_, school0_.SCHOOL_NAME as SCHOOL2_0_0_ from SCHOOL school0_ where school0_.SCHOOL_ID=?
Harvard
Hibernate: select students0_.SCHOOL_ID as SCHOOL3_1_, students0_.PERSON_ID as PERSON2_1_, students0_.PERSON_ID as PERSON1_1_0_, students0_1_.PERSON_NAME as PERSON2_1_0_, students0_1_.PERSON_TYPE as PERSON3_1_0_, students0_.GRADUATION_YEAR as GRADUATION1_2_0_, students0_.SCHOOL_ID as SCHOOL3_2_0_ from STUDENT students0_ inner join PERSON students0_1_ on students0_.PERSON_ID=students0_1_.PERSON_ID where students0_.SCHOOL_ID=?
Exception in thread "main" org.hibernate.exception.SQLGrammarException: could not initialize a collection: [domain.School.students#1]
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:67)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
at org.hibernate.loader.Loader.loadCollection(Loader.java:2001)
at org.hibernate.loader.collection.CollectionLoader.initialize(CollectionLoader.java:36)
at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:565)
at org.hibernate.event.def.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:63)
at org.hibernate.impl.SessionImpl.initializeCollection(SessionImpl.java:1716)
at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:344)
at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:86)
at org.hibernate.collection.PersistentBag.iterator(PersistentBag.java:249)
at domain.TestKlass.main(TestKlass.java:19)
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'students0_.SCHOOL_ID' in 'field list'
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:406)
at com.mysql.jdbc.Util.getInstance(Util.java:381)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1030)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3491)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3423)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1936)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2060)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2542)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1734)
at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1885)
at org.hibernate.jdbc.AbstractBatcher.getResultSet(AbstractBatcher.java:186)
at org.hibernate.loader.Loader.getResultSet(Loader.java:1787)
at org.hibernate.loader.Loader.doQuery(Loader.java:674)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:236)
at org.hibernate.loader.Loader.loadCollection(Loader.java:1994)
... 8 more
Can anyone tell me what changes I need to do with the annotations to be able to use the type "List<Student>" instead of "List<Person>" in the above code ?
/ Tom