JDO : M-N Relationships

You have a M-to-N (or Many-to-Many) relationship if an object of a class A has associated objects of class B, and class B has associated objects of class A. This relationship may be achieved through Java Set, Map, List or subclasses of these, although the only one that supports a true M-N is for a Set/Collection.

With DataNucleus this can be set up as described in this section, using what is called a Join Table relationship. Let's take the following example and describe how to model it with the different types of collection classes. We have 2 classes, Product and Supplier as below.



Here the Product class knows about the Supplier class. In addition the Supplier knows about the Product class, however with DataNucleus (as with the majority of JDO implementations) these relationships are independent.

Please note that RDBMS supports the full range of options on this page, whereas other datastores (ODF, Excel, HBase, MongoDB, etc) persist the Collection in a column in the owner object and a column in the non-owner object rather than using join-tables since that concept is RDBMS-only.

Please note when adding objects to an M-N relation, you MUST add to the owner side as a minimum, and optionally also add to the non-owner side. Just adding to the non-owner side will not add the relation.

The various possible relationships are described below.

equals() and hashCode()

Important : The element of a Collection ought to define the methods equals and hashCode so that updates are detected correctly. This is because any Java Collection will use these to determine equality and whether an element is contained in the Collection. Note also that the hashCode() should be consistent throughout the lifetime of a persistable object. By that we mean that it should not use some basis before persistence and then use some other basis (such as the object identity) after persistence, for this reason we do not recommend usage of JDOHelper.getObjectId(obj) in the equals/hashCode methods.


Using Set

If you define the XML metadata for these classes as follows

<package name="mydomain">
    <class name="Product" identity-type="datastore">
        ...
        <field name="suppliers" table="PRODUCTS_SUPPLIERS">
            <collection element-type="mydomain.Supplier"/>
            <join>
                <column name="PRODUCT_ID"/>
            </join>
            <element>
                <column name="SUPPLIER_ID"/>
            </element>
        </field>
    </class>

    <class name="Supplier" identity-type="datastore">
        ...
        <field name="products" mapped-by="suppliers">
            <collection element-type="mydomain.Product"/>
        </field>
    </class>
</package>

alternatively using annotations

public class Product
{
    ...

    @Persistent(table="PRODUCTS_SUPPLIERS")
    @Join(column="PRODUCT_ID")
    @Element(column="SUPPLIER_ID")
    Set<Supplier> suppliers;
}

public class Supplier
{
    ...

    @Persistent(mappedBy="suppliers")
    Set<Products> products;
}

Note how we have specified the information only once regarding join table name, and join column names as well as the <join>. This is the JDO standard way of specification, and results in a single join table.





See also :-


Using Ordered Lists

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

<package name="mydomain">
    <class name="Product" identity-type="datastore">
        ...

        <field name="suppliers">
            <collection element-type="mydomain.Supplier"/>
            <order>
                <extension vendor-name="datanucleus" key="list-ordering" value="id ASC"/>
            </order>
            <join/>
        </field>
    </class>

    <class name="Supplier" identity-type="datastore">
        ...

        <field name="products">
            <collection element-type="mydomain.Product"/>
            <order>
                <extension vendor-name="datanucleus" key="list-ordering" value="id ASC"/>
            </order>
            <join/>
        </field>
    </class>
</package>

or using annotations

public class Product
{
    ...

    @Persistent(table="PRODUCTS_SUPPLIERS")
    @Join(column="PRODUCT_ID")
    @Element(column="SUPPLIER_ID")
    @Order(extensions=@Extension(vendorName="datanucleus", key="list-ordering", value="id ASC"))
    List<Supplier> suppliers
}

public class Supplier
{
    ...

    @Persistent
    @Order(extensions=@Extension(vendorName="datanucleus", key="list-ordering", value="id ASC"))
    List<Product> products
}

There will be 3 tables, one for Product, one for Supplier, and the join table. The difference from the Set example is that we now have <order-by> at both sides of the relation. This has no effect in the datastore schema but when the Lists are retrieved they are ordered using the specified order-by.



Using indexed Lists

Firstly a true M-N relation with Lists is impossible since there are two lists, and it is undefined as to which one applies to which side etc. What is shown below is two independent 1-N unidirectional join table relations. If you define the Meta-Data for these classes as follows

<package name="mydomain">
    <class name="Product" identity-type="datastore">
        ...
        <field name="suppliers" persistence-modifier="persistent">
            <collection element-type="mydomain.Supplier"/>
            <join/>
        </field>
    </class>

    <class name="Supplier" identity-type="datastore">
        ...
        <field name="products" persistence-modifier="persistent">
            <collection element-type="mydomain.Product"/>
            <join/>
        </field>
    </class>
</package>

alternatively using annotations

public class Product
{
    ...

    @Join
    List<Supplier> suppliers;
}

public class Supplier
{
    ...

    @Join
    List<Products> products;
}

There will be 4 tables, one for Product, one for Supplier, and the join tables. The difference from the Set example is in the contents of the join tables. An index column is added to keep track of the position of objects in the Lists.



In the case of a List at both ends it doesn't make sense to use a single join table because the ordering can only be defined at one side, so you have to have 2 join tables.

Using Map

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

<package name="mydomain">
    <class name="Product" identity-type="datastore">
        ...
        <field name="suppliers" persistence-modifier="persistent">
            <map key-type="java.lang.String" value-type="mydomain.Supplier"/>
            <join/>
        </field>
    </class>

    <class name="Supplier" identity-type="datastore">
        ...
        <field name="products" persistence-modifier="persistent">
            <map key-type="java.lang.String" value-type="mydomain.Product"/>
            <join/>
        </field>
    </class>
</package>

This will create 4 tables in the datastore, one for Product, one for Supplier, and the join tables which also contains the keys to the Maps (a String).



Relationship Behaviour

Please be aware of the following.

  • To add an object to an M-N relationship you need to set it at both ends of the relation since the relation is bidirectional and without such information the JDO implementation won't know which end of the relation is correct.
  • If you want to delete an object from one end of a M-N relationship you will have to remove it first from the other objects relationship. If you don't you will get an error message that the object to be deleted has links to other objects and so cannot be deleted.