Helidon MP: Data Persistence with JPA

  • Add support of Microprofile 3.0
  • Stabilizes JPA support in Helidon MP
  • etc.

Create a Helidon MP project

At the moment I wrote down this post, Helidon still does not provide an interactive interface like Spring Intializr or Quarkus Coding for developers to generate the project skeleton.

mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-mp \
-DarchetypeVersion=1.3.1 \
-DgroupId=com.example \
-DartifactId=mp-jpa \
-Dpackage=com.example \
-DrestResourceName=PostResource \
-DapplicationName=JaxrsActivator

Enabling JPA Support

There are some steps required to contribute JPA support into an existing Helidon MP project.

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.7</version>
</dependency>
version: '3.7' # specify docker-compose versionservices:
blogdb:
image: postgres
ports:
- "5432:5432"
restart: always
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: blogdb
POSTGRES_USER: user
volumes:
- ./data:/var/lib/postgresql
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-datasource-hikaricp</artifactId>
<scope>runtime</scope>
</dependency>
javax:
sql:
DataSource:
blogDataSource:
dataSourceClassName: org.postgresql.ds.PGSimpleDataSource
dataSource:
# https://github.com/brettwooldridge/HikariCP
url: jdbc:postgresql://localhost:5432/blogdb
user: user
password: password
javax:
sql:
DataSource:
blogDataSource:
...
userDataSource:
...
@Inject
@Named("blogDataSource")
DataSource blogDataSource;
@Inject
@Named("userDataSource")
DataSource userDataSource;
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-jta-weld</artifactId>
<scope>runtime</scope>
</dependency>
@Trasactional
public void placeOrder(){}
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-jpa</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-eclipselink</artifactId>
<scope>runtime</scope>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="greeting" transaction-type="JTA">
<description>A persistence unit for the greeting example.</description>
<jta-data-source>blogDataSource</jta-data-source>
<properties>
<property name="eclipselink.deploy-on-startup" value="true"/>
<property name="eclipselink.jdbc.native-sql" value="true"/>
<property name="eclipselink.logging.logger" value="JavaLogger"/>
<property name="eclipselink.logging.parameters" value="true"/>
<property name="eclipselink.target-database" value="org.eclipse.persistence.platform.database.PostgreSQLPlatform"/>
<property name="eclipselink.target-server" value="io.helidon.integrations.cdi.eclipselink.CDISEPlatform"/>
<property name="eclipselink.weaving" value="false"/>
</properties>
</persistence-unit>
</persistence>
@PersistenceContext
EntityManager entityManager;
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>2.2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<plugin>
<groupId>com.ethlo.persistence.tools</groupId>
<artifactId>eclipselink-maven-plugin</artifactId>
<version>2.7.4</version>
<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>weave</id>
<phase>process-classes</phase>
<goals>
<goal>weave</goal>
</goals>
</execution>
<execution>
<id>modelgen</id>
<phase>generate-sources</phase>
<goals>
<goal>modelgen</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- https://mvnrepository.com/artifact/org.eclipse.persistence/org.eclipse.persistence.jpa.modelgen.processor -->
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
<version>2.7.4</version>
<scope>provided</scope>
</dependency>

Migrating Codebase to JPA

Firstly have a look at Post , which is annotated with an @Entity annotation and indicates it is a JPA entity class.

@Entity
public class Post implements Serializable {
@Id
@GeneratedValue(generator = "UUID")
@UuidGenerator(name = "UUID")
String id;
String title;
String content;
LocalDateTime createdAt;
// getters and setters, equals, hashCode, toString
}
@ApplicationScoped
public class PostRepository {
@PersistenceContext
EntityManager entityManager;
public List<Post> findAll() {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
// create query
CriteriaQuery<Post> query = cb.createQuery(Post.class);
// set the root class
Root<Post> root = query.from(Post.class);
//perform query
return this.entityManager.createQuery(query).getResultList();
}
public Optional<Post> findById(String id) {
Post post = null;
try {
post = this.entityManager.find(Post.class, id);
} catch (NoResultException e) {
e.printStackTrace();
}
return Optional.ofNullable(post);
}
@Transactional
public Post save(Post post) {
if (post.getId() == null) {
this.entityManager.persist(post);
return post;
} else {
return this.entityManager.merge(post);
}
}

@Transactional
public int updateStatus(String id, Post.Status status) {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
// create update
CriteriaUpdate<Post> delete = cb.createCriteriaUpdate(Post.class);
// set the root class
Root<Post> root = delete.from(Post.class);
// set where clause
delete.set(root.get(Post_.status), status);
delete.where(cb.equal(root.get(Post_.id), id));
// perform update
return this.entityManager.createQuery(delete).executeUpdate();
}
@Transactional
public int deleteById(String id) {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
// create delete
CriteriaDelete<Post> delete = cb.createCriteriaDelete(Post.class);
// set the root class
Root<Post> root = delete.from(Post.class);
// set where clause
delete.where(cb.equal(root.get(Post_.id), id));
// perform update
return this.entityManager.createQuery(delete).executeUpdate();
}
}
@Entity
public class Comment implements Serializable {
@Id
@GeneratedValue(generator = "UUID")
@UuidGenerator(name = "UUID")
private String id;
@Embedded
@AttributeOverride(
name = "id",
column = @Column(name = "post_id")
)
private PostId post;
private String content;
private LocalDateTime createdAt;
// getters and setters, equals, hashCode, toString
}
@Embeddable
public class PostId implements Serializable {
private String id;
// getters and setters, equals, hashCode, toString
}
@ApplicationScoped
public class CommentRepository {
@PersistenceContext
EntityManager entityManager;
@Transactional
public Comment save(Comment comment) {
if (comment.getId() == null) {
this.entityManager.persist(comment);
return comment;
} else {
return this.entityManager.merge(comment);
}
}
@Transactional
public void deleteById(String id) {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
// create delete
CriteriaDelete<Comment> delete = cb.createCriteriaDelete(Comment.class);
// set the root class
Root<Comment> root = delete.from(Comment.class);
// set where clause
delete.where(cb.equal(root.get(Comment_.id), id));
// perform update
this.entityManager.createQuery(delete).executeUpdate();
}
public List<Comment> findByPostId(String id) {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
// create query
CriteriaQuery<Comment> query = cb.createQuery(Comment.class);
// set the root class
Root<Comment> root = query.from(Comment.class);
query.where(cb.equal(root.get(Comment_.post).get(PostId_.id), id));
//perform query
return this.entityManager.createQuery(query).getResultList();
}
}
<persistence ...>
<persistence-unit ...>
<class>com.example.Post</class>
<class>com.example.PostId</class>
<class>com.example.Comment</class>
//...
</persistence-unit>
</persistence>
java -jar target/mp-jpa.jar

Initializing sample data

Create an ApplicationScoped bean to listen CDI @Initialized(ApplicationScoped.class) event. Inject PostRepository bean, and insert some data at the application initialization stage.

@ApplicationScoped
public class AppInitializer {
private final static Logger LOGGER = Logger.getLogger(AppInitializer.class.getName());

@Inject
private PostRepository posts;

public void onStart(@Observes @Initialized(ApplicationScoped.class) Object init) {
LOGGER.info("The application is starting...");
Post first = Post.of("Hello Helidon", "My first post of Helidon");
Post second = Post.of("Hello Again, Helidon", "My second post of Helidon");

this.posts.save(first);
this.posts.save(second);

this.posts.findAll().forEach(p -> System.out.println("Post:" + p));
}

void onStop(@Observes @Destroyed(ApplicationScoped.class) Object init) {
LOGGER.info("The application is stopping...");
}
}
> curl http://localhost:8080/posts
[{"content":"My first post of Helidon","createdAt":"2019-10-14T08:37:40.560054","id":"7B7479F5-9773-47A2-845A-524D151A73E5","title":"Hello Helidon"},{"content":"My second post of Helidon","createdAt":"2019-10-14T08:37:40.560054","id":"2BC4C17B-303A-4799-A229-11ED20A8D67F","title":"Hello Again, Helidon"}]

Bonus

If you have some experience of Spring Data and Apache DeltaSpike, you may be heavily impressed by their Repository which drastically simplifies the Repository codes. Let's have a look at the codes of PostRepository and CommentRepository , maybe you have realized some code snippets are very similar.

public interface Repository<E, ID> {    abstract EntityManager entityManager();    private Class<E> entityClazz() {
return (Class<E>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
public default List<E> findAll() {
CriteriaBuilder cb = this.entityManager().getCriteriaBuilder();
// create query
CriteriaQuery<E> query = cb.createQuery(this.entityClazz());
// set the root class
Root<E> root = query.from(this.entityClazz());
//perform query
return this.entityManager().createQuery(query).getResultList();
}
public default E findById(ID id) {
E entity = null;
try {
entity = this.entityManager().find(this.entityClazz(), id);
} catch (NoResultException e) {
e.printStackTrace();
}
return entity;
}
public default Optional<E> findOptionalById(ID id) {
E entity = null;
try {
entity = this.entityManager().find(this.entityClazz(), id);
} catch (NoResultException e) {
e.printStackTrace();
}
return Optional.ofNullable(entity);
}
@Transactional
public default E save(E entity) {
if (this.entityManager().contains(entity)) {
return this.entityManager().merge(entity);
} else {
this.entityManager().persist(entity);
return entity;
}
}
@Transactional
public default void deleteById(ID id) {
E entity = this.findById(id);
this.entityManager().remove(entity);
}
}
@ApplicationScoped
public class OrderRepository implements Repository<Order, Long>{
@PersistenceContext
EntityManager entityManager;

@override
public EntityManager entityManager(){
return this.entityManager;
}
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Hantsy

Hantsy

Self-employed technical consultant, solution architect and full-stack developer, open source contributor, freelancer and remote worker