devxlogo

Persistence Pays Offs: Advanced Mapping with JPA

Persistence Pays Offs: Advanced Mapping with JPA

he Java Persistence API is a new addition to the Java specification that updates Java’s method of performing object/relational mapping. In my first article on JPA, I explained the basics of JPA, how to map a one-to-one relationship, and how to query simple objects. In this article I’ll move on to discuss some advanced features. Unlike with Entity Beans 2.x, JPA allows inheritance and can implement it in various ways (SINGLE_TABLE, TABLE_PER_CLASS, and JOINED). Because JPA supports coding-by-exception, developers needn’t use additional technologies to accomplish advanced ORM. However, if you want, you can always use Java’s new annotations to customize mappings and relationships. I’ll also show you how to query complex object graphs with inheritance and collections using JQPL.

In this article I’m going to build on the example application that I created for the first article: a customer address book for a fictional music company called Watermelon. To begin, I’ll imagine that Watermelon has some new requirements. There are now two types of customers: individuals and companies. Individuals are customers who have a first name, a last name, and a date of birth; companies have a name, a contact name, and a number of employees. This will give me the opportunity to map inheritance with JPA.

Customers have a home address but Watermelon also needs to define a set of delivery addresses. Each of these addresses can be used on the weekend or/and in the evenings. I will implement this information by adding tags to an address. To do all this I will use one-to-many and many-to-many relationships with JPA. Figure 1 shows the business model to implement.

What You Need
First of all, you need to have a JDK 1.5 installed. This article uses MySQL 5 and TopLink Essentials as the implementation of JPA. To compile and execute the code that comes with this article you need Ant 1.7. Test classes are written in JUnit 4.1. If you are not familiar with the new JUnit 4 syntax check this article.Before running the code, you need to create a database called watermelonDB in MySQL with the root user and no password, i.e. mysql> create database watermelonDB;

Author’s Note: As in the previous article I will use TopLink Essentials as a JPA provider and MySQL as a relational database.

Inheritance with JPA
As I said, Watermelon deals with two types of customers: individuals and companies. In the model I can describe this using an abstract Customer class from which inherits Individual and Company classes. Customer has an ID, a telephone number, an email address, and a method to validate telephone numbers (see Callback annotations). It also holds the relationships to Address. Both Individual and Company adds specific attributes and behaviors (e.g. a method to calculate the age of an individual).

Figure 1. Watermelon’s address book object model is shown.

Inheritance is a built-in notion in object languages in general and in Java in particular. But storing this hierarchical information in a relational structure is not easy to do. The proof is that, until now, inheritance hasn’t been well handled in the EJB persistence domain. Thanks to JPA, polymorphism and inheritance are now possible in a Java EE and Java SE application. Contrary to associations, which are straightforward to map, with inheritance the developer has to choose between several strategies:

  • A single table per class hierarchy (SINGLE_TABLE) is the default. That means that the three classes Customer, Individual, and Company are flattened into one table only (t_customer, for example). All the attributes of the three classes are mapped down to a single table. This approach is the best in terms of performance, since only a single table is involved. On the other hand, concrete class attributes cannot be mapped into not null columns (as all the subclasses are stored in the same main table).
  • A table per concrete entity class (TABLE_PER_CLASS). All properties of the concrete class, including inherited properties, are mapped to columns of one table. In my example I would only have a t_company and t_individual table. Attributes from the abstract class Customer would be copied into these two tables. If this option provides poor support for polymorphic relationships, it is also still optional in the JPA 1.0 specification. TopLink doesn’t support it yet, Hibernate does.
  • A table per class (JOINED). In this strategy the root of the class hierarchy (Customer) as well as each subclass is represented by a separate table. In the Watermelon example I will have a common base table t_customer with an ID, telephone number, and email address. Subclass tables (t_company and t_individual) will then be joined with the root through their primary key. This option provides support for polymorphic relationships and that’s the option I will use to map inheritance for the Watermelon address book.

The choice of the mapping strategy has to be done only once in the root entity using the @javax.persistence.Inheritance annotation. It is specified on the Customer class like this:

@Entity@Table(name = "t_customer")@Inheritance(strategy = InheritanceType.JOINED)@DiscriminatorColumn(name = "DISC", discriminatorType = DiscriminatorType.STRING, length = 5)public abstract class Customer {  @Id  @GeneratedValue  private Long id;  @Column(length = 15)  protected String telephone;  @Column(name = "e_mail")  protected String email;  // constructors, getters, setters}

To work, the JOINED strategy (as well as the SINGLE_TABLE) needs additional information: a discriminator column. This column, situated in the root table, enables the persistent provider to identify which instance of the concrete class is stored (is it an individual or a company that is stored in the t_customer table?). Without any annotations the coding-by-exception will create a column named DTYPE of varchar(31) which value is defaulted to the concrete class name.

In the code above, I’ve decided to name the discriminator column DISC and to give it a length of 5. I can do this using the @javax.persistence.DiscriminatorColumn annotation. The content of this column can be customized in each concrete class by using the @DiscriminatorValue annotation. The value has to be unique and serves to identify the concrete class. The coding-by-exception will default to the class name, but I’ve decided to change it to indiv and comp (see Table 1).

Table 1. Individual and Company Class Extending Customer

Individual class Company class
@Entity
@Table(name = "t_individual")
@DiscriminatorValue("indiv")

public class Individual extends Customer {
@Column(nullable = false)
private String firstname;
@Column(nullable = false, length = 30)
private String lastname;
@Column(name = "date_of_birth")
@Temporal(TemporalType.DATE)
private Date dateOfBirth;
@Transient
private Integer age;
// constructors, getters, setters
}
@Entity
@Table(name = "t_company")
@DiscriminatorValue("comp")

public class Company extends Customer {
@Column(nullable = false)
private String name;
private String contactName;
@Column(name = "nb_employees")
private Integer numberOfEmployees;
// constructors, getters, setters
}

Notice that each of the three entities is mapped to its own table (using the @Table annotation). As explained in the previous article, the EntityManager and the properties set in the persistent unit will automatically create these tables. The result of a JOINED strategy is the DDLs and the integrity constraints between the three tables, as shown in Table 2.

Table 2. DDLs of t_customer, t_individual and t_company

Customer Individual Company
mysql> desc t_customer

+-----------------+-------------+
| Field | Type                  |
+-----------------+-------------+
| ID              | bigint(20)  |
| DISC            | varchar(5)  |
| e_mail          | varchar(255)|
| TELEPHONE       | varchar(15) |
| home_address_fk | bigint(20)  |
+-----------------+-------------+

mysql> desc t_individual
+---------------+-------------+
| Field | Type |
+---------------+-------------+
| ID | bigint(20) |
| LASTNAME | varchar(30) |
| FIRSTNAME | varchar(255)|
| date_of_birth | date |
+---------------+-------------+
mysql> desc t_company
+--------------+--------------+
| Field | Type |
+--------------+--------------+
| ID | bigint(20) |
| CONTACTNAME | varchar(255) |
| NAME | varchar(255) |
| nb_employees | int(11) |
+--------------+--------------+
            

ALTER TABLE t_individual ADD CONSTRAINT FK_t_individual_ID FOREIGN KEY (ID) REFERENCES t_customer (ID)
ALTER TABLE t_company ADD CONSTRAINT FK_t_company_ID FOREIGN KEY (ID) REFERENCES t_customer (ID)

Once you’ve chosen the strategy to map inheritance, you can CRUD any of these concrete classes using the EntityManager like any other entity. In the following code, I create an instance of a Company and an Individual class that I persist into a database.

public void createCustomers() {  // Gets an entity manager  EntityManagerFactory emf = Persistence.createEntityManagerFactory("watermelonPU");  EntityManager em = emf.createEntityManager();  EntityTransaction trans = em.getTransaction();  // Instantiates a company object  Company company = new Company("Universal", "Mr Universe", 50000, "+15488454", "[email protected]");  company.setHomeAddress(new Address("World Street", "New York", "7448", "UK"));  // Instantiates an individual object  calendar.set(1940, 10, 9);  Individual individual = new Individual("John", "Lennon", "+441909", "[email protected]", calendar.getTime());  individual.setHomeAddress(new Address("Abbey Road", "London", "SW14", "UK"));  // Persists both customers  trans.begin();  em.persist(company);  em.persist(individual);  trans.commit();  // Closes the entity manager and the factory  em.close();  emf.close();}

Querying a hierarchical model is also possible with JPQL. I can query the concrete classes as well as the root abstract class.

JPQL Query Comments
SELECT c FROM Customer c Returns all the customers (individuals and companies)
SELECT c FROM Company c Returns all the companies
SELECT i FROM Individual i Returns all the individuals

And I can use root class attributes on my queries. For example, the email address is defined in the Customer class. I can use this attribute in queries involving the Customer class as well as Individual and Company.

SELECT c FROM Customer c WHERE c.email LIKE ‘%.com’ All customers with .com email address
SELECT c FROM Company c WHERE c.email LIKE ‘%.com’ All companies with .com email address
SELECT i FROM Individual i WHERE i.email LIKE ‘%.com’ All individuals with .com email address

Of course, I cannot use child class attributes to query the root class. For example, the attribute first name is just defined in Individual. I can’t use it to query customers or companies.

SELECT c FROM Customer c WHERE c.firstname=’John’ Will not work because customer has no first name
SELECT c FROM Company c WHERE c.firstname=’John’ Will not work because company has no first name
SELECT i FROM Individual i WHERE i.firstname=’John’ Returns the individuals which first name is ‘John’

In the address book, companies have a number of employees. JPQL has a set of arithmetic functions that you can use in a query.

SELECT MAX (c.numberOfEmployees) FROM Company c Returns the maximum number of employees
SELECT MIN (c.numberOfEmployees) FROM Company c Returns the minimum number of employees
SELECT SUM (c.numberOfEmployees) FROM Company c Returns the sum of employees

I can also use restrictions on these numbers by applying inferior, superior, or equal functions, as well as between to get a range of employees.

SELECT c FROM Company c WHERE c.numberOfEmployees > 10000 Returns companies that have more than 10000 employees
SELECT c FROM Company c WHERE c.numberOfEmployees BETWEEN 10000 AND 50000 Returns companies that between 10000 and 50000 employees

One-to-Many Relationships
Now that I’ve mapped the inheritance, I’ll focus on the delivery addresses. In Figure 1 a customer has one home address (mapped in the previous article) and zero or more delivery addresses. This is represented by a unidirectional, one-to-many relationship between Customer and Address.

There are two different strategies you can use to have a one-to-many relationship in a relational database. The first one is to place a foreign key in the table representing the many (t_address), pointing to the primary key of the table representing the one (t_customer). The second is to use a third table (join table) to serve as a link between the two. Unlike inheritance where the developer can choose between several strategies, one-to-many relationships in JPA use a join table.

The code below shows how to manage a one-to-many relationship. Again, I could leave the defaults of coding-by-exception and let JPA map the collection for me, but I’ve decided to use annotations to customize the mapping.

@Entity@Table(name = "t_customer")@Inheritance(strategy = InheritanceType.JOINED)@DiscriminatorColumn(name = "DISC", discriminatorType = DiscriminatorType.STRING, length = 5)public abstract class Customer {  @Id  @GeneratedValue  private Long id;  @Column(length = 15)  protected String telephone;  @Column(name = "e_mail")  protected String email;  @OneToOne(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.REMOVE})  @JoinColumn(name = "home_address_fk", nullable = false)  private Address homeAddress;  @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)  @JoinTable(name = "t_delivery_addresses",    joinColumns = {@JoinColumn(name = "customer_fk")},    inverseJoinColumns = {@JoinColumn(name = "address_fk")})  private List
deliveryAddresses = new ArrayList
(); public void addDeliveryAddress(Address deliveryAddress) { deliveryAddresses.add(deliveryAddress); } // constructors, getters, setters}

The @javax.persistence.OneToMany annotation is used for one-to-many relationships and has the same attributes as the @OneToOne (see the previous article). What I’m doing in the code above is notifying JPA to lazy load the delivery addresses (fetch attribute) and to cascade all kind of events to them (persist, remove…). As I said, JPA maps a one-to-many relationship using a join table. To customize the name and columns of this join table, I can use the @javax.persistence.JoinTable annotation. According to my code, the join table will be called t_delivery_addresses. To change the names of the foreign keys I have to use the @JoinColumn annotation.

With all these annotations, JPA will create the adequate tables and integrity constraints. Table 3 shows the DDLs of the three tables involved in the relation.

Table 3. DDLs of t_customer, t_address, and the join table

Customer Join Table Address
mysql> desc t_customer
+-----------------+-------------+
| Field | Type |
+-----------------+-------------+
| ID | bigint(20) |
| DISC | varchar(5) |
| e_mail | varchar(255)|
| TELEPHONE | varchar(15) |
| home_address_fk | bigint(20) |
+-----------------+-------------+
 
mysql> desc t_delivery_addresses
+-------------+------------+------+
| Field | Type | Null |
+-------------+------------+------+
| customer_fk | bigint(20) | NO |
| address_fk | bigint(20) | NO |
+-------------+------------+------+
mysql> desc t_address
+----------+--------------+
| Field | Type |
+----------+--------------+
| ID | bigint(20) |
| CITY | varchar(100) |
| zip_code | varchar(10) |
| STREET | varchar(255) |
| COUNTRY | varchar(50) |
+----------+--------------+

Many-to-Many Relationship
To help classify the different addresses a customer may have, Watermelon wants to tag them. A tag is just a label (a String) that can be added to a collection of addresses. Tag and Address have a bi-directional, many-to many relationship. In the database, this information will be stored exactly the same way the one-to-many relationships are: by using a third table that joins the primary keys. In Table 4 you will find the @ManyToMany annotation used in conjunction with @JoinTable and @JoinColumn.

Table 4. Many-to-many relationship between the classes Address and Tag are shown.

Address Tag
@Entity
@Table(name = "t_address")
public class Address {
@Id @GeneratedValue
private Long id;
private String street;
@Column(length = 100)
private String city;
@Column(name = "zip_code", length = 10)
private String zipcode;
@Column(length = 50)
private String country;
@ManyToMany(cascade = CascadeType.PERSIST)
@JoinTable(name = "t_address_tag",
joinColumns = {
@JoinColumn(name = "address_fk")},
inverseJoinColumns = {
@JoinColumn(name = "tag_fk")})

private List tags = new ArrayList();
// constructors, getters, setters}
@Entity
@Table(name = "t_tag")
public class Tag {
@Id private String name;
@ManyToMany
private List

addresses;
// constructors, getters, setters}

The identifier (@Id) of the Tag class is the attribute name of type String. The value is set manually, that’s why there is no @GeneratedValue annotation. Both classes use the @ManyToMany annotation meaning that the relationship is bi-directional. Any of these classes could have defined the join table attributes (@JoinTable) but I’ve decided that the Address class will. The join table between addresses and tags is called t_address_tag. Note that I could have omitted the @ManyToMany annotation in the Tag class because it has no specific parameters (coding-by-exception has the same effect).

@OneToMany and @ManyToMany deal with collections of objects. To query collections, JPQL has a set of keywords such as EMPTY, that checks if a collection is empty or not, or MEMBER OF that checks if an object is a member of the collection. The code below shows you how to persist an address and the related tags, as well as doing some queries.

Tag tag1 = new Tag("24/7");Tag tag2 = new Tag("working hours");Tag tag3 = new Tag("week-ends");Address address = new Address("Central Side Park", "New York", "7845", "US");address.addTag(tag1);address.addTag(tag2);address.addTag(tag3);// Perists the address and its tagstrans.begin();em.persist(address);trans.commit();Query query;List
addresses;// Finds all the addresses either in London or New Yorkquery = em.createQuery("SELECT a FROM Address a WHERE a.city IN ('London', 'New York') ");addresses = query.getResultList();// Finds all the addresses that do not have a tagquery = em.createQuery("SELECT a FROM Address a WHERE a.tags IS EMPTY");addresses = query.getResultList();// Finds all the addresses that have at least one tagquery = em.createQuery("SELECT a FROM Address a WHERE a.tags IS NOT EMPTY");addresses = query.getResultList();// Finds all the addresses that have a "week-ends" tagquery = em.createQuery("SELECT a FROM Address a WHERE :param MEMBER OF a.tags");query.setParameter("param", new Tag("week-ends"));addresses = query.getResultList();

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist