Goal:
This article is an exploration of composition and inheritance relationships with Hibernate and Spring. The article begins with boilerplate project setup using Maven2 and presents individual examples of one-to-one, one-to-many and many-to-many relationships, as well as one way to map inheritance to a database schema. JUnit tests emphasize the interesting features of these relationships. By the end of the tutorial, we will have a framework and the tools for building more complex domains from these simple models.
Project Setup
Our initial project setup is essentially identical to that of the previous post in the series, where we use the M2 plug-in for Eclipse to create a simple Maven2 project. For this example, we specify our dependencies (Spring, Hibernate, JUnit), create a Spring application context for Hibernate session management, create a Hibernate mapping file to register our annotated domain and create a pristine database schema (in Oracle).
The pom.xml contains our Hibernate 3.4, Spring 3.0, JUnit 4.4 and Oracle driver dependencies.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>timezra.blog.hibernate.data_modelling</groupId>
<artifactId>timezra.blog.hibernate.data_modelling</artifactId>
<name>timezra.blog.hibernate.data_modelling</name>
<version>0.0.1-SNAPSHOT</version>
<description>Hibernate Data Modelling Example</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.transaction</artifactId>
<version>3.0.0.M2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.orm</artifactId>
<version>3.0.0.M2</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>3.4.0.GA</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.4.GA</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc14</artifactId>
<version>10.2.0.2.0</version>
</dependency>
<dependency>
<scope>test</scope>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<scope>test</scope>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.test</artifactId>
<version>3.0.0.M2</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>SpringSource Enterprise Bundle Repository - External Bundle Milestones</id>
<url>http://repository.springsource.com/maven/bundles/milestone</url>
</repository>
<repository>
<id>SpringSource Enterprise Bundle Repository - SpringSource Bundle Releases</id>
<url>http://repository.springsource.com/maven/bundles/release</url>
</repository>
<repository>
<id>SpringSource Enterprise Bundle Repository - External Bundle Releases</id>
<url>http://repository.springsource.com/maven/bundles/external</url>
</repository>
</repositories>
</project>
The src/main/resources/applicationContext.xml conatins our Hibernate connection and session configuration and our transaction manager.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>oracle.jdbc.driver.OracleDriver</value></property>
<property name="username"><value>data_modelling</value></property>
<property name="password"><value>data_modelling</value></property>
<property name="url"><value>jdbc:oracle:thin:@localhost:1521:orcl</value></property>
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:/hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.generate_statistics">true</prop>
<prop key="hibernate.use_sql_comments">true</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
<prop key="hibernate.query.factory_class">org.hibernate.hql.classic.ClassicQueryTranslatorFactory</prop>
</props>
</property>
</bean>
<bean id="txManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
The src/main/resources/hibernate.cfg.xml is our registry for annotated Hibernate domain Objects.
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
</session-factory>
</hibernate-configuration>
We can create a pristine database schema from sqlplus.
sqlplus connect as SYSDBA
create user data_modelling identified by data_modelling default tablespace users temporary tablespace temp;
grant connect, resource to data_modelling;
grant create table to data_modelling;
With that infrastructural boilerplate out of the way, we can begin defining our domain. For this example, our domain consists of libraries, books and authors.
A library can contain any number of books, and books can be present in any number of libraries. A book is written by an author. An author can write any number of books.
One to One
Suppose that a Library has an Address, and the Address must be unique per Library. That is, there cannot be more than one Library for a given Address. We can say that the Library and the Address have a one-to-one relationship.
One way to representing such a relationship with tables in a relational database would be for these two entities to share a primary key. That is, the primary key of one table can have a foreign-key reference to another table.
We first need to declare that our domain will contain a Library and an Address in the src/main/resources/hibernate.cfg.xml.
....
<session-factory>
<mapping class="timezra.blog.hibernate.data_modelling.domain.Library" />
<mapping class="timezra.blog.hibernate.data_modelling.domain.Address" />
</session-factory>
....
Suppose we decide that the Library's primary key depends on the primary key of the Address. Our Address object can have a sequence-generated primary key, and the corresponding id of the Library can have a foreign-key dependency on the Address id. You might be asking yourself what would happen if the relationship were reversed, if it would make any functional difference or if the difference is only semantic.
Address.java
package timezra.blog.hibernate.data_modelling.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Table(name = "ADDRESS", schema = "DATA_MODELLING")
public class Address implements java.io.Serializable {
private static final long serialVersionUID = 1384654594047265994L;
private int id;
private String street1;
private String street2;
private String city;
private String state;
private int zipCode;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "address_id_seq")
@SequenceGenerator(name = "address_id_seq", sequenceName = "address_id_seq")
@Column(name = "ID", nullable = false)
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
@Column(name = "STREET_1", nullable = false, length = 50)
public String getStreet1() {
return street1;
}
public void setStreet1(final String street1) {
this.street1 = street1;
}
@Column(name = "STREET_2", nullable = true, length = 50)
public String getStreet2() {
return street2;
}
public void setStreet2(final String street2) {
this.street2 = street2;
}
@Column(name = "CITY", nullable = false, length = 50)
public String getCity() {
return city;
}
public void setCity(final String city) {
this.city = city;
}
@Column(name = "STATE", nullable = false, length = 2)
public String getState() {
return state;
}
public void setState(final String state) {
this.state = state;
}
@Column(name = "ZIP_CODE", nullable = false)
public int getZipCode() {
return zipCode;
}
public void setZipCode(final int zipCode) {
this.zipCode = zipCode;
}
}
The primary key for Library.java is also specified as a generated value, but it uses a foreign-key generator instead of a sequence. We also need to specify that the primary key is a join column with the Address. This foreign-key id generator and the declaration that the Address can be joined to the Library by their primary keys is the essence of this relationship mapping. You now might be asking yourself what would happen if this relationship were reversed and if it would make sense for an Address to have a dependency on a Library.
package timezra.blog.hibernate.data_modelling.domain;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
@Entity
@Table(name = "LIBRARY", schema = "DATA_MODELLING")
public class Library implements java.io.Serializable {
private static final long serialVersionUID = 556729119500294739L;
private int id;
private String name;
private Address address;
@Id
@GeneratedValue(generator = "foreign")
@GenericGenerator(name = "foreign", strategy = "foreign", parameters = { @Parameter(name = "property", value = "address") })
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
@Column(name = "NAME", nullable = false, length = 50)
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
@OneToOne(cascade = CascadeType.ALL, optional = false)
@PrimaryKeyJoinColumn
public Address getAddress() {
return address;
}
public void setAddress(final Address address) {
this.address = address;
}
}
We can verify that exactly one unique Address can be associated with a Library with a test case.
LibraryTest.java
package timezra.blog.hibernate.data_modelling.domain;
import static org.junit.Assert.fail;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.SessionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public class LibraryTest {
@Autowired
private SessionFactory sessionFactory;
@Test
public void onlyOneLibraryCanHaveaGivenAddress() throws Exception {
final Address address = createAddress();
createSeattleMainBranch(address);
try {
createSeattleSatelliteBranch(address);
fail("A given address cannot be associated with more than one library.");
} catch (final NonUniqueObjectException e) {
// pass
}
}
private void createSeattleSatelliteBranch(final Address address) {
final Library seattleSatelliteBranch = new Library();
seattleSatelliteBranch.setName("Ballard Branch");
seattleSatelliteBranch.setAddress(address);
sessionFactory.getCurrentSession().save(seattleSatelliteBranch);
}
private void createSeattleMainBranch(final Address address) {
final Library seattleMainBranch = new Library();
seattleMainBranch.setName("Seattle Central Library");
seattleMainBranch.setAddress(address);
sessionFactory.getCurrentSession().save(seattleMainBranch);
}
private Address createAddress() {
final Address address = new Address();
address.setStreet1("1000 Fourth Ave.");
address.setCity("Seattle");
address.setState("WA");
address.setZipCode(98104);
sessionFactory.getCurrentSession().save(address);
return address;
}
}
One to Many
As previously noted, each Book has a single Author, and an Author can write any number of Books. Such a requirement (while simplistic because in actuality a Book could have multiple Authors) can be mapped to a one-to-many relationship for the purpose of this tutorial.
In our database, this type of compositional relationship can best be represented by a foreign-key dependency from the many side to the one side. Whereas, in our domain Object, an inuitive way to demonstrate this relationship would be for each Author to reference the books she has written and for each Book to have a reference to its Author, the tabular relationship would be indicated as a field only in the composed entity (Book).
Again, we will declare that Books and Authors are part of our domain in src/main/resources/hibernate.cfg.xml.
....
<session-factory>
....
<mapping class="timezra.blog.hibernate.data_modelling.domain.Author" />
<mapping class="timezra.blog.hibernate.data_modelling.domain.Book" />
</session-factory>
....
An Author.java writes Books, and to indicate this relationship, we can use the @OneToMany declaration.
package timezra.blog.hibernate.data_modelling.domain;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Table(name = "AUTHOR", schema = "DATA_MODELLING")
public class Author implements java.io.Serializable {
private static final long serialVersionUID = 3091725775359171905L;
private int id;
private String firstName;
private String lastName;
private Set<Book> books = new HashSet<Book>();
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
@SequenceGenerator(name = "author_id_seq", sequenceName = "author_id_seq")
@Column(name = "ID", nullable = false)
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
@Column(name = "FIRST_NAME", nullable = false, length = 50)
public String getFirstName() {
return firstName;
}
public void setFirstName(final String firstName) {
this.firstName = firstName;
}
@Column(name = "LAST_NAME", nullable = false, length = 50)
public String getLastName() {
return lastName;
}
public void setLastName(final String lastName) {
this.lastName = lastName;
}
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "author")
public Set<Book> getBooks() {
return books;
}
public void setBooks(final Set<Book> books) {
this.books = books;
}
}
Similarly, a Book.java has an Author, so we can declare this relationship as @ManyToOne. We will also specify how the database resolves this reference, so we declare our @JoinColumn here, similar to how the foreign-key reference is in the Book table in the database.
package timezra.blog.hibernate.data_modelling.domain;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "BOOK", schema = "DATA_MODELLING")
public class Book implements java.io.Serializable {
private static final long serialVersionUID = 4347977827397320578L;
private int id;
private String title;
private int pages;
private Author author;
private String publisher;
private Date publicationDate;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_id_seq")
@SequenceGenerator(name = "book_id_seq", sequenceName = "book_id_seq")
@Column(name = "ID", nullable = false)
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
@Column(name = "PUBLISHER", nullable = false, length = 50)
public String getPublisher() {
return publisher;
}
public void setPublisher(final String publisher) {
this.publisher = publisher;
}
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "PUBLICATION_DATE", length = 11)
public Date getPublicationDate() {
return publicationDate;
}
public void setPublicationDate(final Date publicationDate) {
this.publicationDate = publicationDate;
}
@Column(name = "TITLE", nullable = false, length = 200)
public String getTitle() {
return title;
}
public void setTitle(final String title) {
this.title = title;
}
@Column(name = "PAGES", nullable = false)
public int getPages() {
return pages;
}
public void setPages(final int pages) {
this.pages = pages;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "AUTHOR_ID", nullable = false)
public Author getAuthor() {
return author;
}
public void setAuthor(final Author author) {
this.author = author;
}
}
We do not explicitly reference the foreign-key dependency in the Book table except in the meta-data. In a test case, we can execute a SQL query to get the value from this foreign-key column and compare it to the id that it references in the Author table.
BookTest.java
package timezra.blog.hibernate.data_modelling.domain;
import static org.junit.Assert.assertEquals;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import org.hibernate.SessionFactory;
import org.hibernate.classic.Session;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public class BookTest {
@Autowired
private SessionFactory sessionFactory;
@Test
public void aBookHasAForeignKeyReferenceToAnAuthor() throws Exception {
final Author cameronMcKenzie = createCameronMcKenzie();
final Book hibernateMadeEasy = createHibernateMadeEasy(cameronMcKenzie);
final Session session = sessionFactory.getCurrentSession();
session.flush();
final BigDecimal authorId = (BigDecimal) session //
.createSQLQuery("SELECT author_id FROM book WHERE id = :vId") //
.setParameter("vId", hibernateMadeEasy.getId()) //
.uniqueResult();
assertEquals(cameronMcKenzie.getId(), authorId.intValue());
}
private Book createHibernateMadeEasy(final Author cameronMcKenzie) throws ParseException {
final Book hibernateMadeEasy = new Book();
hibernateMadeEasy.setTitle("Hibernate Made Easy");
hibernateMadeEasy.setAuthor(cameronMcKenzie);
hibernateMadeEasy.setPublisher("PulpJava");
hibernateMadeEasy.setPublicationDate(new SimpleDateFormat("MM-yyyy").parse("04-2008"));
hibernateMadeEasy.setPages(444);
sessionFactory.getCurrentSession().save(hibernateMadeEasy);
return hibernateMadeEasy;
}
private Author createCameronMcKenzie() {
final Author cameronMcKenzie = new Author();
cameronMcKenzie.setFirstName("Cameron");
cameronMcKenzie.setLastName("McKenzie");
sessionFactory.getCurrentSession().save(cameronMcKenzie);
return cameronMcKenzie;
}
}
Many to Many
A Library has many Books and a Book can be found in any number of Libraries. In a database, this type of complex relationship cannot be captured by using foreign-key references from Books to Libraries or vice versa. Most problems in computer science can be solved by adding a layer of indirection, and this is no exception. Many-to-many relationships can be represented in a relational database with a cross-reference table.
Fortunately, Hibernate will automatically create such a cross-reference table, so we do not need to register any other Objects with our domain. We can simply annotate our existing domain Objects to indicate that Libraries and Books have a @ManyToMany relationship and that the database can use a @JoinTable to represent this relationship.
Library.java
....
private Set<Book> books = new HashSet<Book>();
....
@ManyToMany(targetEntity = Book.class)
@JoinTable(name = "BOOK_LIBRARY", joinColumns = @JoinColumn(name = "LIBRARY_ID"), inverseJoinColumns = @JoinColumn(name = "BOOK_ID"))
public Set<Book> getBooks() {
return books;
}
public void setBooks(final Set<Book> books) {
this.books = books;
}
....
Book.java
....
private Set<Library> libraries = new HashSet<Library>();
....
@ManyToMany(mappedBy = "books", targetEntity = Library.class)
public Set<Library> getLibraries() {
return libraries;
}
public void setLibraries(final Set<Library> libraries) {
this.libraries = libraries;
}
....
With a LibraryBookTest.java, we can demonstrate the difference between how joins are performed between HQL and SQL.
package timezra.blog.hibernate.data_modelling.domain;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.hibernate.SessionFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public class LibraryBookTest {
@Autowired
private SessionFactory sessionFactory;
private Book hibernateMadeEasy;
private Book javaPersistenceWithHibernate;
private Book springInAction;
@Before
public void setUp() throws Exception {
createSeattleMainBranch( //
createAddress(), //
hibernateMadeEasy = createHibernateMadeEasy(), //
javaPersistenceWithHibernate = createJavaPersistenceWithHibernate());
springInAction = createSpringInAction();
}
@Test
public void findAllTheBooksInTheSeattleMainLibraryWithHQL() throws Exception {
final Collection<?> books = sessionFactory.getCurrentSession() //
.createQuery( //
"select b " + //
"from Library l, Book b " + //
"where b in elements(l.books) " + //
" and l.name = 'Seattle Central Library'") //
.list();
assertTrue(books.contains(hibernateMadeEasy));
assertTrue(books.contains(javaPersistenceWithHibernate));
assertFalse(books.contains(springInAction));
}
@Test
public void findAllTheBooksInTheSeattleMainLibraryWithSQL() throws Exception {
final List<?> books = sessionFactory.getCurrentSession() //
.createSQLQuery( //
"SELECT b.* " + //
"FROM book b, book_library bl, library l " + //
"WHERE b.id = bl.book_id " + //
" AND bl.library_id = l.id " + //
" AND l.name = 'Seattle Central Library'") //
.addEntity(Book.class) //
.list();
assertTrue(books.contains(hibernateMadeEasy));
assertTrue(books.contains(javaPersistenceWithHibernate));
assertFalse(books.contains(springInAction));
}
private Book createHibernateMadeEasy() throws ParseException {
return createBook(createAuthor("Cameron", "McKenzie"), "Hibernate Made Easy", "PulpJava", new SimpleDateFormat(
"MM-yyyy").parse("04-2008"), 444);
}
private Book createJavaPersistenceWithHibernate() throws ParseException {
return createBook(createAuthor("Christian", "Bauer"), "Java Persistence with Hibernate", "Manning Publications",
new SimpleDateFormat("MM-yyyy").parse("11-2006"), 904);
}
private Book createSpringInAction() throws ParseException {
return createBook(createAuthor("Craig", "Walls"), "Spring in Action", "Manning Publications", new SimpleDateFormat(
"MM-yyyy").parse("08-2007"), 650);
}
private Author createAuthor(final String firstName, final String lastName) {
final Author author = new Author();
author.setFirstName(firstName);
author.setLastName(lastName);
sessionFactory.getCurrentSession().save(author);
return author;
}
private Book createBook(final Author author, final String title, final String publisher, final Date publicationDate,
final int pages) throws ParseException {
final Book hibernateMadeEasy = new Book();
hibernateMadeEasy.setTitle(title);
hibernateMadeEasy.setAuthor(author);
hibernateMadeEasy.setPublisher(publisher);
hibernateMadeEasy.setPublicationDate(publicationDate);
hibernateMadeEasy.setPages(pages);
sessionFactory.getCurrentSession().save(hibernateMadeEasy);
return hibernateMadeEasy;
}
private void createSeattleMainBranch(final Address address, final Book... books) {
final Library seattleMainBranch = new Library();
seattleMainBranch.setName("Seattle Central Library");
seattleMainBranch.setAddress(address);
seattleMainBranch.getBooks().addAll(Arrays.asList(books));
sessionFactory.getCurrentSession().save(seattleMainBranch);
}
private Address createAddress() {
final Address address = new Address();
address.setStreet1("1000 Fourth Ave.");
address.setCity("Seattle");
address.setState("WA");
address.setZipCode(98104);
sessionFactory.getCurrentSession().save(address);
return address;
}
}
Inheritance
While relational databases represent flat data structures very well, Object inheritance relationships do not naturally map. Suppose we want our domain to contain regular People as well as Authors. An Author is a Person who has written any number of Books. A Person is not necessarily an Author, however.
One way to represent this relationship in a database is to use a single table to represent all types of People and to use a discriminator to differentiate among the various types of people. Such a table would contain all the attributes for all the types of people in the domain, even if the given attributes do not apply to a particular type of person. For our example because we have such a small number of types of People in our system, the SingleTableInheritance model will suffice. For domains that contain deep or broad inheritance trees, this mapping may not be the best and other mappings should be explored.
People are now part of the domain, and we can update our src/main/resources/hibernate.cfg.xml registry.
....
<mapping class="timezra.blog.hibernate.data_modelling.domain.Person" />
....
Here, all types of Person.java will be present in the same database table because of the configured @Inheritance strategy. Different People can be distinguished with a @DiscriminatorType in a @DiscriminatorColumn.
package timezra.blog.hibernate.data_modelling.domain;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "PERSON_TYPE", discriminatorType = DiscriminatorType.STRING)
@DiscriminatorValue("PERSON")
@Table(name = "PERSON", schema = "DATA_MODELLING")
public class Person implements java.io.Serializable {
private static final long serialVersionUID = -2080047375791491221L;
private int id;
private String firstName;
private String lastName;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "person_id_seq")
@SequenceGenerator(name = "person_id_seq", sequenceName = "person_id_seq")
@Column(name = "ID", nullable = false)
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
@Column(name = "FIRST_NAME", nullable = false, length = 50)
public String getFirstName() {
return firstName;
}
public void setFirstName(final String firstName) {
this.firstName = firstName;
}
@Column(name = "LAST_NAME", nullable = false, length = 50)
public String getLastName() {
return lastName;
}
public void setLastName(final String lastName) {
this.lastName = lastName;
}
}
The updated Author.java must also register a @DiscriminatorValue but does not need to specify a primary key or table mapping because they are inherited from the base class.
package timezra.blog.hibernate.data_modelling.domain;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
@Entity
@DiscriminatorValue("AUTHOR")
public class Author extends Person {
private static final long serialVersionUID = 1792156832094030133L;
private Set<Book> books = new HashSet<Book>();
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "author")
public Set<Book> getBooks() {
return books;
}
public void setBooks(final Set<Book> books) {
this.books = books;
}
}
We can demonstrate the relationship between Person and Author in our AuthorTest.java. These test cases show that an Author is a Person, but a Person is not necessarily an Author.
package timezra.blog.hibernate.data_modelling.domain;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.hibernate.SessionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public class AuthorTest {
@Autowired
private SessionFactory sessionFactory;
@Test
public void anAuthorIsaPerson() throws Exception {
final Author cameronMcKenzie = createCameronMcKenzie();
final Person timMyer = createTimMyer();
final List<?> peopleWithaLastNameStartingWithM = sessionFactory.getCurrentSession() //
.createQuery("from Person person where person.lastName like 'M%'") //
.list();
assertTrue(peopleWithaLastNameStartingWithM.contains(cameronMcKenzie));
assertTrue(peopleWithaLastNameStartingWithM.contains(timMyer));
}
@Test
public void notAllPeopleAreAuthors() throws Exception {
final Author cameronMcKenzie = createCameronMcKenzie();
final Person timMyer = createTimMyer();
final List<?> authorsWithaLastNameStartingWithM = sessionFactory.getCurrentSession() //
.createQuery("from Author author where author.lastName like 'M%'") //
.list();
assertTrue(authorsWithaLastNameStartingWithM.contains(cameronMcKenzie));
assertFalse(authorsWithaLastNameStartingWithM.contains(timMyer));
}
private Person createTimMyer() {
final Person timMyer = new Person();
timMyer.setFirstName("Tim");
timMyer.setLastName("Myer");
sessionFactory.getCurrentSession().save(timMyer);
return timMyer;
}
private Author createCameronMcKenzie() {
final Author cameronMcKenzie = new Author();
cameronMcKenzie.setFirstName("Cameron");
cameronMcKenzie.setLastName("McKenzie");
sessionFactory.getCurrentSession().save(cameronMcKenzie);
return cameronMcKenzie;
}
}
We now have a framework for easily registering new inter-related entities in our domain and for verifying the relationships between those entities.