Thursday, 6 August 2009

Relations made simpler with JPA

One of the differences between rational databases and object oriented programming (OOP) is the way data elements are represented. To handle this difference, additional code (sometimes referred to as data access objects) that translates from records to objects and vice-versa has to be produced and maintained in order for an application to communicate with the database.

This article illustrates how JSR-220, or as better known Java Persistence API (JPA), can help reducing the amount of code and effort required to synchronise both worlds.

In order to complete the examples illustrated here, the following jar files have to be in the classpath: toplink-essentials.jar; toplink-essentials-agent.jar; and derby.jar. These can be downloaded from: Oracle Toplink JPA and JavaDB respectively. These examples can be executed on databases other than the JavaDB (formerly known as Derby). To execute them on other databases you need the appropriate JDBC jar file.

A simple address book application (with no graphical user interface) will be used to illustrate the examples discussed here. The application data is made from four classes: Address; Person; Contact; and Group. The following figure illustrates the relations between these classes.


Figure 1. Relation between Classes

The counter tables are illustrated in the following figure


Figure 2. Relation between Classes

The above relations are summarised below

  • One or more persons can be living in the same address
  • Persons can be grouped together
  • A person can be in more than one group
  • A person has a set of contacts (numbers, emails)
  • A person has a main contact
  • A contact cannot be shared between two persons

Note that even in a simple application like this, the number of classes and tables required to represent the data in the respective format are different. This adds to the complexity required to translate between both worlds.

Setting up the Database

The SetupDatabase class, listed below, can be used to setup the database referred to from this article. It first drops all foreign keys (using the alter command) and tables and then re-creates everything. The altering and dropping processes will produce errors when executed for the first time as the tables in question are not available. Finally it populates them with some default data. Run the SetupDatabase class twice to make sure that everything is in place. The second time round should produce no errors. Note that this class requires the jar file derby.jar (or the libraries of your database) in the classpath.

package x2x;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;

public class SetupDatabase {

  private static String[] buildDatabase = {
      "ALTER TABLE CONTACTS DROP CONSTRAINT CONTACT_PERSON_1",
      "ALTER TABLE PERSONS DROP CONSTRAINT PERSON_ADDRESS_1",
      "ALTER TABLE PERSONS DROP CONSTRAINT PERSON_CONTACT_1",
      "ALTER TABLE PERSONS_GROUPS DROP CONSTRAINT PERSON_GROUP_1",
      "ALTER TABLE PERSONS_GROUPS DROP CONSTRAINT PERSON_GROUP_2",

      "DROP TABLE ADDRESSES",
      "DROP TABLE CONTACTS",
      "DROP TABLE GROUPS",
      "DROP TABLE PERSONS_GROUPS",
      "DROP TABLE PERSONS",

      "CREATE TABLE ADDRESSES (ADDRESS_ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY(START WITH 1, INCREMENT BY 1), ADDRESS VARCHAR(255) NOT NULL)",
      "CREATE TABLE CONTACTS (CONTACT_ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY(START WITH 1, INCREMENT BY 1), CONTACT_TYPE VARCHAR(255) NOT NULL, CONTACT_VALUE VARCHAR(255) NOT NULL, PERSON_ID INTEGER NOT NULL)",
      "CREATE TABLE GROUPS (GROUP_ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY(START WITH 1, INCREMENT BY 1), GROUP_NAME VARCHAR(255) NOT NULL)",
      "CREATE TABLE PERSONS (PERSON_ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY(START WITH 1, INCREMENT BY 1), LAST_NAME VARCHAR(255) NOT NULL, FIRST_NAME VARCHAR(255) NOT NULL, ADDRESS_ID INTEGER NOT NULL, MAIN_CONTACT_ID INTEGER NOT NULL)",
      "CREATE TABLE PERSONS_GROUPS (GROUP_ID INTEGER NOT NULL, PERSON_ID INTEGER NOT NULL, PRIMARY KEY (GROUP_ID, PERSON_ID))",

      "INSERT INTO ADDRESSES (ADDRESS) VALUES ('123, Somewhere road, Some Place')",
      "INSERT INTO ADDRESSES (ADDRESS) VALUES ('456, Anywhere road, Another Place')",
      "INSERT INTO ADDRESSES (ADDRESS) VALUES ('789, Around the corner road, Near By Place')",
      "INSERT INTO ADDRESSES (ADDRESS) VALUES ('012, Up the Hill, Not far away Place')",

      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Phone (Work between 7 and 15)', '00112233', 1)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Mobile (Work)', '99887766', 2)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Fax (Work)', '23452345', 2)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Email (Work)', 'peterwhite@somecompany.com', 1)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Email (Personal)', 'marybrown@somefreemail.com', 4)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Mobile (Personal)', '67678989', 5)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Phone (Home)', '78784545', 3)",
      "INSERT INTO CONTACTS (CONTACT_TYPE, CONTACT_VALUE, PERSON_ID) VALUES ('Phone (Home)', '45453232', 2)",

      "INSERT INTO PERSONS (FIRST_NAME, LAST_NAME, ADDRESS_ID, MAIN_CONTACT_ID) VALUES ('Peter', 'White', 1, 4)",
      "INSERT INTO PERSONS (FIRST_NAME, LAST_NAME, ADDRESS_ID, MAIN_CONTACT_ID) VALUES ('John', 'Smith', 2, 2)",
      "INSERT INTO PERSONS (FIRST_NAME, LAST_NAME, ADDRESS_ID, MAIN_CONTACT_ID) VALUES ('Jane', 'Smith', 2, 7)",
      "INSERT INTO PERSONS (FIRST_NAME, LAST_NAME, ADDRESS_ID, MAIN_CONTACT_ID) VALUES ('Mary', 'Brown', 3, 5)",
      "INSERT INTO PERSONS (FIRST_NAME, LAST_NAME, ADDRESS_ID, MAIN_CONTACT_ID) VALUES ('Jack', 'Black', 4, 6)",

      "INSERT INTO GROUPS (GROUP_NAME) VALUES ('Family')",
      "INSERT INTO GROUPS (GROUP_NAME) VALUES ('Colleagues')",

      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (1, 1)",
      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (1, 2)",
      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (1, 3)",
      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (2, 1)",
      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (2, 2)",
      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (2, 4)",
      "INSERT INTO PERSONS_GROUPS (GROUP_ID, PERSON_ID) VALUES (2, 5)",

      "ALTER TABLE CONTACTS ADD CONSTRAINT CONTACT_PERSON_1 FOREIGN KEY (PERSON_ID) REFERENCES PERSONS (PERSON_ID)",
      "ALTER TABLE PERSONS ADD CONSTRAINT PERSON_ADDRESS_1 FOREIGN KEY (ADDRESS_ID) REFERENCES ADDRESSES (ADDRESS_ID)",
      "ALTER TABLE PERSONS ADD CONSTRAINT PERSON_CONTACT_1 FOREIGN KEY (MAIN_CONTACT_ID) REFERENCES CONTACTS (CONTACT_ID)",
      "ALTER TABLE PERSONS_GROUPS ADD CONSTRAINT PERSON_GROUP_1 FOREIGN KEY (PERSON_ID) REFERENCES PERSONS (PERSON_ID)",
      "ALTER TABLE PERSONS_GROUPS ADD CONSTRAINT PERSON_GROUP_2 FOREIGN KEY (GROUP_ID) REFERENCES GROUPS (GROUP_ID)", };

  public static void main(String[] args) {
    Connection connection = null;
    try {
      String driver = "org.apache.derby.jdbc.EmbeddedDriver";
      try {
        System.out.print("Loading the appropriate driver...");
        Class.forName(driver).newInstance();
        System.out.println("Done");
      } catch (ClassNotFoundException e) {
        System.err.println("Unable to load the JDBC driver " + driver);
        System.err.println("Please check your CLASSPATH.");
        e.printStackTrace();
        System.exit(-1);
      } catch (InstantiationException e) {
        System.err.println("Unable to instantiate the JDBC driver "
            + driver);
        e.printStackTrace();
        System.exit(-1);
      } catch (IllegalAccessException e) {
        System.err.println("Not allowed to access the JDBC driver "
            + driver);
        e.printStackTrace();
        System.exit(-1);
      }

      Properties props = new Properties();
      props.put("user", "x2x");
      props.put("password", "x2x");

      String dbName = "x2x_db";

      System.out.print("Connecting with the x2x database...");
      connection = DriverManager.getConnection("jdbc:derby:" + dbName
          + ";create=true", props);
      connection.setAutoCommit(false);
      System.out.println("Done");

      System.out.println("Executing the drop, create and insert queries");
      for (String sql : buildDatabase) {
        try {
          System.out.print("Executing '" + sql + "' ");
          connection.createStatement().execute(sql);
          connection.commit();
          System.out.println("Done");
        } catch (Throwable t) {
          connection.rollback();
          System.out.println("Failed");
          t.printStackTrace();
        }
      }
      System.out.println("Done");

      System.out.println("Finished");
    } catch (Exception e) {
      System.out.println("Failed");
      e.printStackTrace();
    } finally {
      if(connection != null){
        try {
          connection.close();
        }catch(Throwable t){}
      }
    }
  }
}

Java Persistence API (JPA)

For someone who's new to this field, the persistence API may be somewhat daunting, but one should not judge a book by its cover. The persistence API facilitates programming by reducing it. Before its introduction, a programmer had to manage the connection, and interact with the database him/herself. Now, with the new persistence API, all this code is intelligently incorporated within the API. With the help of annotations, the persistence API knows how to handle the class at hand and synchronise it with the database side seamlessly for the developer using it. We said enough already. Let's start working.

Entity

The Address class is used to represent an address in Java while the ADDRESSES table is used to save the data within the database. How will the persistence API synchronise these two?

For a class to be used with the persistence API, it must include the @Entity annotation. This means that the class will be persisted with the database using the JPA. The @Entity annotation alone is not enough to do the job as it's illustrated in the following listing. Note that all our examples are saved under the same package called x2x.

package x2x;

import javax.persistence.*;

@Entity
@Table(name = "ADDRESSES")
public class Address {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY) 
  @Column(name = "ADDRESS_ID")
  private long id;

  @Column(name = "ADDRESS", nullable = false) 
  private String address;

  //...
}

The above listing includes a total of five annotations. The @Table annotation is used to provide information about the table this class or better entity is to be synchronised with. By default, the class name is taken as the table name, but this can be differently set as required. All entity classes must have a primary key, which is marked with the @Id annotation. The address id, in this case, is also automatically generated by the database. Another annotation called @GeneratedValue is used to indicate that the value of this field is automatically generated by the database. By default the persistence API will use the field's name as the column name. When these are different, the @Column annotation has to be used to set the appropriate column name.

Relationships

Classes are logically related to each other through the use of fields. For example "a person has one address while one address can be used by more than one person" is represented by a reference of the Address class in the Person class as shown in bold in the following listing.

//...
public class Person {

  private Address address;

  //...

The persistence API provides a set of annotations which marks the relation between entities. There are four types of relations:

  • One-to-One
  • One-to-Many
  • Many-to-One
  • Many-to-Many

Each of these is discussed in detail below.

One-to-One

A person object has many contacts, but it has only one main contact. The relation between the main contact and person is one-to-one. The following listing illustrates a fragment from the Person class capturing this relation. Note that in this example, the relation between these two is unidirectional. The Contact class is not aware of this relation.

//...
public class Person {
  
  @OneToOne(optional = false)
  @JoinColumn(name = "MAIN_CONTACT_ID", 
                        referencedColumnName = "CONTACT_ID")
  private Contact mainContact;
  
  //...

Before we can understand how the relation between the entities works, let's recall the database relation between these two tables: The foreign key MAIN_CONTACT_ID in the PERSONS table is pointing to the primary key CONTACT_ID in the CONTACTS table. The @OneToOne annotation is very straightforward. It sets the relation between the two entities as one-to-one. The optional attribute sets the relation as obligatory. The other annotation, @JoinColumn, is very simple too. The name annotation attribute points to the foreign key while the referencedColumnName points to the primary key in the reference table (CONTACTS).

One-to-Many

A person can have many contacts. Thus there are two relations between the same two entities. The first one was discussed before. The second relation between these two is one-to-many. One person can have many contacts. A different fragment from the Person class is illustrated below. What's different from the previous example is that this relation is bidirectional.

//...
public class Person {
  
  @OneToMany(cascade = CascadeType.ALL, mappedBy = "person")
  private List<Contact> contacts;
  
  //...

As many of you would have predicted, the annotation which represents the one-to-many relation is called @OneToMany. Since this relation is bidirectional, the attribute mappedBy must provide the field's name that owns the relation in the other class. Note that this value is case-sensitive. The cascade attribute is an optional attribute defaulted to none. It can be set to one of the javax.persistence.CascadeType enum values: ALL; MERGE; PERSIST; REFRESH; REMOVE. This attribute is used by the persistence API to control what actions to cascade to the target entity. In our example all actions are propagated to the contacts instances related to this one.

Many-to-One

When the above is viewed from the other end we have: many contacts belong to one person. The relation between contacts and person is many-to-one.

//...
public class Contact {
  
  @ManyToOne(cascade = CascadeType.ALL)
  @JoinColumn(nullable = false, name = "PERSON_ID")
  private Person person;
  
  //...

The annotation @ManyToOne set the relation between the two classes through the following field: person. The field's name must match the value given to the mappedBy attribute in the @OneToMany in the Person class.

The annotation @JoinColumn can be seen as the representation of a foreign-key-column which connects the CONTACTS table with the PERSONS table.

Many-to-Many

A person may belong to many groups and a group can contain as many persons as required. The relation between the group and person is said to be many-to-many.

In all the relations discussed so far there was a table for class entity and the same tables used matched the classes involved. The common way for representing the many-to-many relation within a database is to use three tables: one for persons, one for groups and one to map the persons and groups together. In OOP, only two classes are required, the Person class and the Group class. Both classes can in turn contact a list of the other.

The persistence API provides several annotations to ease the linking process as shown in bold the following Group class listing.

//...
public class Group {
  
  @ManyToMany
  @JoinTable(name = "PERSONS_GROUPS")
  private List<Person> contacts;
  
//...
}

The annotation @ManyToMany is used to mark the many-to-many relation between this class (Group) and the field's content type class (Person), referred to as the inverse class. The field must be of type Set, List, Map or Collection (or any subclasses) as only these types are supported. The program will compile if other types are used but will produce a runtime exception.

The persistence API requires more information to link these two tables together. The @JoinTable annotation is used to provide information about the third table, the one that maps the person and group records. The @JoinTable annotation has six parameters, of which only one was used above. Two other parameters which are commonly used are the inverseJoinColumns and the joinColumns. The joinColumns represents the fields used in this entity (Group) to map to the other entity (Person), while the inverseJoinColumns represents the fields used by the other entity (Person) to map with this one (Group).

Together with the @JoinTable, the @JoinColumn annotation can be used to label the respective columns. These were not included in the above listing as the persistence API default values happen to match the database column names. The above listing can be changed as shown in bold in the following listing to also point to the respective column.

@ManyToMany
@JoinTable(name = "PERSONS_GROUPS", 
           joinColumns = { @JoinColumn(name = "GROUP_ID") }, 
           inverseJoinColumns = { @JoinColumn(name = "PERSON_ID") }  
           )
private List<Person> contacts;

The Persistence.xml file

The persistence API requires one xml file called persistence.xml, saved within the META-INF folder. This xml files includes the list of classes which will be managed by the persistence API together with database connection url, username and password and other parameters. The embedded version of the JavaDB is used in this article, thus the transaction type has to be set to resource local and the embedded version of the driver is used instead.

The database name and the persistence unit name do not necessary have to have the same name, but it's a good practice to use at least similar ones. Since the database is already set up, the toplink.ddl-generation is set to none. The persistence.xml is illustrated below.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="x2x_db" transaction-type="RESOURCE_LOCAL">

    <provider>
oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
    </provider>

    <class>x2x.Address</class>
    <class>x2x.Contact</class>
    <class>x2x.Group</class>
    <class>x2x.Person</class>

    <properties>
      <property name="toplink.jdbc.user" 
                   value="x2x" />
      <property name="toplink.jdbc.password" 
                   value="x2x" />
      <property name="toplink.jdbc.url"
                   value="jdbc:derby:x2x_db" />
      <property name="toplink.jdbc.driver"
                   value="org.apache.derby.jdbc.EmbeddedDriver" />
      <property name="toplink.ddl-generation" 
                   value="none" />
    </properties>

  </persistence-unit>
</persistence>

Entity Manager

Now that we have everything set up, we can start querying the database using the javax.persistence.EntityManager. The first thing we need to create an instance of the entity manager using a factory such as javax.persistence.EntityManagerFactory as shown in the following listing.

EntityManagerFactory factory = 
    Persistence.createEntityManagerFactory("x2x_db");
EntityManager manager = factory.createEntityManager();

The persistence unit name must be provided to the factory to load the proper information from the persistence.xml file. With our entity manager created we can do anything with it without having to worry about complex data access objects. Selecting all persons and put them in a list can be achieved in two lines of code as shown below.

Query selectAllQuery = 
      manager.createQuery("SELECT p FROM Person p");
List allPersons = selectAllQuery.getResultList();
for (Person p : allPersons) {
  //...
}

It's good to understand that even though the parameter passed to the create query method above is very similar to SQL, it's not. Note that the persons table is called PERSONS and not Person. So how did it work? The provided name is the name of the entity we're querying.

This query not only loaded the persons' data from the PERSONS table but also loaded the address, groups and contacts information from the same database. All this was done without additional code. We can do more than simply loading data. A new address, for example, can be created and inserted without having to wonder about the SQL code behind it as illustrated below.

Address newAddress = new Address();
newAddress.setAddress("The new Place, Persistence Ave, Java");
manager.getTransaction().begin();
manager.persist(newAddress);
manager.getTransaction().commit();
System.out.println("New Address id: " + newAddress.getId());

The same address can be updated and deleted in a similar fashion.

// Update
newAddress.setAddress("The other Place, Persistence Ave, Java");
manager.getTransaction().begin();
manager.persist(newAddress);
manager.getTransaction().commit();

// Delete
manager.getTransaction().begin();
manager.remove(newAddress);
manager.getTransaction().commit();

Conclusion

JPA like other new APIs introduced new features that improve the productivity and minimise the maintenance costs. The traditional data access object classes can be replaced by the use of annotation. This article merely covers the relational aspect of this vast API. Further reading about the class Query, and annotations @NamedQuery and @NamedQueries would prove very useful especially when it comes to retrieving information from the database.

No comments:

Post a Comment