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")
}
Eclipse link is a also popular and is the reference JPA implementation.
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>LATEST_VERSION_HERE</version>
</dependency>
dependencies {
compile("org.eclipse.persistence:eclipselink: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
}
}