|
Of all of the possible association mappings, probably the most difficult and confusing is the bi-directional, many to many relationship that we are going to look at now. Don't fret though, Hibernate makes mapping a bi-directional, many to many relationship about as easy as it could possibly get. This is a big step forward for database persistence, as only a few years ago, many of the big vendor's container mapping tools actually ran out of steam when it came to properly managing complex many to many relationships.
Many to many relationships happen fairly frequently in enterprise applications, making it a fairly common pattern. For this tutorial, I'm going to rather glibly model the relationship between a Student and a college or university Course.
Conceptually, the student-course, many to
many relationship, is fairly easy to conceptualize. For example, a
single student named Bart could be in several college courses, such as
Java-101, English-101 and UML-101. So, a student can be in many
courses.
At the same time, a course, such as Java-101, can also have many students. Not only could Bart be enrolled in Java-101, but so could Kerri and Samantha and Leslie, etc. So, since the many relationship goes both ways, we call this a many to many relationship.
One of the challenges of dealing with many to many relationships is the fact that they manifest themselves in such a specific way at the database level, as compared to their implementation at the Java programming level. In a Java program, two classes maintaining a many to many relationship with each other simply maintain a list or collection of the corresponding class. So, with an example such as the Student has a many to many relationship with a Course, a student would maintain a collection listing the courses in which they are enrolled, and a course would maintain a collection or a list of attending students. However, at the database level, the implementation is much different.
At the database level, a many to many relationship between two classes is maintained with a third table that is known as a join table. Each table participating in the many to many relationship, which in this case would be a course and a student table, maintains its own primary key. However, any relationships between the two tables is maintained by a third, join table, where matching, or intersecting primary keys of course-student matches, are maintained. Again, the contrast between how many to many relationships are implemented in Java code, as opposed to how they are implemented at the database level, is a complicating factor. However, Hibernate does a pretty good job at simplifying the situation, allowing Java developers to concentrate on their code, while allowing the underlying database structure to elute naturally from the Java domain model.
A Left and Right Simplification
When mapping and working with a many to many relationship, one class in the relationship needs to make reference to the other, and vice-versa. This back and forth referencing can often be confusing. I always find it easier to conceptualize a many to many relationship with one class being on the left, one class being on the right, and the third, join class, placed in the middle. For this chapter, I am going to graph the class and table relationships in a left-right-middle manner; I'm even going to name the classes accordingly. The class names I will use in this chapter are LeftManyStudent and RightManyCourse, and the join table will simply be named join_table.
Also, it should be noted that there is no Java class that represents the join table, although there will be a leftmanystudent and rightmanycourse table to map to the LeftManyStudent and RightManyCourse Java classes.
Tables and Joins
Class Diagram
Database Entity Mappings
LeftManyStudent and RightManyCourse
Classes
Because the annotation used for mapping a many to many relationship is probably the most complicated of them all, I'm going to keep the two classes we have here as simple as possible.
The LeftManyStudent class will have an id field of type long to track the primary key of the class, along with a simple String property called studentName. Furthermore, the LeftManyStudent class will have a property of type List named courses that keeps track of the many courses in which a student might be enrolled.
Similarly, the RightManyCourse class will have an id of type long, a String property called courseCode, and an object of type List to keep track of the many students enrolled in the course.
LeftManyStudent with the Initial
Annotations
package com.examscam.mappings;
import java.util.*;
import javax.persistence.*;
@Entity
public class LeftManyStudent {
long id;
String studentName;
List<RightManyCourse> courses = new Vector();
@Id
@GeneratedValue
public long getId() {return id;}
public void setId(long id) {this.id = id;}
public List<RightManyCourse> getCourses(){return courses;}
public void setCourses(List<RightManyCourse> righties) {
this.courses = righties;
}
public String getStudentName() {return studentName;}
public void setStudentName(String s) {studentName = s;}
}
RightManyCourse with the Initial
Annotations
package com.examscam.mappings;
import java.util.*;import javax.persistence.*;
@Entity
public class RightManyCourse {
long id;
String courseCode;
List<LeftManyStudent> students = new Vector();
public List<LeftManyStudent> getStudents() {return students;}
@Id
@GeneratedValue
public long getId() {return id;}
public void setId(long id) {this.id = id;}
public String getCourseCode() {return courseCode;}
public void setCourseCode(String courseCode) {
this.courseCode = courseCode;
}
public void setStudents(List<LeftManyStudent> lefties) {
this.students = lefties;
}
}
Student and Course Classes as Entities
Both the LeftManyStudent and RightManyCourse classes are individual entities whose state will be mapped back to respective database tables. As a result, both the Student and Course classes must have the required @Entity decoration before the class declaration, and the standard @Id and @GeneratedValue annotations before the getId() method. Of course, these are the standard annotations that we've been throwing on just about every JPA annotated POJO that we have created. What makes the Student and Course relationship interesting is the bi-directional many-to-many mapping that exists between them.
The fact that a Student can be enrolled in many Courses manifests itself in the form of a java.util.List named courses in the LeftManyStudent class. Subsequently, the getCourses() method in the LeftManyStudent class that returns the List of RightManyCourse instances must be decorated with the @ManyToMany annotation. Of course, this is a bi-directional, many to many relationship, with a join table that needs to be used to keep track of the relationship in the database. As a result, the getCourses() method not only needs the @ManyToMany annotation, but it needs the @JoinTable annotation to describe the mapping of the details of the Student and Course instances into the join table. The @JoinTable is a little intimidating the first time you see it in a bi-directional, many-to-many join. For now, just take a look at how the getCourses() method is decorated; an explanation will follow.
public class LeftManyStudent {
@ManyToMany
@JoinTable(name = "join_table",
joinColumns = { @JoinColumn(name = "lmstudent_id")},
inverseJoinColumns={@JoinColumn(name="rmcourse_id")}
)
public List <RightManyCourse> getCourses() {
return courses;
}
}
Documenting the Join Table
The manner in which the @JoinTable uses two nested @JoinColumn tags within the annotation is a little scary, but when you think about it, it actually makes a lot of sense.
To map a many to many relationship in a database, you need a join table. The join table we are going to use is simply named join_table, thus the sensible beginning of the @JoinTable annotation:
@JoinTable( name = "join_table", )
Of course, we are currently coding this annotation in the LeftManyStudent class, and the @JoinTable tag wants to know the name of the column in the join_table that will be used to store the primary key of the LeftManyStudent instance. We'll use the letters ?l' and ?m' plus the word student, followed by an _id as the name of the column to store the LeftManyStudent id. Put it together, and you get lmstudent_id, and the first @JoinColumn part of the @JoinTable tag looks like this:
@JoinTable(name = "join_table",
joinColumns={@JoinColumn(name= "lmstudent_id")},
)
Of course, the first @JoinColumn annotation only tells Hibernate how to save the primary key of the LeftManyStudent class, not how to find out the name of the column that maps the Student to it's associated RightManyCourse. So, to help Hibernate process the inverse, Course to Student relationship, we add a second @JoinColumn annotation, but associate it with the inverseJoinColumns attribute of the @JoinTable annotation:
@ManyToMany
@JoinTable(name = "join_table",
joinColumns={ @JoinColumn(name="lmstudent_id")},
inverseJoinColumns
={ @JoinColumn (name = "rmcourse_id") } )
Mapping Both Side of the Many To Many
After mastering the @JoinTable annotation on the left hand side of the many-to-many relationship, understanding the right hand side of the relationship is a lead pipe cinch. Basically, it's the same, while at the same time, it's a little different.
Let's look at the LeftManyStudent mapping for its side of the many to many relationship:
public class LeftManyStudent {
@ManyToMany
@JoinTable(name = "join_table",
joinColumns = { @JoinColumn(name = "lmstudent_id")},
inverseJoinColumns={@JoinColumn(name="rmcourse_id")})
public List<RightManyCourse> getCourses(){return courses;}
}
 Now, take a look at the RightManyCourse side of the many to many relationship.
public class RightManyCourse {
@ManyToMany
@JoinTable(name = "join_table",
joinColumns={@JoinColumn(name="rmcourse_id")},
inverseJoinColumns={@JoinColumn(name="lmstudent_id")})
public List<LeftManyStudent> getStudents(){return students;}
}
Comparing the Many Sides
You will notice that the @JoinTable annotation in the RightManyCourse class only differs from the @JoinTable annotation in the LeftManyStudent class in the ordering of the lmstudent_id and the rmcourse_id fields from the inverseJoinColumns and joinColumns attributes of the @JoinTable annotation. This obvious similarity makes sense, as both the LeftManyStudent and RightManyCourse classes, while on opposite sides, are participating in the same many to many relationship.
It's also worth noting that the values of lmstudent_id and rm_courseid, which are used in the @JoinTable annotation, do not actually manifest themselves as instance variables anywhere in the Java code. The JoinColumns are purely a manifestation of how a database, not a Java program, manages a many to many relationship.
Completed LeftManyStudent with Full
Annotations
package com.examscam.mappings;
import java.util.*;
import javax.persistence.*;
@Entity
public class LeftManyStudent {
long id;
String studentName;
List<RightManyCourse> courses = new Vector();
@ManyToMany
@JoinTable(name = "join_table",
joinColumns = { @JoinColumn(name = "lmstudent_id")},
inverseJoinColumns={@JoinColumn(name="rmcourse_id")}
)
public List<RightManyCourse> getCourses(){
return courses;
}
public void setCourses(List<RightManyCourse> righties){
this.courses = righties;
}
@Id
@GeneratedValue
public long getId() {return id;}
public void setId(long id) {this.id = id;}
public String getStudentName() {
return studentName;
}
public void setStudentName(String s){
studentName=s;
}
}Completed RightManyCourse with the Full
Annotations
package com.examscam.mappings;
import java.util.*;
import javax.persistence.*;
@Entity
public class RightManyCourse {
long id;
String courseCode;
List<LeftManyStudent> students = new Vector();
@ManyToMany
@JoinTable(name = "join_table",
joinColumns={@JoinColumn(name="rmcourse_id")},
inverseJoinColumns={@JoinColumn(name="lmstudent_id")})
public List<LeftManyStudent> getStudents() {
return students;
}
public void setStudents(List<LeftManyStudent> lefties){
this.students = lefties;
}
@Id
@GeneratedValue
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getCourseCode() {
return courseCode;
}
public void setCourseCode(String courseCode) {
this.courseCode = courseCode;
}
}
Testing the Many to Many Mapping
As with all Hibernate mappings that leverage JPA annotations, the classes in question must be added to Hibernate's AnnotationConfiguration object in the central spot where the Hibernate Configuration is initialized.
AnnotationConfiguration config =
new AnnotationConfiguration();
config.addAnnotatedClass(LeftManyStudent.class);
config.addAnnotatedClass(RightManyCourse.class);
config.configure();
Testing the Many to Many Mapping
To test the many to many mappings, we need to create instances of Student and Course objects. So, a Student object might be created like this:
LeftManyStudent student01 = new LeftManyStudent();
student01.setStudentName("Jim Jump");
A course object would be created like this:
RightManyCourse java101 = new RightManyCourse();
java101.setCourseCode("Java-101");
And once a student and a course exists, we can associate them with each other. So, we can have student01 enrolled in java101 by executing the following code:
java101.getStudents().add(student01);
To save this relationship to the database, we need to have each entity ?touch' the Hibernate session within the scope of a transaction. Using our HibernateUtil class, here's how we'd persist the fact that student01 is enrolled in java101.
Session session = HibernateUtil.beginTransaction();
session.save(student01);
session.save(java101);
HibernateUtil.commitTransaction();
A Full Many-to-Many Test Method
The following main method, when executed, will create many students, and have them associated with various courses. See if you can keep track of who is enrolled in which course ? the initials of the student's first and last name is a big hint.
public static void main (String args[]) {
HibernateUtil.recreateDatabase();
LeftManyStudent student01 = new LeftManyStudent();
student01.setStudentName("Jim Jump");
LeftManyStudent student02 = new LeftManyStudent();
student02.setStudentName("Julie Camp");
LeftManyStudent student03 = new LeftManyStudent();
student03.setStudentName("Cam Johnson");
LeftManyStudent student04 = new LeftManyStudent();
student04.setStudentName("Marcus McKenzie");
RightManyCourse java101 = new RightManyCourse();
java101.setCourseCode("Java-101");
RightManyCourse cplus101 = new RightManyCourse();
cplus101.setCourseCode("C++ - 101");
RightManyCourse math101 = new RightManyCourse();
math101.setCourseCode("Math - 101");
java101.getStudents().add(student01);
java101.getStudents().add(student02);
java101.getStudents().add(student03);
cplus101.getStudents().add(student02);
cplus101.getStudents().add(student03);
math101.getStudents().add(student04);
Session session = HibernateUtil.beginTransaction();
session.save(student01);
session.save(student02);
session.save(student03);
session.save(student04);
session.save(java101);
session.save(cplus101);
session.save(math101);
HibernateUtil.commitTransaction();
}
Running the Test Method
When Hibernate commits the transaction in the main method that tests the many to many mapping, it will spit out some interesting SQL, so long as you have enabled the hibernate.show_sql property in your hibernate.cfg.xml file. Here's the SQL that Hibernate generates:
Hibernate:
insert into LeftManyStudent (studentName) values (?)
Hibernate:
insert into LeftManyStudent (studentName) values (?)
Hibernate:
insert into LeftManyStudent (studentName) values (?)
Hibernate:
insert into LeftManyStudent (studentName) values (?)
Hibernate:
insert into RightManyCourse (courseCode) values (?)
Hibernate:
insert into RightManyCourse (courseCode) values (?)
Hibernate:
insert into RightManyCourse (courseCode) values (?)
Hibernate:
insert into join_table (rmcourse_id, lmstudent_id) values (?, ?)
Hibernate:
insert into join_table (rmcourse_id, lmstudent_id) values (?, ?)
Hibernate:
insert into join_table (rmcourse_id, lmstudent_id) values (?, ?)
Hibernate:
insert into join_table (rmcourse_id, lmstudent_id) values (?, ?)
Hibernate:
insert into join_table (rmcourse_id, lmstudent_id) values (?, ?)
Hibernate:
insert into join_table (rmcourse_id, lmstudent_id) values (?, ?)
Simply by annotating our Java classes with the appropriate @ManyToMany annotations, and corresponding @JoinTable tags, Hibernate knows how to manage the primary keys of the classes participating in a many to many join, and it knows how to subsequently map those primary keys to a corresponding join table in order to permanently persist the relationship between instances.
Viewing the Results
After running the test method, viewing the contents of the database reveals three tables that have been populated with data, namely the leftmanystudent, rightmanycourse and the join_table.
As you can see, individual students are still saved in the leftmanystudent table, and courses are stored in the rightmanycourse table. However, the fact that Julie Camp (id of 2)` is enrolled in Java (id of 1) 101 and C++ 101 (id of 2) is maintained by the join table, as it maintains the matching primary keys of entities in the student and course tables. The key lines of code that map Julie to the Java 101 and C++ 101 courses is below:
LeftManyStudent student02 = new LeftManyStudent();
student02.setStudentName("Julie Camp");
RightManyCourse java101 = new RightManyCourse();
java101.setCourseCode("Java-101");
RightManyCourse cplus101 = new RightManyCourse();
cplus101.setCourseCode("C++ - 101");
java101.getStudents().add(student02);
cplus101.getStudents().add(student02);
session.save(student02);
session.save(java101);
session.save(cplus101);
|