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<Tag> tags = new ArrayList<Tag>(); // constructors, getters, setters}
|
@Entity @Table(name = "t_tag") public class Tag { @Id private String name; @ManyToMany private List<Address> 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 tags
trans.begin();
em.persist(address);
trans.commit();
Query query;
List<Address> addresses;
// Finds all the addresses either in London or New York
query = 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 tag
query = em.createQuery("SELECT a FROM Address a WHERE a.tags IS EMPTY");
addresses = query.getResultList();
// Finds all the addresses that have at least one tag
query = 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" tag
query = em.createQuery("SELECT a FROM Address a WHERE :param MEMBER OF a.tags");
query.setParameter("param", new Tag("week-ends"));
addresses = query.getResultList();