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

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.bndfile:

# 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.