EJB3 Persistence Jumpstart
By Jeff Brown, OCI Principal Software Engineer
April 2006
Introduction
The EJB 3.0 specification is being defined by JSR-220. Part of that specification is the new EJB 3 Persistence API. This persistence API is the new standard for object-relational mapping solutions in Java. This article will explore the fundamentals of the new API and provide practical code samples to serve as a tool to help developers quickly get started with the API. This article aims to introduce the API and provide a flavor for the powerful capabilities that this new standard introduces.
What Is EJB 3 Persistence
Part of the specification being defined by JSR-220 defines the EJB 3 Persistence API. This API is an all java standards based solution for persistence. EJB 3 Persistence uses an object-relational mapping approach to bridge the gap between your object oriented object model and a relational database. Applications written using the EJB 3 Persistence API should be portable to many relational databases. In addition to being portable across databases, a well designed EJB 3 application should be portable in terms of the persistence provider. Many vendors may provide implementations of the EJB 3 Persistence specification, some free and open-source, others commercial. Replacing one implementation with another should be a simple task for most applications.
Not Your Father's EJB
EJBs have traditionally been associated with heavy weight containers like JBoss, WebSphere, WebLogic and others. These containers provide a lot of services to distributed applications. These containers are not necessarily part of an EJB 3 application. For many applications, these deployment scenarios may still make sense. However, the EJB 3 Persistence API is specifically designed to be used with or without a container. In particular, the persistence provider is required by the specification to work outside of the container. This flexibility means that the same standards based persistence engine may be used in all of your Java applications. This includes distributed applications, web apps, thick client applications etc.
Tools To Get Started With
To get started with EJB 3 persistence you will need to have a few things installed:
- EJB 3 Persistence Provider
- Relational Database
- JDBC Driver
The examples covered in this document should work with many combinations of the above tools. Remember that one of the great benefits of using a standards based approach is that you are not tied to a vendor. EJB 3 Persistence is in fact standards based.
Hibernate is the most popular free and open-source implementation of the EJB 3 Persistence API. The examples shown here were developed using Hibernate 3.1.2 in combination with the Hibernate Entity Manager 3.1 beta6. All of the code demonstrated in this document should work with those versions of Hibernate or any other implementation of the EJB 3 Persistence API. The Hibernate documentation provides details for setting up your CLASSPATH. For most situations you will configure your development environment to include all of the jars in the Hibernate distribution in your CLASSPATH.
There are a lot of good open source relational databases that are compatible with Hibernate and EJB 3 Persistence. MySQL and its Connector/J JDBC driver work very well. See the references section at the end of this document for links to all of the necessary software.
Introducing The EntityManager
In EJB 3 Persistence, the EntityManager
is responsible for querying and managing persistent instances known as Entities. The EntityManager
is the starting place for most EJB 3 Persistence operations. The technique for retrieving an EntityManager
in a Java EE environment is slightly different than the technique for retrieving and EntityManager
in a Java SE environment. For simplicity, the samples here will focus on a Java SE environment so no container needs to be configured.
- import java.util.HashMap;
- import javax.persistence.EntityManager;
- import javax.persistence.EntityManagerFactory;
- import javax.persistence.Persistence;
-
- ...
-
- EntityManagerFactory emf =
- Persistence.createEntityManagerFactory("mycontext", new HashMap());
- EntityManager em = emf.createEntityManager();
The first argument to createEntityManagerFactory
is the name of a persistence context. This persistence context must be configured in META-INF/persistence.xml
. That configuration file may be in a .jar file that is on the CLASSPATH or it may be loaded directly from a directory on the CLASSPATH. If the configuration file is not in a .jar file, take care how the CLASSPATH is configured. The configuration file must be loadable as a resource named META-INF/persistence.xml
so if that file is in /local/dev/stuff/META-INF/persistence.xml
then/local/dev/stuff/
must be on the CLASSPATH, not /local/dev/stuff/META-INF/
.
Note the second argument to createEntityManagerFactory
is an empty java.util.HashMap
. This is required only because of a limitation in the version of Hibernate used while developing this article. A fix for this has been checked in to the Hibernate CVS repository but has not been released yet. For now, pass the empty HashMap
or a NullPointerException
will be thrown.
Here is a sample persistence context descriptor:
- <?xml version="1.0" encoding="UTF-8"?>
- <persistence>
- <persistence-unit name="mycontext">
- <provider>org.hibernate.ejb.HibernatePersistence</provider>
- <class>com.persistencedemo.Person</class>
- <class>com.persistencedemo.Employee</class>
- <class>com.persistencedemo.SomeOtherClass</class>
- <properties>
- <property name="hibernate.connection.url" value="jdbc:mysql://localhost/demodb"></property>
- <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"></property>
- <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"></property>
- <property name="hibernate.connection.password" value="javadude"></property>
- <property name="hibernate.connection.username" value="javauser"></property>
- <property name="hibernate.hbm2ddl.auto" value="update"></property>
- </properties>
- </persistence-unit>
- </persistence>
The persistence-unit name is an arbitrary label that could be just about anything. The descriptor could contain any number of persistence-units and they must each have a unique name. This name is the first argument to Persistence.createEntityManagerFactory
in the code above. If the descriptor contained numerous persistence-units they could each be configured with their own settings. The name passed to Persistence.createEntityManagerFactory
tells the persistence engine which configuration to load.
The provider element defines the name of your persistence provider. This name is a vendor specific class name. org.hibernate.ejb.HibernatePersistence
happens to be the name of the Hibernate provider. If you are using an implementation other than Hibernate, this value will need to correspond to your vendor's provider class name. Check your vendor documentation.
The descriptor should contain a element for each Entity
type associated with this persistence unit. Entities are described in more detail later.
Inside of the element are any number of elements. These elements allow arbitrary properties to be set and passed to the persistence provider. These are usually vendor specific configuration properties.
Inside of this descriptor is the only place where you are going to see details about any particular implementation of the EJB 3 Persistence API. All of the application code shown below is vendor neutral. We happen to be using Hibernate and MySQL and some of those details show up in this configuration file but all of the Java code shown below will work with any implementation of the EJB 3 Persistence API and with any compatible relational database.
Entities
Entities are the instances that can be stored and retrieved using the EJB 3 Persistence API. An Entity
is pretty much a Plain 'Ol Java Object (POJO). Requirements for Entities:
- Must have a no-arg constructor
- Must not have any final methods
- Class must not be final
- Must be marked with
@Entity
(or have an appropriate xml descriptor)
NOTE: Many things in EJB 3 Persistence take advantage of Java 5 annotations. EJB 3 Persistence does not require Java 5. All of the things that can be accomplished with annotations can also be accomplished using xml deployment descriptors. Our examples will take advantage of the cleaner, more simple annotation based approach.
A POJO
Here is a simple POJO that we may want to store using EJB 3 Persistence:
- package com.persistencedemo;
-
- public class Person {
-
- private String firstName;
-
- private String lastName;
-
- public Person() {
- }
-
- public Person(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
-
- public String getFirstName() {
- return firstName;
- }
-
- public void setFirstName(String firstName) {
- this.firstName = firstName;
- }
-
- public String getLastName() {
- return lastName;
- }
-
- public void setLastName(String lastName) {
- this.lastName = lastName;
- }
-
- public String toString() {
- return lastName + ", " + firstName;
- }
- }
So far that is pretty generic Java code. This class defines a couple of fields that represent the state of a Person
(firstName
and lastName
) along with some standard methods to set and retrieve those values. Before we can store Person
objects in the database we have just a couple of small changes to make to the Person
class. First, we must mark the class with @Entity
.
- package com.persistencedemo;
-
- import javax.persistence.Entity;
-
- @Entity
- public class Person {
-
- ...
-
- }
Next we are going to add an id field to the Person
class. While this isn't strictly required, in practice you are going to want an identifier field in most of your entities. This id field is what we will use to represent identity in the database. If we tried to use lastName
or even lastName
+ firstName
for identity, we couldn't store 2 people with the same name in the database. That would be a problem in many situations. Sometimes your object model will already contain something that represents unique identity. When that isn't the case adding a synthetic id like the one shown below is fine.
- package com.persistencedemo;
-
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
-
- public class Person {
-
- private String firstName;
-
- private String lastName;
-
- private Integer id; // new field to represent identity
-
- public Person(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
-
- public String getFirstName() {
- return firstName;
- }
-
- public void setFirstName(String firstName) {
- this.firstName = firstName;
- }
-
- public String getLastName() {
- return lastName;
- }
-
- public void setLastName(String lastName) {
- this.lastName = lastName;
- }
-
- @Id
- @GeneratedValue
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String toString() {
- return lastName + ", " + firstName;
- }
- }
The getId
method has 2 separate annotations on it. The @Id
annotation tells the persistence provider that this property should be used to represent object identity in the database. The @GeneratedValue
annotation tells the persistence provider that the application isn't going to assign a value to this field and that one should be generated automatically. If the identity field were something like a Social Security Number or an employee number then you would not want automatically generated values to be used so this annotation would be left off.
That is it! The code shown above is a full and complete example of an EJB 3 Persistence Entity. Instances of Person
may now be stored in the database by passing them to the persist()
method on an EntityManager
. Unlike JDO and earlier versions of Hibernate, no xml descriptor is required. The need for those often difficult to manage xml descriptors has been abolished by EJB 3 Persistence.
Saving Entities
The way to save an Entity
(like Person
objects) is to pass the Entity
to the persist()
method on an EntityManager
. Any time Entities are being added to the database, deleted from the database or when Entities that have been retrieved from the database are being modified, those operations need to happen within the context of an EJB 3 Persistence transaction known as an EntityTransaction
. The sample code below shows how to retrieve and interact with an EntityTransaction
. This trivial example simply commits the transaction after persisting a Person
.
Saving a Person
Entity
is very straightforward at this point.
- import java.util.HashMap;
-
- import javax.persistence.EntityManager;
- import javax.persistence.EntityManagerFactory;
- import javax.persistence.EntityTransaction;
- import javax.persistence.Persistence;
-
- public class SavePerson {
-
- public static void main(String[] args) {
- EntityManagerFactory emf =
- Persistence.createEntityManagerFactory("mycontext", new HashMap());
- EntityManager em = emf.createEntityManager();
- EntityTransaction tx = em.getTransaction();
- tx.begin();
- em.persist(new Person("Ben", "Franklin"));
- tx.commit();
- em.close();
- }
- }
The first time the above code is run a new table will be created for storing Person
objects. There is a Hibernate specific property being set in our persistence configuration file that causes that to happen. Take another look at persistence.xml
and notice the hibernate.hbm2ddl.auto
property is being set to "update".
- <?xml version="1.0" encoding="UTF-8"?>
- <persistence>
- <persistence-unit name="mycontext">
- <provider>org.hibernate.ejb.HibernatePersistence</provider>
- <class>com.persistencedemo.Person</class>
- <properties>
- <property name="hibernate.connection.url" value="jdbc:mysql://localhost/demodb"></property>
- <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"></property>
- <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"></property>
- <property name="hibernate.connection.password" value="javadude"></property>
- <property name="hibernate.connection.username" value="javauser"></property>
- <property name="hibernate.hbm2ddl.auto" value="update"></property>
- </properties>
- </persistence-unit>
- </persistence>
This property tells Hibernate to auto create tables if the don't exist. By default the table name will match that of the class name and 1 column will be added to the table to represent each persistent field in the class. Here is the table that Hibernate will generate by default for the Person
class:
There are a lot of specific details about the object to relational mapping that you have full control over using EJB 3 annotations. For example, if we want to store Person
objects in a table named "people" instead of a table named "person" an annotation needs to be added to the Person
class.
- package com.persistencedemo;
-
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
- import javax.persistence.Table;
-
- @Entity
- @Table(name="people")
- public class Person {
-
- ...
- }
Another example of controlling the object to relational mapping has to do with the width of the columns used to store firstName
and lastName
. Note from the picture of the "person" table above that these columns defaulted to be 255 characters wide. If we want to narrow that to 35 characters then annotations need to be added to each of those properties.
- package com.persistencedemo;
-
- import javax.persistence.Column;
- import javax.persistence.Entity;
- import javax.persistence.Table;
-
- @Entity
- @Table(name="people")
- public class Person {
-
- ...
-
- @Column(length=35)
- public String getFirstName() {
- return firstName;
- }
-
- @Column(length=35)
- public String getLastName() {
- return lastName;
- }
- }
Now when Hibernate creates the table it will use values represented in those annotations. If the table already exists, the table will need to be dropped. When Hibernate recreates the table, the table will have the new column widths.
Querying Entities
Querying the database for entities can be just about as simple as the code required to store entities in the database. Once an EntityManager
has been retrieved a Query
may be retrieved from the EntityManager
for executing queries. Here is a simple block of code that retrieves all Person
instances from the database.
- package com.persistencedemo;
-
- import java.util.HashMap;
- import java.util.List;
-
- import javax.persistence.EntityManager;
- import javax.persistence.EntityManagerFactory;
- import javax.persistence.Persistence;
- import javax.persistence.Query;
-
- public class ReadPeople {
-
- public static void main(String[] args) {
- EntityManagerFactory emf =
- Persistence.createEntityManagerFactory("mycontext", new HashMap());
- EntityManager em = emf.createEntityManager();
-
- // retrieve all Person entities...
- Query query = em.createQuery("from Person");
- List<Person> results = query.getResultList();
- for (Person p : results) {
- System.out.println(p);
- }
- }
- }
The query string is "from Person
". Our Person
class is annotated so that the table name is "people", not "person" but the query string still queries from "Person
". That query string is referring to a class name, not a table name.
The query above retrieves all Person
instances from the database. Query strings may be parameterized to narrow the results. The following code will only retrieve Person
instances where lastName
is "Gosling".
- ...
-
- // retrieve all Person entities with lastName = "Gosling"
- String qryString = "from Person where lastName = :lastNameParam";
- Query query = em.createQuery(qryString);
- query.setParameter("lastNameParam", "Gosling");
- List<Person> results = query.getResultList();
-
- ...
In the query string "lastName
" refers to a field in the Person
class. Note that the column name in the database isn't necessarily the same as the field name in the class. The ":lastNameParam
" token represents a parameter in the query that must be populated before the query is executed. The parameter is populated with the call to setParameter
where the first argument is a string that corresponds to a token in the query string and the second argument is the value that will be inserted in place of that token. In the query string the token is preceded with a colon and the colon is omitted in the call to setParameter
. A query string may contain any number of parameters.
Entity Relationships
The Person
examples above demonstrate some fundamentals of storing and retrieving entities from the database. The Person
class lives in isolation without any relationships to other entities. It turns out that storing complex graphs of entities is not much more complicated.
Let's explore a slightly more complicated object model. Consider a Car
class and an Engine
class where the relationship between the 2 is 1-to-1. Every Car
has exactly 1 Engine
and an Engine
cannot belong to more than 1 Car
. The Car
and Engine
classes might look something like this.
- package com.persistencedemo;
-
- import javax.persistence.CascadeType;
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
- import javax.persistence.OneToOne;
-
- @Entity
- public class Car {
-
- private String make;
- private String model;
- private Engine engine;
- private Integer id;
-
- public Car() {
- this(null, null, null);
- }
-
- public Car(String make, String model, Engine engine) {
- this.make = make;
- this.model = model;
- this.engine = engine;
- }
-
- @OneToOne(cascade = CascadeType.PERSIST)
- public Engine getEngine() {
- return engine;
- }
-
- public void setEngine(Engine engine) {
- this.engine = engine;
- }
-
- public String getMake() {
- return make;
- }
-
- public void setMake(String make) {
- this.make = make;
- }
-
- public String getModel() {
- return model;
- }
-
- public void setModel(String model) {
- this.model = model;
- }
-
- @Id
- @GeneratedValue
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String toString() {
- return make + " " + model;
- }
- }
- package com.persistencedemo;
-
-
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
-
- @Entity
- public class Engine {
-
- private Integer id;
-
- private int numberOfCylinders;
-
- private int cubicCentimeters;
-
- private String manufacturer;
-
- public Engine() {
- }
-
- public Engine(int numberOfCylinders, int cubicCentimeters, String manufacturer) {
- this.numberOfCylinders = numberOfCylinders;
- this.cubicCentimeters = cubicCentimeters;
- this.manufacturer = manufacturer;
- }
-
- public int getCubicCentimeters() {
- return cubicCentimeters;
- }
-
- public void setCubicCentimeters(int cubicCentimeters) {
- this.cubicCentimeters = cubicCentimeters;
- }
-
-
- @Id
- @GeneratedValue
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getManufacturer() {
- return manufacturer;
- }
-
- public void setManufacturer(String manufacturer) {
- this.manufacturer = manufacturer;
- }
-
- public int getNumberOfCylinders() {
- return numberOfCylinders;
- }
-
- public void setNumberOfCylinders(int numberOfCylinders) {
- this.numberOfCylinders = numberOfCylinders;
- }
- }
The getEngine
method in the Car
class is annotated with a @OneToOne
annotation to tell the persistence engine that the relationship between a Car
and its Engine
is a 1-to-1 relationship. In addition to representing the cardinality, the "cascade = CascadeType.PERSIST" is there to tell the persistence engine to automatically persist the Engine
property when the Car
is persisted. Without this, the Engine
would have to be explicitly persisted on its own. With the Car
annotated as above, that step is not required. The code below creates some Cars
and Engines
and saves them to the database.
- EntityManagerFactory emf = Persistence
- .createEntityManagerFactory("mycontext", new HashMap());
- EntityManager em = emf.createEntityManager();
- EntityTransaction tx = em.getTransaction();
- tx.begin();
-
- Engine truckEngine = new Engine(8, 350, "Triton");
- Car truck = new Car("Ford", "F-150", truckEngine);
-
- Engine vueEngine = new Engine(6, 280, "GM");
- Car vue = new Car("Saturn", "Vue", vueEngine);
-
- // the Engines will automatically be persisted because
- // the Cars refer to them...
- em.persist(truck);
- em.persist(vue);
- tx.commit();
- em.close();
Before running that code make sure that persistence.xml has been updated with the appropriate class elements.
- <?xml version="1.0" encoding="UTF-8"?>
- <persistence>
- <persistence-unit name="mycontext">
- <provider>org.hibernate.ejb.HibernatePersistence</provider>
- <class>com.persistencedemo.Car</class>
- <class>com.persistencedemo.Engine</class>
-
- ...
The default schema generated for this object model will look something like this.
Car
instances may be retrieved using the same technique we have seen already for retrieving Person
instances. There is no need to say anything about Engine
in the query string.
- Query query = em.createQuery("from Car");
- List<Car> results = query.getResultList();
- for (Car c : results) {
- System.out.println("car: " + c);
- Engine e = c.getEngine();
- System.out.println("number of cylinders: " + e.getNumberOfCylinders());
- }
Navigating Relationships In Queries
The relationships between entities may be navigated in a query string to narrow the results based on some property that is arbitrarily deep in the object graph. For example, Cars
may be queries based on the number of cylinders in their Engine
like this.
- String qryString = "from Car where engine.numberOfCylinders = :cylinderCount";
- Query query = em.createQuery(qryString);
-
- // only retrieve cars with 8 cylinder engines...
- query.setParameter("cylinderCount", 8);
- List<Car> results = query.getResultList();
- for (Car p : results) {
- System.out.println(p);
- }
One To Many Relationships
The relationship between a Car
and its Engine
is a 1-to-1 relationship. To represent a 1-to-many relationship replace the @OneToOne
annotation with a @OneToMany
annotation. The @OneToMany
annotation is applied to the collection property in the owning side of the relationship. Consider the relationship between a music CD
and all of the Tracks
contained on that CD
.
- package com.persistencedemo;
-
- import java.util.ArrayList;
- import java.util.List;
-
- import javax.persistence.CascadeType;
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
- import javax.persistence.OneToMany;
-
- @Entity
- public class CD {
-
- private String artist;
- private String title;
- private List<Track> tracks = new ArrayList<Track>();
- private Integer id;
-
- public CD() {
- }
-
- public CD(String artist, String title) {
- this.artist = artist;
- this.title = title;
- }
-
- public void addTrack(Track track) {
- tracks.add(track);
- }
-
- public String getArtist() {
- return artist;
- }
-
- public void setArtist(String artist) {
- this.artist = artist;
- }
-
- @Id
- @GeneratedValue
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- @OneToMany(cascade = { CascadeType.PERSIST })
- public List<Track> getTracks() {
- return tracks;
- }
-
- public void setTracks(List<Track> tracks) {
- this.tracks = tracks;
- }
-
- public String toString() {
- return title;
- }
- }
- package com.persistencedemo;
-
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
-
- @Entity
- public class Track {
-
- private int durationInSeconds;
- private String title;
- private Integer id;
-
- public Track() {
- }
-
- public Track(int durationInSeconds, String title) {
- this.durationInSeconds = durationInSeconds;
- this.title = title;
- }
-
- public int getDurationInSeconds() {
- return durationInSeconds;
- }
-
- public void setDurationInSeconds(int durationInSeconds) {
- this.durationInSeconds = durationInSeconds;
- }
-
- @Id
- @GeneratedValue
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
- }
The default schema for a relationship like that will look something like this.
Saving CD
instances works very much like saving Car
instances.
- EntityManagerFactory emf =
- Persistence.createEntityManagerFactory("mycontext", new HashMap());
- EntityManager em = emf.createEntityManager();
- EntityTransaction tx = em.getTransaction();
- tx.begin();
-
- CD cd = new CD("The Beatles", "Rubber Soul");
- cd.addTrack(new Track(220, "Norwegian Wood"));
- cd.addTrack(new Track(180, "Drive My Car"));
- em.persist(cd);
-
- cd = new CD("Deep Purple", "Machine Head");
- cd.addTrack(new Track(200, "Smoke On The Water"));
- cd.addTrack(new Track(480, "Lazy"));
- em.persist(cd);
-
- tx.commit();
- em.close();
Navigating the relationship between a CD
and its Tracks
requires a slightly different syntax than is required to navigate a 1-to-1 relationship.
- String qryString = "select cd from CD as cd, in (cd.tracks) track where track.title = :trackTitle";
- Query query = em.createQuery(qryString);
-
- // retrieve all CDs that contain a Track called "Lazy"
- query.setParameter("trackTitle", "Lazy");
- List<CD> results = query.getResultList();
- for (CD c : results) {
- System.out.println(c);
- }
The query string there is "select cd from CD as cd, in (cd.tracks) track where track.title = :trackTitle". The lower case "cd" that appears in several places in that string is an alias that can be just about anything. It doesn't have to match the class name, but it happens to in this case. That query string can be broken into several pieces to help clarify what is going on there.
"select cd from CD as cd" says to select CDs
and refer to them with the label "cd".
"in (cd.tracks) track" says to assign the name "track" to each element in the "cd.tracks" collection.
"where track.title = :trackTitle" says only match cases where cd.tracks contains a track with a name that matches the value of the "trackTitle" parameter which is populated later with a call to the setParameter
method.
Mapping Inheritance Relationships
One of the most challenging aspects of object-to-relational mapping is the handling of inheritance relationships. Ideally, developers should be able to define their object relationships without making comprimises to store those object instances in a relational database. Indeed the EJB 3 Persistence API provides a good deal of flexibility in this area. Here we will look at some of the basic approaches to mapping inheritance relationships.
Consider an object model consisting of Vehicle
, Car
and Bicycle
. Vehicle
is the parent class of Car
and Bicycle
. The Vehicle
class contains an attribute representing the number of wheels this vehicle has. The Car
class has a make, a model and an engine attribute. The engine attribute refers to an Engine object. The Bicycle
class defines a single attribute that represents the number of gears that the Bicycle
has. Here is some of the code related to those classes:
- @Entity
- @Inheritance(strategy = InheritanceType.JOINED)
- public abstract class Vehicle {
-
- private Integer id;
-
- private int numberOfWheels;
-
- public Vehicle(int numberOfWheels) {
- this.numberOfWheels = numberOfWheels;
- }
-
- ...
-
- }
- @Entity
- public class Car extends Vehicle {
-
- private String make;
- private String model;
- private Engine engine;
-
- public Car() {
- this(null, null, null);
- }
-
- public Car(String make, String model, Engine engine) {
- super(4);
- this.make = make;
- this.model = model;
- this.engine = engine;
- }
-
- ...
-
- }
- @Entity
- public class Bicycle extends Vehicle {
-
- private int numberOfGears;
-
- public Bicycle() {
- this(0);
- }
-
- public Bicycle(int numberOfGears) {
- super(2);
- this.numberOfGears = numberOfGears;
- }
-
- ...
-
- }
Notice the "@Inheritance(strategy = InheritanceType.JOINED)
" annotation on the Vehicle
class. That annotation tells the persistence engine that inheritance mapping should be implemented using joins in the database. This means that the Bicycle
class and the Car
class will each have their own table in the database in addition to a shared table that contains the attributes represented in the Vehicle
class. Both the car and bicycle tables will relate to the vehicle table using a join.
The default schema generated for these classes would look something like this:
Another way to map that same relationship is using a single table approach. A single table mapping approach means that all classes in a an inheritance hierarchy share a single table. If Car
and Bicycle
instances are going to be stored together in the same table there must be someting in the table to indicate which type of Vehicle
is represented in each row in that table. EJB 3 Persistence uses a discriminator column to handle this. The discriminator column is simply a column which will contain a value that indicates which type of object is stored in that particular row.
The code below shows the annotations required to map our 3 classes using the single table approach.
- @Entity
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
- public abstract class Vehicle {
-
- ...
-
- }
- @Entity
- @DiscriminatorValue("C")
- public class Car extends Vehicle {
-
- ...
-
- }
- @Entity
- @DiscriminatorValue("B")
- public class Bicycle extends Vehicle {
-
- ...
-
- }
The DiscriminatorValue annotations in Car
and Bicycle
tell the persistence engine which values should be used to represent each of those classes in the shared vehicle table.
The default schema for this mapping will look something like this:
The TYPE column is where the discriminator values are stored.
Another approach to mapping this inheritance relationship is using what is referred to as a "mapped superclass" approach. The mapped superclass approach results in each of the subclasses having their own table and attributes inherited from the super class are pushed down into the subclass table, eliminating the need for a shared table for the super class.
- @MappedSuperclass
- public abstract class Vehicle {
-
- ...
-
- }
- @Entity
- public class Bicycle extends Vehicle {
-
- ...
-
- }
- @Entity
- public class Car extends Vehicle {
-
- ...
-
- }
The default schema for this mapping will look something like this:
Notice that the numberOfWheels attribute shows up in both the bicycle table and the car table.
Developers have a lot of control over the how the object model gets mapped to the schema. The simple examples above show that significant changes to the schema do not require changes to the object model. Each of the approaches shown above use the exact same object model. The schema differences are driven only by meta data represented by annotations.
Conclusion
The examples covered here really only begin to cover the capabilities defined by the EJB 3 Persistence specfication. The hope is that this information will serve as a practical guide to help developers get started with the API. The best way to get started with the API is to setup a development environment that will allow examples like those discussed in this article to be executed and experimented with. Developers will find that compared with writing JDBC code or even working with other object-to-relational mapping solutions, the EJB 3 Persistence API is very straightforward and easy to work with.
References
- [1] JSR-220
http://jcp.org/en/jsr/detail?id=220 - [2] Hibernate
http://hibernate.org/ - [3] MySQL Database
http://www.mysql.com/products/database/ - [4] MySQL Connector/J
http://www.mysql.com/products/connector/j/