Blog-Archiv

Mittwoch, 25. Dezember 2019

Setting up a JPA Test Project

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

  • pom.xml

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:

  1. persistence configuration to point to your database, and
  2. @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.




Keine Kommentare: