Defining JPA 2.0 relationships

RelationshipsBanner

In the examples in the previous part of the tutorial, we looked at how simple JPA could make accessing the data in our database. Now we are going to explore how to configure JPA to give us this functionality.

In the next tutorial, we will discuss the persisting of data (e.g.: saving data to the database). So don’t get frustrated that we don’t cover this immediately–your patience will be rewarded!

Overview

In this installment we will show you how to annotate the following types of relationships

  • Bidirectional One-to-Many / Many-to-One
  • Unidirectional One-to-Many
  • Unidirectional Many-to-One
  • Bidirectional Many-to-Many

These four relationships should cover the bulk of use cases for most applications. But before we dive into these annotations, let’s review the database schema for the application.

DB_Model
Figure 1. ChatServer database schema.

One-to-Many / Many-to-One bidirectional relationship :
Comment ↔ Attachment

The relationship of Comments to Attachments is a One-to-Many relationship in the Comment → Attachment direction (since every Comment can contain many Attachments), and is a Many-to-One relationship in the Attachment → Comment direction (since every Attachment is contained in a single Comment). Now, since this is JPA, we would like to be able to write code like what we have below :

One-to-Many : Comment Attachment direction
(‘contains’ role)

// Get access to a Comment object
Comment comment = em.find(Comment.class, commentId);

// Get a list of the many attachments contained in this Comment
List<Attachment> attachments = comment.getAttachments();

Listing 1

Many-to-One : Attachment Comment direction
(‘contained in’ role)

// Get an Attachment object
Attachment attachment = em.find(Attachment.class, attachmentId);

// Get the one Comment that it is contained in
Comment comment = attachment.getContainerComment();

Listing 2

Note that, because we will be defining methods for both sides of this relation (e.g.: from a Comment object, we can get a list of its attachments, and, from an Attachment object we can get its comment), we refer to this relationship as bidirectional.

RELATION_comment_attachment_CONTAINS
Figure 2. One-to-Many / Many-to-One bidirectional relationship. The foreign key is in the attachment (owner) table.

In the diagram above, we see that it is the Many side of the relationship (attachment) that holds a foreign key to the One side (comment). Specifically, the attachment table contains the commentId foreign key that associates it with the comment it is contained in. In JPA terminology, this makes Attachment the owning side of this bidirectional relationship. Conversely, the other end of the relationship is called the inverse side (e.g.: non-owning side) of the relationship.

Entity Annotations

Many-to-One : Attachment Comment direction

Let’s start by annotating the owning side of the relationship (the Attachment entity). The first thing we need to do is create a variable to hold the Comment entity this Attachment is contained in.

(in the Attachment entity - owning side)

  // holds the Comment this Attachment is contained in
  private Comment containerComment;

Listing 3

The next thing we do is create the accessors for the comment variable, and annotate the getter :

(in the Attachment entity - owning side)

  @ManyToOne(cascade = CascadeType.PERSIST)
  @JoinColumn(name="commentId")
  public Comment getContainerComment() {
      return containerComment;
  }

  public void setContainerComment(Comment containerComment) {
      this.comment = containerComment;
  }

Listing 4

The first annotation @ManyToOne tells JPA that this is a Many-to-One relationship with the Comment entity (JPA automatically infers that this is a relationship with the Comment entity, since the getter we’re annotating returns a Comment). As for the cascade = CascadeType.PERSIST code–we will get into that in the next section where we discuss persistence.

The second annotation is @JoinColumn. The name element of @JoinColumn always tells JPA what column of the owning side is the foreign key for the relationship. Since Attachment is the owning side, the value commentId is a column of the attachment table that references the comment table.

One-to-Many : Comment Attachment direction

OK. So we’ve annotated the Attachment → Comment relationship (e.g.: the ‘contained in’ role) on the Attachment side. We will now annotate the inverse Comment → Attachmentcontains’ role.

As with the Attachment side, we need to create a variable on the Comment side to hold the elements joined by the relationship. However, in this case, a single Comment may be related to multiple Attachments (since a comment can contain multiple attachments). For this, we need to use a Collection interface. We could use a Set, List, Map etc… it just has to extend the java.util.Collection interface.

(in Comment entity - inverse side)

  // stores the Attachments that this Comment contains
  private List<Attachment> attachments;

Listing 5

The next step is to (like before) create the accessors, and then annotate the getter :

(in Comment entity - inverse side)

  @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "containerComment")
  public List<Attachment> getAttachments() {
      return attachments;
  }

  public void setAttachments(List<Attachment> attachments) {
      this.attachments = attachments;
  }	

Listing 6

Here we only have one annotation. As usual, we have the cascade element (which will be covered later), but we also have mappedBy.The mappedBy element is only ever used on the inverse side of a bidirectional relationship. The value of mappedBy tells JPA the name of the variable on owning side that holds the inverse-side-entity. In this case, the value of mappedBy is the containerComment variable in the Attachment object. JPA is able to infer that containerComment is in the Attachment class since the getter we annotate returns a collection of Attachment entities.

But why do we have to specify the specific variable that holds a reference to this Comment? Well, if we didn’t, JPA would assume that Comment was the owning side of the relationship and expect that the foreign key for the relationship was in the Comment entity–which it is not.

One-to-Many unidirectional relationship :
User → Channel

In this section, we’re going to create a unidirectional One-to-Many relationship from User to Channel. This is a “created by” role, since–given a user u–we can retrieve all of the channels that u created.

RELATION_user_channel_CREATOR
Figure 3. One-to-Many unidirectional from User → Channel, where User is the inverse (non-owning) side.

Once we configure JPA to be aware of this relationship, then we will be able to write code like the following :

// find a user via its userId
User user = em.find(User.class, userId);

// Get a list of the channels it has created 
List<Channel> usersChannels = user.getChannels();

Listing 7

Note that, since this is a unidirectional relationship, we won’t be able to write code like

User creator = channel.getCreator();

since the relationship is only accessible from one side (the User entity in this case).

So, how do we inform JPA of this unidirectional One-to-Many “created by” relationship between User and Channel?

Entity Annotations

To inform JPA of the One-to-Many relationship between User and Channel, the first thing we do is create a private collection variable to hold the channels the user has created :

(in the User entity - inverse side)

    // Collection to hold all the channels this user created
  private List<Channel> createdChannels;

Listing 8

We then create accessors (a getter and a setter) for the createdChannels collection, and annotate its getter with information about the One-to-Many relationship :

(excerpt from the User entity - inverse side)

    @OneToMany(cascade = CascadeType.PERSIST)
    @JoinColumn(name="userId")
    public List<Channel> getCreatedChannels() {
        return createdChannels;
    }

    public void setCreatedChannels(List<Channel> createdChannels) {
        this.createdChannels = createdChannels;
    }

Listing 9

The first @OneToMany annotation tells JPA that this is a One-to-Many relationship with a cascade type of PERSIST. The mappedBy element in the @JoinColumn annotation tells JPA that the name of the foreign key for this One-to-Many relationship is in the channel table and that its name is userId. But, how does JPA know that it needs to look for the foreign key in the channel table? Well, as we learned before, the value of the name element in the @JoinTable tag always refers to a foreign key on the owning side of a relationship. Therefore JPA knows it needs to look at the table on the other end of the relationship for the foreign key. But how does JPA know which table is on the other end? Well, the fact that the @OneToMany annotation is placed over a getter that returns a Collection of Channel objects allows JPA to infer that the table on the other end is the channel table.

OK, so that was quite a bit of explanation for a tiny bit of code and annotations. You’ll find this a lot with JPA–with only a few lines of code, you can get JPA to do a lot of deep things–you just have to know what you’re doing.

Having annotated the User entity, the code in listing 7 will now work; with JPA doing all the database queries, all the conversion from SQL data into Java objects, and automatically populating the channels List variable with Channel objects as needed.

Many-to-One unidirectional relationship :
Comment → User

In the previous example, we outlined how to create a unidirectional One-to-Many relationship. Here we will show how to create a unidirectional Many-to-One relationship, from Comment → User. Comment → User is Many-to-One since every Comment has exactly one user that authors it, yet every user can author many comments.

RELATION_comment_user_AUTHOR
Figure 4. Many-to-One unidirectional from Comment → User, where Comment is the owning side.

Once complete, we will be able to write code like the following :

(in Comment entity)

  // retrieve a Comment
  Comment comment = em.find(Comment.class, commentId);

  // retrieve the Comment's author
  User author = comment.getAuthor();

Listing 10

The annotations for this Many-to-One, unidirectional relationship from Comment → User is straight-forward and is very similar to the other relationship annotations we’ve previously encountered.

We first create a variable to store data from the One side (User side) of the relation…

(in Comment entity)

  private User author;

Listing 11

…and then create accessors for it, and annotate the getter :

(in Comment entity)

  @ManyToOne(cascade = CascadeType.PERSIST)
  @JoinColumn(name = "userId")	
  public User getAuthor() {
      return author;
  }
  
  public void setAuthor(User author) {
      this.author = author;
  }	

Listing 12

The first @ManyToOne annotation specifies that this is a Many-to-One relation. The second @JoinColumn annotation tells JPA the name of the foreign key for this relationship. As always, the value of the name element in the @JoinTable annotation always refers to a foreign key on the owning side of the relationship, and hence, JPA knows that the “userId” value in the @JoinColumn(name = “useId”) annotation refers to a column in the comment table.

Many-to-Many bidirectional relationship :
Channel ↔ User

Here, we will describe how to annotate for a Many-to-Many relationship.

RELATION_channel_user_MEMBERSHIP
Figure 5. Many-to-Many bidirectional relationship Channel ↔ User.

Channel ↔ User is Many-to-Many since every channel can have many member users, and every user can be a member of many channels.

(in User entity - owning side)

  private Set<Channel> channelMemberships;

  ...

  @JoinTable(name = "user_to_channel", 
      joinColumns = @JoinColumn(name = "userId"),
      inverseJoinColumns = @JoinColumn(name = "channelId")
  )    
  @ManyToMany(cascade = CascadeType.PERSIST)	
  public Set<Channel> getChannelMemberships() {
      return channelMemberships;
  }

    public void setChannelMemberships(Set<Channel> channelMemberships) {
      this.channelMemberships = channelMemberships;
  }		
	
Listing 15

As you can see, the annotations for a Many-to-Many relationship are a bit more complex than what we’ve seen up until now.

Owning side

Since a Many-to-Many relationship uses a join table, neither side has a foreign key referencing the other. This means that we get to choose which side is the owning side. In this example, we have chosen the User entity to be the owning side.

Entity Annotations – Owning Side

The first annotation is a @JoinTable annotation. This defines the properties of the Many-to-Many join table that relates the user and channel tables. JPA will generate this table for us based on how we define the annotation.

At its simplest, a Many-to-Many join table is simply a two column table that holds pairs of primary keys–one primary key from the user table, and one primary key from the channel table. JPA needs to know (1) the name of this table, (2) what primary keys the columns of this table should hold.

In this example, we are naming the join table user_to_channel. This is specified in the name element :

@JoinTable(name = “user_to_channel”, …).

Within the @JoinTable annotation, we specify the primary keys of the user_to_channel table in joinColumns and inverseJoinColumns elements.

  • name : this is the name that will be given for this foreign key column in the join table
  • referencedColumnName : the name of the foreign key
    • inside a joinColumns element, this will be the name of the key on the owning side (i.e: user.userId)
    • inside an inverseJoinColumns element, this will be the name of the key on the inverse side (channel.channelId)

Since the User entity is the owning side of the relation, we set joinColumns to the user table’s primary key :

joinColumns = @JoinColumn(name = “userId”, referencedColumnName = “userId”)

Similarly, we set inverseJoinColumns to the primary key of the inverse (non-owning) side :

inverseJoinColumns = @JoinColumn(name = “channelId”, referencedColumnName = “channelId”)

Note : In our case, the names of the join table columns are the same as the names of the primary keys in the user and channel tables, so–in fact–we could omit the referencedColumnName element and JPA would automatically use userId and channelId for the join table columns. However, if you need to integrate with an existing database with existing join tables, you could specify different column names for your join table.

In the second annotation, we tell JPA this is a Many-to-Many relationship :

@ManyToMany(cascade = CascadeType.PERSIST)

And that is all we need to set on the owning side.

Entity Annotations – Inverse Side

Comparatively, annotating the inverse side is much easier since we’ve already provided so much information on the owning side.

(in Channel entity)

  private Set<User> members;

  @ManyToMany(mappedBy = "channelMemberships", cascade = CascadeType.PERSIST)
  public Set<User> getMembers() {
      return members;
  }
	
  public void setMembers(Set<User> members) {
      this.members = members;
  }
  
Listing 17

As you can see, we have only a single annotation on the inverse side–the @ManyToMany annotation–and its only properties are mappedBy = "channelMemberships" and (as usual) cascade = CascadeType.PERSIST. Similar to our other bidirectional relationship, use of the mappedBy element on the inverse side allows JPA to find the owning side of the relation which contains the join table information–this allows us to define the join table information in just one spot.

Summary

The remainder of the relationships our application needs are all represented in the examples above. I will provide the full source code of this application on GitHub if you want to see how I’ve done the remainder–however, I’d suggest you attempt to do so yourself before perusing the code.

In the next section, we will discuss JPA persistence. To do so, we will need to get into the JPA architecture a bit more. So, until next time!


Leave a Reply

Your email address will not be published. Required fields are marked *