J3 Limited
 
Google
WWW J3Ltd
 
EJB ManyToMany Relationships

Introduction

EJB 3.0 annotations can be used to define relationships between entity EJB's. ManyToMany relationships can cause some confusion at first. This article explains some of the considerations using a simple example.

JBoss 4.0.4 GA, MySQL and Eclipse were used to develop this example. The option of creating and dropping the tables was used (defined in the file persistence.xml). By allowing the EJB container to create and drop the database tables, it is possible to run the application and then inspect the database to see how the entities have been mapped to database tables.

The source code can be downloaded as a zip archive here.

Entities

The following EJB3 entities are coded:

  • Home
  • Person
  • Role
  • UserRole

Home Entity

Home has a many to many relationship with Person. A person can have zero or more homes, and a home can have zero or more persons.

package com.j3ltd.server.ejb;

import java.io.Serializable;
import javax.persistence.*;

import java.util.*;

@Entity
@NamedQueries({
  @NamedQuery(name="findHomeAll", query="SELECT o FROM Home o"),
  @NamedQuery(name="findHomeByStreetAddress", 
      query="SELECT o FROM Home o WHERE o.streetAddress = :streetAddress")
})
public class Home implements Serializable {
  static final long serialVersionUID = 1;  
  
  private int id;
  private String streetAddress;
  private List<Person> persons;
  
  @Id
  @GeneratedValue
  public int getId() {
    return id;
  }
	
  public void setId(int id) {
    this.id = id;
  }
	
  public String getStreetAddress() {
    return streetAddress;
  }
	
  public void setStreetAddress(String streetAddress) {
    this.streetAddress = streetAddress;
  }
  
  @ManyToMany
  @JoinTable(name="HomePersons", 
    joinColumns=
        @JoinColumn(name="homeId", referencedColumnName="id"),
      inverseJoinColumns=
      @JoinColumn(name="personId", referencedColumnName="id")
      )
  public List<Person> getPersons() {
    return persons;
  }
	
  public void setPersons(List<Person> persons) {
    this.persons = persons;
  }
	
  public void addPerson(Person toAdd) {
    this.getPersons().add(toAdd);
  }
	
  public void dropPerson(Person toDrop) {
    this.getPersons().remove(toDrop);
  }
}

Named queries are placed at the start of the class. They are used by the stateless session facade bean. Named queries can be placed in a variety of places, placing them in the entity class on which they operate is one way.

A serialVersionUIDc is given to all entities, otherwise passing a List (or Collection) to the web application client can result in a class cast exception. Each time a change is made to this entity, the version number is incremented.

The primary key id is generated by MySQL.

The many to many relationship ensures that when a person or a home is deleted, no cascade delete is performed. For example, the dropPerson() method only removes the relationship between this home entity and the person. The person entity is not removed.

Person Entity

Person has a one to many relationship with UserRole. A person can have zero or more UserRoles's. A user role has one person associated with it.

package com.j3ltd.server.ejb;

import java.io.Serializable;

import javax.persistence.*;

import java.util.*;

@Entity
@NamedQueries({
  @NamedQuery(name="findPersonAll", query="SELECT o FROM Person o"),
  @NamedQuery(
    name="findPersonByName", query="SELECT o FROM Person o WHERE o.name = :name")
})
public class Person implements Serializable {
  static final long serialVersionUID = 2;
  
  private String name;
  private int id;
  private List<Home> homes;
  private List<UserRole> userRoles;
  
  @ManyToMany(mappedBy="persons") 
  protected List<Home> getHomes() {
    return homes;
  }
  
  protected void setHomes(List<Home> homes) {
    this.homes = homes;
  }
  
  @OneToMany(cascade=CascadeType.ALL, mappedBy="person")
  public List<UserRole> getUserRoles() {
    return userRoles;
  }

  public void setUserRoles(List<UserRole> userRoles) {
    this.userRoles = userRoles;
  }
  
  public void addUserRole(UserRole userRole) {
    this.getUserRoles().add(userRole);
    userRole.setPerson(this);
  }

  @Id
  @GeneratedValue
  public int getId() {
    return id;
  }
	
  public void setId(int id) {
    this.id = id;
  }
	
  public String getName() {
    return name;
  }
	
  public void setName(String name) {
    this.name = name;
  }
}
            

The many to many is defined here for the Home. MappedBy specifies which field in the home returns the persons.

A one to many relationship is defined for the UserRole entity.

It is worth noting that the addUserRole() method adds the user role to its list of user roles, and also calls setPerson() on the user role. Failure to do the latter would cause the UserRole table to have a null value for the person id foreign key.

UserRole Entity

UserRole has a many to one relationship with Role. A Role can have zero or more UserRole's, but a UserRole has only one Role.

package com.j3ltd.server.ejb;

import java.io.Serializable;
import javax.persistence.*;

@Entity
public class UserRole implements Serializable {
  static final long serialVersionUID = 1;
  
  private int id;
  private Role role;
  private Person person;
  
  public UserRole() {
    
  }
  
  public UserRole(Role role) {
    this.role = role;
  }
  
  @ManyToOne
  public Person getPerson() {
    return person;
  }
	
  public void setPerson(Person person) {
    this.person = person;
  }
  
  @Id
  @GeneratedValue
  public int getId() {
    return id;
  }
	
  public void setId(int id) {
    this.id = id;
  }
  
  @ManyToOne(cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
  @JoinColumn(name="roleName", 
        referencedColumnName="roleName")
  public Role getRole() {
    return role;
  }
	
  public void setRole(Role role) {
    this.role = role;
  }
}

Role Entity

package com.j3ltd.server.ejb;

import java.io.Serializable;
import javax.persistence.*;
import java.util.*;

@Entity
public class Role implements Serializable {
  static final long serialVersionUID = 1;
  
  private String roleName;
  private String roleDescription;
  private List<UserRole> userRoles;
  
  public String getRoleDescription() {
    return roleDescription;
  }
  public void setRoleDescription(String roleDescription) {
    this.roleDescription = roleDescription;
  }
  
  @Id
  public String getRoleName() {
    return roleName;
  }
	
  public void setRoleName(String roleName) {
    this.roleName = roleName;
  }
  
  @OneToMany(mappedBy="role")
  public List<UserRole> getUserRoles() {
    return userRoles;
  }
	
  public void setUserRoles(List<UserRole> userRoles) {
    this.userRoles = userRoles;
  }
}

Entity Relationships

In essence the Home - Person is a many to many relationship, where the link table is left to the EJB container to work out. The Person - Role relationship, is also a many to many relationship, but in this case, the link table is implemented manually, and the relationships are broken down using one to many and many to one relationships.

Below is the diagram of the resulting database tables.

Stateless Session Facade

A stateless session bean is coded, this bean is used by the client to perform the database operations.

package com.j3ltd.server.ejb;

import javax.ejb.*;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import java.util.*;

@Stateless public class StatelessEJBBean implements StatelessEJB {
  @PersistenceContext(unitName="EJBRelationshipsPU") EntityManager em;
  
  public List<Person> getAllPersons() {
    ArrayList<Person> toReturn = new ArrayList<Person>();
    Query q = em.createNamedQuery("findPersonAll"); 
    for (Object po : q.getResultList()) {
      toReturn.add((Person) po);
    }
    return toReturn;
  }
  
  public List<Home> getAllHomes() {
    ArrayList<Home> toReturn = new ArrayList<Home>();
    Query q = em.createNamedQuery("findHomeAll");
    for (Object ho : q.getResultList()) {
      toReturn.add((Home) ho);
    }
    return toReturn;
  }
  
  /**
   * Client can't call getHomes() on person, as it's detached &
   * lazilly fetched
   */
  public List<Home> getPersonHomes(Person person) {
    em.refresh(person);
    List<Home> homes = person.getHomes();
    ArrayList<Home> toReturn = new ArrayList<Home>(homes.size());
    for (Home home : homes) {
      toReturn.add(home);
    }
    return toReturn;
  }
  /**
   * Client can't call getPersons() on home, as it's detached &
   * lazilly fetched
   */  
  public List<Person> getHomePersons(Home home) {
    em.refresh(home);
    List<Person> persons = home.getPersons();
    ArrayList<Person> toReturn = new ArrayList<Person>(persons.size());
    for (Person person : persons) {
      toReturn.add(person);
    }
    return toReturn;
  }
  
  public List<UserRole> getPersonRoles(Person person) {
    em.refresh(person);
    List<UserRole> roles = person.getUserRoles();
    ArrayList<UserRole> toReturn = new ArrayList<UserRole>(roles.size());
    for (UserRole role : roles) {
      toReturn.add(role);
    }
    return toReturn;    
  }
  
  public void createTestData() {
    
    // Create the role entities
    Role pm = new Role();
    pm.setRoleName("pm");
    pm.setRoleDescription(("Prime Minister"));
    em.persist(pm);
    
    Role pres = new Role();
    pres.setRoleName("pres");
    pres.setRoleDescription("President");
    em.persist(pres);
    
    Role vp = new Role();
    vp.setRoleName("vp");
    vp.setRoleDescription("Vice President");
    em.persist(vp);
    
    // create home 10 downing street 
    Home home = new Home();
    home.setStreetAddress("10 Downing Street");
    em.persist(home);
    // need to refresh otherwise home.addPerson() fails: 
    // persons List member is null.
    em.refresh(home);
    
    // create mr blair
    Person person = new Person();
    person.setName("Mr Blair");
    em.persist(person);
    // add user role pm
    UserRole userRole = new UserRole(pm);
    em.persist(userRole);
    em.refresh(person);
    person.addUserRole(userRole);
    // add user role pres
    userRole = new UserRole(pres);
    em.persist(userRole);
    person.addUserRole(userRole);
    home.addPerson(person);
    
    // create Mr Brown
    person = new Person();
    person.setName("Mr Brown");
    em.persist(person);  
    em.refresh(person);
    home.addPerson(person);
    // add user role vp
    userRole = new UserRole(vp);
    em.persist(userRole);
    person.addUserRole(userRole);
    
    // create 11 downing street
    home = new Home();
    home.setStreetAddress("11 Downing Street");
    em.persist(home);
    em.refresh(home);
    // add current person Mr Brown
    home.addPerson(person);
    
    // create whitehouse
    home = new Home();
    home.setStreetAddress("Whitehouse, Washington DC");
    em.persist(home);
    em.refresh(home);
    // add Mr Bush
    person = new Person();
    person.setName("Mr Bush");
    em.persist(person);
    em.refresh(person);
    // Add user role pres
    userRole = new UserRole(pres);
    em.persist(userRole);
    person.addUserRole(userRole);
    home.addPerson(person);
    // create mr Cheney
    person = new Person();
    person.setName("Mr Cheney");
    em.persist(person);  
    em.refresh(person);
    // add user role vp
    userRole = new UserRole(vp);
    em.persist(userRole);
    person.addUserRole(userRole);
    home.addPerson(person);
    

    // create Élisée Palace
    home = new Home();
    home.setStreetAddress("Élisée Palace");
    em.persist(home);
    em.refresh(home);
    // create Mr Chirac
    person = new Person();
    person.setName("Mr Chirac");
    em.persist(person);
    em.refresh(person);
    // add user role pres
    userRole = new UserRole(pres);
    em.persist(userRole);
    person.addUserRole(userRole);
    home.addPerson(person);
  }
  
  public void deleteSomeData() {
    
    // remove 11 downing street
    Query q = em.createNamedQuery("findHomeByStreetAddress");
    q.setParameter("streetAddress", "11 Downing Street");
    List list = q.getResultList();
    for (Object home : list) {
      em.remove(home);
    }
    
    // remove Mr blair
    q = em.createNamedQuery("findPersonByName");
    q.setParameter("name", "Mr Blair");
    list = q.getResultList();
    for (Object person : list) {
      removePerson((Person)person);
    }   
    
    // remove Mr Chirac
    q.setParameter("name", "Mr Chirac");
    list = q.getResultList();
    for (Object person : list) {
      removePerson((Person)person);
    }
  }
  
  /**
   * To remove a Person record, the person must be removed from
   * all the homes first. Otherwise a foreign key constraint
   * is thrown when removing the person.
   *  
   * @param toRemove the person to remove.
   */
  private void removePerson(Person toRemove) {
    List<Home> homes = toRemove.getHomes();
    for (Home home : homes) {
      home.dropPerson(toRemove);
    }
    em.remove(toRemove);
  }
}

Servlet

A servlet is coded to act as a test client.

package com.j3ltd.web;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.j3ltd.server.ejb.*;

public class RelationshipServlet  extends HttpServlet {
  private StatelessEJB statelessBean;
  
  public void init() throws ServletException {
    try {
      Context context = new InitialContext();
      statelessBean = (StatelessEJB) context.lookup("StatelessEJBBean/remote");
    } catch (NamingException e) {
      e.printStackTrace();
    }
  }
  
  public void doPost(HttpServletRequest req, HttpServletResponse resp) 
                     throws ServletException, IOException {
    doGet(req, resp);
  }
  
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
                    throws ServletException, IOException {
    PrintWriter writer = resp.getWriter();
    writer.write("Create test data...");
    statelessBean.createTestData();
    writer.write("done\n\n");
    displayPeople(resp);
    displayHomes(resp);
    writer.write("\nDelete some data...");
    statelessBean.deleteSomeData();    
    writer.write("done\n\n");
    displayPeople(resp);
    displayHomes(resp);  
  }
  
  private void displayPeople(HttpServletResponse resp) throws IOException {
    PrintWriter writer = resp.getWriter();
    writer.write("List of Person ejbs:\n");
    List<Person> people = statelessBean.getAllPersons();
    for (Person person : people) {
      writer.write("Person retrieved: " + person.getName() + "\n");
      List<UserRole> roles = statelessBean.getPersonRoles(person);
      if (roles != null) {
        writer.write("   role: ");
        for (UserRole role : roles) {
          writer.write(role.getRole().getRoleName() + " ");
        }
        writer.write("\n");
      }
      List<Home> homes = statelessBean.getPersonHomes(person);
      if (homes != null) {
        for (Home home : homes) {
          writer.write("   Home: " + home.getStreetAddress() + "\n");
        }
      }
    }    
  }
  
  private void displayHomes(HttpServletResponse resp) throws IOException {  
    PrintWriter writer = resp.getWriter();
    writer.write("\nList of Home ejbs:\n");
    List<Home> homes = statelessBean.getAllHomes();
    for (Home home : homes) {
      writer.write("Home retrieved: " + home.getStreetAddress() + "\n");
      List<Person> people = statelessBean.getHomePersons(home);
      if (people != null) {
        for (Person person : people) {
          writer.write("   Person: " + person.getName() + "\n");
          List<UserRole> roles = statelessBean.getPersonRoles(person);
          if (roles != null) {
            writer.write("      role: ");
            for (UserRole role : roles) {
              writer.write(role.getRole().getRoleName() + " ");
            }
            writer.write("\n");
          }          
        }
      }
    }    
  }
}

Packaging And Deployment

The project is created as an EJB3 JBoss IDE project.:

The libraries should look like the following:

The J2EE 1.5 JBoss IDE library was added to the project, so that servlets are supported.

The packaging configuration creates a jar file for the EJB's and a war file for the web client.

In MySQL a database schema is created. See here, for an overview of this process. The database is called ejbrelationships.

A datasource file called ejbrelationships-ds.xml is created, and saved in the jboss/server/default/deploy folder

<datasources>
  <local-tx-datasource>
    <jndi-name>EJBRelationshipsDS</jndi-name>
    <connection-url>jdbc:mysql://192.168.0.8:3306/ejbrelationships</connection-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>
    <user-name>user</user-name>
    <password>password</password>
    <metadata>
        <type-mapping>mySQL</type-mapping>
    </metadata>      
  </local-tx-datasource>
</datasources>

The project has a file called persistence.xml, which is used by the EJB's to connect to the datasource.

<?xml version="1.0" encoding="UTF-8"?>

<persistence>
  <persistence-unit name="EJBRelationshipsPU">
    <jta-data-source>java:/EJBRelationshipsDS</jta-data-source>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
      <property name="hibernate.hbm2ddl.auto"
                value="create-drop"/>
    </properties>    
  </persistence-unit>
</persistence>

The property hibernate.hbm2ddl.auto has a value of create-drop. This informs JBoss to create the database tables on deployment, and to drop them, when the application is undeployed.

The web application deployment descriptor, web.xml is shown below.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
  xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation=
   "http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  <servlet>
    <servlet-name>EJBRelationships</servlet-name>
    <servlet-class>com.j3ltd.web.RelationshipServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>EJBRelationships</servlet-name>
    <url-pattern>/relationships</url-pattern>
  </servlet-mapping>
</web-app>

When the url http://localhost:8080/EJBRelationships/relationships is opened in a browser, the following output is received:

Create test data...done

List of Person ejbs:
Person retrieved: Mr Blair
   role: pm pres 
   Home: 10 Downing Street
Person retrieved: Mr Brown
   role: vp 
   Home: 10 Downing Street
   Home: 11 Downing Street
Person retrieved: Mr Bush
   role: pres 
   Home: Whitehouse, Washington DC
Person retrieved: Mr Cheney
   role: vp 
   Home: Whitehouse, Washington DC
Person retrieved: Mr Chirac
   role: pres 
   Home: Élisée Palace

List of Home ejbs:
Home retrieved: 10 Downing Street
   Person: Mr Blair
      role: pm pres 
   Person: Mr Brown
      role: vp 
Home retrieved: 11 Downing Street
   Person: Mr Brown
      role: vp 
Home retrieved: Whitehouse, Washington DC
   Person: Mr Bush
      role: pres 
   Person: Mr Cheney
      role: vp 
Home retrieved: Élisée Palace
   Person: Mr Chirac
      role: pres 

Delete some data...done

List of Person ejbs:
Person retrieved: Mr Brown
   role: vp 
   Home: 10 Downing Street
Person retrieved: Mr Bush
   role: pres 
   Home: Whitehouse, Washington DC
Person retrieved: Mr Cheney
   role: vp 
   Home: Whitehouse, Washington DC

List of Home ejbs:
Home retrieved: 10 Downing Street
   Person: Mr Brown
      role: vp 
Home retrieved: Whitehouse, Washington DC
   Person: Mr Bush
      role: pres 
   Person: Mr Cheney
      role: vp 
Home retrieved: Élisée Palace

 

 

 

  Copyright © 2006 J3 Ltd Permission is granted to reproduce material on this page, on the condition that a reference to "WWW.J3Ltd.com" is given as the source of the material.