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 parentBatch
-
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 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
-
In the domain entity, map
AsciiDoc
type using@Column(jdbcType = "CLOB")
:MyEntity.javapublic 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.javapublic 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:
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:
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 |