Enterprise Java Development@TOPIC@
Built on: 2014-03-07 00:01 EST
Copyright © 2014 jim stafford (jcstaff@apl.jhu.edu)
Abstract
This document contains a series of exercises for mapping Java classes (without relationships) to the database using JPA. It covers many of the core and corner mapping issues and demonstrates various issues that come up.
To provide hands on experience
Defining JPA entity classes and mapping them to a relational database schema
Using JPA annotations and XML descriptors
Defining primary key mechanisms for entities
Defining alternate entity/table mappings that do not require JPA relationships
At the completion of this exercise, the student will be able to
Create a legal POJO to be used as a JPA entity
Map POJO classes and properties to database tables and columns
Define entity mappings with Java annotations and XML descriptors
Define different primary key strategies
Define multi-table/single-class joins
Define embedded object mappings
Generate a database schema based on their entity definitions
The project created from this archetype has two mechanisms for managing schema; maven plugin-based and entitymanager-based. Know that when you start this exercise the two mechanisms may be both activated and competing (last one wins). One type may be better for some usages while the other may be more helpful in others. Know which is best to use in which circumstance and which one is active or turned off when encountering a database setup issue.
The entitymanager mechanism is activated and deactivated through the hibernate.hbm2ddl.auto property in src/test/resources/hibernate.properties. Set to create to turn on or comment out to turn off.
# src/test/resources/hibernate.properties ... #hibernate.hbm2ddl.auto=create
The maven plugin mechanism is turned activated by the sql-maven-plugin within the local pom.xml. You can either comment out the entire plugin, or comment out or mangle key sections of the plugin definition in order to turn off this capability.
# pom.xml
<!-- runs schema against the DB
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sql-maven-plugin</artifactId>
...
</plugin>
-->
The entitymanager technique is useful when making quick schema changes to your entities within Eclipse but not as good if you want to preserve data for analysis. The maven plugin technique is useful when you commonly build from maven after making entity changes. It is also useful in generating schema that can be analyzed after the build is complete.
Add the following to your .m2/settings.xml file. This will allow you to resolve the exercise archetype and set a default database for the exercise.
<profiles>
<profile>
<id>webdev-repositories</id>
<repositories>
<repository>
<id>webdev-baseline</id>
<name>webdev.apl.jhu.edu baseline</name>
<url>http://webdev.apl.jhu.edu/~jcs/maven2</url>
</repository>
<repository>
<id>webdev-snapshot</id>
<name>webdev.apl.jhu.edu SNAPSHOT</name>
<url>http://webdev.apl.jhu.edu/~jcs/maven2-snapshot</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>webdev-plugins</id>
<name>ejava webdev repository</name>
<url>http://webdev.apl.jhu.edu/~jcs/maven2</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>h2db</activeProfile>
<!--
<activeProfile>h2srv</activeProfile>
-->
</activeProfiles>
Use the ejava.jpa:jpa-archetype to setup a new Maven project for this exercise. Activate the webdev-repositories profile (-Pwebdev-repositories) so that you can resolve the archetype off the Internet.
$ mvn archetype:generate -B \ -DarchetypeGroupId=ejava.jpa -DarchetypeArtifactId=jpa-archetype \ -DgroupId=myorg.entityex -DartifactId=entityEx \ -Pwebdev-repositories [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building JPA Entity 3.0.2013.1-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ jpa-entity >>> [INFO] [INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ jpa-entity <<< [INFO] [INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ jpa-entity --- [INFO] Generating project in Batch mode [INFO] Archetype [ejava.jpa:jpa-archetype:3.0.2013.1-SNAPSHOT] found in catalog local [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating project from Archetype: jpa-archetype:3.0.2013.1-SNAPSHOT [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: myorg.entityex [INFO] Parameter: artifactId, Value: entityEx [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: package, Value: myorg.entityex [INFO] Parameter: packageInPathFormat, Value: myorg/entityex [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: package, Value: myorg.entityex [INFO] Parameter: groupId, Value: myorg.entityex [INFO] Parameter: artifactId, Value: entityEx [INFO] Parent element not overwritten in /home/jcstaff/workspaces/ejava-javaee/git/jpa/jpa-entity/entityEx/pom.xml [INFO] project created from Archetype in dir: /home/jcstaff/workspaces/ejava-javaee/git/jpa/jpa-entity/entityEx [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.535s [INFO] Finished at: Sat Feb 09 23:04:10 EST 2013 [INFO] Final Memory: 15M/59M [INFO] ------------------------------------------------------------------------
You should now have an instantiated template for a JPA project
entityEx/
├── pom.xml
└── src
├── main
│ └── java
│ └── myorg
│ └── entityex
│ └── Auto.java
└── test
├── java
│ └── myorg
│ └── entityex
│ └── AutoTest.java
└── resources
├── hibernate.properties
├── log4j.xml
└── META-INF
└── persistence.xml
Verify the instantiated template builds in your environment
Activate the h2db profile (and deactivate the h2srv profile) to use an embedded file as your database. This option does not require a server but is harder to inspect database state in between tests.
relationEx]$ mvn clean test -Ph2db -P\!h2srv ... -HHH000401: using driver [org.h2.Driver] at URL [jdbc:h2:/home/jcstaff/workspaces/ejava-javaee/git/jpa/jpa-relation/relationEx/target/h2db/ejava] ... [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
Activate the h2srv profile (and deactivate the h2db profile) to use a running H2 database server. This option provides more interaction with your database but does require the server to be running.
relationEx]$ mvn clean test -P\!h2db -Ph2srv ... -HHH000401: using driver [org.h2.Driver] at URL [jdbc:h2:tcp://127.0.0.1:9092/h2db/ejava] ... [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
You may now import the instantiated template into Eclipse as an "Existing Maven Project"
This chapter will take you through the steps to register a Java POJO with the JPA persistence unit using both orm.xml mapping-file descriptors and Java class annotations. It will also take you through the steps to define a POJO class legal to be used as JPA entity class.
JPA Classes are required to ...
Be identified as being a JPA entity class
Have a default constructor
At least have one property defined as the primary key
Create a POJO Java class in the ...mapped Java package
package myorg.entityex.mapped;
import java.util.Date;
public class Animal {
private int id;
private String name;
private Date dob;
private double weight;
public Animal(String name, Date dob, double weight) {
this.name = name;
this.dob = dob;
this.weight = weight;
}
public int getId() { return id; }
public void setId(int id) {
this.id = id;
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public Date getDob() { return dob; }
public void setDob(Date dob) {
this.dob = dob;
}
public double getWeight() { return weight; }
public void setWeight(double weight) {
this.weight = weight;
}
}
Copy the existing AutoTest.java to AnimalTest.java and remove (or ignore) references to the Auto class from AnimalTest.java
Attempt to persist the Animal by adding the following @Test method to the AnimalTest.java JUnit class.
# src/test/java/myorg/entityex/AnimalTest.java
@Test
public void testCreateAnimal() {
log.info("testCreateAnimal");
Animal animal = new Animal("bessie",
new GregorianCalendar(1960, 1, 1).getTime(), 1400.2);
em.persist(animal);
assertNotNull("animal not found", em.find(Animal.class,animal.getId()));
}
Attempt to build and run your test. Your test should fail with the following error message. This means that although your class is a valid Java POJO, it has not been made known to the persistence unit as a JPA entity.
testCreateAnimal(myorg.entityex.AutoTest): Unknown entity: myorg.entityex.mapped.Animal
...
java.lang.IllegalArgumentException: Unknown entity: myorg.entityex.mapped.Animal
at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:856)
at myorg.entityex.AutoTest.testCreateAnimal(AutoTest.java:100)
Add the POJO class to the persistence unit by adding an orm.xml JPA mapping file to your project. Place the file in the src/main/resources/orm directory.
# src/main/resources/orm/Animal-orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd" version="2.0">
<entity class="myorg.entityex.mapped.Animal"/>
</entity-mappings>
Register the orm.xml file with the persistence unit by adding a mapping-file element reference.
# src/test/resources/META-INF/persistence.xml
<persistence-unit name="entityEx-test">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<mapping-file>orm/Animal-orm.xml</mapping-file>
<class>myorg.entityex.Auto</class>
<properties>
...
Attempt to build and run your test. Your test should fail with the following error message. The specifics of the error message will depend upon whether you are running just the JUnit test or building within Maven since the pom is configured to build database schema from the JPA mappings prior to running the JUnit test.
PersistenceUnit: entityEx-test] Unable to configure EntityManagerFactory: No identifier specified for entity: myorg.entityex.mapped.Animal
Caused by: org.hibernate.AnnotationException: No identifier specified for entity: myorg.entityex.mapped.Animal
Although the class is a valid POJO and we followed the deployment descriptor mechanism for registering it with the persistence unit, it is not a legal entity. The error message indicates it is lacking a primary key field.
Update the orm.xml file and define the "id" column as the primary key property for the entity.
<entity class="myorg.entityex.mapped.Animal">
<attributes>
<id name="id"/>
</attributes>
</entity>
Rebuild your module and it should now persist the POJO as a JPA entity. The SQL should be printed in the debug output.
$ mvn clean test
...
Hibernate:
insert
into
Animal
(dob, name, weight, id)
values
(?, ?, ?, ?)
-tearDown() complete, em=org.hibernate.ejb.EntityManagerImpl@12a80ea3
-closing entity manager factory
-HHH000030: Cleaning up connection pool [jdbc:h2:/home/jcstaff/workspaces/ejava-javaee/git/jpa/jpa-entity/entityEx/target/h2db/ejava]
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.94 sec
Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Update your JUnit test method to look like the following. The unit test now clears the cache of entities and forces the entity manager to instantiate a new instance for the value returned from the find().
@Test
public void testCreateAnimal() {
log.info("testCreateAnimal");
Animal animal = new Animal("bessie",
new GregorianCalendar(1960, 1, 1).getTime(), 1400.2);
em.persist(animal);
assertNotNull("animal not found", em.find(Animal.class,animal.getId()));
em.flush(); //make sure all writes were issued to DB
em.clear(); //purge the local entity manager entity cache to cause new instance
assertNotNull("animal not found", em.find(Animal.class,animal.getId()));
}
Attempt to rebuild your module. It should fail because the entity class does not have a default constructor. Remember that default constructors are provided for free in POJOs until you add the first constructor. Once you add a custom constructor you are required to add a default constructor to make it a legal entity class.
javax.persistence.PersistenceException: org.hibernate.InstantiationException: No default constructor for entity: myorg.entityex.mapped.Animal
Update the POJO with a default constructor.
public Animal() {} //must have default ctor
public Animal(String name, Date dob, double weight) {
this.name = name;
this.dob = dob;
this.weight = weight;
}
Rebuild the module. It should now pass because you have defined and registered a compliant entity class. The class was
Registered as an entity using and orm.xml deployment descriptor wired into the persistence unit through a mapping-file reference in the persistence.xml
Assigned an identity property to use for a primary key. The Java "id" property existed from the start, but the property had to be identified to JPA.
Provided with a default constructor. If we removed the custom constructor -- we would get this constructor for free.
Copy the POJO class to a new java package and class name (Animal2).
package myorg.entityex.annotated;
import java.util.Date;
public class Animal2 {
private int id;
private String name;
private Date dob;
private double weight;
public Animal2() {} //must have default ctor
...
}
Add a javax.persistence.Entity annotation to the class
import javax.persistence.Entity;
@javax.persistence.Entity
public class Animal2 {
Register the new entity with the persistence.xml using a class element reference
<persistence-unit name="entityEx-test">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<mapping-file>orm/Animal-orm.xml</mapping-file>
<class>myorg.entityex.Auto</class>
<class>myorg.entityex.annotated.Animal2</class>
<properties>
Classes annotated with @Entity are automatically located when placed in the same archive as the persistence.xml or correctly referenced by a jarfile element. However, since this exercise has placed the persistence.xml outside of the src/test tree with the @Entity classes, we must define them in the persistence unit. Note how the current structure looks -- the persistence.xml and Animal2 class are in what is considered separate archives.
$ tree target/*classes
target/classes
├── ddl
├── myorg
│ └── entityex
│ ├── annotated
│ │ └── Animal2.class
│ ├── Auto.class
│ └── mapped
│ └── Animal.class
└── orm
└── Animal-orm.xml
target/test-classes
├── hibernate.properties
├── log4j.xml
├── META-INF
│ └── persistence.xml
└── myorg
└── entityex
└── AutoTest.class
Add a new test method to work with the new class added to the module.
@Test
public void testCreateAnimalAnnotated() {
log.info("testCreateAnimalAnnotated");
myorg.entityex.annotated.Animal2 animal = new myorg.entityex.annotated.Animal2("bessie",
new GregorianCalendar(1960, 1, 1).getTime(), 1400.2);
em.persist(animal);
assertNotNull("animal not found", em.find(myorg.entityex.annotated.Animal2.class,animal.getId()));
em.flush(); //make sure all writes were issued to DB
em.clear(); //purge the local entity manager entity cache to cause new instance
assertNotNull("animal not found", em.find(myorg.entityex.annotated.Animal2.class,animal.getId()));
Attempt to build/run your module at this point. You should get a familiar error about Animal2 not having an identifier.
Unable to configure EntityManagerFactory: No identifier specified for entity: myorg.entityex.annotated.Animal2
Since we want to use annotations for the new class, fix the issue by adding a @javax.persistence.Id annotation to the id attribute. This is called FIELD access in JPA. You can alternately use PROPERTY access by moving the annotation to the getId() method.
@javax.persistence.Id
private int id;
Re-run you test. It should succeed this time.
$ mvn clean test ... [INFO] BUILD SUCCESS $ cat
If you would like to observe the data in the database, do two things
Turn off the hibernate.hbm2ddl.auto=create option by commenting it out in hibernate.properties
$ more src/test/resources/hibernate.properties hibernate.dialect=org.hibernate.dialect.H2Dialect hibernate.connection.url=jdbc:h2:/home/jenkins/workspace/javaee-class-deploy/jpa/jpa-entity/jpa-entity-ex-docbook/target/h2db/ejava hibernate.connection.driver_class=org.h2.Driver hibernate.connection.password= hibernate.connection.username=sa #hibernate.hbm2ddl.auto=create hibernate.show_sql=true hibernate.format_sql=true #hibernate.jdbc.batch_size=0
Make sure you are running with the h2srv (server) profile and have the server running. The easier way to run with the server profile within Eclipse is to choose it as one of your active profiles in .m2/settings/settings.xml and have Eclipse re-read your settings.xml
# start server
$ java -jar (localRepository)/com/h2database/h2/1.3.168/h2-1.3.168.jar
# .m2/settings.xml
<activeProfiles>
<!--
<activeProfile>h2db</activeProfile>
-->
<activeProfile>h2srv</activeProfile>
# or manually from command line
$ mvn clean test -P\!h2db -Ph2srv
Type the following command in the H2 browser UI
SELECT * FROM ANIMAL2; ID DOB NAME WEIGHT 0 1960-02-01 00:00:00.0 bessie 1400.2
In this chapter we will create custom class/database mappings for some class properties
Map a class to a specific table
Map a property to a specific column
Define constraints for properties
Take a look at using getters and setters
Copy your Animal.java class to Cat.java
package myorg.entityex.mapped;
import java.util.Date;
public class Cat {
private int id;
private String name;
private Date dob;
private double weight;
public Cat() {} //must have default ctor
public Cat(String name, Date dob, double weight) {
this.name = name;
this.dob = dob;
this.weight = weight;
}
public int getId() { return id; }
...
Copy your Animal2.java class to Cat2.java
package myorg.entityex.annotated;
import java.util.Date;
@javax.persistence.Entity
public class Cat2 {
private int id;
private String name;
private Date dob;
private double weight;
public Cat2() {} //must have default ctor
public Cat2(String name, Date dob, double weight) {
this.name = name;
this.dob = dob;
this.weight = weight;
}
@javax.persistence.Id
public int getId() { return id; }
...
Name the new Cat entity class in the Animal-orm.xml
# src/main/resources/orm/Animal-orm.xml
<entity class="myorg.entityex.mapped.Animal">
...
<entity class="myorg.entityex.mapped.Cat">
<attributes>
<id name="id"/>
</attributes>
</entity>
Name the new Cat2 entity class in the persistence.xml
# src/test/resources/META-INF/persistence.xml
<mapping-file>orm/Animal-orm.xml</mapping-file>
<class>myorg.entityex.Auto</class>
<class>myorg.entityex.annotated.Animal2</class>
<class>myorg.entityex.annotated.Cat2</class>
Rebuild your module form the command line and observe the create schema generated for Cat and Cat2. Notice that the JPA provider used the class name as the default entity name and will be attempting to map the entity to a database table by the same name as the entity.
$ more target/classes/ddl/*
...
create table Cat (
id integer not null,
dob timestamp,
name varchar(255),
weight double not null,
primary key (id)
);
create table Cat2 (
id integer not null,
dob timestamp,
name varchar(255),
weight double not null,
primary key (id)
);
Add a table element to the orm.xml definition to map Cat to the ENTITYEX_CAT table.
<entity class="myorg.entityex.mapped.Cat">
<table name="ENTITYEX_CAT"/>
<attributes>
Add a @javax.persistence.Table annotation to the Cat2 class to map instances to the ENTITYEX_CAT table.
@javax.persistence.Entity
@javax.persistence.Table(name="ENTITYEX_CAT")
public class Cat2 {
private int id;
Rebuild your module form the command line and observe the create schema generated for Cat and Cat2. Notice now that we have mapped two entity classes to the same table using a custom table name.
$ more target/classes/ddl/*
...
create table ENTITYEX_CAT (
id integer not null,
dob timestamp,
name varchar(255),
weight double not null,
primary key (id)
);
Map the id property for both the Cat and Cat2 to the CAT_ID column. Also have the persistence provider automatically generate a value for the primary key during the persist(). The exercise will go into generated primary key types in more detaiu
@javax.persistence.Id
@javax.persistence.Column(name="CAT_ID")
@javax.persistence.GeneratedValue
private int id;
<entity class="myorg.entityex.mapped.Cat">
<table name="ENTITYEX_CAT"/>
<attributes>
<id name="id">
<column name="CAT_ID"/>
<generated-value/>
</id>
</attributes>
</entity>
Make the name column mandatory (nullable=false) and define the length of the string to be 20 characters. Note that these property assignments are only useful as documentation and generating schema. Many of the column properties are not used at runtime by the provider.
@javax.persistence.Column(nullable=false, length=20)
private String name;
<basic name="name">
<column nullable="false" length="20"/>
</basic>
Have the weight column stored with a precision of 3 digits, with 1 digit (scale) to the right of the decimal place. You will need to change the datatype of the mapped property to BigDecimal to fully leverage this capability.
# src/main/java/myorg/entityex/annotated/Cat2.java
@javax.persistence.Column(precision=3, scale=1) //10.2lbs
private BigDecimal weight;
...
public Cat2(String name, Date dob, BigDecimal weight) {
...
public BigDecimal getWeight() { return weight; }
public void setWeight(BigDecimal weight) {
# src/main/java/myorg/entityex/mapped/Cat.java
private BigDecimal weight;
...
public Cat(String name, Date dob, BigDecimal weight) {
...
public BigDecimal getWeight() { return weight; }
public void setWeight(BigDecimal weight) {
# src/main/resources/orm/Animal-orm.xml
<basic name="weight">
<column precision="3" scale="1"/>
</basic>
Rebuild the module from the command line and observe the database schema generated generated for the ENTITEX_CAT table.
# target/classes/ddl/entityEx-createJPA.ddl
create table ENTITYEX_CAT (
CAT_ID integer generated by default as identity,
dob date,
name varchar(20) not null,
weight decimal(3,1),
primary key (CAT_ID)
);
Notice how
All defaults not overwritten are preserved (e.g., column names)
The Cat and Cat2 entities have been mapped to the ENTITYEX_CAT table.
The id property has been mapped to the CAT_ID column
The id property will have a unique value automatically generated and assigned
The id and name properties are required (i.e., "not null")
The dob and weight properties continue to be optional since that is the default and not overridden
The weight property will be stored with 3 digits and one of those digits is to the right of the decimal place.
In the above example, you used FIELD access to the property values. This is the preferred method if your business object attributes provide an accurate representation as to what should be stored in the database. FIELD access was chosen by the provider by the fact that our annotated class placed the @Id annotation on a Java attribute and not a Java getter().
# implies FIELD access
@javax.persistence.Id
@javax.persistence.Column(name="CAT_ID")
@javax.persistence.GeneratedValue
private int id;
...
public int getId() { return id; }
If moved the @Id property definitions to the getter(), then the access would have been switched to PROPERTY. That was how JPA 1.0 annotated classed worked and it was always one way or another.
# implies PROPERTY access
private int id;
...
@javax.persistence.Id
@javax.persistence.Column(name="CAT_ID")
@javax.persistence.GeneratedValue
public int getId() { return id; }
Since it was always one way or the other with JPA 1.0, the specification in the orm.xml file was placed on the root element of the entity
<entity class="myorg.entityex.mapped.Cat"
access="FIELD">
Starting with JPA 2.0, we can also make the specification more explicit (like the XML technique) with the addition of the @Access annotation
@javax.persistence.Access(javax.persistence.AccessType.FIELD)
public class Cat2 {
Although switching between FIELD and PROPERTY access was always a capability in JPA 1.0 -- JPA 2.0 added the ability to chose on a per-property basis. This is done by applying the @Access annotation to the getter() you want to have property access. In this section, we will continue to expose all our properties to the provider through FIELD access, but define a PROPERTY access for the "weight" property.
Update the annotated Cat2 entity to store weight as a double and expose it to the provider as a BigDecimal.
private double weight;
...
public BigDecimal getWeight() {
return new BigDecimal(weight);
}
public void setWeight(BigDecimal weight) {
this.weight = weight==null ? 0 : weight.doubleValue();
}
Add a logger and some log statements to help identify the calls to the getter and setter methods
# src/main/java/myorg/entityex/annotated/Cat2.java
private static final Log log = LogFactory.getLog(Cat2.class);
...
public BigDecimal getWeight() {
log.debug("annotated.getWeight()");
return new BigDecimal(weight);
}
public void setWeight(BigDecimal weight) {
log.debug("annotated.setWeight()");
this.weight = weight==null ? 0 : weight.doubleValue();
}
Add the following test method to your AnimalTest. By persisting the entity, we will force the provider to get properties from the entity. By clearing the persistence unit of the entity prior to executing the find, we will force the provider to instantiate a new entity instance and set the properties within the entity.
# src/test/java/myorg/entityex/AnimalTest.java
@Test
public void testCreateCatAnnotated() {
log.info("testCreateCatAnnotated");
myorg.entityex.annotated.Cat2 cat = new myorg.entityex.annotated.Cat2("fluffy", null, 99.9);
em.persist(cat); //get provider to call getters
em.flush(); em.detach(cat);
cat = em.find(myorg.entityex.annotated.Cat2.class, cat.getId()); //get provider to call setters
}
Run your new test method and observe the calls to getWeight and setWeight printed.
$ mvn clean test -Dtest=myorg.entityex.AnimalTest#testCreateCatAnnotated
...
-testCreateCatAnnotated
-annotated.getWeight() //<----------------
Hibernate:
insert
into
ENTITYEX_CAT
(CAT_ID, dob, name, weight)
values
(null, ?, ?, ?)
-annotated.getWeight() //<----------------
-annotated.getWeight() //<----------------
Hibernate:
select
cat2x0_.CAT_ID as CAT1_2_0_,
cat2x0_.dob as dob2_0_,
cat2x0_.name as name2_0_,
cat2x0_.weight as weight2_0_
from
ENTITYEX_CAT cat2x0_
where
cat2x0_.CAT_ID=?
-annotated.setWeight() //<----------------
Make the same code changes for weight and debugging in the mapped entity class. These changes expose weight to the provider as a different type than it is stored locally. The debug will help use track the calls to the getter and setter.
# src/main/java/myorg/entityex/mapped/Cat.java
public class Cat {
private static final Log log = LogFactory.getLog(Cat.class);
...
private double weight;
...
public Cat(String name, Date dob, double weight) {
...
public BigDecimal getWeight() {
log.debug("mapped.getWeight()");
return new BigDecimal(weight);
}
public void setWeight(BigDecimal weight) {
log.debug("mapped.setWeight()");
this.weight = weight==null ? 0 : weight.doubleValue();
}
Add the following test method to your JUnit class.
@Test
public void testCreateCatMapped() {
log.info("testCreateCatMapped");
myorg.entityex.mapped.Cat cat = new myorg.entityex.mapped.Cat("fluffy", null, 99.9);
em.persist(cat); //get provider to call getters
em.flush(); em.detach(cat);
cat = em.find(myorg.entityex.mapped.Cat.class, cat.getId()); //get provider to call setters
}
Add the access="PROPERTY" to the weight definition within the orm.xml
<basic name="weight" access="PROPERTY">
<column precision="3" scale="1"/>
</basic>
I had to remove the access="FIELD" attribute from the entity element for the provider to honor the per-property specification at the weight level.
Build and run the test for the mapped version of the class. Look for the debug statements coming from the getter and setter
-testCreateCatMapped
-mapped.getWeight() //<----------------
Hibernate:
insert
into
ENTITYEX_CAT
(CAT_ID, dob, name, weight)
values
(null, ?, ?, ?)
-mapped.getWeight() //<----------------
-mapped.getWeight() //<----------------
Hibernate:
select
cat0_.CAT_ID as CAT1_2_0_,
cat0_.dob as dob2_0_,
cat0_.name as name2_0_,
cat0_.weight as weight2_0_
from
ENTITYEX_CAT cat0_
where
cat0_.CAT_ID=?
-mapped.setWeight() //<----------------
In this chapter you performed some core class/database table mappings that allowed you to
Define the name of the table used to store instances of the entity class
Define the column name used to store properties within the entity class
Define property constraints to require a property to exist or continue to be optional
Define a generated value for your primary key
Define maximum column lengths for string properties witin your entity class
Define precision and scale for BigDecimal property mappings
You also had a chance to change the provider access from FIELD to PROPERTY either for the entire entity or on a per-property basis.
This chapter will take you through the steps to map an enumerated class to the database. Enums are a specialized cross between a typed collection and an inheritance hiearchy. They are very convenient for expressing well known, type-safe values for a property. Enumerated classes can be mapped to the database by
(Java)name value
ordinal value
With what we know about PROPERTY mapping, we can also leverage some object-based tricks to map more customized value types.
For simplicity, the exercise will only deal with annotated classes from this point forward. As you should have realized -- anything you can do with an annotation has a parallel construct within the ORM descriptor. It should be a trivial exercise to locate the orm.xml equivalent for the annotation should you need to use the concepts discussed from here on in a mapped entity.
We will first look at mapping an ordinal enum value using a Dog entity. The ordinal is stored in a efficient integer column type but can be more cryptic to read in a raw query result.
Put the following entity class in place in your src/main tree.
package myorg.entityex.annotated;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="ENTITYEX_DOG")
@Access(AccessType.FIELD)
public class Dog {
public enum Color {
WHITE, BLACK, BROWN, MIX
}
@Id @GeneratedValue
private int id;
private Sex gender;
public int getId() { return id; }
public void setId(int id) {
this.id = id;
}
public Sex getGender() { return gender; }
public Dog setGender(Sex gender) {
this.gender = gender;
return this;
}
}
Add the new entity to your persistence unit in src/test
# src/test/resources/META-INF/persistence.xml
<class>myorg.entityex.annotated.Dog</class>
Build the module with the new entity and observe the generated schema for the Dog. The property, by default is being mapped by its ordinal property. That means the value stored will be a 0, 1, 2, ... value that represents the values position within the defined enum.
create table ENTITYEX_DOG (
id integer generated by default as identity,
gender integer,
primary key (id)
);
Optionally add an explicit specification to use ordinal mapping.
@Enumerated(EnumType.ORDINAL)
private Sex gender;
Add the following test method to your AnimalTest.java JUnit test case. It will persist an instance of a Dog, poke into the raw database to verify what is being stored, and obtain a new Dog instance to verify the enum was materialized properly.
@Test
public void testEnums() {
log.info("testEnums");
Dog dog = new Dog()
.setGender(Dog.Sex.FEMALE);
em.persist(dog);
em.flush();
//check the raw value stored in the database
Object o = em.createNativeQuery("select GENDER from ENTITYEX_DOG where id=" + dog.getId())
.getSingleResult();
log.debug("col=" + o);
assertEquals("unexpected gender", Dog.Sex.FEMALE.ordinal(), ((Number)o).intValue());
//get a new instance
em.detach(dog);
Dog dog2 = em.find(Dog.class, dog.getId());
assertEquals("unexpected dog gender", dog.getGender(), dog2.getGender());
}
Build module and run the new test method
$ mvn clean test -Dtest=myorg.entityex.AnimalTest#testEnums ... -col=1 ... [INFO] BUILD SUCCESS
Okay -- the first goal is now complete. You have mapped an enum as an ordinal value to the database. It may also be useful to take at the row(s) in the table using the DB browser UI.
For this and the next section, the runtime process will throw an IllegalArgumentException if the database contains a property that is not correctly represented in our enum set of values.
We will next look at mapping a name value for the enum. Names are convenient since they are more expressive in a raw database query and are not sensitive to their order within the Enum. However, the string value does take more space to represent and would be less efficient to implement certain comparisons.
Add the following Color Enum to the Dog class. This will define the Enum Color, declare an instance of Color and define its mapping as the STRING name.
public enum Color {
WHITE, BLACK, BROWN, MIX
}
...
@Enumerated(EnumType.STRING)
private Color color;
...
public Color getColor() { return color; }
public Dog setColor(Color color) {
this.color = color;
return this;
}
Rebuild the module and take a look at the generated schema. Color has been modeled as a varchar type with a default maximum size.
create table ENTITYEX_DOG (
id integer generated by default as identity,
color varchar(255),
gender integer,
primary key (id)
);
You can optionally supply a length specification to the enum string to create a tuned column size for the Color type. This length value will get relfected in the new DDL output.
@Column(length=16)
private Color color;
Update the test method with the following updates for the color property.
@Test
public void testEnums() {
log.info("testEnums");
Dog dog = new Dog()
.setGender(Dog.Sex.FEMALE)
.setColor(Dog.Color.MIX);
em.persist(dog);
em.flush();
//check the raw value stored in the database
Object[] o = (Object[])em.createNativeQuery("select GENDER, COLOR from ENTITYEX_DOG where id=" + dog.getId())
.getSingleResult();
log.debug("cols=" + Arrays.toString(o));
assertEquals("unexpected gender", Dog.Sex.FEMALE.ordinal(), ((Number)o[0]).intValue());
assertEquals("unexpected color", Dog.Color.MIX.name(), ((String)o[1]));
//get a new instance
em.detach(dog);
Dog dog2 = em.find(Dog.class, dog.getId());
assertEquals("unexpected dog gender", dog.getGender(), dog2.getGender());
assertEquals("unexpected dog color", dog.getColor(), dog2.getColor());
}
Rebuild the module and observe that our new property storage has passed the provided asserts.
mvn clean test -Dtest=myorg.entityex.AnimalTest#testEnums ... -cols=[1, MIX] ... [INFO] BUILD SUCCESS
If you look at the row values in the DB UI, you should be able to easily read the color string value.
In the previous sections we mapped enums based on built-in capabilities; ordinal and name. There are times when mapping a column is not as clean as above. Maybe there is a non-contiguous error code or maybe there is a pretty string with spaces in the name. In this section, we will look to make a pretty name to an internally stored enum. Officially it will be mapped as a string as far as JPA is concerned, but we will leverage getter/setter methods to covert to what we want to work with internally.
Add the following Breed enum to the Dog entity class. Follow it up with a property of that type. Leave off any JPA mappings in this area. We will use PROPERTY mapping.
public enum Breed {
LABRADOR("Lab"),
SAINT_BERNARD("Saint Bernard");
public final String prettyName;
private Breed(String prettyName) { this.prettyName = prettyName; }
public static Breed getBreed(String prettyName) {
for (Breed breed : values()) {
if (breed.prettyName.equals(prettyName)) {
return breed;
}
}
return null;
}
}
private Breed breed;
Define the getter/setter to be used by JPA to access the breed property. Note that we have defined these methods separate from the getBreed/setBreed methods because they accept and return the Breed enum. These methods form a contract with the provider to return a string. We also need to tell the provider the database column name to use because the default column naming rules will not work in in this case.
@Access(AccessType.PROPERTY)
@Column(name="BREED", length=32)
protected String getDBBreed() {
return breed==null ? null : breed.prettyName;
}
protected void setDBBreed(String dbValue) {
breed=Breed.getBreed(dbValue);
}
Notice how the methods declared above are non-public. They where defined that way since the public clients of the Dog class want to work with the Breed enum and not strings. To have them both public could be confusing and -- if they want strings -- they can always get that from the Breed enum itself.
Define the existing property as transient since the persistence of the property is being handled through dBBreed and leaving it non-Transient would cause an extra database column to be created for the enum.
@Transient
private Breed breed;
Rebuild the module and observe the generated schema for the updated entity. Note the varchar BREED column that has also been restricted to 32 characters according to our @Column specification. Notice that there is only a single column for breed and it comes from the dBBreed property definition.
create table ENTITYEX_DOG (
id integer generated by default as identity,
BREED varchar(32),
color varchar(16),
gender integer,
primary key (id)
);
Update the test method with the following to assign and test the persistence of breed using an alternate mapping.
@Test
public void testEnums() {
log.info("testEnums");
Dog dog = new Dog()
.setGender(Dog.Sex.FEMALE)
.setColor(Dog.Color.MIX)
.setBreed(Dog.Breed.SAINT_BERNARD);
em.persist(dog);
em.flush();
//check the raw value stored in the database
Object[] o = (Object[])em.createNativeQuery("select GENDER, COLOR, BREED from ENTITYEX_DOG where id=" + dog.getId())
.getSingleResult();
log.debug("cols=" + Arrays.toString(o));
assertEquals("unexpected gender", Dog.Sex.FEMALE.ordinal(), ((Number)o[0]).intValue());
assertEquals("unexpected color", Dog.Color.MIX.name(), ((String)o[1]));
assertEquals("unexpected breed", Dog.Breed.SAINT_BERNARD.prettyName, ((String)o[2]));
//get a new instance
em.detach(dog);
Dog dog2 = em.find(Dog.class, dog.getId());
assertEquals("unexpected dog gender", dog.getGender(), dog2.getGender());
assertEquals("unexpected dog color", dog.getColor(), dog2.getColor());
assertEquals("unexpected dog breed", dog.getBreed(), dog2.getBreed());
}
Re-run the enum test and note how text "Saint Bernard" with camel case and spaces was stored in the database and not the ordinal or SAINT_BERNARD name.
mvn clean test -Dtest=myorg.entityex.AnimalTest#testEnums ... -cols=[1, MIX, Saint Bernard] ... [INFO] BUILD SUCCESS
You may want to verify the contents of the database using the DB UI
SELECT * FROM ENTITYEX_DOG; ID BREED COLOR GENDER 1 Saint Bernard MIX 1
In this chapter we took a detailed look at mapping a special property type -- the enum. You mapped it using two built-in mapping techniques (ordinal and string/name) and a custom way using alternate getters/setters and PROPERTY access. Since the additional getter/setter pair was strictly for the OR mapping, you defined them as non-public accessors to keep the interface from being poluted with OR mapping details. You also declared the enum breed attribute as @Transient so the provider knew to ignore that particular FIELD.
This chapter will take you through mapping temporal (i.e. Dates) to columns in the dataabase.
Put the following class with three temporal types in place. Note that although the names of the properties indicate an intent to store DATE, TIME, and TIMESTAMP information, their Java datatype does not offer enough clues to the provider to make the distinction.
package myorg.entityex.annotated;
import java.util.Calendar;
import java.util.Date;
import javax.persistence.*;
@Entity
@Table(name="ENTITYEX_SHARK")
public class Shark {
@Id @GeneratedValue
private int id;
private Calendar aDate;
private Date aTime;
private Date aTimestamp;
public int getId() { return id; }
public Shark setId(int id) {
this.id = id; return this;
}
public Calendar getDate() { return aDate; }
public Shark setDate(Calendar date) {
this.aDate = date; return this;
}
public Date getTime() { return aTime; }
public Shark setTime(Date time) {
this.aTime = time; return this;
}
public Date getTimestamp() { return aTimestamp; }
public Shark setTimestamp(Date timestamp) {
this.aTimestamp = timestamp; return this;
}
@Override
public String toString() {
return new StringBuilder()
.append("aDate=").append(aDate.getTime())
.append(", aTime=").append(aTime)
.append(", aTimestamp=").append(aTimestamp)
.toString();
}
}
Add the new entity class to the persistence unit
<class>myorg.entityex.annotated.Shark</class>
Build the module and note the default schema created. The aDate, aTime, and aTimestamp where all created using a timestamp column type.
create table ENTITYEX_SHARK (
id integer generated by default as identity,
aDate timestamp,
aTime timestamp,
aTimestamp timestamp,
primary key (id)
);
Add the following test method to your existing JUnit test case. This test method just prints the temporal fields of the original object and then of the object coming form the database.
@Test
public void testTemporal() {
log.info("testTemporal");
Shark shark = new Shark()
.setDate(new GregorianCalendar(1776, Calendar.JULY, 4))
.setTime(new Date())
.setTimestamp(new Date());
em.persist(shark);
log.info("initial object=" + shark);
//flush commands to DB and get new instance
em.flush(); em.detach(shark);
Shark shark2 = em.find(Shark.class, shark.getId());
log.info("object from DB=" + shark2);
}
Rebuild the module with the new test method. Observe the dates printed and how they are not quite what we are looking for. The Calendar date looks okay, but the time contains both time and date (because it is declared as a timestamp)
-testTemporal
Hibernate:
insert
into
ENTITYEX_SHARK
(id, aDate, aTime, aTimestamp)
values
(null, ?, ?, ?)
-initial object=aDate=Thu Jul 04 00:00:00 EST 1776, aTime=Sun Feb 24 02:21:44 EST 2013, aTimestamp=Sun Feb 24 02:21:44 EST 2013
Hibernate:
select
shark0_.id as id4_0_,
shark0_.aDate as aDate4_0_,
shark0_.aTime as aTime4_0_,
shark0_.aTimestamp as aTimestamp4_0_
from
ENTITYEX_SHARK shark0_
where
shark0_.id=?
-object from DB=aDate=Thu Jul 04 00:00:00 EST 1776, aTime=2013-02-24 02:21:44.861, aTimestamp=2013-02-24 02:21:44.861
Add Temporal specifications to the three properties.
@Temporal(TemporalType.DATE)
private Calendar aDate;
@Temporal(TemporalType.TIME)
private Date aTime;
@Temporal(TemporalType.TIMESTAMP)
private Date aTimestamp;
Rebuild the module and note the new database schema created. Our three fields now have more distinct values.
create table ENTITYEX_SHARK (
id integer generated by default as identity,
aDate date,
aTime time,
aTimestamp timestamp,
primary key (id)
);
In looking at the output from the entity pulled from the database, each of the temporals has the desired granularity. Note that the first printed output is of the Date objects before they have been massaged by the database.
-testTemporal
Hibernate:
insert
into
ENTITYEX_SHARK
(id, aDate, aTime, aTimestamp)
values
(null, ?, ?, ?)
-initial object=aDate=Thu Jul 04 00:00:00 EST 1776, aTime=Sun Feb 24 02:19:47 EST 2013, aTimestamp=Sun Feb 24 02:19:47 EST 2013
Hibernate:
select
shark0_.id as id4_0_,
shark0_.aDate as aDate4_0_,
shark0_.aTime as aTime4_0_,
shark0_.aTimestamp as aTimestamp4_0_
from
ENTITYEX_SHARK shark0_
where
shark0_.id=?
-object from DB=aDate=Thu Jul 04 00:00:00 EST 1776, aTime=02:19:47, aTimestamp=2013-02-24 02:19:47.112
</listitem>
<listitem><para></para>
<programlisting language=""><![CDATA[
This chapter will take you through mapping large objects (Clobs and Blobs) to your database.
The normal database types like varchar can be indexed and searched for but usually have a maximum length somewhere between 2,000 and 5,000 characters. If you need so store larger documents or images databases provide two additional types:
Clob for storing large character/string data
Blob for storing large binary data
The actual database type(s) are not necessarily called Clob and Blob, so JPA provides a layer of abstraction between what the application needs and how it is declared in the database.
JPA has built-in rules to map string/character data to a varchar and we must define a @javax.persistence.Lob metadata property to have it mapped differently. Lets start this exercise by using the default mapping and then add overrides.
Create the following class in your src/main tree.
package myorg.entityex.annotated;
import javax.persistence.*;
@Entity
@Table(name="ENTITYEX_HORSE")
public class Horse {
@Id @GeneratedValue
private int id;
private String name;
private String description;
private char[] history;
private byte[] photo;
private Jockey jockey;
public int getId() { return id; }
public void setId(int id) {
this.id = id;
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public String getDescription() { return description; }
public void setDescription(String description) {
this.description = description;
}
public char[] getHistory() { return history; }
public void setHistory(char[] history) {
this.history = history;
}
}
Add the new class to the persistence unit.
<class>myorg.entityex.annotated.Horse</class>
Build the module and observe the database schema that is created. Note that all of our string and char[] properties are being mapped to a varchar.
create table ENTITYEX_HORSE (
id integer generated by default as identity,
description varchar(255),
history varchar(255),
name varchar(255),
primary key (id)
);
Add the following test method to the existing JUnit test case.
@Test
public void testLob() {
log.info("testLob");
//create our host object with Lob objects
Horse horse = new Horse();
horse.setName("Mr. Ed");
horse.setDescription("There once was a horse of course and his name was Mr. Ed...");
horse.setHistory("Mister Ed is a fictional talking horse residing in Mount Kisco, New York,...".toCharArray());
em.persist(horse);
//flush to DB and get a new instance
em.flush(); em.detach(horse);
Horse horse2 = em.find(Horse.class, horse.getId());
assertEquals("unexpected description", horse.getDescription(), horse2.getDescription());
assertTrue("unexpected history", Arrays.equals(horse.getHistory(), horse2.getHistory()));
}
Using the database server profile, run the tests and observe the data left in the database tables.
$ mvn clean test -Ph2srv -P\!h2db ... SELECT * FROM ENTITYEX_HORSE; ID DESCRIPTION HISTORY NAME 1 There once was a horse of course and his name was Mr. Ed... Mister Ed is a fictional talking horse residing in Mount Kisco, New York,... Mr. Ed
Update the hosting class and supply @Lob for description and history properties
@Lob
private String description;
@Lob
private char[] history;
Rebuild the module and observe the change in database types. The mapping has been changed to a Clob type and the database column for the H2 database looks like it is also "clob".
create table ENTITYEX_HORSE (
id integer generated by default as identity,
description clob,
history clob,
name varchar(255),
primary key (id)
);
At this point in time you now have string fields that can be used to store large amounts of data. As a side-exercise, try storing 100K character strings stored as Clobs and then switch them back to varchar to see the difference in size constraints. Try setting the @Column.length field to a high value to accomodate the string mapped as a varchar. Where is the maximum? That maximum value is not the same on all databases.
The above section worked with character/string data. We also may need tp store binary information. JPA will
Add the following byte[] property to the host class. Annotate it as a @Lob type to assure we bet the right storage type.
@Lob
private byte[] photo;
public byte[] getPhoto() { return photo; }
public void setPhoto(byte[] photo) {
this.photo = photo;
}
Rebuild the module and observe the database schema created. We now have a "blob" type for photo.
create table ENTITYEX_HORSE (
id integer generated by default as identity,
description clob,
history clob,
name varchar(255),
photo blob,
primary key (id)
);
Add a Java Serializable type to the host class. JPA can store this type of object in a blob as well.
public static class Jockey implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
@Lob
private Jockey jockey;
public Jockey getJockey() { return jockey; }
public void setJockey(Jockey jockey) {
this.jockey = jockey;
}
Update the test method with the following to exercise the Blob fields.
...
byte[] picture = new byte[10*1000];
new Random().nextBytes(picture);
horse.setPhoto(picture);
Horse.Jockey jockey = new Horse.Jockey();
jockey.setName("Wilbur Post");
horse.setJockey(jockey);
em.persist(horse);
...
Horse horse2 = em.find(Horse.class, horse.getId());
...
assertTrue("unexpected photo", Arrays.equals(horse.getPhoto(), horse2.getPhoto()));
assertEquals("unexpected jockey", horse.getJockey().getName(), horse2.getJockey().getName());
Rebuild/test the module to generate schema and verify functionality. Notice we now have photo (the byte[] type) and jockey (the Serializable type) mapped to a database blob.
create table ENTITYEX_HORSE (
id integer generated by default as identity,
description clob,
history clob,
jockey blob,
name varchar(255),
photo blob,
primary key (id)
);
If you took a look at the results in the H2 DB browser UI using a default query, you should notice that binary information is now stored in the two additional fields. Note that the jockey name is not stored as a simple String. It is stored as a serialized string within a serialized Jockey class within a Blob field of the database. We can only get and set the Jockey and not search for their name using this mapping mechanism.
In this chapter we mapped large string/character types to Clobs and binary content to Blobs. One thing we still need to point out is that Clobs and Blobs can come at a performance cost. You likely will want to model Clob and Blob data in lazily loaded child tables (using JPA relationships) to allow quick and efficient access to the traditional column data and then optionally provide the large payloads on demand.
This chapter will take you through the steps to setting up entity classes for three different primary key generation strategies:
IDENTITY
SEQUENCE
TABLE
There is a fourth strategy called AUTO and is the default. Since the job of most applications is to map to a known database schema, I would not consider this to be usable outside of quick prototypes like we went through in the previous sections. When using AUTO (the default), you are saying "I don't care what you do -- just get me a primary key value" when you actually do care for production code. Fore this reason I would suggest you always supply a strategy and not depend on the default always doing what you need.
Primary key generation has three fundamental requirements
The primary key must be a simple key (ie., not compound)
The data type of the primary key must be a numeric
The primary key value of the entity must be "unassigned" when passed to persist. Any "assigned" value will cause the provider to through an exception since the entity is presumed to already have a primary key and is not in the proper state. Unassigned states are "0" for built-in numeric types (e.g., int, long) and null for Object numeric types (e.g., Integer, Long)
Lets start with the easiest case. For IDENITY, the primary key is generated by the database on a per-table basis. Thus it is possible to have two separate entity instances with the same primary key value when mapped to two different tables.
Put the following class in place in your src/main tree. The class has two properties; an ID and Name. The ID is being automatically generated using the IDENTITY strategy.
package myorg.entityex.annotated;
import javax.persistence.*;
@Entity
@Table(name="ENTITYEX_BUNNY")
@Access(AccessType.FIELD)
public class Bunny {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String name;
public int getId() { return id; }
public void setId(int id) {
this.id = id;
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
Add the new entity to the orm.xml file (and not the persistence.xml file). We are going to use primary key generation to demonstrate some points about metadata overrides in a moment.
# src/main/resources/orm/Animal-orm.xml
<entity class="myorg.entityex.annotated.Bunny"/>
Build the module and notice the database schema generated for the entity using IDENTITY.
create table ENTITYEX_BUNNY (
id integer generated by default as identity,
name varchar(255),
primary key (id)
);
Add the following test method to the existing AnimalTest. Notice that it creates and persists several entities, checks they are received a unique primary key value, and prints all the assigned values at the end.
@Test
public void testPKGen() {
log.info("testPKGen");
Bunny bunny = new Bunny();
bunny.setName("fuzzy");
assertTrue("primary key unexpectedly assigned", bunny.getId()==0);
em.persist(bunny);
em.flush();
log.info("bunny.getId()=" + bunny.getId());
assertFalse("primary key not assigned", bunny.getId()==0);
Set<Integer> ids = new HashSet<Integer>();
ids.add(bunny.getId());
for (String name: new String[]{"peter", "march hare", "pat"}) {
Bunny b = new Bunny();
b.setName(name);
em.persist(b);
em.flush();
assertTrue("id not unique:" + b.getId(), ids.add(b.getId()));
}
log.debug("ids=" + ids);
}
The em.flush() exists after the persist because we want to make sure the creation has been pushed to the database prior to the transaction committing because we want to check in while the transaction happens to be in progress.
Note too that the entity client code does not really know the strategy used. We will take advatange of that in the next section.
Build and test the primary key generation. Observe the following output and not assert errors.
$ mvn test -Dtest=myorg.entityex.AnimalTest#testPKGen
-testPKGen
Hibernate:
insert
into
ENTITYEX_BUNNY
(id, name)
values
(null, ?)
-bunny.getId()=1
...
-ids=[1, 2, 3, 4]
You have now successfully mapped an entity to the database using the IDENTITY strategy and it was pretty simple. The only down side to using this technique is that it is not portable to all database providers. Most notably, Oracle does not support the IDENTITY strategy.
In this section, we are going to change the primary key generation strategy through the use of the XML descriptor rather than changing the Java annotation. This is an example of how your application could be deployed with one version of the orm.xml for development and another for production when your development and production databases don't support the same features.
SEQUENCES are specialized constratucts databases have implemented to efficiently generate primary key values across tables. Using a common sequence means that two entities mapped to two separate database tables will not have the same primary key value (as long as the tables use the same sequence). We can define multiple sequences.
Update the Animal-orm.xml definition for the entity to include a generated-value strategy of SEQUENCE.
<entity class="myorg.entityex.annotated.Bunny">
<attributes>
<id name="id">
<generated-value strategy="SEQUENCE"/>
</id>
</attributes>
</entity>
Rebuild the module and observe the new schema generated for the entity no longer has the identity as part of the table definition and has added a sequence with the default name "hibernate-sequence"
create table ENTITYEX_BUNNY (
id integer not null,
name varchar(255),
primary key (id)
);
create sequence hibernate_sequence;
Optionally remove the IDENTITY strategy from the entity class that was overridden by the XML file so that there is less confusion which technique is being used. This change should not impact any behavior since the definition in the XML file already has priority over the Java annotations.
@GeneratedValue//(strategy=GenerationType.IDENTITY)
Rebuild the module and observe the output from the test method already in place. Remember that -- since the client does not care which strategy is used -- we are able to change the implementation and re-run with a different strategy and not change the client.
$ mvn test -Dtest=myorg.entityex.AnimalTest#testPKGen
...
-testPKGen
Hibernate:
call next value for hibernate_sequence
Hibernate:
insert
into
ENTITYEX_BUNNY
(name, id)
values
(?, ?)
-bunny.getId()=10
...
-ids=[10, 11, 12, 13]
Notice that hibernate went to the database and obtained the next value to start with (which was 10 in this case) and was free to generate one-up numbers from that point within a window of values. Since hibernate has been told it is using a database specific dialect and can communicate with the database at runtime, you hope that the default values for the sequence are well understood. In the next step we will make this more explicit.
Update the Animal-orm.xml entity definition to include a seguence-generator and generator reference from the strategy. The sequence-generator will have an internal JPA name, a name used within the schema and a database name (be sure to use under_score and not dash-es).
<entity class="myorg.entityex.annotated.Bunny">
<sequence-generator name="animal-sequence-gen"
sequence-name="animal_sequence">
</sequence-generator>
<attributes>
<id name="id">
<generated-value strategy="SEQUENCE"
generator="animal-sequence-gen"/>
</id>
</attributes>
</entity>
The sequence-generator element allows for the specification of an initial value and increment and the H2 documentation states it supports these values in its definition. However, if supplied, they do not seem to make it into the database schema so I left that level of specification out of the exercise. This sounds worse than it really is. Production schema is nearly always generated wither by hand or with some manual adjustments.
create table ENTITYEX_BUNNY (
id integer not null,
name varchar(255),
primary key (id)
);
create sequence animal_sequence;
Re-build the module and re-run the PKGen test method and you should see the new sequence being used.
-testPKGen
Hibernate:
call next value for animal_sequence
Hibernate:
insert
into
ENTITYEX_BUNNY
(name, id)
values
(?, ?)
-bunny.getId()=10
...
-ids=[10, 11, 12, 13]
We have successfully mapped the primary key generation to a database sequence and did so through a specification in the deployment descriptor to highlight the fact that the orm.xml file overrides and augments the class annotations. We also defined a custom generator which was used to generate the primary key values. If you were to do that with annotations -- the sequence-generator annotation would have gone over any one of the JPA entity classes.
In this section we will be mapping the primary key generation using a database table strategy. In this strategy a table and key name is identified for a sequence that will be used to bootstrap unique value generators. This technique always seems the most portable (and possibly the most expensive) since it involves no unique database behavior.
Change the generation strategy from SEQUENCE to TABLE in the orm.xml descriptor. You can leave the sequence generator definition in place.
<entity class="myorg.entityex.annotated.Bunny">
...
<attributes>
<id name="id">
<generated-value strategy="TABLE"/>
</id>
</attributes>
</entity>
Rebuild the module and note the database schema generated. The table name was defaulted to hibernate_sequences and was given some default column names as well.
create table ENTITYEX_BUNNY (
id integer not null,
name varchar(255),
primary key (id)
);
create table hibernate_sequences (
sequence_name varchar(255),
sequence_next_hi_value integer
) ;
In watching the output of the unit test, you should observe the following initialization of the sequence row prior to getting started with the actual work of persisting our entities.
-testPKGen
Hibernate:
select
sequence_next_hi_value
from
hibernate_sequences
where
sequence_name = 'ENTITYEX_BUNNY' for update
Hibernate:
insert
into
hibernate_sequences
(sequence_name, sequence_next_hi_value)
values
('ENTITYEX_BUNNY', ?)
Hibernate:
update
hibernate_sequences
set
sequence_next_hi_value = ?
where
sequence_next_hi_value = ?
and sequence_name = 'ENTITYEX_BUNNY'
Hibernate:
insert
into
ENTITYEX_BUNNY
(name, id)
values
(?, ?)
-bunny.getId()=1
...
-ids=[1, 2, 3, 4]
Add a table generator definition as shown below to the orm.xml file. Attempt to override many of the defaults.
<entity class="myorg.entityex.annotated.Bunny">
<table-generator name="animal-table-gen"
table="animal_sequences"
initial-value="3"
allocation-size="10"
pk-column-name="key"
pk-column-value="animals"
value-column-name="seq"/>
<attributes>
<id name="id">
<generated-value strategy="TABLE"
generator="animal-table-gen"/>
</id>
</attributes>
Re-build the module and observe how the generated schema matches what was specified in the table generator specification.
create table ENTITYEX_BUNNY (
id integer not null,
name varchar(255),
primary key (id)
);
create table animal_sequences (
key varchar(255),
seq integer
) ;
Run the unit test and observe pretty much the same behavior as above except this time using our customized table definitions. Note the initial values and allocation size is not yet noticeable.
-testPKGen
Hibernate:
select
seq
from
animal_sequences
where
key = 'animals' for update
Hibernate:
insert
into
animal_sequences
(key, seq)
values
('animals', ?)
Hibernate:
update
animal_sequences
set
seq = ?
where
seq = ?
and key = 'animals'
Hibernate:
insert
into
ENTITYEX_BUNNY
(name, id)
values
(?, ?)
-bunny.getId()=1
...
-ids=[1, 2, 3, 4]
You have now successfully mapped your entity class using a TABLE primary key mechanism. This is likely the most portable between databases but like also the most expensive because of all the knowledge and management coming from the client side of the connection.
In this chapter you defined your primary key as being automatically generated by the database and then specified one of three types of implementations (IDENTITY, SEQUENCE, and TABLE) to implement that value generation. The orm.xml file was also used to show how the actual primary key mechanism can be changed without changing the entity or client code. This can be helpful when switching between development and production databases that do not support the same features.
This chapter will take you through mapping a set of two or more natural fields as a compound primary key. Nothing is being generated with natural/compound keys. We are combining multiple fields to represent the object's identity.
A compound primary key is required to...
Be Serializable
Have a default constructor (either built-in or declared)
Supply a hash() and equals() method
Provide public access to
Embedded compound primary keys are primary key classes that are integrated into the using class in its aggregate form. The using class makes only a reference to the primary key class and not to any of the primary key class properties.
Create an instance of the following JPA primary key class in your src/main tree.
package myorg.entityex.annotated;
import java.io.Serializable;
import javax.persistence.*;
@Embeddable
public class CowPK implements Serializable { //required to be Serializable
private static final long serialVersionUID = 1L;
private String herd;
private String name;
public CowPK(){} //required default ctor
public CowPK(String herd, String name) {
this.herd = herd;
this.name = name;
};
public String getHerd() { return herd; }
public String getName() { return name; }
@Override
public int hashCode() { //required hashCode method
return herd.hashCode() + name.hashCode();
}
@Override
public boolean equals(Object obj) { //required equals method
try {
return herd.equals(((CowPK)obj).herd) &&
name.equals(((CowPK)obj).name);
} catch (Exception ex) {
return false;
}
}
}
Note the class is...
Annotated as @Embeddable
Implement Serializable
Provide a default constructor
Provides overrides for hashCode and equals
Provides attributes that will be used for the primary key fields
Note the class also...
Optionally implemented a convenience constructor
Optionally removed the setter methods so the primary key fields could not be accidentally changed
Add the following entity class to the src/main tree. This class will declare an @EmbdeddedId and no @Id property. It will also use FIELD level access to properties.
package myorg.entityex.annotated;
import javax.persistence.*;
@Entity
@Table(name="ENITYEX_COW")
public class Cow {
@EmbeddedId
private CowPK pk;
private int weight;
public Cow() {}
public Cow(CowPK cowPK) {
this.pk = cowPK;
}
public CowPK getPk() { return pk; }
public void setPk(CowPK pk) {
this.pk = pk;
}
public int getWeight() { return weight; }
public void setWeight(int weight) {
this.weight = weight;
}
}
Note the entity class makes wholesale use of the embedded primary key class and is not required to have any interaction with the properties of the embedded class.
Add the new entity class to your persistence unit.
<class>myorg.entityex.annotated.Cow</class>
Build the module with the new entity and primary key class. Note the schema that is produced. The herd and name from the primary key class have been integrated into the using class' table.
create table Cow (
herd varchar(255) not null,
name varchar(255) not null,
weight integer not null,
primary key (herd, name)
);
Notice in the above database schema that the ID columns are set to their default values. We can set the properties from either the primary key class or within the using entity class. Add the following to your two classes to help define the database columns used by the primary key class.
@Column(name="HERD", length=16)
private String herd;
@EmbeddedId
@AttributeOverrides({
@AttributeOverride(name="name", column=@Column(name="NAME", length=16))
})
private CowPK pk;
Note the technique used for the "herd" is to annotate the property directly within the primary key class. This is useful when the primary key class is dedicated for use by a single entity class or the annotations are common across entity classes. The technique used for "name" is to annotate the primary key property of the entity class using an @AttributeOverrride. Note there can only be a single @AnnotationOverride per property -- so we show the use of the @AttrinbuteOverrides({}) annotation to hold one or more @AttributeOverride elements.
Rebuild the module and note the generated database schema produced. The herd and name columns have been constrained to 16 characters. The names of the columns are also showing up as CAPITALIZED since that is how we typed them in the @Column.name mapping.
create table Cow (
HERD varchar(16) not null,
NAME varchar(16) not null,
weight integer not null,
primary key (HERD, NAME)
);
Add the following test method to your JUnit test case
@Test
public void testEmbeddedId() {
log.info("testEmbedded");
Cow cow = new Cow(new CowPK("Ponderosa", "Bessie"));
cow.setWeight(900);
em.persist(cow);
//flush to DB and get a new instance
em.flush(); em.detach(cow);
Cow cow2 = em.find(Cow.class, new CowPK("Ponderosa", "Bessie"));
assertNotNull("cow not found", cow2);
assertEquals("unexpected herd", cow.getPk().getHerd(), cow2.getPk().getHerd());
assertEquals("unexpected name", cow.getPk().getName(), cow2.getPk().getName());
assertEquals("unexpected weight", cow.getWeight(), cow2.getWeight());
}
Note how an instance of the compound primary key class is passed to the find() method to locate the entity class by primary key.
Rebuild and test your module with the new test method in place. You should notice the following information within the database when complete.
$ mvn clean test -Ph2srv -P\!h2db
...
-testEmbedded
Hibernate:
insert
into
ENITYEX_COW
(weight, HERD, NAME)
values
(?, ?, ?)
Hibernate:
select
cow0_.HERD as HERD5_0_,
cow0_.NAME as NAME5_0_,
cow0_.weight as weight5_0_
from
ENITYEX_COW cow0_
where
cow0_.HERD=?
and cow0_.NAME=?
...
[INFO] BUILD SUCCESS
SELECT * FROM ENITYEX_COW;
HERD NAME WEIGHT
Ponderosa Bessie 900
In this section you have modeled an embedded class as a compound primary key class that has encapsulated all properties within the primary key class.
In this section we will address the case were the entity class wishes to module one or more of the primary key properties as direct properties of the entity class.
Put the following entity class in place in your src/main tree.
package myorg.entityex.annotated;
import javax.persistence.*;
@Entity
@Table(name="ENITYEX_COW2")
@IdClass(CowPK.class)
@AttributeOverrides({
@AttributeOverride(name="name", column=@Column(name="NAME", length=16))
})
public class Cow2 {
@Id
private String herd;
@Id
private String name;
private int weight;
public Cow2() {}
public Cow2(String herd, String name) {
this.herd = herd;
this.name = name;
}
public String getHerd() { return herd; }
public String getName() { return name; }
public int getWeight() { return weight; }
public void setWeight(int weight) {
this.weight = weight;
}
}
Note this class...
Declares the primary key class as an @IdClass
Identifies two properties as @Id properties and these must match the properties of the primary key class.
This class also happens to...
Provide a custom override of the "name" primary key property using @AttributeOverride at the class level.
Provides read-only access to the primary key values by only supplying getters
Add the new entity class to the persistence unit.
<class>myorg.entityex.annotated.Cow2</class>
Build the module and not the generated database schema is identical to the embedded primary key mapping we created in the previous section).
create table ENITYEX_COW2 (
HERD varchar(16) not null,
NAME varchar(16) not null,
weight integer not null,
primary key (HERD, NAME)
);
Add the following test method for the new entity class to your existing Junit test case.
@Test
public void testIdClass() {
log.info("testIdClass");
Cow2 cow = new Cow2("Ponderosa", "Bessie");
cow.setWeight(900);
em.persist(cow);
//flush to DB and get a new instance
em.flush(); em.detach(cow);
Cow2 cow2 = em.find(Cow2.class, new CowPK("Ponderosa", "Bessie"));
assertNotNull("cow not found", cow2);
assertEquals("unexpected herd", cow.getHerd(), cow2.getHerd());
assertEquals("unexpected name", cow.getName(), cow2.getName());
assertEquals("unexpected weight", cow.getWeight(), cow2.getWeight());
}
Note how the use of the primary key is the same when interfacing with the entitymanager. The only real difference is that the entity deals directly with the properties of the primary key class and not the primary key class itself.
Rebuild the module with the new test method. You should get the following in the database when complete.
$ mvn clean test -Ph2srv -P\!h2db
...
-testIdClass
Hibernate:
insert
into
ENITYEX_COW2
(weight, HERD, NAME)
values
(?, ?, ?)
Hibernate:
select
cow2x0_.HERD as HERD6_0_,
cow2x0_.NAME as NAME6_0_,
cow2x0_.weight as weight6_0_
from
ENITYEX_COW2 cow2x0_
where
cow2x0_.HERD=?
and cow2x0_.NAME=?
...
[INFO] BUILD SUCCESS
SELECT * FROM ENITYEX_COW2;
HERD NAME WEIGHT
Ponderosa Bessie 900
In this section you successfully mapped a compound primary key class as an IdClass for an entity where multiple @Id properties were directly exposed by the entity. The IdClass declared properties that were identical to the number and name of @Id properties within the entity class.
In this chapter we worked with primary keys made up of multiple values. There were two styles of usage for the primary key class; embedded and idclass. For the embedded case, the entity worked with the primary key class directly and never the primary key class properties. In the idclass case, the entity class declared properties that matched the idclass (or vice-versa) but never interacted with the primary key class. We showed how both mappings to the database were identical and how to define custom mappings for the targeted database schema.
This chapter will take you through mapping nested classes to the database. In this case we have classes within classes (within classes, etc.) that get mapped to a flat database table. Note there are cases when the embedded class contains the meat of what we want mapped to our database and the wrapping entity may only be created to provide the necessary primary key property.
In this section we will map a simple embedded object within an entity class.
Add the following class to your src/main tree. In this case the entity class contains an instance of a Name class which contains two properties mapped to the database. The Name class is annotated as @Embeddable and the name property in the entity is annotated as @Embedded.
package myorg.entityex.annotated;
import javax.persistence.*;
@Entity
@Table(name="ENTITYEX_BEAR")
public class Bear {
@Embeddable
public static class Name {
private String firstName;
private String lastName;
public String getFirstName() { return firstName; }
public Name setFirstName(String firstName) { this.firstName = firstName; return this; }
public String getLastName() { return lastName; }
public Name setLastName(String lastName) { this.lastName = lastName; return this; }
}
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Embedded
private Name name;
public int getId() { return id; }
public void setId(int id) {
this.id = id;
}
public Name getName() { return name; }
public void setName(Name name) {
this.name = name;
}
}
Add the new entity class to the persistence unit.
<class>myorg.entityex.annotated.Bear</class>
Build the module and note the database schema created for the entity class. Note the embedded properties are now mapped at the same level as the columns for the entity properties.
create table ENTITYEX_BEAR (
id integer generated by default as identity,
firstName varchar(255),
lastName varchar(255),
primary key (id)
);
Add custom table mappings for the firstName and lastName properties. Define the mapping for firstName from within the embedded class. Define the mapping for lastName from within the entity class.
public static class Name {
@Column(name="FIRST_NAME", length=16)
private String firstName;
...
@AttributeOverrides({
@AttributeOverride(name="lastName", column=@Column(name="LAST_NAME", length=16))
})
@Embedded
private Name name;
Rebuild the module and note the change in definition for the firstName and lastName columns. We were able to control the mapping from either within the embedded or entity class.
create table ENTITYEX_BEAR (
id integer generated by default as identity,
FIRST_NAME varchar(16),
LAST_NAME varchar(16),
primary key (id)
);
At this point we have shown how to map a single nested object. Note how similar this was to the @EmbeddedId case we went thru during the compound primary key chapter.
The above is an example of a single-level embedded object that has been supported since JPA 1.0. In the next step, add a nested embedded object. Support for multiple levels of nesting was added in JPA 2.0.
Add the following nested classes to the Bear entity class.
@Embeddable
public static class Street {
private int number;
private String name;
public int getNumber() { return number; }
public Street setNumber(int number) { this.number = number; return this; }
public String getName() { return name; }
public Street setName(String name) { this.name = name; return this; }
}
@Embeddable
public static class Address {
private Street street; //a second level of embedded
//@Column(name="CITY", length=16)
private String city;
//@Column(name="STATE", length=16)
private String state;
public Street getStreet() { return street; }
public Address setStreet(Street street) { this.street = street; return this; }
public String getCity() { return city; }
public Address setCity(String city) { this.city = city; return this; }
public String getState() { return state; }
public Address setState(String state) { this.state = state; return this; }
}
@Embedded
private Address address;
public Address getAddress() { return address; }
public void setAddress(Address address) {
this.address = address;
}
Rebuild the module with the new, multi-level embedded class and note the database schema created. Both levels of the Address were flattened into the entity table.
create table ENTITYEX_BEAR (
id integer generated by default as identity,
city varchar(255),
state varchar(255),
name varchar(255),
number integer not null,
FIRST_NAME varchar(16),
LAST_NAME varchar(16),
primary key (id)
);
Define custom table mappings for the address.
Leave Street un-customized
public static class Street {
private int number;
private String name;
Map Street.number to the STREET_NUMBER column from the Address class.
@Embeddable
public static class Address {
@AttributeOverrides({
@AttributeOverride(name="number", column=@Column(name="STREET_NUMBER")),
})
private Street street; //a second level of embedded
Map Street.name to a 16 character STREET_NAME column from the entity class. Note the multiple level syntax here.
@AttributeOverrides({
@AttributeOverride(name="street.name", column=@Column(name="STREET_NAME", length=16)),
})
@Embedded
private Address address;
Rebuild the module and note the generated database schema. Our custom database mappings are in place.
create table ENTITYEX_BEAR (
id integer generated by default as identity,
CITY varchar(16),
STATE varchar(16),
STREET_NAME varchar(16),
STREET_NUMBER integer,
FIRST_NAME varchar(16),
LAST_NAME varchar(16),
primary key (id)
);
Put the following test method within the existing JUnit test case.
@Test
public void testEmbeddedObject() {
log.info("testEmbeddedObject");
Bear bear = new Bear();
bear.setName(new Bear.Name().setFirstName("Yogi").setLastName("Bear"));
bear.setAddress(new Bear.Address()
.setCity("Jellystone Park")
.setState("???")
.setStreet(new Bear.Street().setNumber(1).setName("Picnic")));
em.persist(bear);
//flush to DB and get a new instance
em.flush(); em.detach(bear);
Bear bear2 = em.find(Bear.class, bear.getId());
assertEquals("unexpected firstName", bear.getName().getFirstName(), bear2.getName().getFirstName());
assertEquals("unexpected lastName", bear.getName().getLastName(), bear2.getName().getLastName());
assertEquals("unexpected street number",
bear.getAddress().getStreet().getNumber(), bear2.getAddress().getStreet().getNumber());
assertEquals("unexpected street name",
bear.getAddress().getStreet().getName(), bear2.getAddress().getStreet().getName());
assertEquals("unexpected city",
bear.getAddress().getCity(), bear2.getAddress().getCity());
assertEquals("unexpected state",
bear.getAddress().getState(), bear2.getAddress().getState());
}
Rebuild the module with the new test method in place.
-testEmbeddedObject
Hibernate:
insert
into
ENTITYEX_BEAR
(id, CITY, STATE, STREET_NAME, STREET_NUMBER, FIRST_NAME, LAST_NAME)
values
(null, ?, ?, ?, ?, ?, ?)
Hibernate:
select
bear0_.id as id7_0_,
bear0_.CITY as CITY7_0_,
bear0_.STATE as STATE7_0_,
bear0_.STREET_NAME as STREET4_7_0_,
bear0_.STREET_NUMBER as STREET5_7_0_,
bear0_.FIRST_NAME as FIRST6_7_0_,
bear0_.LAST_NAME as LAST7_7_0_
from
ENTITYEX_BEAR bear0_
where
bear0_.id=?
The above capability of mapping multi-nested classes was added in JPA 2.0 and allows us to map more complicated structures to the database.
In this chapter we mapped a nested object within the table used to host and enclosing entity class. Note there are times when the entity is created purely for persistence purposes and the embedded class is the real meat we are after. In that case, the entity class is imply providing the primary key property and embedding the rest.
This chapter will take you through mapping a single class to multiple tables. We will reuse the example from the embedded object mapping case because conceptually they are trying to do the same thing except
In the embedded object case we had a single table and multiple objects
In this case we have multiple tables and a single object/class
In the embedded object case, our child objects did not have a primary key
In this case our child objects are entities with a primary key that are joined with the primary object's primary key.
Add the following Java class to your src/main tree. We are not done mapping just yet but lets see what this maps to before making changes.
package myorg.entityex.annotated;
import javax.persistence.*;
@Entity
@Table(name="ENTITYEX_BEAR2")
public class Bear2 {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String firstName;
private String lastName;
private int streetNumber;
private String streetName;
private String city;
private String state;
public int getId() { return id; }
public Bear2 setId(int id) { this.id = id; return this; }
public String getFirstName() { return firstName; }
public Bear2 setFirstName(String firstName) {
this.firstName = firstName; return this;
}
public String getLastName() { return lastName; }
public Bear2 setLastName(String lastName) {
this.lastName = lastName; return this;
}
public int getStreetNumber() { return streetNumber; }
public Bear2 setStreetNumber(int streetNumber) {
this.streetNumber = streetNumber; return this;
}
public String getStreetName() { return streetName; }
public Bear2 setStreetName(String streetName) {
this.streetName = streetName; return this;
}
public String getCity() { return city; }
public Bear2 setCity(String city) {
this.city = city; return this;
}
public String getState() { return state; }
public Bear2 setState(String state) {
this.state = state; return this;
}
}
Add the new entity class to your persistence unit
<class>myorg.entityex.annotated.Bear2</class>
Build the module with the new entity class and observe how the class is mapped to the database. Of no surprise, it is a simple, flat mapping to a single table by default.
create table ENTITYEX_BEAR2 (
id integer generated by default as identity,
city varchar(255),
firstName varchar(255),
lastName varchar(255),
state varchar(255),
streetName varchar(255),
streetNumber integer not null,
primary key (id)
);
Define a secondary table to host the name properties
@Entity
@Table(name="ENTITYEX_BEAR2")
@SecondaryTables({
@SecondaryTable(name="ENTITYEX_BEAR2_NAME")
})
public class Bear2 {
Assign the firstName and lastName properties to the secondary table.
@Column(table="ENTITYEX_BEAR2_NAME", name="FIRST_NAME", length=16)
private String firstName;
@Column(table="ENTITYEX_BEAR2_NAME", name="LAST_NAME", length=16)
private String lastName;
Rebuild the module and notice the database schema generated. The firstName and lastName are mapped to the secondary table, the two tables are joined by primary key values, and the primary key generation and propagation is taken care of by the provider.
create table ENTITYEX_BEAR2 (
id integer generated by default as identity,
city varchar(255),
state varchar(255),
streetName varchar(255),
streetNumber integer not null,
primary key (id)
);
create table ENTITYEX_BEAR2_NAME (
FIRST_NAME varchar(16),
LAST_NAME varchar(16),
id integer not null,
primary key (id)
);
...
alter table ENTITYEX_BEAR2_NAME
add constraint FKED0C2F35D7F6CC81
foreign key (id)
references ENTITYEX_BEAR2;
Add a second secondary table for the address properties
@Entity
@Table(name="ENTITYEX_BEAR2")
@SecondaryTables({
@SecondaryTable(name="ENTITYEX_BEAR2_NAME"),
@SecondaryTable(name="ENTITYEX_BEAR2_ADDRESS")
})
public class Bear2 {
Assign the address properties to the new table
@Column(table="ENTITYEX_BEAR2_ADDRESS", name="STREET_NUMBER", length=16)
private int streetNumber;
@Column(table="ENTITYEX_BEAR2_ADDRESS", name="STREET_NAME", length=16)
private String streetName;
@Column(table="ENTITYEX_BEAR2_ADDRESS", name="CITY", length=16)
private String city;
@Column(table="ENTITYEX_BEAR2_ADDRESS", name="STATE", length=16)
private String state;
Rebuild the module and note the database schema generated. We now have a second table with a primary key join to the primary table.
create table ENTITYEX_BEAR2 (
id integer generated by default as identity,
primary key (id)
);
create table ENTITYEX_BEAR2_ADDRESS (
CITY varchar(16),
STATE varchar(16),
STREET_NAME varchar(16),
STREET_NUMBER integer,
id integer not null,
primary key (id)
);
create table ENTITYEX_BEAR2_NAME (
FIRST_NAME varchar(16),
LAST_NAME varchar(16),
id integer not null,
primary key (id)
);
...
alter table ENTITYEX_BEAR2_ADDRESS
add constraint FKD1DF32EAD7F6CC81
foreign key (id)
references ENTITYEX_BEAR2;
alter table ENTITYEX_BEAR2_NAME
add constraint FKED0C2F35D7F6CC81
foreign key (id)
references ENTITYEX_BEAR2;
Add the following test method to the existing JUnit test case. Note this test method is similar to the embedded object test method except that all properties are directly accessible from the parent entity class.
@Test
public void testMultiTableMapping() {
log.info("testMultiTableMapping");
Bear2 bear = new Bear2()
.setFirstName("Yogi")
.setLastName("Bear")
.setStreetNumber(1)
.setStreetName("Picnic")
.setCity("Jellystone Park")
.setState("???");
em.persist(bear);
//flush to DB and get a new instance
em.flush(); em.detach(bear);
Bear2 bear2 = em.find(Bear2.class, bear.getId());
assertEquals("unexpected firstName", bear.getFirstName(), bear2.getFirstName());
assertEquals("unexpected lastName", bear.getLastName(), bear2.getLastName());
assertEquals("unexpected street number",
bear.getStreetNumber(), bear2.getStreetNumber());
assertEquals("unexpected street name",
bear.getStreetName(), bear2.getStreetName());
assertEquals("unexpected city",
bear.getCity(), bear2.getCity());
assertEquals("unexpected state",
bear.getState(), bear2.getState());
}
Rebuild the module and observe the pass/fail results of the new test as well as the database interaction.
-testMultiTableMapping
Hibernate:
insert
into
ENTITYEX_BEAR2
(id)
values
(null)
Hibernate:
insert
into
ENTITYEX_BEAR2_ADDRESS
(CITY, STATE, STREET_NAME, STREET_NUMBER, id)
values
(?, ?, ?, ?, ?)
Hibernate:
insert
into
ENTITYEX_BEAR2_NAME
(FIRST_NAME, LAST_NAME, id)
values
(?, ?, ?)
Hibernate:
select
bear2x0_.id as id8_0_,
bear2x0_1_.CITY as CITY10_0_,
bear2x0_1_.STATE as STATE10_0_,
bear2x0_1_.STREET_NAME as STREET3_10_0_,
bear2x0_1_.STREET_NUMBER as STREET4_10_0_,
bear2x0_2_.FIRST_NAME as FIRST1_9_0_,
bear2x0_2_.LAST_NAME as LAST2_9_0_
from
ENTITYEX_BEAR2 bear2x0_
left outer join
ENTITYEX_BEAR2_ADDRESS bear2x0_1_
on bear2x0_.id=bear2x0_1_.id
left outer join
ENTITYEX_BEAR2_NAME bear2x0_2_
on bear2x0_.id=bear2x0_2_.id
where
bear2x0_.id=?
In this chapter we mapped multiple tables mapped thru a one-to-one primary key join into a single class. You will find this very similar to when we map a Java class inheritance hierachy to multiple tables using the JOIN strategy. However, in that case, each of the tables is mapped to a specific class within the hierarchy rather than a single class as we did here.