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

The source code for this tutorial is available on GitHub in the Holon Platform examples repository:

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.1</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 the Datastore API data manipulation operations, which provides any auto-generated key value through the getInsertedKey method.

  • Or by using the BRING_BACK_GENERATED_IDS default write option, which brings back any auto-generated key value value into the PropertyBox which was subject of a data manipulation operation, setting the corresponding property value (the ID 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());
}

Putting it all together

Finally, we create the actual JUnit test method (annotated with @Test) to invoke all the methods defined so far:

@Test
public void testDatastore() {
  testSave();
  testQuery();
  testOperations();
  testBulk();
}

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 the Holon Platform tutorials to explore other available tutorials.