Prev Next

Persistence with JPA


The previous Microservices example which uses jdbc provides the start-point for this 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.

  mvn -s ../settings.xml archetype:generate -DarchetypeGroupId=org.osgi.enroute.archetype -DarchetypeArtifactId=ds-component -DarchetypeVersion=7.0.0-SNAPSHOT

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 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 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 dao-impl-jpa/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/jpa/entities

Add the following file 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 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 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:

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>

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

 $ mvn -s ../settings.xml archetype:generate -DarchetypeGroupId=org.osgi.enroute.archetype -DarchetypeArtifactId=application -DarchetypeVersion=7.0.0-SNAPSHOT
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: : 
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: dao-impl-jpa
impl-groupId: org.osgi.enroute.examples.microservice
impl-version: 0.0.1-SNAPSHOT
Y: : 

Define Runtime Entity

Add the following sections to 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 because the 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.

The -runsystempackages is required because Hibernate has versioned imports for JTA, and its dependency dom4j has versioned imports for the STAX API. Both of these should come from the JRE.

Edit the -runrequires section in 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)',\
    bnd.identity;version='0.0.1.201801031655';id='org.osgi.enroute.examples.microservice.rest-app-jpa'

Dependencies

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

mvn install

Note - if rest-app fails, run the following resolve command and then re-run mvn install

mvn bnd-resolver:resolve
mvn package
java -jar rest-app/target/rest-app.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.