JPA, Step-by-Step

This guide details step-by-step how to configure JPA in SeedStack, to access an existing relational database.

Step 1: The dependencies

JPA add-on

First, add the SeedStack JPA add-on:

<dependency>
    <groupId>org.seedstack.addons.jpa</groupId>
    <artifactId>jpa</artifactId>
</dependency>
Show version
dependencies {
    compile("org.seedstack.addons.jpa:jpa:4.2.0")
}

JPA provider

Beyond the JPA add-on, an actual JPA implementation, called a JPA provider, is required:

Hibernate is a very popular JPA implementation. When using Hibernate, SeedStack is able to stream results from the database without putting them all in memory (useful when retrieving for result sets).

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("org.hibernate:hibernate-entitymanager:LATEST_VERSION_HERE")
}

Connection pooling

If you want connection pooling, you have to add the dependency for one of the following pooling libraries:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("com.zaxxer:HikariCP:LATEST_VERSION_HERE")
}

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("com.mchange:c3p0:LATEST_VERSION_HERE")
}

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("org.apache.commons:commons-dbcp2:LATEST_VERSION_HERE")
}

JDBC driver

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("com.h2database:h2:LATEST_VERSION_HERE")
}

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("org.hsqldb:hsqldb:LATEST_VERSION_HERE")
}

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("org.postgresql:postgresql:LATEST_VERSION_HERE")
}

<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("org.mariadb.jdbc:mariadb-java-client:LATEST_VERSION_HERE")
}

<dependency>
    <groupId>com.oracle.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
    compile("com.oracle.jdbc:ojdbc8:LATEST_VERSION_HERE")
}

The JDBC driver can be obtained from the directory of your DB2 installation.

Step 2: Datasource configuration

JPA relies on a JDBC datasource to access the database. We are now going to configure such datasource through the SeedStack JDBC add-on (pulled transitively by the JPA add-on):

jdbc:
  datasources:
    myDatasource:
      provider: org.seedstack.jdbc.internal.datasource.HikariDataSourceProvider
      url: jdbc:h2:mem:mydb
jdbc:
  datasources:
    myDatasource:
      provider: org.seedstack.jdbc.internal.datasource.HikariDataSourceProvider
      url: jdbc:hsqldb:mem:mydb
jdbc:
  datasources:
    myDatasource:
      provider: org.seedstack.jdbc.internal.datasource.HikariDataSourceProvider
      url: jdbc:postgresql://localhost:5740/mydb
jdbc:
  datasources:
    myDatasource:
      provider: org.seedstack.jdbc.internal.datasource.HikariDataSourceProvider
      url: jdbc:mariadb://localhost:3306/mydb
jdbc:
  datasources:
    myDatasource:
      provider: org.seedstack.jdbc.internal.datasource.HikariDataSourceProvider
      url: jdbc:oracle:thin:@localhost:1521:mydb
jdbc:
  datasources:
    myDatasource:
      provider: org.seedstack.jdbc.internal.datasource.HikariDataSourceProvider
      url: jdbc:db2://localhost:5021/mydb

Step 3: Unit configuration

JPA organizes persisted classes in buckets called «JPA units». All classes persisted within a JPA unit share a common datasource and configuration. A local transaction is bound to a JPA unit (it cannot span over multiple units). We are now going to configure a JPA unit that uses the datasource we specified before:

jpa:
  units:
    myUnit:
      datasource: myDatasource
      properties:
        hibernate.dialect: org.hibernate.dialect.H2Dialect
        hibernate.hbm2ddl.auto: update
jpa:
  units:
    myUnit:
      datasource: myDatasource
      properties:
        hibernate.dialect: org.hibernate.dialect.HSQLDialect
        hibernate.hbm2ddl.auto: update
jpa:
  units:
    myUnit:
      datasource: myDatasource
      properties:
        hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
        hibernate.hbm2ddl.auto: update
jpa:
  units:
    myUnit:
      datasource: myDatasource
      properties:
        hibernate.dialect: org.hibernate.dialect.MariaDB53Dialect
        hibernate.hbm2ddl.auto: update
jpa:
  units:
    myUnit:
      datasource: myDatasource
      properties:
        hibernate.dialect: org.hibernate.dialect.Oracle12cDialect
        hibernate.hbm2ddl.auto: update
jpa:
  units:
    myUnit:
      datasource: datasource1
      properties:
        hibernate.dialect: org.hibernate.dialect.DB2Dialect
        hibernate.hbm2ddl.auto: update

Step 4: Classes configuration

To automatically configure the JPA unit, SeedStack automatically detected persisted class (i.e. JPA entities). However it needs additional information to associate a particular class to a particular unit. This is done with class configuration:

classes:
  org:
    generated:
      project:
        domain:
          model:
            jpaUnit: myUnit

Every JPA entity inside the package org.generated.project.domain.model and its subpackages will be associated with the JPA unit myUnit.

This «tag» can be specified at any package level and an upper-level tag can be overridden by a lower-level tag.

Step 5: Entity definition

Although it is possible to write a plain vanilla JPA entity, we are going to leverage a «Domain-Driven Design» (DDD) approach by using the SeedStack business framework. To do so, we are going for a very simple DDD-oriented model with a single customer aggregate. It models a Customer with its identity being the CustomerId value-object.

The CustomerId class:

package org.generated.project.domain.model.customer;

import javax.persistence.Embeddable;
import org.seedstack.business.domain.BaseValueObject;

@Embeddable
public class CustomerId extends BaseValueObject {

    private String value;

    private CustomerId() {
        // A default constructor is needed by JPA but can be kept private
    }

    public CustomerId(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

The Customer class:

package org.generated.project.domain.model.customer;

import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import org.seedstack.business.domain.BaseAggregateRoot;

@Entity
public class Customer extends BaseAggregateRoot<CustomerId> {
    @EmbeddedId
    private CustomerId id;
    private String firstName;
    private String lastName;
    private String address;
    private String deliveryAddress;
    private String password;

    private Customer() {
        // A default constructor is needed by JPA but can be kept private
    }

    public Customer(CustomerId customerId) {
        this.id = customerId;
    }

    @Override
    public CustomerId getId() {
        return id;
    }
}

Note how this model is first and foremost a DDD-oriented model but with JPA annotations mixed in. The JPA annotations simply define the model-to-database mapping of the business concepts.

Step 6: Repository usage

We have defined a DDD aggregate, so we are going to use a repository to store and retrieve it. A repository is a business-oriented encapsulation of the model persistence.

The business framework provides a Repository interface, defining basic repository features. To obtain a default JPA implementation you must qualify the injection point with the @Jpa annotation.

Repositories are often used in services or directly in interface-related classes like in REST resources.

Let’s define a service interface:

@Service
public interface SomeService {
    void doSomething(CustomerId customerId);
}

And an implementation using a JPA-capable implementation of the Repository interface:

public class SomeServiceImpl implements SomeService {
    @Inject
    @Jpa
    private Repository<Customer, CustomerId> customerRepository;
    
    @Transactional
    @JpaUnit("myUnit")
    public void doSomething(CustomerId customerId) {
        Customer customer = customerRepository.get(customerId)
                            .orElseThrow(() -> new CustomerNotFoundException(customerId));
        
        // ... do the work
    }
}

JPA database access must be done in a transaction. Note how the @Transactional and @JpaUnit annotations are specified together on the method to mark a JPA transaction happening on unit myUnit.

Bonus: Custom repository

The base operations provided by the Repository interface are useful but often not enough, especially if you want to define business-oriented persistence operations for a particular aggregate.

To define a custom repository, an interface is needed:

public interface CustomerRepository extends Repository<Customer, CustomerId> { 
    Customer findVipCustomers();
}

This custom interface must then be implemented using JPA. The easiest way is to create an implementation class extending BaseJpaRepository. That way, you’ll only have to implement your custom methods:

public class CustomerJpaRepository extends BaseJpaRepository<Customer, CustomerId> 
                                   implements CustomerRepository {
    @Override
    public List<Customer> findVipCustomers() {
        EntityManager entityManager = getEntityManager();
        // do work with entityManager
    }
}

To inject your custom implementation, use the custom interface at your injection point:

public class SomeServiceImpl implements SomeService {
    @Inject
    private CustomerRepository customerRepository;
    
    @Transactional
    @JpaUnit("myUnit")
    public void doSomething(CustomerId customerId) {
        List<Customer> vipCustomers = customerRepository.findVipCustomers();
        // ... do the work
    }
}

On this page


Edit