Introduction
This tutorial covers the basics about the Holon Platform Datastore API, showing how to setup a Datastore
implementation and how to manage a persistent data entity, defined through a Property
model, using the Datastore
API operations.
The Holon Platform Spring Boot integration is used for Datastore
auto-configuration.
What You’ll learn
-
How to use the
Datastore
API to manage a persistent data entity -
How to auto-configure a
Datastore
implementation using the Holon platform Spring Boot starters
What You’ll need
-
About 30 minutes
-
A favorite text editor or IDE
-
JDK 1.8 or later
Reference documentation and source code
-
See the Property model reference manual for detailed documentation about the
Property
model -
See the Datastore API reference manual for detailed documentation about the
Datastore
API
The source code for this tutorial is available on GitHub in the Holon Platform examples repository:
-
Example for the JDBC Datastore implementation
-
Example for the JPA Datastore implementation
Project setup
We’ll use Maven to create and setup the example project of this tutorial. So, create a standard Maven project and use the following decalrations in your pom.xml
to obtain the required Holon Platform artifacts:
2. Use a holon.platform.version
property to declare the Holon Platform version number in the properties
section, for example:
<properties>
<holon.platform.version>5.2.6</holon.platform.version>
</properties>
See Getting started to know about the last Holon Platform release version. |
2. Import the Holon Platform BOM in the dependencyManagement
section, using the last platform version:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.holon-platform</groupId>
<artifactId>bom</artifactId>
<version>${holon.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. Declare the holon-starter
artifact dependency, which represents the base Holon Spring Boot starter:
<dependencies>
<dependency>
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-starter</artifactId>
</dependency>
</dependencies>
Step #1: The property model
Let’s suppose we have to manage a data entity which represents a simple product, with the following attributes:
-
The product ID (represented by a Java
Long
type) -
The product Stock Keeping Unit (SKU) (represented by a Java
String
type) -
The product description (represented by a Java
String
type) -
The product category (represented by a Java
String
type) -
The product unit price (represented by a Java
Double
type) -
The product status (whether it was withdrawn from market or not) (represented by a Java
Boolean
type)
We want the WITHDRAWN
property to be of Boolean
type, but we suppose the persistent source does not support the boolean type, forcing us to use a different model type, for example an integer
type. For this reason, we configure a property value converter for the WITHDRAWN
property, to automatically convert integer model values into booleans and back.
Furthermore, we define a DataTarget
named products
to be used with Datastore
operations, which represents the persistent entity target, i.e. the reference to the persistence object.
We’ll model the product entity using the Holon Platform Property model, defining an interface named Product
to collect the properties (create this class in the standard Maven src/main/java
folder, using a package name of your choice):
public interface Product {
public static final NumericProperty<Long> ID = NumericProperty.longType("id");
public static final StringProperty SKU = StringProperty.create("sku");
public static final StringProperty DESCRIPTION = StringProperty.create("description");
public static final StringProperty CATEGORY = StringProperty.create("category");
public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price")
// not negative value validator
.withValidator(Validator.notNegative());
public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn")
// set a property value converter from Integer model type to Boolean
.converter(PropertyValueConverter.numericBoolean(Integer.class)); (1)
// Product property set
public static final PropertySet<?> PRODUCT = PropertySet
.builderOf(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE, WITHDRAWN) (2)
.identifier(ID) (3)
.build();
// "products" DataTarget
public static final DataTarget<?> TARGET = DataTarget.named("products"); (4)
}
1 | A numeric boolean converter is configured for the WITHDRAWN property, to convert an Integer model value type to a Boolean property value type |
2 | A PropertySet is defined to collect the product properties and to represent the product entity |
3 | The ID property is declared as identifier property |
4 | The DataTarget to use to refer to the persistent entity |
Step #2: Create the Spring Boot application
Next we create a basic Spring Boot entry point application class, to leverage on the Spring Boot auto configuration features:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Step #3: Test the Datastore
API
We use a JUnit test class to show how main Datastore
API operations work, using the product data entity.
To import the required dependecies, you can use the holon-starter-test
artifact, which includes the Spring Boot test dependencies:
<dependency>
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-starter-test</artifactId>
<scope>test</scope>
</dependency>
Now you want to create TestDatastore
class (create this class in the standard Maven src/test/java
test folder, using a package name of your choice), using the @SpringBootTest
annotation to enable Spring Boot based tests and injecting the Datastore
instance to use leveraging on Spring’s depencencies injection:
@SpringBootTest
public class TestDatastore {
@Autowired
private Datastore datastore;
For sake of semplicity, import statically the Product
data model fields, to refer to them more concisely, using the import static
Java statement like this: import static my.package.Product.*
Save operation and generated ids
The Datastore
API supports auto-generated ids, if provided by the underlying persistence technology. We suppose the product ID
is auto-generated by the persistence engine. The generated value can be retrieved in two ways:
-
By using the
OperationResult
interface returned by theDatastore
API data manipulation operations, which provides any auto-generated key value through thegetInsertedKey
method. -
Or by using the
BRING_BACK_GENERATED_IDS
default write option, which brings back any auto-generated key value value into thePropertyBox
which was subject of a data manipulation operation, setting the corresponding property value (theID
property in this example) if available.
You want to create a testSave()
test method to verify product entity data creation and id auto-generation:
public void testSave() {
// Create a new product
PropertyBox product = PropertyBox.builder(PRODUCT).set(SKU, "prod1-sku").set(DESCRIPTION, "The first product")
.set(CATEGORY, "C1").set(UNIT_PRICE, 10.90).build();
// store the product
OperationResult result = datastore.save(TARGET, product); (1)
// check to operation succeded
assertEquals(1, result.getAffectedCount());
// get the created product ID (the ID column is configured as auto-increment)
Long productId = result.getInsertedKey(ID).orElse(null); (2)
assertEquals(Long.valueOf(1), productId);
// Create another product
PropertyBox product2 = product.cloneBox();
product2.setValue(SKU, "prod2-sku");
product2.setValue(UNIT_PRICE, 12.90);
product2.setValue(DESCRIPTION, "The second product");
// store the product using the BRING_BACK_GENERATED_IDS option
datastore.save(TARGET, product2, DefaultWriteOption.BRING_BACK_GENERATED_IDS); (3)
// the generated id now is stored in the PropertyBox by the Datastore
Long productId2 = product2.getValue(ID); (4)
assertEquals(Long.valueOf(2), productId2);
}
1 | save (insert data if not exists, update it otherwise) the product data using PropertyBox as property values container |
2 | The OperationResult object is used to obtain the auto-generated product ID |
3 | In this case, the BRING_BACK_GENERATED_IDS write option is specified when saving the product data |
4 | So we expect to obtain the auto-generated product ID in the saved PropertyBox container |
Query execution
Now we’ll show how to query the products data, using the basic Datastore Query API operations.
The product data model will be used to define query restrictions (filters), sorting, aggregations and to project and obtain the query results:
public void testQuery() {
// get all products (as a Stream) and print the description
datastore.query().target(TARGET).stream(PRODUCT).map(p -> p.getValue(DESCRIPTION)).forEach(description -> {
System.out.println(description);
});
// get a product by id
PropertyBox product = datastore.query().target(TARGET).filter(ID.eq(1L)).findOne(PRODUCT).orElse(null);
assertNotNull(product);
// get all product ids, ordered by SKU
List<Long> ids = datastore.query().target(TARGET).sort(SKU.asc()).stream(ID).collect(Collectors.toList());
assertEquals(2, ids.size());
// get only the ID and DESCRITION product property values
Stream<PropertyBox> productIdDescription = datastore.query().target(TARGET).stream(ID, DESCRIPTION);
assertEquals(2, productIdDescription.count());
// get the products with a price greater then 11
List<PropertyBox> products = datastore.query().target(TARGET).filter(UNIT_PRICE.gt(11.00)).list(PRODUCT);
assertEquals(1, products.size());
assertEquals(Long.valueOf(2), products.get(0).getValue(ID));
// get the products with a price greater then 11 and description starting with "The"
products = datastore.query().target(TARGET).filter(UNIT_PRICE.gt(11.00).and(DESCRIPTION.startsWith("The")))
.list(PRODUCT);
assertEquals(1, products.size());
// get the max price grouping by category
Double maxPrice = datastore.query().target(TARGET).aggregate(CATEGORY).stream(UNIT_PRICE.max()).findFirst()
.orElse(null);
assertEquals(Double.valueOf(12.90), maxPrice);
}
Other data manipulation operations
Next you want to create a testOperations()
test method to use other Datastore
API data manipulation operations: update, insert, delete:
public void testOperations() {
// update the WITHDRAWN status for product 1
datastore.query().target(TARGET).filter(ID.eq(1L)).findOne(PRODUCT).ifPresent(product -> {
// update the product
product.setValue(WITHDRAWN, true);
datastore.update(TARGET, product);
});
assertTrue(datastore.query().target(TARGET).filter(ID.eq(1L)).findOne(WITHDRAWN).orElse(false));
// insert a new product
PropertyBox third = PropertyBox.builder(PRODUCT).set(SKU, "prod3-sku").set(DESCRIPTION, "The third product")
.build();
datastore.insert(TARGET, third, DefaultWriteOption.BRING_BACK_GENERATED_IDS);
// remove the product
OperationResult result = datastore.delete(TARGET, third);
assertEquals(1, result.getAffectedCount());
}
Bulk operations
The Datastore
API supports bulk data manipulation operations too:
public void testBulk() {
// bulk update: update all the products with category C1 to category C2
OperationResult result = datastore.bulkUpdate(TARGET).set(CATEGORY, "C2").filter(CATEGORY.eq("C1")).execute();
assertEquals(2, result.getAffectedCount());
// bulk delete: delete all the products with category C2
result = datastore.bulkDelete(TARGET).filter(CATEGORY.eq("C2")).execute();
assertEquals(2, result.getAffectedCount());
}
Step #4: Selecting and configuring the Datastore
implementation
In order for the application to work, we need to select and confgure a concrete Datastore
implementation. We’ll rely on the Holon Platform Spring Boot support (through the provided starter artifacts) to auto-configure the Datastore
implementation to use.
We’ll use a relational database to store the products data and H2 as in-memory RDMBS. So you want to add the H2 artifact dependency to the project pom
:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
JDBC Datastore
To use the JDBC Datastore
implementation we’ll need the following steps:
1. Declare the JDBC Datastore Spring Boot starter in the project pom
dependecies. We’ll use HikariCP as DataSource
:
<dependency>
<groupId>com.holon-platform.jdbc</groupId>
<artifactId>holon-starter-jdbc-datastore-hikaricp</artifactId>
</dependency>
2. Create a schema.sql
script (in the default Maven src/test/resources
test resources folder) to create the products
database table, using column names that match the PathProperty
path names used in the Product
property model and declaring the id
column as auto_increment
:
create table products (
id bigint primary key auto_increment,
sku varchar(100) not null,
description varchar(500),
category varchar(10),
price double,
withdrawn integer default 0
)
The schema.sql
script will be automatically executed by Spring Boot at DataSource
setup time.
3. Configure a DataSource
, using Spring Boot auto-configuration, by creating a application.yml
file (in the default Maven src/test/resources
test resources folder) like this:
spring:
datasource:
url: "jdbc:h2:mem:test"
username: "sa"
Now you’re ready to run the TestDatastore
JUnit test using mvn test
or your favorite IDE command.
JPA Datastore
To replace the JDBC Datastore
implementation with the JPA one we need to create the JPA entity which maps the products database table.
For sake of semplicity, we’ll name the JPA entity products
using the @Entity
JPA annotation name
attribute. This way we can use the previously defined product DataTarget
(named products
) without change anything.
So you want to create a ProductEntity
class to map the products
table, using the product model PathProperty
path names as bean field names:
@Entity(name = "products") (1)
@Table(name = "products")
public class ProductEntity {
@Id
@GeneratedValue
private Long id;
private String sku;
private String description;
private String category;
private Double price;
private Integer withdrawn;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getWithdrawn() {
return withdrawn;
}
public void setWithdrawn(Integer withdrawn) {
this.withdrawn = withdrawn;
}
// equals and hashCode omitted
}
1 | The JPA entity is named products to reuse the JDBC product DataTarget . Alternatively, a specific JPA target could be used like this: DataTarget<ProductEntity> TARGET = JpaTarget.of(ProductEntity.class) |
Finally, you only need to replace the JDBC Datastore starter with a JPA starter. Using Hibernate as ORM, the JDBC starter dependency has to be replaced by:
<dependency>
<groupId>com.holon-platform.jpa</groupId>
<artifactId>holon-starter-jpa-hibernate</artifactId>
</dependency>
Now you’re ready to run the TestDatastore
JUnit test using mvn test
or your favorite IDE command.
Summary
You’ve learned how to use the Datastore
API to manage the persistence of simple data entity and to leverage on the Holon Platfom Spring Boot support to easily configure a Datastore
implementation.
In this example, the JDBC and JPA Datastore
implementation were used, showing the high level of abstraction and technology-independence ensured by the Property model, allowing to switch from one implementation to another with a minimal effort.
See also
The source code of this tutorial is available on GitHub:
See also: Example using the MongoDB Datastore.
See the Holon Platform tutorials to explore other available tutorials.