Lab 7: JPA
Mapping Java objects to database tables and vice versa is called Object-relational mapping(ORM). The Java Persistence API (JPA) is one possible approach to ORM (JDO, Web Objects, Hibernate, …). Via JPA the developer can map, store, update and retrieve data from relational databases to Java objects and vice versa.
JPA is a specification and several implementations are available. Popular implementations are Hibernate, EclipseLink, Data Nucleus and Apache OpenJPA. The reference implementation of JPA is EclipseLink.
JPA permits the developer to work directly with objects rather than with SQL statements. The JPA implementation is typically called persistence provider.
The mapping between Java objects and database tables is defined via persistence metadata. The JPA provider will use the persistence metadata information to perform the correct database operations.
JPA metadata is typically defined via annotations in the Java class. Alternatively, the metadata can be defined via XML or a combination of both. A XML configuration overwrites the annotations.
Project
Create a new gradle based project named lab7. Add dependencies for hibernate and H2 database to the build file.
plugins { id 'java' } group 'mis283' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile 'org.hibernate:hibernate-core:5.2.11.Final' testCompile 'junit:junit:4.12' testRuntime 'com.h2database:h2:1.4.196' }
Model
The model classes or entities we use are just modified versions of the classes from lab6. We add the necessary JPA annotations to let the JPA provider generate the database schema for our object model as well as handle all persistence operations.
Site
Site
package mis283.model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.Table; import java.net.URL; import java.util.Objects; @Entity @Table( indexes = @Index( name = "unq_site_name", columnList = "name", unique = true ) ) public class Site implements EntityWithId<Integer> { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Column(nullable = false) private String name; @Column(nullable = false) private URL url; @Override public Integer getId() { return id; } @Override public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public URL getUrl() { return url; } public void setUrl(URL url) { this.url = url; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Site site = (Site) o; return getId() != null && Objects.equals(getId(), site.getId()); } @Override public int hashCode() { return getClass().getName().hashCode(); } }
Article
package mis283.model; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; 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.Index; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import java.util.Objects; import java.util.Optional; @Entity @Table( indexes = { @Index( name = "idx_article_title", columnList = "title" ), @Index( name = "idx_article_site", columnList = "site_id" ) } ) public class Article implements EntityWithId<Integer> { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Column(nullable = false) private String title; private String subtitle; private String description; @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = "site_id") @OnDelete(action = OnDeleteAction.CASCADE) private Site site; @Override public Integer getId() { return id; } @Override public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSubtitle() { return subtitle; } public void setSubtitle(String subtitle) { this.subtitle = subtitle; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Optional<Site> getSite() { return Optional.ofNullable(site); } public void setSite(Optional<Site> site) { this.site = site.isPresent() ? site.get() : null; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Article article = (Article) o; return getId() != null && Objects.equals(getId(), article.getId()); } @Override public int hashCode() { return getClass().getName().hashCode(); } }
JPA Configuration
Create a persistence.xml file under src/main/resources/META-INF. This file is used by the JPA provider to determine which classes it needs to manage, the database to use etc.
<?xml version="1.0" encoding="utf-8" ?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="lab7" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <class>mis283.model.Site</class> <class>mis283.model.Article</class> <properties> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:h2:./lab7" /> <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/> <property name="javax.persistence.schema-generation.scripts.action" value="drop-and-create"/> <property name="javax.persistence.schema-generation.scripts.create-target" value="sampleCreate.ddl"/> <property name="javax.persistence.schema-generation.scripts.drop-target" value="sampleDrop.ddl"/> <property name="dialect" value="org.hibernate.dialect.H2Dialect" /> <property name="hibernate.show_sql" value="true" /> </properties> </persistence-unit> </persistence>
Repository
We will modify the repository abstraction classes from lab6 to use JPA instead of raw JDBC.
Datastore
package mis283.repository; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; public class Datastore { private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("lab7" ); private static final Datastore singleton = new Datastore(); public static Datastore getDatastore() { return singleton; } private Datastore() { Runtime.getRuntime().addShutdownHook( new Thread( () -> emf.close() ) ); } EntityManager getManager() { return emf.createEntityManager(); } }
BaseRepository
package mis283.repository; import mis283.model.EntityWithId; import javax.persistence.EntityManager; import java.util.Collection; import java.util.Optional; import static java.lang.String.format; import static mis283.repository.Datastore.getDatastore; abstract class BaseRepository<T extends EntityWithId> { Collection<T> retrieveAll( final Class<T> t ) { final EntityManager em = getDatastore().getManager(); try { return retrieveAll( t, em ); } finally { em.close(); } } Collection<T> retrieveAll( final Class<T> t, final EntityManager em ) { try { em.getTransaction().begin(); return em.createQuery( format( "select c from %s c", t.getSimpleName() ) ).getResultList(); } finally { em.getTransaction().commit(); } } public Optional<T> retrieve( final T entity ) { final EntityManager em = getDatastore().getManager(); try { return retrieve( entity, em ); } finally { em.close(); } } Optional<T> retrieve( final T entity, final EntityManager em ) { try { em.getTransaction().begin(); final T t = em.find( (Class<T>) entity.getClass(), entity.getId() ); return Optional.ofNullable( t ); } finally { em.getTransaction().commit(); } } T save( final T entity, final EntityManager em ) { try { em.getTransaction().begin(); if ( entity.getId() == null ) { em.persist( entity ); return entity; } else return em.merge( entity ); } catch (final Throwable t) { em.getTransaction().rollback(); throw t; } finally { if (em.getTransaction().isActive()) em.getTransaction().commit(); } } void delete( final T entity ) { final EntityManager em = getDatastore().getManager(); try { delete( entity, em ); } finally { em.close(); } } void delete( final T entity, final EntityManager em ) { try { em.getTransaction().begin(); final T e = em.merge( entity ); em.remove( e ); } catch (final Throwable t) { em.getTransaction().rollback(); throw t; } finally { if (em.getTransaction().isActive()) em.getTransaction().commit(); } } }
SiteRepository
package mis283.repository; import mis283.model.Site; import javax.persistence.EntityManager; import javax.persistence.Query; import java.util.Collection; import java.util.Optional; import static mis283.repository.Datastore.getDatastore; public class SiteRepository extends BaseRepository<Site> { private static final SiteRepository singleton = new SiteRepository(); public static SiteRepository getSiteRepository() { return singleton; } public Collection<Site> retrieveAll() { return retrieveAll( Site.class ); } public Optional<Site> retrieve( final int id ) { final Site site = new Site(); site.setId( id ); return retrieve( site ); } public Optional<Site> retrieve( final String name ) { final EntityManager em = getDatastore().getManager(); try { em.getTransaction().begin(); final Query query = em.createQuery( "select s from Site s where s.name = :name" ); query.setParameter( "name", name ); final Site site = (Site) query.getSingleResult(); return Optional.ofNullable( site ); } finally { em.getTransaction().commit(); em.close(); } } public Site save( final Site entity ) { final EntityManager em = getDatastore().getManager(); try { return save( entity, em ); } finally { em.close(); } } @Override public void delete( final Site entity ) { super.delete( entity ); entity.setId( 0 ); } }
ArticleRepository
package mis283.repository; import mis283.model.Article; import mis283.model.Site; import javax.persistence.EntityManager; import javax.persistence.Query; import java.util.Collection; import java.util.List; import java.util.Optional; import static mis283.repository.Datastore.getDatastore; public class ArticleRepository extends BaseRepository<Article> { private static final ArticleRepository singleton = new ArticleRepository(); public static ArticleRepository getArticleRepository() { return singleton; } public Collection<Article> retrieveAll() { return retrieveAll( Article.class ); } public Optional<Article> retrieve( final int id ) { final Article article = new Article(); article.setId( id ); return retrieve( article ); } public List<Article> retrieve( final Site site ) { final EntityManager em = getDatastore().getManager(); try { em.getTransaction().begin(); final Query query = em.createQuery( "select a from Article a where a.site = :site order by a.title" ); query.setParameter( "site", site ); return query.getResultList(); } finally { em.getTransaction().commit(); em.close(); } } public List<Article> retrieve( final String title ) { final EntityManager em = getDatastore().getManager(); try { em.getTransaction().begin(); final Query query = em.createQuery( "select a from Article a where a.title = :title" ); query.setParameter( "title", title ); return query.getResultList(); } finally { em.getTransaction().commit(); em.close(); } } public Article save( final Article entity ) { final EntityManager em = getDatastore().getManager(); try { if ( entity.getSite().isPresent() ) { final Site merged = em.merge( entity.getSite().get() ); entity.setSite( Optional.of( merged ) ); } return super.save( entity, em ); } finally { em.close(); } } @Override public void delete( final Article article ) { super.delete( article ); article.setId( 0 ); } }
Test Suite
We will modify the tests from lab6 to match the modifications we made to the object model.
SiteRepositoryTest
package mis283.repository; import mis283.model.Site; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import java.net.URL; import java.util.Collection; import java.util.Optional; import static java.lang.String.format; import static mis283.repository.SiteRepository.getSiteRepository; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class SiteRepositoryTest { private static final String name = "Unit Test Site"; private static final URL url; private static int id = 0; static { URL temp = null; try { temp = new URL( "http://test.com/" ); } catch ( final Throwable t ) { throw new RuntimeException( t ); } url = temp; } @BeforeClass public static void create() { final Site site = new Site(); site.setName( name ); site.setUrl( url ); final Site result = getSiteRepository().save( site ); assertTrue( result.getId() > 0 ); id = result.getId(); } @Test public void retrieveAll() { final Collection<Site> results = getSiteRepository().retrieveAll(); assertFalse( results.isEmpty() ); boolean found = false; final Site test = defaultSite(); for ( final Site site : results ) { if ( test.equals( site ) ) { found = true; break; } } assertTrue( found ); } @Test public void retrieve() { final Optional<Site> optional = getSiteRepository().retrieve( id ); assertTrue( optional.isPresent() ); assertEquals( defaultSite(), optional.get() ); } @Test public void retrieveByName() { final Optional<Site> optional = getSiteRepository().retrieve( name ); assertTrue( optional.isPresent() ); assertEquals( defaultSite(), optional.get() ); } @Test public void save() { final Site site = defaultSite(); final String modified = format( "%s modified", name ); site.setName( modified ); getSiteRepository().save( site ); Optional<Site> optional = getSiteRepository().retrieve( id ); assertTrue( optional.isPresent() ); assertEquals( modified, optional.get().getName() ); site.setName( name ); getSiteRepository().save( site ); optional = getSiteRepository().retrieve( id ); assertTrue( optional.isPresent() ); assertEquals( name, optional.get().getName() ); } @AfterClass public static void delete() { final Site site = defaultSite(); getSiteRepository().delete( site ); assertEquals( 0, site.getId().intValue() ); final Optional<Site> optional = getSiteRepository().retrieve( id ); assertFalse( optional.isPresent() ); } static Site defaultSite() { final Site site = new Site(); site.setId( id ); site.setName( name ); site.setUrl( url ); return site; } }
ArticleRepositoryTest
package mis283.repository; import mis283.model.Article; import mis283.model.Site; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Optional; import static java.lang.String.format; import static mis283.repository.ArticleRepository.getArticleRepository; import static mis283.repository.SiteRepository.getSiteRepository; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; public class ArticleRepositoryTest { private static final String title = "Unit Test Title"; private static final String subtitle = "Unit Test Sub Title"; private static final String description = "Unit Test Description for article."; private static int id = 0; private static Site site; @BeforeClass public static void create() { SiteRepositoryTest.create(); site = SiteRepositoryTest.defaultSite(); final Article article = new Article(); article.setTitle( title ); article.setSubtitle( subtitle ); article.setDescription( description ); article.setSite( Optional.of( site ) ); final Article result = getArticleRepository().save( article ); assertTrue( result.getId() > 0 ); id = result.getId(); } @Test public void retrieveAll() { final Collection<Article> results = getArticleRepository().retrieveAll(); assertFalse( results.isEmpty() ); check( results ); } @Test public void retrieve() { final Optional<Article> optional = getArticleRepository().retrieve( id ); assertTrue( optional.isPresent() ); assertEquals( defaultArticle(), optional.get() ); assertEquals( site, optional.get().getSite().get() ); } @Test public void retrieveBySite() { final List<Article> list = getArticleRepository().retrieve( site ); assertFalse( list.isEmpty() ); check( list ); } @Test public void retrieveByTitle() { final List<Article> list = getArticleRepository().retrieve( title ); assertFalse( list.isEmpty() ); check( list ); } @Test public void save() { final Article article = defaultArticle(); final String modified = format( "%s modified", subtitle ); article.setSubtitle( modified ); getArticleRepository().save( article ); Optional<Article> optional = getArticleRepository().retrieve( id ); assertTrue( optional.isPresent() ); assertEquals( modified, optional.get().getSubtitle() ); article.setSubtitle( subtitle ); getArticleRepository().save( article ); optional = getArticleRepository().retrieve( id ); assertTrue( optional.isPresent() ); assertEquals( subtitle, optional.get().getSubtitle() ); } @Test public void objectGraph() throws MalformedURLException { final Article article = defaultArticle(); article.setId( null ); final Site site = new Site(); site.setName( "From Article" ); site.setUrl( new URL( "http://something.com/" ) ); article.setSite( Optional.of( site ) ); getArticleRepository().save( article ); assertNotNull( article.getId() ); assertTrue( article.getSite().isPresent() ); assertNotNull( article.getSite().get().getId() ); Optional<Article> optional = getArticleRepository().retrieve( article.getId() ); assertTrue( optional.isPresent() ); assertEquals( article, optional.get() ); getSiteRepository().delete( article.getSite().get() ); optional = getArticleRepository().retrieve( article.getId() ); assertFalse( optional.isPresent() ); } @AfterClass public static void delete() { final Article article = defaultArticle(); getArticleRepository().delete( article ); assertEquals( 0, article.getId().intValue() ); final Optional<Article> optional = getArticleRepository().retrieve( id ); assertFalse( optional.isPresent() ); SiteRepositoryTest.delete(); } private void check( final Collection<Article> collection ) { boolean found = false; final Article test = defaultArticle(); for ( final Article article : collection ) { if ( test.getId().equals( article.getId() ) ) { found = true; break; } } assertTrue( found ); } private static Article defaultArticle() { final Article article = new Article(); article.setId( id ); article.setTitle( title ); article.setSubtitle( subtitle ); article.setDescription( description ); article.setSite( Optional.of( site ) ); return article; } }