Map Relations and Compound Primary Keys
Resin 3.0

Features
Installation
Configuration
Web Applications
IOC/AOP
Resources
JSP
Servlets and Filters
Portlets
Databases
Admin (JMX)
CMP
EJB
Amber
EJB 3.0
Security
XML and XSLT
XTP
JMS
Performance
Protocols
Third-party
Troubleshooting/FAQ

User's Guide
Reference
Tutorials
Scrapbook

Basic CMP
Find with EJB-QL
Creating and Removing
1-n Relationship
ejbSelect EJB-QL
1-1 Relationship
n-m Relationship
Map/Compound PK
n-m Relationship
Tutorials
Scrapbook

This tutorial introduces compound primary keys and map relations. Both were initially supported in Resin-CMP 1.0.4. Applications can take advantage of these more advanced CMP features to more cleanly model database tables.

  1. Database Schema
  2. Client Servlet using Map
  3. Compound Primary Keys
  4. Conclusion

Compound primary keys are used for beans with two or more fields as the primary key. A compound primary key is implemented by creating a special key class containing the fields.

Map relations are allowed for entity beans with a primary key pair where one of the primary keys is an identifying relation. The child bean's identifying relation implicitly selects the parent bean and the other key is used as the Map relation's key field.

An identifying relation with a two-field compound primary key can support a map relation. In this example, each Student has a Grade for each Course he takes. The Grade bean has two primary keys: the Student and the Course. The two keys are identifying relations since they refer directly to the Student and Grade objects, not indirectly through their keys.

Part of the the primary key class looks like:

public class GradeKey {
  public Student student;
  public Course course;
  ...

The read-only getGrades map relation belongs to the Student bean. The map's key is the Course object (not the key for the Course.) Resin-CMP will use the Student object as the student field of the composite key, and it will use a Course argument to the Map.get(course) call as the other key. Since a Student and a Course uniquely identify a Grade, Resin-CMP can use the combined key to look up the Grade object.

public interface Student extends EJBLocalObject {
  String getName();
 
  java.util.Map getGrades();
}

The map relation is read-only since its data comes from the Grade table. In other words, it makes no sense to call getGrades().put(...). Since Grades beans are created with ejbCreate and destroyed with ejbRemove, it doesn't make sense for the Map to be writable.

The Map is a live object. In other words, when a new Grade is created or an old one is destroyed, the Map object is automatically updated. Of course, the Map needs to be used in the same transaction as the getGrades() call.

Database Schema

The database has tables for Students, Courses, and Grades. The Grade table is dependent on both the Students and the Courses. Because of this dependency, Resin-CMP will automatically delete a student's grades when the student is removed.

select.sql
CREATE TABLE map_students (
  name VARCHAR(250) NOT NULL,

  PRIMARY KEY(name)
);

CREATE TABLE map_courses (
  name VARCHAR(250) NOT NULL,

  PRIMARY KEY(name)
);

CREATE TABLE map_grades (
  student VARCHAR(250) NOT NULL REFERENCES map_students(name),
  course VARCHAR(250) NOT NULL REFERENCES map_courses(name),

  grade VARCHAR(10),

  PRIMARY KEY(student, course)
);

The Student's map relation is implemented with one main SELECT query for the keySet(). The get() method is implemented by calling GuestHome's findByPrimaryKey method. The keySet() returns a set of Course beans and the query looks like:

The get(Object) method takes a Course argument and returns a Grade object. The query looks like:

getGrades.get(Course) query
SELECT g.course FROM map_grades AS g WHERE g.student=?

Client Servlet using Map

The example client servlet uses the Map like any other java.util.Map. The servlet iterates over all the students and displays their grades. The Courses are found using the getGrades().keySet() iterator and then the Grade itself is found using the getGrades().get(Object) map.

public void service(HttpServletRequest req, HttpServletResponse res)
  throws java.io.IOException, ServletException
{
  res.setContentType("text/html");

  PrintWriter out = res.getWriter();

  // for all students, print their grades.
  Collection students = null;
  try {
    students = studentHome.findAll();
  } catch(javax.ejb.FinderException e) {
    throw new ServletException(e);
  }

  out.println("<title>Student Grades</title>");
  out.println("<h1>Student Grades</h1>");

  Iterator iter = students.iterator();
  while (iter.hasNext()) {
    Student student = (Student) iter.next();

    out.println("<h3>" + student.getName() + "</h3>");

    Map grades = student.getGrades();
      
    out.println("<table>");
    out.println("<tr><th>Course<th>Grade");

    Iterator courseIter = grades.keySet().iterator();
    while (courseIter.hasNext()) {
      Course course = (Course) courseIter.next();

      Grade grade = (Grade) grades.get(course);

      out.print("<tr><td>" + course.getName() + "<td>" + grade.getGrade());
    }
    out.println("</table>");
  }
}

<h3>Harry Potter</h3>
<tr><td>Course         <td>Grade
<tr><td>Potions        <td>C-
<tr><td>Transfiguration<td>B+
<h3>Hermione Granger</h3>
<tr><td>Course         <td>Grade
<tr><td>Potions        <td>A
<tr><td>Transfiguration<td>A+
<h3>Ron Weasley</h3>
<tr><td>Course         <td>Grade
<tr><td>Potions        <td>C+
<tr><td>Transfiguration<td>B

Compound Primary Keys

A compound primary key is made up of a combination of persistent fields or identifying relations. This example uses two identifying relations: Student and Course. Only the home interface is different for a compound primary key bean. The local interface and the bean implementation are just like single key beans.

Compound primary keys may have any number of fields. The special case of two fields with at least on being an identifying relation allows for java.util.Map collections in the parent map, like the getGrades() method in the Student bean. The identifying key corresponding to the bean, e.g. Student, is used to select the grades. The second key, in this case Course, is used as the keySet(). The bean itself is the value.

Grade.java
package example.cmp.map;

public interface Grade extends javax.ejb.EJBLocalObject {
  public Student getStudent();
  public Course getCourse();

  public String getGrade();
}

The bean key is a public class with public fields for each key component. In this case, the GradeKey class has both a public field for the student and the Course.

The compound key class must override the equals and the hashCode methods. If all the component keys are identical, the keys must be identical, even if they are separate Java objects. Resin-CMP needs both hashCode and equals for the bean's caching.

GradeKey.java
package example.cmp.map;

public class GradeKey {
  public Student student;
  public Course course;
  
  public GradeKey()
  {
  }
  
  public GradeKey(Student student, Course course)
  {
    this.student = student;
    this.course = course;
  }

  public boolean equals(Object obj)
  {
    if (! (obj instanceof GradeKey))
      return false;

    GradeKey key = (GradeKey) obj;

    return student.equals(key.student) && course.equals(key.course);
  }

  public int hashCode()
  {
    return 65521 * student.hashCode() + course.hashCode();
  }
}

Conclusion

Many database entities need to store named attributes, e.g. a Student needs to store her Grades, indexed by the Course she took. By implementing this common pattern directly with the standard Map API, and avoiding working directly with JDBC, applications using Resin-CMP can create cleaner Java code, avoid common maintenance issues during development, and take automatic advantage of database caching.


n-m Relationship
Tutorials
Scrapbook
Copyright © 1998-2005 Caucho Technology, Inc. All rights reserved.
Resin® is a registered trademark, and HardCoretm and Quercustm are trademarks of Caucho Technology, Inc.