Enterprise Java Development@TOPIC@
Create a JUnit test class to host tests for the one-to-many mappings.
Put the following JUnit test case base class in your src/test tree. You can delete the sample test method once we add our first real test. JUnit will fail a test case if it cannot locate a @Test to run.
package myorg.relex;
import static org.junit.Assert.*;
import javax.persistence.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.*;
public class One2ManyTest extends JPATestBase {
private static Logger log = LoggerFactory.getLogger(One2ManyTest.class);
@Test
public void testSample() {
log.info("testSample");
}
}
Verify the new JUnit test class builds and executes to completion
relationEx]$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest ... -HHH000401: using driver [org.h2.Driver] at URL [jdbc:h2:tcp://localhost:9092/./h2db/ejava] ... [INFO] BUILD SUCCESS
Mapping one-to-many, uni-directional relationships require some help since the owning side is a single item and we must model relationships to many entities. To do this we can either use a join table (which forms a one-to-one, uni-directional relationship with the many side) or insert a foreign key into the table representing the many side. We will take a look at the join table approach first.
In this section we will demonstrate how to form a one-to-many uni-directional relationship using a join table. The join table is necessary since the many side is unaware of the relationship and "many" cannot be represented by a single row in the owning table.
Place the following entity class in your src/main tree. This class provides an example of the many side of a one-to-many, uni-directional relation. Because of this, this entity class has no reference to the owning/parent entity class.
package myorg.relex.one2many;
import javax.persistence.*;
/**
* This class provides an example of an entity class on the many side of a one-to-many,
* uni-directional relationship that will be referenced through a JoinTable.
*/
@Entity
@Table(name="RELATIONEX_RIDER")
public class Rider {
@Id @GeneratedValue
private int id;
@Column(length=32)
private String name;
public Rider() {}
public Rider(int id) {
this.id = id;
}
public int getId() { return id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
Place the following entity class in your src/main tree. This class provides an example of the one side of a one-to-many, uni-directional relation. The implementation is incomplete at the moment and relies on defaults to complete the relation.
package myorg.relex.one2many;
import java.util.List;
import javax.persistence.*;
/**
* This entity class provides an example of the one side of a one-to-many, uni-directional relation
* that is realized through a JoinTable.
*/
@Entity
@Table(name="RELATIONEX_BUS")
public class Bus {
@Id
private int number;
@OneToMany
// @JoinTable(
// name="RELATIONEX_BUS_RIDER",
// joinColumns={@JoinColumn(name="BUS_NO")},
// inverseJoinColumns={@JoinColumn(name="RIDER_ID")}
// )
private List<Rider> passengers;
protected Bus() {}
public Bus(int number) {
this.number = number;
}
public int getNumber() { return number; }
public List<Rider> getPassengers() {
if (passengers==null) { passengers = new ArrayList<Rider>(); }
return passengers;
}
public void setPassengers(List<Rider> passengers) {
this.passengers = passengers;
}
}
Add the two entity classes to the persistence unit.
<class>myorg.relex.one2many.Rider</class>
<class>myorg.relex.one2many.Bus</class>
Build the module and look at the default database schema generated for the two entities and relationship
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_BUS ( number integer not null, primary key (number) ); create table RELATIONEX_BUS_RELATIONEX_RIDER ( RELATIONEX_BUS_number integer not null, passengers_id integer not null, unique (passengers_id) ); ... create table RELATIONEX_RIDER ( id integer generated by default as identity, name varchar(32), primary key (id) ); ... alter table RELATIONEX_BUS_RELATIONEX_RIDER add constraint FK3F295C59773EE4ED foreign key (RELATIONEX_BUS_number) references RELATIONEX_BUS; alter table RELATIONEX_BUS_RELATIONEX_RIDER add constraint FK3F295C5994D90F30 foreign key (passengers_id) references RELATIONEX_RIDER;
Note that...
The default mapping for a @OneToMany is a @JoinTable
A default join table name is created using a combination of the one and many table names
A foreign key to the one table is based on the table name and primary key column name
A foreign key to the many table is based on the property name and primary key column name
The foreign key to the many side is unique since the child can only be related to one parent. There can only be a single row in this table linking the child to a parent.
Fill in the relationship mapping with non-default values.
Explicitly use a @JoinTable mapping
Name the join name RELATIONEX_BUS_RIDER
Name the foreign key to the Bus "BUS_NO"
Name the foreign key to the Rider "RIDER_ID
@OneToMany
@JoinTable(
name="RELATIONEX_BUS_RIDER",
joinColumns={@JoinColumn(name="BUS_NO")},
inverseJoinColumns={@JoinColumn(name="RIDER_ID")}
)
private List<Rider> passengers;
Rebuild the module and re-generate the database schema for the entities and relationship. Nothing will functionally change with the relationship except that we will be more in control of the database table and column names chosen in case we have to map to a legacy database.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_BUS ( number integer not null, primary key (number) ); create table RELATIONEX_BUS_RIDER ( BUS_NO integer not null, RIDER_ID integer not null, unique (RIDER_ID) ); ... create table RELATIONEX_RIDER ( id integer generated by default as identity, name varchar(32), primary key (id) ); ... alter table RELATIONEX_BUS_RIDER add constraint FKE78EAB2B869BB455 foreign key (BUS_NO) references RELATIONEX_BUS; alter table RELATIONEX_BUS_RIDER add constraint FKE78EAB2B3961502F foreign key (RIDER_ID) references RELATIONEX_RIDER;
Add the following test method to your existing one-to-many JUnit test case to demonstrate the capabilities of a one-to-many, uni-directional relationship mapped using a join table.
@Test
public void testOneToManyUniJoinTable() {
log.info("*** testOneToManyUniJoinTable ***");
Bus bus = new Bus(302);
em.persist(bus);
List<Rider> riders = new ArrayList<Rider>();
for (int i=0; i<2; i++) {
Rider rider = new Rider();
rider.setName("rider" + i);
em.persist(rider);
riders.add(rider);
}
log.debug("relating entities");
bus.getPassengers().addAll(riders);
em.flush(); em.clear();
log.debug("verify we have expected objects");
Bus bus2 = em.find(Bus.class, bus.getNumber());
assertNotNull("bus not found", bus2);
for (Rider r: bus.getPassengers()) {
assertNotNull("rider not found", em.find(Rider.class, r.getId()));
}
log.debug("verify they are related");
assertEquals("unexpected number of riders", bus.getPassengers().size(), bus2.getPassengers().size());
}
Rebuild the module and run the new test method. All entity instances will be created and then the relationships added.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniJoinTable ... -relating entities Hibernate: insert into RELATIONEX_BUS_RIDER (BUS_NO, RIDER_ID) values (?, ?) Hibernate: insert into RELATIONEX_BUS_RIDER (BUS_NO, RIDER_ID) values (?, ?)
Notice that since we are not requiring an EAGER fetch, the parent object can be retrieved without a join of the JoinTable and child/many table.
-verify we have expected objects Hibernate: select bus0_.number as number23_0_ from RELATIONEX_BUS bus0_ where bus0_.number=?
Notice that once we needed the collection a join of the JoinTable and child table was performed to provide the result.
log.debug("verify they are related");
assertEquals("unexpected number of riders", bus.getPassengers().size(), bus2.getPassengers().size());
-verify they are related Hibernate: select passengers0_.BUS_NO as BUS1_23_1_, passengers0_.RIDER_ID as RIDER2_1_, rider1_.id as id22_0_, rider1_.name as name22_0_ from RELATIONEX_BUS_RIDER passengers0_ inner join RELATIONEX_RIDER rider1_ on passengers0_.RIDER_ID=rider1_.id where passengers0_.BUS_NO=?
Add the following code snippet to the test method to attempt to remove one of the child objects. There is an error with code because it attempts to remove the child object without removing the relationship to the parent.
log.debug("remove one of the child objects");
em.remove(bus2.getPassengers().get(0)); //ouch!!! this will violate a FK-constraint
em.flush();
Rebuild the module and attempt to re-run the test method. Not the foreign key constraint violatation with the current implementation of the test method.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniJoinTable ... -remove one of the child objects Hibernate: delete from RELATIONEX_RIDER where id=? -SQL Error: 23503, SQLState: 23503 -Referential integrity constraint violation: "FKE78EAB2B3961502F: PUBLIC.RELATIONEX_BUS_RIDER FOREIGN KEY(RIDER_ID) REFERENCES PUBLIC.RELATIONEX_RIDER(ID) (1)"; SQL statement: delete from RELATIONEX_RIDER where id=? [23503-168]
Update the test method to remove the relationship prior to removing the child object. This should satisfy all constraints.
log.debug("remove one of the child objects");
Rider rider = bus2.getPassengers().get(0);
log.debug("removing the relationship");
assertTrue("ride not found in relation", bus2.getPassengers().remove(rider));
em.flush();
log.debug("removing the object");
em.remove(rider);
em.flush();
Rebuild the module and re-run the test method. The provider now knows to delete the relationship prior to deleting the child object. However, it is of some interest that the provider chose to delete all relationships and re-add only teh remaining ones rather than remove a specific one.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniJoinTable ... -remove one of the child objects -removing the relationship Hibernate: delete from RELATIONEX_BUS_RIDER where BUS_NO=? Hibernate: insert into RELATIONEX_BUS_RIDER (BUS_NO, RIDER_ID) values (?, ?) -removing the object Hibernate: delete from RELATIONEX_RIDER where id=? ... [INFO] BUILD SUCCESS
You have finished a brief section on implementing a one-to-many, uni-directional relationship using a join table. You should have noticed that the join table is the default mapping for such a construct and in the following section we will eliminate the join table and insert a foreign key into the child table.
In this section we will demonstrate how to form a one-to-many uni-directional relationship without a join table. In this case the @JoinColumn in the owning/one side actually maps the relationship to a foreign key in the dependent entity table.
Place the following entity class in your src/main tree. It provides an example of the many side of a one-to-many, uni-directional relationship that we will map using a foreign key in the table for this entity class.
package myorg.relex.one2many;
import javax.persistence.*;
/**
* This class provides an example of the many side of a one-to-many, uni-directional relationship
* mapped using a foreign key in the child entity table. Note that all mapping will be from the one/owning
* side and no reference to the foreign key exists within this class.
*/
@Entity
@Table(name="RELATIONEX_STOP")
public class Stop {
@Id @GeneratedValue
private int id;
@Column(length=16)
private String name;
public int getId() { return id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
Place the following entity class in your src/main tree. It provides an example of the one side of a one-to-many, uni-directional relationship that will define a foreign key mapping in the remote child table. The @JoinColumn referenced in the @OneToMany relationship is referring to a column in the child's table and not the parent's table.
package myorg.relex.one2many;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;
/**
* This class provides an example of the one side of a one-to-many, uni-directional relationship
* mapped using a foreign key inserted into the child/many table. The @JoinColumn is referencing
* the child table and not this entity's table.
*/
@Entity
@Table(name="RELATIONEX_ROUTE")
public class Route {
@Id
private int number;
@OneToMany
@JoinColumn
private List<Stop> stops;
protected Route() {}
public Route(int number) {
this.number = number;
}
public int getNumber() { return number; }
public List<Stop> getStops() {
if (stops == null) { stops = new ArrayList<Stop>(); }
return stops;
}
public void setStops(List<Stop> stops) {
this.stops = stops;
}
}
Add the two new entity classes to the persistence unit.
<class>myorg.relex.one2many.Stop</class>
<class>myorg.relex.one2many.Route</class>
Build the module and inspect the default database schema generated for the two entities and their one-to-many, uni-directional relationship.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_ROUTE ( number integer not null, primary key (number) ); ... create table RELATIONEX_STOP ( id integer generated by default as identity, name varchar(16), stops_number integer, primary key (id) ); ... alter table RELATIONEX_STOP add constraint FK35604A92586DC195 foreign key (stops_number) references RELATIONEX_ROUTE;
Notice that...
A foreign key was added to the child/many table to reference the parent/one table
There is no property within the child/many entity to module this foreign key
Add the following test method to your existig one-to-many JUnit test case.-
@Test
public void testOneToManyUniFK() {
log.info("*** testOneToManyUniFK ***");
Route route = new Route(302);
em.persist(route);
List<Stop> stops = new ArrayList<Stop>();
for (int i=0; i<2; i++) {
Stop stop = new Stop();
stop.setName("stop" + i);
em.persist(stop);
stops.add(stop);
}
log.debug("relating entities");
route.getStops().addAll(stops);
em.flush(); em.clear();
log.debug("verify we have expected objects");
Route route2 = em.find(Route.class, route.getNumber());
assertNotNull("route not found", route2);
for (Stop s: route.getStops()) {
assertNotNull("stop not found", em.find(Stop.class, s.getId()));
}
log.debug("verify they are related");
assertEquals("unexpected number of stops", route.getStops().size(), route2.getStops().size());
}
Rebuild and module and run the new test method to demonstrate creating the entities and relating them through the one-to-many, uni-directional foreign key from the child table. In this case the provider simply updates a foreign key column in the child table rather than inserting a row in a separate join table.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniFK ... -relating entities Hibernate: update RELATIONEX_STOP set stops_number=? where id=? Hibernate: update RELATIONEX_STOP set stops_number=? where id=?
Notice how there is no longer a join required when obtaining a list of all children
-verify we have expected objects ... -verify they are related Hibernate: select stops0_.stops_number as stops3_25_1_, stops0_.id as id1_, stops0_.id as id24_0_, stops0_.name as name24_0_ from RELATIONEX_STOP stops0_ where stops0_.stops_number=?
Add the following snippet to verify we can delete a child entity. Note that we have more flexibility in this @JoinColumn case than we did with the previous @JoinTable case. Since there is no separate foreign key referencing the child row and the foreign key is part of the child row -- a delete of the child will automatically remove the relationship.
log.debug("remove one of the child objects");
log.debug("removing the object and relationship");
em.remove(route2.getStops().get(0));
em.flush();
Rebuild the module and re-run the unit test to verify we can delete one of the child objects. Note that we only delete a single row from the child table. In the previous case the provider deleted all instances referencing the parent from the join table and re-added them.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniFK ... -remove one of the child objects -removing the object and relationship Hibernate: delete from RELATIONEX_STOP where id=? ... [INFO] BUILD SUCCESS
You have finished implementing a one-to-many, uni-directional relationship using a @JoinColumn from the child table. This case was designed to solve the common situation in Java and databases where the database is mapped using a simple foreign key in the child table and Java is mapped using only a collection in the parent/one entity. The child/member Java entity has no direct knowledge of which parent entities it will be associated with.
In this section we will demonstrate how to map a collection of a simple data type to a database using a parent/dependent set of tables and a foreign key. This case is similar to the one-to-many, uni-directional relationship with a @JoinColumn where all mapping is being done from the owning/one side. However, in this case, the child/many side is not an entity and does not have a primary key.
Place the following entity class in your src/main tree. The class models a collection of alias strings we will want mapped to a separate dependent/child table. However, the class is currently incomplete and will initially define a mapping we do not want.
package myorg.relex.one2many;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
/**
* This class provides an example of the owning side of a collection of base data types.
* In this case we want a unique set of strings (aliases) mapped to this entity using
* a separate dependent table and a foreign key relationship.
*/
@Entity
@Table(name="RELATIONEX_SUSPECT")
public class Suspect {
@Id @GeneratedValue
private int id;
@Column(length=32)
private String name;
// @ElementCollection
// @CollectionTable(
// name="RELATIONEX_SUSPECT_ALIASES",
// joinColumns=@JoinColumn(name="SUSPECT_ID"),
// uniqueConstraints=@UniqueConstraint(columnNames={"SUSPECT_ID", "ALIAS"}))
// @Column(name="ALIAS", length=32)
@Lob
private Set<String> aliases;
public int getId() { return id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public Set<String> getAliases() {
if (aliases==null) { aliases = new HashSet<String>(); }
return aliases;
}
public void setAliases(Set<String> aliases) {
this.aliases = aliases;
}
}
Add the new entity class to the persistence unit
<class>myorg.relex.one2many.Suspect</class>
Build the module and look at the database schema generated. Notice how the set of strings was mapped to a single column within the parent table of type blob where the set will be serialized into that column when saved.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_SUSPECT ( id integer generated by default as identity, aliases blob, name varchar(32), primary key (id) );
Fix the mapping such that the list of aliases are kept in a separate dependent/child table with a value column containing the property value and a foreign key back to the parent entity.
@ElementCollection // @CollectionTable( // name="RELATIONEX_SUSPECT_ALIASES", // joinColumns=@JoinColumn(name="SUSPECT_ID"), // uniqueConstraints=@UniqueConstraint(columnNames={"SUSPECT_ID", "ALIAS"})) // @Column(name="ALIAS", length=32) private Set<String> aliases;
Rebuild the module and observe the new database schema generated.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_SUSPECT ( id integer generated by default as identity, name varchar(32), primary key (id) ); ... create table Suspect_aliases ( Suspect_id integer not null, aliases varchar(255) ); ... alter table Suspect_aliases add constraint FK9AC56596DE29C9CF foreign key (Suspect_id) references RELATIONEX_SUSPECT;
Notice that...
A new table was created to house the collection values
The new table has no primary key
The new table has a foreign key that references the parent table
Update the table mapping to control the table and column properties of the child table.
@ElementCollection
@CollectionTable(
name="RELATIONEX_SUSPECT_ALIASES",
joinColumns=@JoinColumn(name="SUSPECT_ID"),
uniqueConstraints=@UniqueConstraint(columnNames={"SUSPECT_ID", "ALIAS"}))
@Column(name="ALIAS", length=32)
private Set<String> aliases;
Rebuild the module and notice the change in database schema generated.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_SUSPECT ( id integer generated by default as identity, name varchar(32), primary key (id) ); create table RELATIONEX_SUSPECT_ALIASES ( SUSPECT_ID integer not null, ALIAS varchar(32), unique (SUSPECT_ID, ALIAS) ); ... alter table RELATIONEX_SUSPECT_ALIASES add constraint FK3FD160E6DE29C9CF foreign key (SUSPECT_ID) references RELATIONEX_SUSPECT;
Notice that...
@ColumnTable.name was used to name the dependent/child table
@ColumnTable.joinColumns was used to name the foreign key to the parent table
@ColumnTable.uniqueConstraints was used to restrict columns in table to unique values
@Column.name was used to name the value column used to hold the member of collection
@Column.length was used to control the size of the value column
Add a new test method to your existing one-to-many JUnit test case to demonstrate mapping of simple value collections to parent/dependent tables using a foreign key relationship.
@Test
public void testOneToManyUniElementCollection() {
log.info("*** testOneToManyUniElementCollection ***");
Suspect suspect = new Suspect();
suspect.setName("william");
em.persist(suspect);
suspect.getAliases().add("bill");
suspect.getAliases().add("billy");
em.flush(); em.clear();
log.debug("verify we have expected objects");
Suspect suspect2 = em.find(Suspect.class, suspect.getId());
assertNotNull("suspect not found", suspect2);
for (String a: suspect.getAliases()) {
assertNotNull("alias not found", suspect2.getAliases().contains(a));
}
}
Rebuild the module and run the new test method to demonstrate the construction of the object graph and the mapping to parent/dependent tables.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniElementCollection ... -*** testOneToManyUniElementCollection *** Hibernate: insert into RELATIONEX_SUSPECT (id, name) values (null, ?) Hibernate: insert into RELATIONEX_SUSPECT_ALIASES (SUSPECT_ID, ALIAS) values (?, ?) Hibernate: insert into RELATIONEX_SUSPECT_ALIASES (SUSPECT_ID, ALIAS) values (?, ?)
-verify we have expected objects Hibernate: select suspect0_.id as id26_0_, suspect0_.name as name26_0_ from RELATIONEX_SUSPECT suspect0_ where suspect0_.id=? Hibernate: select aliases0_.SUSPECT_ID as SUSPECT1_26_0_, aliases0_.ALIAS as ALIAS0_ from RELATIONEX_SUSPECT_ALIASES aliases0_ where aliases0_.SUSPECT_ID=?
Add the following code snippet to demonstrate we can remove a row from the dependent/child table by removing the value from the collecion.
log.debug("remove one of the child objects");
String alias = suspect2.getAliases().iterator().next();
assertTrue("alias not found", suspect2.getAliases().remove(alias));
em.flush();
Rebuild the module and re-run the test method to demonstrate the removal of an element from the collection. Note how the provider is removing a row from the database table.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniElementCollection ... -remove one of the child objects Hibernate: delete from RELATIONEX_SUSPECT_ALIASES where SUSPECT_ID=? and ALIAS=? ... [INFO] BUILD SUCCESS
Our example was able to take advantage of the fact that an alias should be unique for a suspect. If that was not the case, we would have had to remove the uniqueness constraint for SUSPECT_ID+ALIAS and risked performing a full table scan if we didn't either a) manually define a value index on ALIAS outside of schema generation or b) use a vendor-specific annotation to define the necessary non-unique index. Since database schema generally goes through manual manipulation prior to operational deployment and the generation here is just an aide -- this should not be much of an issue.
You have finished mapping a collection of simple types to a set of parent and dependent tables linked through a foreign key. The value within the collection was mapped directly to a column within the dependent table. By mapping the collection to a separate table instead of storing a serialized block into a single column -- we gain the ability to use values in the collection within future database queries.
In the previous section we demonstrated how we could map a collection of simple data values to a set of parent/dependent tables and link them through a foreign key. In this section we are going to add a small complexity where the simple value is now an embeddable type and will map to multiple columns in the dependent entity table.
Put the following entity class into your src/main tree. This class provides and example of an embeddable non-entity class that will be mapped using an @ElementCollection mapping.
package myorg.relex.one2many;
import java.util.Date;
import javax.persistence.*;
/**
* This class is an example of a non-entity class that will be mapped to a dependent table
* and form the many side of an @ElementCollection.
*/
@Embeddable
public class Produce {
public static enum Color { RED, GREEN, YELLOW }
@Column(length=16)
private String name;
@Enumerated(EnumType.STRING)
@Column(length=10)
private Color color;
@Temporal(TemporalType.DATE)
private Date expires;
public Produce() {}
public Produce(String name, Color color, Date expires) {
this.name = name;
this.color = color;
this.expires = expires;
}
public String getName() { return name; }
public Color getColor() { return color; }
public Date getExpires() { return expires; }
}
Put the following entity class in place in your src/main tree. This class provides and example of using an @ElementCollection to persist a list of embeddable, non-entity classes within a dependent/child table linked to this entity table using a foreign key. The class defines some of the CollectionTable mappings but leaves some of the default values to what is defined within the Embeddable class.
package myorg.relex.one2many;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;
/**
* This entity class provides an example of mapping a collection of non-entity/embeddable class instances
* to a dependent/child table and relating the child table to this entity table using a foreign key.
*/
@Entity
@Table(name="RELATIONEX_BASKET")
public class Basket {
@Id @GeneratedValue
private int id;
@ElementCollection
@CollectionTable(
name="RELATIONEX_BASKET_PRODUCE",
joinColumns=@JoinColumn(name="BASKET_ID"))
// @AttributeOverrides({
// @AttributeOverride(name="name", column=@Column(name="ITEM_NAME")),
// @AttributeOverride(name="color", column=@Column(name="ITEM_COLOR"))
// })
private List<Produce> contents;
@Column(length=16)
private String name;
public int getId() { return id; }
public List<Produce> getContents() {
if (contents == null) { contents = new ArrayList<Produce>(); }
return contents;
}
public void setContents(List<Produce> contents) {
this.contents = contents;
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
Add the two entity classes to the persistence unit.
<class>myorg.relex.one2many.Basket</class>
<class>myorg.relex.one2many.Produce</class>
Build the module and observe the generated schema produced. Notice how the @Column mappings from the @Embeddable class show up in the resultant schema.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_BASKET ( id integer generated by default as identity, name varchar(16), primary key (id) ); create table RELATIONEX_BASKET_PRODUCE ( BASKET_ID integer not null, color varchar(10), expires date, name varchar(16) ); ... alter table RELATIONEX_BASKET_PRODUCE add constraint FKA97DDDD776CDC1E5 foreign key (BASKET_ID) references RELATIONEX_BASKET;
Assume we wish to control the schema from the parent class. Update the parent class to control the column names for the product name and color but not the expiration date. You can do this using an @AttributeOverride for each property you wish to change. However multiple @AttributeOverride instances must be wrapped within an array and defined within an @AttributeOverrides instance.
@ElementCollection
@CollectionTable(
name="RELATIONEX_BASKET_PRODUCE",
joinColumns=@JoinColumn(name="BASKET_ID"))
@AttributeOverrides({
@AttributeOverride(name="name", column=@Column(name="ITEM_NAME")),
@AttributeOverride(name="color", column=@Column(name="ITEM_COLOR"))
})
private List<Produce> contents;
Rebuild the module and observe the updated database schema. Notice how the name of the columns specified in the parent definition of the mapping was used over the default name. It also eliminated the @Column length. We could fix that in the @Column definition of the @AttributeOverride as well.
$ mvn clean process-test-classes; more target/classes/ddl/relationEx-createJPA.ddl ... create table RELATIONEX_BASKET ( id integer generated by default as identity, name varchar(16), primary key (id) ); create table RELATIONEX_BASKET_PRODUCE ( BASKET_ID integer not null, ITEM_COLOR varchar(255), expires date, ITEM_NAME varchar(255) ); ... alter table RELATIONEX_BASKET_PRODUCE add constraint FKA97DDDD776CDC1E5 foreign key (BASKET_ID) references RELATIONEX_BASKET;
Add the following test method to your existing one-to-many JUnit test case.
@Test
public void testOneToManyUniEmbeddableElementCollection() {
log.info("*** testOneToManyUniEmbeddableElementCollection ***");
Basket basket = new Basket();
basket.setName("assorted fruit");
em.persist(basket);
basket.getContents().add(new Produce("apple", Produce.Color.RED, new Date(System.currentTimeMillis()+(3600000*24*30))));
basket.getContents().add(new Produce("banana", Produce.Color.YELLOW, new Date(System.currentTimeMillis()+(3600000*24*14))));
em.flush(); em.clear();
log.debug("verify we have expected objects");
Basket basket2 = em.find(Basket.class, basket.getId());
assertNotNull("basket not found", basket2);
assertEquals("unexpected contents", basket.getContents().size(), basket2.getContents().size());
Build the module and run the new test method to demonstrate the building of the object graph and the mapping to the database.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniEmbeddableElementCollection ... -*** testOneToManyUniEmbeddableElementCollection *** Hibernate: insert into RELATIONEX_BASKET (id, name) values (null, ?) Hibernate: insert into RELATIONEX_BASKET_PRODUCE (BASKET_ID, ITEM_COLOR, expires, ITEM_NAME) values (?, ?, ?, ?) Hibernate: insert into RELATIONEX_BASKET_PRODUCE (BASKET_ID, ITEM_COLOR, expires, ITEM_NAME) values (?, ?, ?, ?)
-verify we have expected objects Hibernate: select basket0_.id as id27_0_, basket0_.name as name27_0_ from RELATIONEX_BASKET basket0_ where basket0_.id=? Hibernate: select contents0_.BASKET_ID as BASKET1_27_0_, contents0_.ITEM_COLOR as ITEM2_0_, contents0_.expires as expires0_, contents0_.ITEM_NAME as ITEM4_0_ from RELATIONEX_BASKET_PRODUCE contents0_ where contents0_.BASKET_ID=?
Add the following code snippet to verify we can delete one of the embeddable instances from the collection and the database.
log.debug("remove one of the child objects");
Produce produce = basket2.getContents().get(0);
assertTrue("produce not found", basket2.getContents().remove(produce));
em.flush();
Rebuild the module and re-run the test method to observe how the provider will be deleting our embeddable instance. Notice, in this case, the provider is deleting all related instances and re-adding the remaining instance(s) that are still associated to the parent. That is likely due to the fact that the instance has no primary key and no field combinations were labelled as unique. The provider has no way to tell one instance from another -- so it must delete and re-add.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniEmbeddableElementCollection ... -remove one of the child objects Hibernate: delete from RELATIONEX_BASKET_PRODUCE where BASKET_ID=? Hibernate: insert into RELATIONEX_BASKET_PRODUCE (BASKET_ID, ITEM_COLOR, expires, ITEM_NAME) values (?, ?, ?, ?) ... [INFO] BUILD SUCCESS
You have finished mapping a collection of embeddable, non-entity class instances to a dependent table related to the parent table using a foreign key. You were also able to control the table and column properties through @Column definitions directly applied to the embeddable class and override them in the parent class use @AttributeOverride(s).
In this chapter we will assume what was covered in the one-to-one CASCADE portion is suitable for now and focus this section on orphanRemoval.
Orphan removal is specific to one-to-one and one-to-many relationships. For a one-to-many relationship, this capability allows members of a collection to be removed once they stop being members of that collection. Thus the child member is only there to support the parent entity.
Put the following child entity class in your src/main tree. This class provides an example many side of a one-to-many relation. Thus it has no reference to the one side.
package myorg.relex.one2many;
import javax.persistence.*;
/**
* This class is an example of the many side of a one-to-many, uni-directional relationship
* which uses orphanRemoval of target entities on the many side. This entity exists for the
* sole use of the one side of the relation.
*/
@Entity
@Table(name="RELATIONEX_TODO")
public class Todo {
@Id @GeneratedValue
private int id;
@Column(length=32)
private String title;
public Todo() {}
public Todo(String title) {
this.title = title;
}
public int getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) {
this.title = title;
}
}
Place the following parent entity in your src/main tree. This class provides an example of the one/owning side of a one-to-many, uni-directional relationship. The relationship is realized through a foreign key from the child entity table to the parent. The parent has enabled cascade.PERSIST to allow for easy storage of the child entities but its currently incomplete when it comes to orphan removal. We will fix in a moment.
package myorg.relex.one2many; import java.util.ArrayList; import java.util.List; import javax.persistence.*; /** * This class provides an example owning entity in a one-to-many, uni-directional relationship * where the members of the collection are subject to orphanRemoval when they are removed from the * collection. */ @Entity @Table(name="RELATIONEX_TODOLIST") public class TodoList { @Id @GeneratedValue private int id; @OneToMany(cascade={CascadeType.PERSIST} // ,orphanRemoval=true ) @JoinColumn private List<Todo> todos; public int getId() { return id; } public List<Todo> getTodos() { if (todos==null) { todos = new ArrayList<Todo>(); } return todos; } public void setTodos(List<Todo> todos) { this.todos = todos; } }
Add the new entity classes to your persistence unit.
<class>myorg.relex.one2many.Todo</class>
<class>myorg.relex.one2many.TodoList</class>
Add the following test method to your JUnit test case. This portion of the test will simply create and parent and first child entity.
@Test
public void testOneToManyUniOrphanRemoval() {
log.info("*** testOneToManyUniEmbeddableElementCollection ***");
//check how many child entities exist to start with
int startCount = em.createQuery("select count(t) from Todo t", Number.class).getSingleResult().intValue();
log.debug("create new TODO list with first entry");
TodoList list = new TodoList();
list.getTodos().add(new Todo("get up"));
em.persist(list);
em.flush();
}
Build the module and run the new test method. Note the creation of the parent, child, and the relating of the child to the parent.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniOrphanRemoval ... -*** testOneToManyUniEmbeddableElementCollection *** Hibernate: select count(todo0_.id) as col_0_0_ from RELATIONEX_TODO todo0_ limit ? -create new TODO list with first entry Hibernate: insert into RELATIONEX_TODOLIST (id) values (null) Hibernate: insert into RELATIONEX_TODO (id, title) values (null, ?) Hibernate: update RELATIONEX_TODO set todos_id=? where id=? ... [INFO] BUILD SUCCESS
Add the following lines to the unit test to verify a child instance was created.
log.debug("verifying we have new child entity");
assertEquals("new child not found", startCount +1,
em.createQuery("select count(t) from Todo t", Number.class).getSingleResult().intValue());
Rebuild the module and re-run the test method. Notice we get the expected additional child entity in the table.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniOrphanRemoval ... -verifying we have new child entity Hibernate: select count(todo0_.id) as col_0_0_ from RELATIONEX_TODO todo0_ limit ? ... [INFO] BUILD SUCCESS
Add the following lines to your test method. This will remove the child entity from the parent collection and test whether it was subjected to an orphan removal.
log.debug("removing child from list");
list.getTodos().clear();
em.flush();
assertEquals("orphaned child not deleted", startCount,
em.createQuery("select count(t) from Todo t", Number.class).getSingleResult().intValue());
Rebuild the module and re-run the test method. Notice how the test currently fails. This is because we never enabled orphanRemoval on the relationship.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniOrphanRemoval ... -removing child from list Hibernate: update RELATIONEX_TODO set todos_id=null where todos_id=? Hibernate: select count(todo0_.id) as col_0_0_ from RELATIONEX_TODO todo0_ limit ? ... Failed tests: testOneToManyUniOrphanRemoval(myorg.relex.One2ManyTest): orphaned child not deleted expected:<0> but was:<1> ... [INFO] BUILD FAILURE
Enable orphanRemoval on the relationship on the parent side.
@OneToMany(cascade={CascadeType.PERSIST}
,orphanRemoval=true
)
@JoinColumn
private List<Todo> todos;
Rebuild the module and re-run the test method. Notice how the child entity is removed shortly after it was removed from the parent collection.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.relex.One2ManyTest#testOneToManyUniOrphanRemoval ... -removing child from list Hibernate: update RELATIONEX_TODO set todos_id=null where todos_id=? Hibernate: delete from RELATIONEX_TODO where id=? Hibernate: select count(todo0_.id) as col_0_0_ from RELATIONEX_TODO todo0_ limit ? ... [INFO] BUILD SUCCESS
You have finished taking a look at orphanRemoval within the context of a one-to-many, uni-directional relationship. With this capability, entities removed from a parent collection are automatically deleted from the database.
In this chapter we you worked with several types of one-to-many, uni-directional relationships. In this type of relationship -- the many/dependent/child entity knows nothing of the relationship and it must be defined from the one/parent side. You formed that mapping using a join table as well as inserting a foreign key into the child table (without the child entity knowing about it). You also created mappings to many/child/dependent non-entity classes like Strings and JPA @Embeddable classes. This allows you to create entity classes with collections of non-entity POJOs and have them mapped to database tables that can be used for searches.
We will pause our look at relationship types take a detour into collection types in the next chapter. We want to make sure we understand many of the options available and requirements associated with those options before we get too far into more relationships involving a "many" side.