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
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
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:
# 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
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
:
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
:
@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
<?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
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
:
{
// 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.
Prev Next