JPA : 1-N Relationships with Maps

You have a 1-N (one to many) when you have one object of a class that has a Map of objects of another class. There are two general ways in which you can represent this in a datastore. Join Table (where a join table is used to provide the relationship mapping between the objects), and Foreign-Key (where a foreign key is placed in the table of the object contained in the Map.

The various possible relationships are described below.

This page is aimed at Map fields and so applies to fields of Java type java.util.HashMap, java.util.Hashtable, java.util.LinkedHashMap, java.util.Map, java.util.SortedMap, java.util.TreeMap, java.util.Properties

Please note that RDBMS supports the full range of options on this page, whereas other datastores (ODF, Excel, HBase, MongoDB, etc) persist the Map in a column in the owner object rather than using join-tables or foreign-keys since those concepts are RDBMS-only


1-N Map using Join Table

We have a class Account that contains a Map. With a Map we store values using keys. As a result we have the following combinations of key and value, bearing in mind whether the key or value is persistable.

Map[Simple, Entity

Here our key is a simple type (in this case a String) and the values are persistable. Like this

If you define the Meta-Data for these classes as follows

@Entity
public class Account
{
    @OneToMany
    @JoinTable
    Map<String, Address> addresses;

    ...
}

@Entity
public class Address {...}

This will create 3 tables in the datastore, one for Account, one for Address and a join table also containing the key.


You can configure the names of the columns in the join table using the joinColumns attributes of the various annotations.

Please note that the column ADPT_PK_IDX is added by DataNucleus when the column type of the key is not valid to be part of a primary key (with the RDBMS being used). If the column type of your key is acceptable for use as part of a primary key then you will not have this "ADPT_PK_IDX" column.


Map[Simple, Simple]

Here our keys and values are of simple types (in this case a String). Like this

If you define the Meta-Data for these classes as follows

@Entity
public class Account
{
    @ElementCollection
    @CollectionTable
    Map<String, String> addresses;

    ...
}

This results in just 2 tables. The "join" table contains both the key AND the value.


You can configure the names of the columns in the join table using the joinColumns attributes of the various annotations.

Please note that the column ADPT_PK_IDX is added by DataNucleus when the column type of the key is not valid to be part of a primary key (with the RDBMS being used). If the column type of your key is acceptable for use as part of a primary key then you will not have this "ADPT_PK_IDX" column.


Map[Entity, Simple]

Here our key is an entity type and the value is a simple type (in this case a String). Please note that JPA does NOT properly allow for this in its specification. Other implementations introduced the following hack so we also provide it. Note that there is no OneToMany annotation here so this is seemingly not a relation to JPA (hence our description of this as a hack). Anyway use it to workaround JPA's lack of feature.

If you define the Meta-Data for these classes as follows

@Entity
public class Account
{
    @ElementCollection
    @JoinTable
    Map<Address, String> addressLookup;

    ...
}

@Entity
public class Address {...}

This will create 3 tables in the datastore, one for Account, one for Address and a join table also containing the value.

You can configure the names of the columns in the join table using the joinColumns attributes of the various annotations.


1-N Map using Foreign-Key

1-N Foreign-Key Unidirectional (key stored in value)

In this case we have an object with a Map of objects and we're associating the objects using a foreign-key in the table of the value. We're using a field (alias) in the Address class as the key of the map.

In this relationship, the Account class has a Map of Address objects, yet the Address knows nothing about the Account. In this case we don't have a field in the Address to link back to the Account and so DataNucleus has to use columns in the datastore representation of the Address class. So we define the MetaData like this

<entity-mappings>
    <entity class="Account">
        <table name="ACCOUNT"/>
        <attributes>
            <id name="id">
                <column name="ACCOUNT_ID"/>
            </id>
            ...
            <one-to-many name="addresses" target-entity="com.mydomain.Address">
                <map-key name="alias"/>
                <join-column name="ACCOUNT_ID_OID"/>
            </one-to-many>
        </attributes>
    </entity>

    <entity class="Address">
        <table name="ADDRESS"/>
        <attributes>
            <id name="id">
                <column name="ADDRESS_ID"/>
            </id>
            ...
            <basic name="alias">
                <column name="KEY" length="20"/>
            </basic>
        </attributes>
    </entity>
</entity-mappings>

Again there will be 2 tables, one for Address, and one for Account. If you wish to specify the names of the columns used in the schema for the foreign key in the Address table you should use the join-column element within the field of the map.



In terms of operation within your classes of assigning the objects in the relationship. You have to take your Account object and add the Address to the Account map field since the Address knows nothing about the Account. Also be aware that each Address object can have only one owner, since it has a single foreign key to the Account.


1-N Foreign-Key Bidirectional (key stored in value)

In this case we have an object with a Map of objects and we're associating the objects using a foreign-key in the table of the value.

With these classes we want to store a foreign-key in the value table (ADDRESS), and we want to use the "alias" field in the Address class as the key to the map. If you define the Meta-Data for these classes as follows

<entity-mappings>
    <entity class="Account">
        <table name="ACCOUNT"/>
        <attributes>
            <id name="id">
                <column name="ACCOUNT_ID"/>
            </id>
            ...
            <one-to-many name="addresses" target-entity="com.mydomain.Address" mapped-by="account">
                <map-key name="alias"/>
            </one-to-many>
        </attributes>
    </entity>

    <entity class="Address">
        <table name="ADDRESS"/>
        <attributes>
            <id name="id">
                <column name="ADDRESS_ID"/>
            </id>
            ...
            <basic name="alias">
                <column name="KEY" length="20"/>
            </basic>
            <many-to-one name="account">
                <join-column name="ACCOUNT_ID_OID"/>
            </many-to-one>
        </attributes>
    </entity>
</entity-mappings>

This will create 2 tables in the datastore. One for Account, and one for Address. The table for Address will contain the key field as well as an index to the Account record (notated by the mapped-by tag).