Prev
Next
Persistence with JPA
The previous Microservices example which uses jdbc
provides the start-point for this tutorial.
For this tutorial we put the project in the ~
(AKA /home/user
) directory. If you put your project in a different directory, be sure to replace the ~
with your directory path when it appears in shell snippets in the tutorial.
In this tutorial we’ll modify the Microservice to switch the data-layer from a JDBC to a JPA. Because of the de-coupling provided by the DTOs ’s, all we need do is re-implement dao-impl
and the composite application.
Note - because of the use of DTOs, OSGi allows us, via setting one property, to separate the data-layer and REST Services layers of our Microservice across a local IP local network using secure low latency Remote Services .
A JPA Implementation
In the microservice
project root directory, create the jpa
project.
~/microservice $ mvn archetype:generate \
-DarchetypeGroupId=org.osgi.enroute.archetype \
-DarchetypeArtifactId=ds-component \
-DarchetypeVersion=7.0.0
input the following values:
Define value for property 'groupId': org.osgi.enroute.examples.microservice
Define value for property 'artifactId': dao-impl-jpa
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples.microservice.dao.impl.jpa: :
Confirm properties configuration:
groupId: org.osgi.enroute.examples.microservice
artifactId: dao-impl-jpa
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples.microservice.dao.impl.jpa
Y: :
Add the following file ~/microservice/dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/AddressDaoImpl.java
AddressDaoImpl.java
package org.osgi.enroute.examples.microservice.dao.impl.jpa ;
import static java . util . stream . Collectors . toList ;
import java.sql.SQLException ;
import java.util.List ;
import java.util.Map ;
import javax.persistence.EntityManager ;
import javax.persistence.criteria.CriteriaBuilder ;
import javax.persistence.criteria.CriteriaDelete ;
import javax.persistence.criteria.CriteriaQuery ;
import javax.persistence.criteria.Root ;
import org.osgi.enroute.examples.microservice.dao.AddressDao ;
import org.osgi.enroute.examples.microservice.dao.dto.AddressDTO ;
import org.osgi.enroute.examples.microservice.dao.impl.jpa.entities.AddressEntity ;
import org.osgi.enroute.examples.microservice.dao.impl.jpa.entities.PersonEntity ;
import org.osgi.service.component.annotations.Activate ;
import org.osgi.service.component.annotations.Component ;
import org.osgi.service.component.annotations.Reference ;
import org.osgi.service.transaction.control.TransactionControl ;
import org.osgi.service.transaction.control.jpa.JPAEntityManagerProvider ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
@Component
public class AddressDaoImpl implements AddressDao {
private static final Logger logger = LoggerFactory . getLogger ( AddressDaoImpl . class );
@Reference
TransactionControl transactionControl ;
@Reference ( name = "provider" )
JPAEntityManagerProvider jpaEntityManagerProvider ;
EntityManager em ;
@Activate
void activate ( Map < String , Object > props ) throws SQLException {
em = jpaEntityManagerProvider . getResource ( transactionControl );
}
@Override
public List < AddressDTO > select ( Long personId ) {
return transactionControl . notSupported (() -> {
CriteriaBuilder builder = em . getCriteriaBuilder ();
CriteriaQuery < AddressEntity > query = builder . createQuery ( AddressEntity . class );
Root < AddressEntity > from = query . from ( AddressEntity . class );
query . where ( builder . equal ( from . get ( "person" ). get ( "personId" ), personId ));
return em . createQuery ( query ). getResultList (). stream ()
. map ( AddressEntity: : toDTO )
. collect ( toList ());
});
}
@Override
public AddressDTO findByPK ( String pk ) {
return transactionControl . supports (() -> {
AddressEntity address = em . find ( AddressEntity . class , pk );
return address == null ? null : address . toDTO ();
});
}
@Override
public void save ( Long personId , AddressDTO data ) {
transactionControl . required (() -> {
PersonEntity person = em . find ( PersonEntity . class , personId );
if ( person == null ) {
throw new IllegalArgumentException ( "There is no person with id " + personId );
}
em . persist ( AddressEntity . fromDTO ( person , data ));
return null ;
});
}
@Override
public void update ( Long personId , AddressDTO data ) {
transactionControl . required (() -> {
AddressEntity address = em . find ( AddressEntity . class , data . emailAddress );
if ( address == null ) {
throw new IllegalArgumentException ( "There is no address with email " + data . emailAddress );
}
address . setCity ( data . city );
address . setCountry ( data . country );
logger . info ( "Updated Person Address : {}" , data );
return null ;
});
}
@Override
public void delete ( Long personId ) {
transactionControl . required (() -> {
CriteriaBuilder builder = em . getCriteriaBuilder ();
CriteriaDelete < AddressEntity > query = builder . createCriteriaDelete ( AddressEntity . class );
Root < AddressEntity > from = query . from ( AddressEntity . class );
query . where ( builder . equal ( from . get ( "person" ). get ( "personId" ), personId ));
em . createQuery ( query ). executeUpdate ();
return null ;
});
}
}
Add the following file ~/microservice/dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/PersonDaoImpl.java
PersonDaoImpl.java
package org.osgi.enroute.examples.microservice.dao.impl.jpa ;
import static java . util . stream . Collectors . toList ;
import java.sql.SQLException ;
import java.util.List ;
import java.util.Map ;
import javax.persistence.EntityManager ;
import javax.persistence.criteria.CriteriaBuilder ;
import javax.persistence.criteria.CriteriaDelete ;
import javax.persistence.criteria.CriteriaQuery ;
import javax.persistence.criteria.Root ;
import org.osgi.enroute.examples.microservice.dao.PersonDao ;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO ;
import org.osgi.enroute.examples.microservice.dao.impl.jpa.entities.PersonEntity ;
import org.osgi.service.component.annotations.Activate ;
import org.osgi.service.component.annotations.Component ;
import org.osgi.service.component.annotations.Reference ;
import org.osgi.service.transaction.control.TransactionControl ;
import org.osgi.service.transaction.control.jpa.JPAEntityManagerProvider ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
@Component
public class PersonDaoImpl implements PersonDao {
private static final Logger logger = LoggerFactory . getLogger ( PersonDaoImpl . class );
@Reference
TransactionControl transactionControl ;
@Reference ( name = "provider" )
JPAEntityManagerProvider jpaEntityManagerProvider ;
EntityManager em ;
@Activate
void activate ( Map < String , Object > props ) throws SQLException {
em = jpaEntityManagerProvider . getResource ( transactionControl );
}
@Override
public List < PersonDTO > select () {
return transactionControl . notSupported (() -> {
CriteriaBuilder builder = em . getCriteriaBuilder ();
CriteriaQuery < PersonEntity > query = builder . createQuery ( PersonEntity . class );
query . from ( PersonEntity . class );
return em . createQuery ( query ). getResultList (). stream ()
. map ( PersonEntity: : toDTO )
. collect ( toList ());
});
}
@Override
public void delete ( Long primaryKey ) {
transactionControl . required (() -> {
CriteriaBuilder builder = em . getCriteriaBuilder ();
CriteriaDelete < PersonEntity > query = builder . createCriteriaDelete ( PersonEntity . class );
Root < PersonEntity > from = query . from ( PersonEntity . class );
query . where ( builder . equal ( from . get ( "personId" ), primaryKey ));
em . createQuery ( query ). executeUpdate ();
logger . info ( "Deleted Person with ID : {}" , primaryKey );
return null ;
});
}
@Override
public PersonDTO findByPK ( Long pk ) {
return transactionControl . supports (() -> {
PersonEntity person = em . find ( PersonEntity . class , pk );
return person == null ? null : person . toDTO ();
});
}
@Override
public Long save ( PersonDTO data ) {
return transactionControl . required (() -> {
PersonEntity entity = PersonEntity . fromDTO ( data );
if ( entity . getPersonId () == null ) {
em . persist ( entity );
} else {
em . merge ( entity );
}
logger . info ( "Saved Person with ID : {}" , entity . getPersonId ());
return entity . getPersonId ();
});
}
@Override
public void update ( PersonDTO data ) {
transactionControl . required (() -> {
PersonEntity entity = PersonEntity . fromDTO ( data );
if ( entity . getPersonId () <= 0 ) {
throw new IllegalStateException ( "No primary key defined for the Entity" );
} else {
em . merge ( entity );
}
logger . info ( "Updated person : {}" , data );
return null ;
});
}
}
To address a hibernate bug we need to add the following dao-impl-jpa/bnd.bnd
file:
bnd.bnd
# Due to a long standing bug in Hibernate's entity enhancement these packages must
# be imported when Hibernate is used (https://hibernate.atlassian.net/browse/HHH-10742)
Import-Package: \
org.hibernate.proxy,\
javassist.util.proxy,\
*
Note - it is rare to declare an Import-Package
when using bnd . As in this case, this is only usually needed to work around a bug.
The JPA Entities
Create the directory ~/microservice/dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/entities
Add the following file ~/microservice/dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/entities/AddressEntity.java
AddressEntity.java
package org.osgi.enroute.examples.microservice.dao.impl.jpa.entities ;
import javax.persistence.Column ;
import javax.persistence.Entity ;
import javax.persistence.ForeignKey ;
import javax.persistence.Id ;
import javax.persistence.JoinColumn ;
import javax.persistence.ManyToOne ;
import javax.persistence.Table ;
import org.osgi.enroute.examples.microservice.dao.dto.AddressDTO ;
@Entity
@Table ( name = "addresses" )
public class AddressEntity {
@ManyToOne
@JoinColumn ( name = "person_id" , foreignKey = @ForeignKey ( name = "person" ))
private PersonEntity person ;
@Id
@Column ( name = "email_address" )
private String emailAddress ;
private String city ;
private String country ;
public static AddressEntity fromDTO ( PersonEntity person , AddressDTO dto ) {
AddressEntity entity = new AddressEntity ();
entity . person = person ;
entity . emailAddress = dto . emailAddress ;
entity . city = dto . city ;
entity . country = dto . country ;
return entity ;
}
public AddressDTO toDTO () {
AddressDTO dto = new AddressDTO ();
dto . personId = person . getPersonId ();
dto . emailAddress = emailAddress ;
dto . city = city ;
dto . country = country ;
return dto ;
}
public void setCity ( String city ) {
this . city = city ;
}
public void setCountry ( String country ) {
this . country = country ;
}
}
Add the following file ~/microservice/dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/entities/PersonEntity.java
:
PersonEntity.java
package org.osgi.enroute.examples.microservice.dao.impl.jpa.entities ;
import static java . util . stream . Collectors . toList ;
import static javax . persistence . CascadeType . ALL ;
import static javax . persistence . GenerationType . IDENTITY ;
import java.util.ArrayList ;
import java.util.List ;
import javax.persistence.Column ;
import javax.persistence.Entity ;
import javax.persistence.GeneratedValue ;
import javax.persistence.Id ;
import javax.persistence.OneToMany ;
import javax.persistence.Table ;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO ;
@Entity
@Table ( name = "persons" )
public class PersonEntity {
@GeneratedValue ( strategy = IDENTITY )
@Id
@Column ( name = "person_id" )
private Long personId ;
@Column ( name = "first_name" )
private String firstName ;
@Column ( name = "last_name" )
private String lastName ;
@OneToMany ( mappedBy = "person" , cascade = ALL )
private List < AddressEntity > addresses = new ArrayList <>();
public Long getPersonId () {
return personId ;
}
public PersonDTO toDTO () {
PersonDTO dto = new PersonDTO ();
dto . personId = personId ;
dto . firstName = firstName ;
dto . lastName = lastName ;
dto . addresses = addresses . stream ()
. map ( AddressEntity: : toDTO )
. collect ( toList ());
return dto ;
}
public static PersonEntity fromDTO ( PersonDTO dto ) {
PersonEntity entity = new PersonEntity ();
if ( dto . personId != 0 ) {
entity . personId = Long . valueOf ( dto . personId );
}
entity . firstName = dto . firstName ;
entity . lastName = dto . lastName ;
entity . addresses = dto . addresses . stream ()
. map ( a -> AddressEntity . fromDTO ( entity , a ))
. collect ( toList ());
return entity ;
}
}
The resultant persistence bundle has a Requirement for a JPA service extender. Hence we add the following file ~/microservice/dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/entities/package-info.java
:
package-info.java
@org . osgi . annotation . bundle . Requirement ( namespace = "osgi.extender" , name = "osgi.jpa" , version = "1.1.0" )
@org . osgi . annotation . bundle . Header ( name = "Meta-Persistence" , value = "META-INF/persistence.xml" )
package org.osgi.enroute.examples.microservice.dao.impl.jpa.entities ;
JPA Resources
Create the following JPA resources:
~/microservice/dao-impl-jpa/src/main/resources/META-INF/persistence.xml
persistence.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns= "http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" version= "2.1" xsi:schemaLocation= "http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" >
<persistence-unit name= "microservice-dao" >
<description> Microservice Example Persistence Unit</description>
<properties>
<property name= "javax.persistence.schema-generation.database.action" value= "create" />
<property name= "javax.persistence.schema-generation.create-script-source" value= "META-INF/tables.sql" />
</properties>
</persistence-unit>
</persistence>
~/microservice/dao-impl-jpa/src/main/resources/META-INF/tables.sql
tables.sql
CREATE TABLE IF NOT EXISTS persons ( person_id bigint generated by default as identity , first_name varchar ( 255 ), last_name varchar ( 255 ), primary key ( person_id ))
CREATE TABLE IF NOT EXISTS addresses ( email_address varchar ( 255 ) not null , city varchar ( 255 ), country varchar ( 255 ), person_id bigint , primary key ( email_address ))
ALTER TABLE addresses ADD CONSTRAINT IF NOT EXISTS person FOREIGN KEY ( person_id ) REFERENCES persons
Dependencies
Edit ~/microservice/dao-impl-jpa/pom.xml
to add the following dependencies in the <dependencies>
section:
<dependency>
<groupId> org.osgi.enroute</groupId>
<artifactId> enterprise-api</artifactId>
<type> pom</type>
</dependency>
<dependency>
<groupId> org.osgi.enroute.examples.microservice</groupId>
<artifactId> dao-api</artifactId>
<version> 0.0.1-SNAPSHOT</version>
</dependency>
The JPA Composite Application
Create the alternative JPA application project.
~/microservice $ mvn archetype:generate \
-DarchetypeGroupId=org.osgi.enroute.archetype \
-DarchetypeArtifactId=application \
-DarchetypeVersion=7.0.0
Define value for property 'groupId': org.osgi.enroute.examples.microservice
Define value for property 'artifactId': rest-app-jpa
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples.microservice: :
Define value for property 'impl-artifactId': rest-service
Define value for property 'impl-groupId' org.osgi.enroute.examples.microservice: :
Define value for property 'impl-version' 0.0.1-SNAPSHOT: :
Define value for property 'app-target-java-version' 8: :
Confirm properties configuration:
groupId: org.osgi.enroute.examples.microservice
artifactId: rest-app-jpa
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples.microservice
impl-artifactId: rest-service
impl-groupId: org.osgi.enroute.examples.microservice
impl-version: 0.0.1-SNAPSHOT
app-target-java-version: 8
Y: :
Define Runtime Entity
Add the following sections to ~/microservice/rest-app-jpa/rest-app-jpa.bndrun
:
-resolve.effective: active
-runpath: org.jboss.spec.javax.transaction.jboss-transaction-api_1.2_spec; version = 1.0.1.Final
-runsystempackages: \
javax.transaction; version = 1.2.0,\
javax.transaction.xa; version = 1.2.0,\
javax.xml.stream; version = 1.0.0,\
javax.xml.stream.events; version = 1.0.0,\
javax.xml.stream.util; version = 1.0.0
The -runpath
needs to be specified for Java 8 because the Java 8 JRE has a split package for javax.transaction and a uses constraint between javax.sql and javax.transaction. This breaks JPA unless the JTA API is always provided from outside of the OSGi framework. When using Java 9 and above the javax.transaction package is no longer provided by the JRE,
The -runsystempackages
is required because Hibernate has versioned imports for JTA, and its dependency dom4j has versioned imports for the STAX API. When using Java 8 both of these should come from the JRE. When using Java 9 and above the APIs can be provided by bundles in the OSGi framework.
Edit the -runrequires
section in ~/microservice/rest-app-jpa/res-app-jpa.bndrun
to include the composite application’s requirements:
-runrequires: \
osgi.identity; filter:= '(osgi.identity=org.osgi.enroute.examples.microservice.rest-service)' ,\
osgi.identity; filter:= '(osgi.identity=org.apache.johnzon.core)' ,\
osgi.identity; filter:= '(osgi.identity=org.h2)' ,\
osgi.identity; filter:= '(osgi.identity=org.osgi.enroute.examples.microservice.rest-app-jpa)'
Dependencies
Edit ~/microservice/rest-app-jpa/pom.xml
adding the following dependencies in the <dependencies>
section:
<dependency>
<groupId> org.osgi.enroute.examples.microservice</groupId>
<artifactId> dao-impl-jpa</artifactId>
<version> 0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId> org.apache.johnzon</groupId>
<artifactId> johnzon-core</artifactId>
<version> 1.1.0</version>
</dependency>
<dependency>
<groupId> com.h2database</groupId>
<artifactId> h2</artifactId>
<version> 1.4.196</version>
<scope> runtime</scope>
</dependency>
<dependency>
<groupId> org.hibernate</groupId>
<artifactId> hibernate-osgi</artifactId>
<version> 5.2.12.Final</version>
</dependency>
<dependency>
<groupId> org.apache.servicemix.bundles</groupId>
<artifactId> org.apache.servicemix.bundles.antlr</artifactId>
<version> 2.7.7_5</version>
</dependency>
<dependency>
<groupId> org.apache.servicemix.bundles</groupId>
<artifactId> org.apache.servicemix.bundles.dom4j</artifactId>
<version> 1.6.1_5</version>
</dependency>
Runtime Configuration
Add the following configuration file ~/microservice/rest-app-jpa/src/main/resources/OSGI-INF/configurator/configuration.json
:
configuration.json
{
// Global Settings
":configurator:resource-version" : 1 ,
":configurator:symbolic-name" : "org.osgi.enroute.examples.microservice.jpa.config" ,
":configurator:version" : "0.0.1.SNAPSHOT" ,
// Configure a JPA resource provider
"org.apache.aries.tx.control.jpa.xa~microservice" : {
"name" : "microservice.database" ,
"osgi.jdbc.driver.class" : "org.h2.Driver" ,
"url" : "jdbc:h2:./data/database" ,
"osgi.unit.name" : "microservice-dao" },
// Target the Dao impls at the provider we configured
"org.osgi.enroute.examples.microservice.dao.impl.jpa.PersonDaoImpl" : {
"provider.target" : "(name=microservice.database)" },
"org.osgi.enroute.examples.microservice.dao.impl.jpa.AddressDaoImpl" : {
"provider.target" : "(name=microservice.database)" }
}
Build & Run
We build and run the examples as in the previous JDBC Microservices example.
~/microservice $ mvn install
Note - if rest-app-jpa
fails, run the following resolve command and then re-run mvn install
~/microservice $ mvn bnd-resolver:resolve
Once the mvn install
command succeeds, run the following command to build the jar.
~/microservice $ mvn package
Run the newly created jar.
~/microservice $ java -jar rest-app-jpa/target/rest-app-jpa.jar
The REST Service can be seen by pointing a browser to http://localhost:8080/microservice/index.html
Stop the application using Ctrl+C in the console.