Mapping Guide

The best resource for learning how to map JDO entities is the DataNucleus website. Take a look at:

The remainder of this page provides guidance on several specific mapping use cases.

1-m Bidirectional relationships

Consider a bidirectional one-to-many association between two entities; a collection member in the "parent" and a property member on the "child".

We can tell DataNucleus about the bidirectionality using @Persistent(mappedBy=…​), or we can take responsibility for this aspect ourselves.

In addition, the two entities can be associated either without or with a join table (indicated by the @Join annotation):

  • without a join table is more common; a regular foreign key in the child table for FermentationVessel points back up to the associated parent Batch

  • with a join table; a link table holds the tuple representing the linkage.

Testing (against dn-core 4.1.7/dn-rdbms 4.1.9) has determined there are two main rules:

  • If not using @Join, then the association must be maintained by setting the child association on the parent.

    It is not sufficient to simply add the child object to the parent’s collection.

  • @Persistent(mappedBy=…​) and @Join cannot be used together.

    Put another way, if using @Join then you must maintain both sides of the relationship in the application code.

In the examples that follow, we use two entities, Batch and FermentationVessel (from a brewery domain). In the original example domain the relationship between these two entities was optional (a FermentationVessel may have either none or one Batch associated with it); for the purpose of this article we’ll explore both mandatory and optional associations.

Mandatory, no @Join

In the first scenario we have use @Persistent(mappedBy=…​) to indicate a bidirectional association, without any @Join:

public class Batch {

    // getters and setters omitted

    @Persistent(mappedBy = "batch", dependentElement = "false")     (1)
    private SortedSet<FermentationVessel> vessels = new TreeSet<FermentationVessel>();
}
1 "mappedBy" means this is bidirectional

and

public class FermentationVessel implements Comparable<FermentationVessel> {

    // getters and setters omitted

    @Column(allowsNull = "false")       (1)
    private Batch batch;

    @Column(allowsNull = "false")
    private State state;                (2)
}
1 mandatory association up to parent
2 State is an enum (omitted)

Which creates this schema:

CREATE TABLE "batch"."Batch"
(
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY,
    ...
    "version" BIGINT NOT NULL,
    CONSTRAINT "Batch_PK" PRIMARY KEY ("id")
)
CREATE TABLE "fvessel"."FermentationVessel"
(
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY,
    "batch_id_OID" BIGINT NOT NULL,
    "state" NVARCHAR(255) NOT NULL,
    ...
    "version" TIMESTAMP NOT NULL,
    CONSTRAINT "FermentationVessel_PK" PRIMARY KEY ("id")
)

That is, there is an mandatory foreign key from FermentationVessel to Batch.

In this case we can use this code:

public Batch transfer(final FermentationVessel vessel) {
    vessel.setBatch(this);                                  (1)
    vessel.setState(FermentationVessel.State.FERMENTING);
    return this;
}
1 set the parent on the child

This sets up the association correctly, using this SQL:

UPDATE "fvessel"."FermentationVessel"
   SET "batch_id_OID"=<0>
       ,"state"=<'FERMENTING'>
       ,"version"=<2016-07-07 12:37:14.968>
 WHERE "id"=<0>

The following code will also work:

public Batch transfer(final FermentationVessel vessel) {
    vessel.setBatch(this);                                  (1)
    getVessels().add(vessel);                               (2)
    vessel.setState(FermentationVessel.State.FERMENTING);
    return this;
}
1 set the parent on the child
2 add the child to the parent’s collection.

However, obviously the second statement is redundant.

Optional, no @Join

If the association to the parent is made optional:

public class FermentationVessel implements Comparable<FermentationVessel> {

    // getters and setters omitted

    @Column(allowsNull = "true")       (1)
    private Batch batch;

    @Column(allowsNull = "false")
    private State state;
}
1 optional association up to parent

Which creates this schema:

CREATE TABLE "batch"."Batch"
(
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY,
    ...
    "version" BIGINT NOT NULL,
    CONSTRAINT "Batch_PK" PRIMARY KEY ("id")
)
CREATE TABLE "fvessel"."FermentationVessel"
(
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY,
    "batch_id_OID" BIGINT NULL,
    "state" NVARCHAR(255) NOT NULL,
    ...
    "version" TIMESTAMP NOT NULL,
    CONSTRAINT "FermentationVessel_PK" PRIMARY KEY ("id")
)

This is almost exactly the same, except the foreign key from FermentationVessel to Batch is now nullable.

In this case then setting the parent on the child still works:

public Batch transfer(final FermentationVessel vessel) {
    vessel.setBatch(this);                                  (1)
    vessel.setState(FermentationVessel.State.FERMENTING);
    return this;
}
1 set the parent on the child

HOWEVER, if we (redundantly) update both sides, then - paradoxically - the association is NOT set up

public Batch transfer(final FermentationVessel vessel) {
    vessel.setBatch(this);                                  (1)
    getVessels().add(vessel);                               (2)
    vessel.setState(FermentationVessel.State.FERMENTING);
    return this;
}
1 set the parent on the child
2 add the child to the parent’s collection.

It’s not clear if this is a bug in dn-core 4.1.7/dn-rdbms 4.19; an earlier thread on the mailing list from 2014 actually gave the opposite advice, see this thread and in particular this message.

In fact we also have had a different case raised (url lost) which argues that the parent should only be set on the child, and the child not added to the parent’s collection. This concurs with the most recent testing.

Therefore, the simple advice is that, for bidirectional associations, simply set the parent on the child, and this will work reliably irrespective of whether the association is mandatory or optional.

With @Join

Although DataNucleus does not complain if @Persistence(mappedBy=…​) and @Join are combined, testing (against dn-core 4.1.7/dn-rdbms 4.19) has shown that the bidirectional association is not properly maintained.

Therefore, we recommend that if @Join is used, then manually maintain both sides of the relationship and do not indicate that the association is bidirectional.

For example:

public class Batch {

    // getters and setters omitted

    @Join(table = "Batch_vessels")
    @Persistent(dependentElement = "false")
    private SortedSet<FermentationVessel> vessels = new TreeSet<FermentationVessel>();
}

and

public class FermentationVessel implements Comparable<FermentationVessel> {

    // getters and setters omitted

    @Column(allowsNull = "true")       (1)
    private Batch batch;

    @Column(allowsNull = "false")
    private State state;
}
1 optional association up to parent

creates this schema:

CREATE TABLE "batch"."Batch"
(
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY,
    ...
    "version" BIGINT NOT NULL,
    CONSTRAINT "Batch_PK" PRIMARY KEY ("id")
)
CREATE TABLE "fvessel"."FermentationVessel"
(
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY,
    "state" NVARCHAR(255) NOT NULL,
    ...
    "version" TIMESTAMP NOT NULL,
    CONSTRAINT "FermentationVessel_PK" PRIMARY KEY ("id")
)
CREATE TABLE "batch"."Batch_vessels"
(
    "id_OID" BIGINT NOT NULL,
    "id_EID" BIGINT NOT NULL,
    CONSTRAINT "Batch_vessels_PK" PRIMARY KEY ("id_OID","id_EID")
)

That is, there is NO foreign key from FermentationVessel to Batch, instead the Batch_vessels table links the two together.

These should then be maintained using:

public Batch transfer(final FermentationVessel vessel) {
    vessel.setBatch(this);                                  (1)
    getVessels().add(vessel);                               (2)
    vessel.setState(FermentationVessel.State.FERMENTING);
    return this;
}
1 set the parent on the child
2 add the child to the parent’s collection.

that is, explicitly update both sides of the relationship.

This generates this SQL:

INSERT INTO "batch"."Batch_vessels" ("id_OID","id_EID") VALUES (<0>,<0>)
UPDATE "batch"."Batch"
   SET "version"=<3>
 WHERE "id"=<0>
UPDATE "fvessel"."FermentationVessel"
   SET "state"=<'FERMENTING'>
      ,"version"=<2016-07-07 12:49:21.49>
 WHERE "id"=<0>

It doesn’t matter in these cases whether the association is mandatory or optional; it will be the same SQL generated.

Mandatory Properties in Subtypes

If you have a hierarchy of classes then you need to decide which inheritance strategy to use.

  • "table per hierarchy", or "rollup" (InheritanceStrategy.SUPERCLASS_TABLE)

    whereby a single table corresponds to the superclass, and also holds the properties of the subtype (or subtypes) being rolled up

  • "table per class" (InheritanceStrategy.NEW_TABLE)

    whereby there is a table for both superclass and subclass, in 1:1 correspondence

  • "rolldown" (InheritanceStrategy.SUBCLASS_TABLE)

    whereby a single table holds the properties of the subtype, and also holds the properties of its supertype

In the first "rollup" case, we can have a situation where - logically speaking - the property is mandatory in the subtype - but it must be mapped as nullable in the database because it is n/a for any other subtypes that are rolled up.

In this situation we must tell JDO that the column is optional, but to Apache Causeway we want to enforce it being mandatory. This can be done using the @Property(optionality=Optionality.MANDATORY) annotation.

For example:

import javax.jdo.annotations.Column;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.InheritanceStrategy;
import lombok.Getter;
import lombok.Setter;

@Inheritance(strategy = InheritanceStrategy.SUPER_TABLE)
public class SomeSubtype extends SomeSuperType {
    @Column(allowsNull="true")
    @Property(optionality=Optionality.MANDATORY)
    @Getter @Setter
    private LocalDate date;
}

Mapping to a View

JDO/DataNucleus supports the ability to map the entity that is mapped to a view rather than a database table. Moreover, DataNucleus itself can create/maintain this view.

One use case for this is to support use cases which act upon aggregate information. An example is in the (non-ASF) Estatio application, which uses a view to define an "invoice run": a representatoin of all pending invoices to be sent out for a particular shopping centre. (Note that example also shows the entity as being "non-durable", but if the view is read/write then — I think — that this isn’t necessary required).

For more on this topic, see the DataNucleus documentation.

Custom Value Types

The framework provides a number of custom value types. Some of these are wrappers around a single value (eg AsciiDoc or Password) while others map onto multiple values (eg Blob).

This section shows how to map each (and can be adapted for your own custom types or @Embedded values).

Mapping AsciiDoc

The AsciiDoc value type is used for documentation written using the AsciiDoc syntax:

  • In the domain entity, map AsciiDoc type using @Column(jdbcType = "CLOB"):

    MyEntity.java
    public class MyEntity ... {
    
        @Column(allowsNull = "false", jdbcType = "CLOB")
        @Property
        @Getter @Setter
        private AsciiDoc documentation;
    
    }
  • in the webapp module, register the JDO specific converter by:

    • adding a dependency to this module:

      pom.xml
      <dependency>
          <groupId>org.apache.causeway.valuetypes</groupId>
          <artifactId>causeway-valuetypes-asciidoc-persistence-jdo</artifactId>
      </dependency>
    • and adding reference the corresponding module in the AppManifest:

      AppManifest.java
      @Configuration
      @Import({
              ...
              CausewayModuleValAsciidocPersistenceJdo.java
              ...
      })
      public class AppManifest {
      }

Mapping Markdown

The Markdown value type is used for documentation written using markdown:

  • In the domain entity, map Markdown type using @Column(jdbcType = "CLOB"):

    MyEntity.java
    public class MyEntity ... {
    
        @Column(allowsNull = "false", jdbcType = "CLOB")
        @Property
        @Getter @Setter
        private Markdown documentation;
    
    }
  • in the webapp module, register the JDO specific converter by:

    • adding a dependency to this module:

      pom.xml
      <dependency>
          <groupId>org.apache.causeway.valuetypes</groupId>
          <artifactId>causeway-valuetypes-markdown-persistence-jdo</artifactId>
      </dependency>
    • and adding reference the corresponding module in the AppManifest:

      AppManifest.java
      @Configuration
      @Import({
              ...
              CausewayModuleValMarkdownPersistenceJdo.java
              ...
      })
      public class AppManifest {
      }

Mapping Blobs and Clobs

The JDO ObjectStore integration of DataNucleus ORM can automatically persist Blob and Clob values into multiple columns, corresponding to their constituent parts.

Blobs

To map a Blob, use:

MyEntity.java
public class MyEntity ... {

    @Persistent(defaultFetchGroup="false", columns = {
            @Column(name = "pdf_name"),                 (1)
            @Column(name = "pdf_mimetype"),             (2)
            @Column(name = "pdf_bytes")                 (3)
    })
    @Getter @Setter
    private Blob pdf;

}
1 string, maps to a varchar in the database
2 string, maps to a varchar in the database
3 byte array, maps to a Blob or varbinary in the database

Clobs

To map a Clob, use:

MyEntity.java
public class MyEntity ... {

    @Persistent(defaultFetchGroup="false", columns = {
            @Column(name = "xml_name"),                 (1)
            @Column(name = "xml_mimetype"),             (2)
            @Column(name = "xml_chars"                  (3)
                    , jdbcType = "CLOB"
            )
    })
    @Getter @Setter
    private Clob xml;

}
1 string, maps to a varchar in the database
2 string, maps to a varchar in the database
3 char array, maps to a Clob or varchar in the database