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 classQuery
, and annotations @NamedQuery
and @NamedQueries
would prove very useful especially when it comes to retrieving information from the database.
No comments:
Post a Comment