Setting up a JPA Test Project
Published: 2019-12-25
Updated: 2020-02-14
Web: https://fritzthecat-blog.blogspot.com/2019/12/setting-up-java-jpa-test-project.html
The introduction of Java Persistence API in 2006 has standardized the APIs of Object-Relational Mappers (ORM). This article is about setting up a JPA test project.
Why Use JPA?
When you implement against JPA (import javax.persistence
classes and annotations only), you could replace Hibernate by EclipseLink, or vice versa, simply by editing your project's persistence.xml and adding the appropriate libraries in CLASSPATH. Hibernate is older, but EclipseLink is the JPA reference implementation. Unfortunately there are no more serious free JPA competitors:
- OpenJPA: I could not make this work, long series of exceptions, out-dated documentation
- DataNucleus: although a little better documented I could not make this work, this is more JDO-oriented
- Batoo is 5 years out-dated, no documentation, just a project on github
- Kundera is for Apache Cassandra DB only
- ObjectDB the free version restricts the number of entity classes to 10
- Versant and OrientDB are databases that provide JPA for their products only
Thus I can not show more than two JPA providers in the following test project. It builds upon JPA 2.1 and Java 1.8. I had to use the newest EclipseLink version 2.7.5, because 2.5 silently(!) crashes on classpath-scanning when anywhere in the @Entity classes there is a lambda expression.
[class ...] uses a non-entity class [...] as target entity in the relationship attribute [field ...]
You may see this (misleading) error message then.
Maven Project Object Model
Here is the Maven
file, to be placed in the project's root directory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>fri</groupId> <artifactId>jpaTest</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <!-- START JPA deps --> <!-- JPA providers --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>5.4.4.Final</version> </dependency> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.7.5</version> </dependency> <!-- JDBC driver H2 database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.199</version> <scope>runtime</scope> </dependency> <!-- END JPA deps --> <!-- Test scope --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13-beta-3</version> <scope>test</scope> </dependency>
</dependencies> </project>
|
I included both Hibernate and EclipseLink using just one dependency for each (that's how it should be with Maven!).
Then I referenced the H2 database, because it is really nice for development (the server-variant opens a web-browser interface on startup, where you can clean-up the database).
The unit tests included afterwards will load the JDBC driver from that dependency.
Mind tat this needs Java 1.8, because I want to use lambdas in my source-code.
JPA Configuration
You need two things to use JPA:
- persistence configuration to point to your database, and
@Entity
annotations on your entity-classes (that refer to database tables).
Here is the first, standardized as
src/main/resources/META-INF/persistence.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <?xml version="1.0" encoding="UTF-8"?> <persistence 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://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1">
<persistence-unit name="HibernateTestPU"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <properties> <!-- Standard JPA properties --> <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test" /> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.jdbc.user" value="sa" /> <property name="javax.persistence.jdbc.password" value="" /> <!-- Auto-drop and -recreate the database tables on startup (not the database schema!) --> <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" /> <!-- Never do this in production-mode, use "update" for partial updating database to entities DDL. --> <!-- Proprietary properties. --> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <!-- Scan for classes annotated by @Entity on startup, instead of hardcoding all classes here --> <property name="hibernate.archive.autodetection" value="class" /> <!-- Display all database statements on console log --> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.format_sql" value="true" /> </properties> </persistence-unit>
<persistence-unit name="EclipselinkTestPU"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <!-- Scan for classes annotated by @Entity on startup, instead of hardcoding all classes here. --> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <!-- Standard JPA properties --> <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test" /> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.jdbc.user" value="sa" /> <property name="javax.persistence.jdbc.password" value="" /> <!-- Auto-drop and -recreate the database tables on startup (not the database schema!) --> <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" /> <!-- Never do this in production-mode, use "update" for partial updating database to entities DDL. --> <!-- Proprietary properties. --> <property name="eclipselink.logging.level.sql" value="FINE"/> <property name="eclipselink.logging.parameters" value="true"/> </properties> </persistence-unit> </persistence>
|
I defined two persistence-units, one pointing to Hibernate, one to EclipseLink. The provider
element gives the fully-qualified class name of the persistence provider. Normally the entity class names are also here, but both providers have a CLASSPATH-scan solution that automatically searches for classes annotated by @Entity
. To make this work you use hibernate.archive.autodetection
for Hibernate, or exclude-unlisted-classes
for EclipseLink. For both providers you don't need explicit class-enhancement, this works under the hood (which is very different to DataNucleus and OpenJPA).
Inside the properties
section you have JPA-standardized elements that describe the JDBC database connection. But this section can also contain provider-specific elements. I used such to turn on SQL statement logging.
Java Entities
Entity Abstraction
Like many projects I use an abstraction for entities, holding basic functionality:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.MappedSuperclass;
@MappedSuperclass public abstract class BaseEntity { @Id @GeneratedValue // make JPA create an id-value on first save private Long id;
/** @return the primary key of this entity. */ public final Long getId() { return id; }
/** Overridden to delegate to class-equality and id (when not null). */ @Override public final boolean equals(Object o) { if (this == o) // performance optimization return true; if (o == null || getClass() != o.getClass()) // exclude aliens return false; // and one-to-one entities with same id final BaseEntity other = (BaseEntity) o; if (id == null || other.id == null) // can't use id return super.equals(o); return id.equals(other.id); // delegate equality to id } /** Overridden to delegate to id when not null, else to super. */ @Override public final int hashCode() { return (id != null) ? id.hashCode() : super.hashCode(); } @Override public String toString() { return super.toString()+": id="+id; } }
|
The @MappedSuperclass
annotation is for super-classes that contain common properties, although you can not have relations in a @MappedSuperclass
. The primary key id
will be present in all extensions of this class. If you don't put the @GeneratedValue
on the field, you will have to care for a unique id value by yourself.
Mind that I left out the setId()
method, this is to prevent application abuse. Having field-access (the @Id
annotation being on the field, not the method), both JPA providers map ALL fields (except @Transient
), even if they don't have setters or getters and are private. On the other hand, the getId()
method may be useful for the application to re-attach objects, or to serve as symbolic reference in case no direct dependency is wanted (check the uniqueness scope of such ids in distributed environments!).
It makes sense to override hashCode()
and equals()
and delegate them to the primary key when you deal with detached and attached entities, like most web applications do. The hashCode/equals contract is implemented here by referring to the id
. In case the id has not been set yet, it delegates to the super-class implementation (which may use the unique memory-address of the object). Mind that such objects would dynamically change their hashcode when receiving their id
, and this could lead to strange effects when they have been used as keys in hash-containers.
Example Entities
The idea of following domain-model is to have a team that consists of responsibilities, each responsibility (or role) refers to exactly one person. Persons will exist independently of teams, and one person can be member of several teams. Resources also exist independently, but are bound directly to the team, not to a responsibility, and a resource can belong to several teams.
- Team 1:n Responsibility
- Responsibility n:1 Person
- Team m:n Resource
import javax.persistence.Entity;
@Entity
public class Person extends BaseEntity
{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
@Entity
public class Responsibility extends BaseEntity
{
private String name;
@ManyToOne(optional = false)
private Team team;
@ManyToOne(optional = false)
private Person person;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
The annotation
@ManyToOne(optional = false)
expresses that a responsibility must have both a person and a team.
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
@Entity
public class Team extends BaseEntity
{
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Responsibility> responsibilities = new HashSet<>();
@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private Set<Resource> resources = new HashSet<>();
public Set<Responsibility> getResponsibilities() {
return responsibilities;
}
public void add(Responsibility responsibility) {
responsibilities.add(responsibility);
responsibility.setTeam(this);
}
public Set<Resource> getResources() {
return resources;
}
}
The annotation
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
says that responsibilities will be deleted when their team gets deleted. Such is a hierarchical relation, bidirectional, that requires a @ManyToOne
annotation on the backlink in Responsibility
.
The annotation
@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
says that resources in the team's collection will be saved and updated together with the team. This enables a team editor to create new resources by simply adding them to the team. The resources saved that way will not be deleted when removed from the team's collection, and thus will be available independently from any team (so resources may aggregate over time!). This is an unidirectional relation, Resource
doesn't have a backlink to Team
.
Mind that I left out the setResponsibilities()
method, this again is to avoid abuse, and the JPA provider doesn't care about the missing method. Changing the responsibilities is legal just through the add()
method, because only this sets the backlink correctly. Of course you could still do a getResponsibilities().add(r)
and forget the backlink, but having the getter is inevitable. This is an API weakness.
import javax.persistence.Entity;
@Entity
public class Resource extends BaseEntity
{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
When running the unit tests below and then looking into the database, you will find following tables created:
- PERSON (ID, NAME)
- RESOURCE (ID, NAME)
- RESPONSIBILITY (ID, NAME, PERSON_ID, TEAM_ID)
- TEAM (ID)
- TEAM_RESOURCE (TEAM_ID, RESOURCES_ID)
To generate id
values, Hibernate creates a sequence called "hibernate_sequence", and EclipseLink creates a table called "SEQUENCE". But this may depend on the database product, because GenerationType.AUTO
is the default for GeneratedValue(strategy=...)
and lets the persistence provider choose the strategy (AUTO, IDENTITY, SEQUENCE, TABLE
). Mind that having just one sequence (or SEQUENCE table) for all ids of all tables may perform badly when batch-migrations insert large amounts of records, because all inserts have to queue and wait for their id at the same source.
Unit Test
To integrate both JPA providers into the same tests I abstracted the persistence-unit name from the test. Two classes extending the unit test then set the persistence-unit name to either HibernateTestPU or EclipselinkTestPU (see persistence.xml above).
public class HibernateJpaTest extends JpaTest
{
@Override
protected String getPersistenceUnitName() {
return "HibernateTestPU";
}
}
public class EclipselinkJpaTest extends JpaTest
{
@Override
protected String getPersistenceUnitName() {
return "EclipselinkTestPU";
}
}
Running the tests for both JPA providers would require running these two test classes.
Here is their base:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import org.junit.Assert; import org.junit.Before; import org.junit.Test;
public abstract class JpaTest { private EntityManager em;
/** @return the name of the persistence-unit to use for all tests. */ protected abstract String getPersistenceUnitName(); @Before public void setUp() { em = Persistence.createEntityManagerFactory(getPersistenceUnitName()).createEntityManager(); }
@Test public void testCreateDelete() { final Person peter = newPerson("Peter"); transactional(em::persist, peter); Assert.assertEquals(1, findPersons().size()); transactional(em::remove, peter); Assert.assertEquals(0, findPersons().size()); } @Test public void testModelGraph() { // build test data Assert.assertEquals(0, findPersons().size()); Assert.assertEquals(0, findResources().size()); Assert.assertEquals(0, findTeams().size()); // create persons final Person peter = newPerson("Peter"); transactional(em::persist, peter); final Person mary = newPerson("Mary"); transactional(em::persist, mary); Assert.assertEquals(2, findPersons().size()); // create a team and resources final Team team = new Team(); final String DEVELOPER = "Developer"; final Responsibility developer = newResponsibility(DEVELOPER, peter); team.add(developer); final String ADMINISTRATOR = "Administrator"; final Responsibility administrator = newResponsibility(ADMINISTRATOR, mary); team.add(administrator); final String SCANNER = "Scanner"; final Resource scanner = newResource(SCANNER); team.getResources().add(scanner); transactional(em::persist, team); Assert.assertEquals(1, findResources().size()); // due to cascaded persist // assert result final List<Team> persistentTeams = findTeams(); Assert.assertNotNull(persistentTeams); Assert.assertEquals(1, persistentTeams.size()); final Team persistentTeam = persistentTeams.get(0); Assert.assertEquals(2, persistentTeam.getResponsibilities().size()); final List<Responsibility> responsibilities = new ArrayList<>(persistentTeam.getResponsibilities()); final Responsibility r1 = responsibilities.get(0); // identical with developer final Responsibility r2 = responsibilities.get(1); // identical with administrator Assert.assertTrue(DEVELOPER.equals(r1.getName()) || DEVELOPER.equals(r2.getName())); Assert.assertTrue(ADMINISTRATOR.equals(r1.getName()) || ADMINISTRATOR.equals(r2.getName())); Assert.assertEquals(1, persistentTeam.getResources().size()); final Resource resource = persistentTeam.getResources().iterator().next(); Assert.assertTrue(SCANNER.equals(resource.getName())); // clean up transactional(em::remove, team); Assert.assertEquals(0, findTeams().size()); Assert.assertEquals(0, findResponsibilities().size()); Assert.assertEquals(1, findResources().size()); Assert.assertEquals(2, findPersons().size()); transactional(em::remove, scanner); Assert.assertEquals(0, findResources().size()); transactional(em::remove, peter); transactional(em::remove, mary); Assert.assertEquals(0, findPersons().size()); }
private <P> void transactional(Consumer<P> entityManagerFunction, P parameter) { final EntityTransaction transaction = em.getTransaction(); try { transaction.begin(); entityManagerFunction.accept(parameter); transaction.commit(); } catch (Throwable th) { th.printStackTrace(); transaction.rollback(); throw th; } } private Person newPerson(String name) { final Person person = new Person(); person.setName(name); return person; }
private List<Person> findPersons() { return em .createQuery("select p from "+Person.class.getName()+" p", Person.class) .getResultList(); } private Resource newResource(String name) { final Resource resource = new Resource(); resource.setName(name); return resource; }
private List<Resource> findResources() { return em .createQuery("select r from "+Resource.class.getName()+" r", Resource.class) .getResultList(); } private Responsibility newResponsibility(String name, Person person) { final Responsibility responsibility = new Responsibility(); responsibility.setName(name); responsibility.setPerson(person); return responsibility; }
private List<Responsibility> findResponsibilities() { return em .createQuery("select r from "+Responsibility.class.getName()+" r", Responsibility.class) .getResultList(); } private List<Team> findTeams() { return em .createQuery("select t from "+Team.class.getName()+" t", Team.class) .getResultList(); } }
|
There is one short test, and a second one that builds a complex graph of objects. I used the new functional features of Java 1.8 to implement transactions.
Mind line 54 that calls
team.add(responsibility)
against line 62 that calls
team.getResources().add(resource)
In line 54 I must make sure that the backlink is set correctly in passed Responsibility
, thus I call team.add()
which does this. In line 63 I don't need to set a backlink, thus I can use team.getResources().add()
. This difference is an API problem that can lead to mistakes and thus should be targeted. A similar API problem is that responsibility.setTeam(newTeam)
is public, but calling it without removing the responsibility
from its old team.getResponsiblities()
collection and then adding it to the new one also is illegal.
Conclusion
In this Blog I introduced a JPA example project that we can use to try out relational structures represented through objects. Testing JPA means testing against more than one JPA-provider. Unfortunately there are only two that are nicely maintained, in a way that you don't need hours to make it run.
ɔ⃝ Fritz Ritzberger, 2019-12-25