Working with Data
This section describes how data is represented and used in the CAP Java SDK.
Content
Predefined Types
The predefined CDS types are mapped to Java types and as follows:
CDS Type | Java Type | Remark |
---|---|---|
cds.UUID |
java.lang.String |
|
cds.Boolean |
java.lang.Boolean |
|
cds.Integer |
java.lang.Integer |
|
cds.Integer64 |
java.lang.Long |
|
cds.Decimal |
java.math.BigDecimal |
|
cds.DecimalFloat |
java.math.BigDecimal |
deprecated |
cds.Double |
java.lang.Double |
|
cds.Date |
java.time.LocalDate |
date without a time-zone (year-month-day) |
cds.Time |
java.time.LocalTime |
time without a time-zone (hour-minute-second) |
cds.DateTime |
java.time.Instant |
instant on the time-line with sec precision |
cds.Timestamp |
java.time.Instant |
instant on the time-line with µs precision |
cds.String |
java.lang.String |
|
cds.LargeString |
java.lang.String |
java.io.Reader (1) if annotated with @Core.MediaType |
cds.Binary |
byte[] |
|
cds.LargeBinary |
byte[] |
java.io.InputStream (1) if annotated with @Core.MediaType |
SAP HANA-Specific Data Types
To facilitate using legacy CDS models, the following SAP HANA-specific data types are supported:
CDS Type | Java Type | Remark |
---|---|---|
hana.TINYINT |
java.lang.Short |
|
hana.SMALLINT |
java.lang.Short |
|
hana.SMALLDECIMAL |
java.math.BigDecimal |
|
hana.REAL |
java.lang.Float |
|
hana.CHAR |
java.lang.String |
|
hana.NCHAR |
java.lang.String |
|
hana.VARCHAR |
java.lang.String |
|
hana.CLOB |
java.lang.String |
java.io.Reader (1) if annotated with @Core.MediaType |
hana.BINARY |
byte[] |
(1) Although the API to handle large objects is the same for every database, the streaming feature, however, is supported (and tested) in SAP HANA, PostgreSQL, and H2. See section Database Support in Java for more details on database support and limitations.
❗ Warning
The framework isn’t responsible for closing the stream when writing to the database. You decide when the stream is to be closed. If you forget to close the stream, the open stream can lead to a memory leak.
These types are used for the values of CDS elements with primitive type. In the Model Reflection API, they’re represented by the enum CdsBaseType.
Structured Data
Structured data is used as input for Insert, Update, and Upsert statements and represents the results of Select statements.
In the following we use this CDS model:
entity Books {
key ID : Integer;
title : String;
author : Association to one Authors;
}
entity Authors {
key ID : Integer;
name : String;
books : Association to many Books on books.author = $self;
}
Find this source also in cap/samples.
Entities and Structured Types
Entities or structured types are represented in Java as a Map<String, Object>
that maps the element names to the element values.
The following example shows JSON data and how it can be constructed in Java:
{
"ID" : 97,
"title" : "Dracula"
}
//java
Map<String, Object> book = new HashMap<>();
book.put("ID", 97);
book.put("title", "Dracula");
Data of structured types and entities can be sparsely populated.
Nested Structures and Associations
Nested structures and single-valued associations, are represented by elements where the value is structured. In Java, the value type for such a representation is a map.
The following example shows JSON data and how it can be constructed in Java:
{
"ID" : 97,
"author" :
{
"ID" : 23,
"name" : "Bram Stoker"
}
}
// java
Map<String, Object> author = new HashMap<>();
author.put("ID", 23);
author.put("name", "Bram Stoker");
Map<String, Object> book = new HashMap<>();
book.put("ID", 97);
book.put("author", author);
A to-many association is represented by a List<Map<String, Object>>
.
The following example shows JSON data and how it can be constructed in Java:
{
"ID" : 23,
"books" :
[
{
"ID" : 97,
"title" : "Dracula"
},
{
"ID" : 98,
"name" : "Miss Betty"
}
],
"name" : "Bram Stoker"
}
// java
Map<String, Object> book1 = new HashMap<>();
book1.put("ID", 97;
book1.put("title", "Dracula");
Map<String, Object> book2 = new HashMap<>();
book2.put("ID", 98);
book2.put("title", "Miss Betty");
Map<String, Object> author = new HashMap<>();
author.put("ID", 23);
author.put(books, Arrays.asList(book1, book2));
author.put("name", "Bram Stoker");
Typed Access
Representing data given as Map<String, Object>
is flexible and interoperable with other frameworks. But it also has some disadvantages:
- Names of elements are checked only at runtime
- No code completion in the IDE
- No type safety
To ease the handling of data, the CAP Java SDK additionally provides typed access to data through accessor interfaces:
Let’s assume following data for a book:
Map<String, Object> book = new HashMap<>();
book.put("ID", 97);
book.put("title", "Dracula");
You can now either define an accessor interface or use a generated accessor interface. The accessor interface then looks like in the following example:
interface Book extends Map<String, Object> {
@CdsName("ID") // name of the CDS element
Integer getID();
String getTitle();
void setTitle(String title);
}
At runtime, the Struct.access
method is used to create a proxy that gives typed access to the data through the accessor interface:
import static com.sap.cds.Struct.access;
...
Book book = access(data).as(Book.class);
String title = book.getTitle(); // read the value of the element 'title' from the underlying map
book.setTitle("Miss Betty"); // update the element 'title' in the underlying map
title = data.get("title"); // direct access to the underlying map
title = book.get("title"); // hybrid access to the underlying map through the accessor interface
To support hybrid access, like simultaneous typed and generic access, the accessor interface just needs to extend Map<String, Object>
.
The name of the CDS element referred to by a getter or setter, is defined through @CdsName
annotation. If the annotation is missing, it’s determined by removing the get/set from the method name and lowercasing the first character.
Generated Accessor Interfaces
For all structured types of the CDS model, accessor interfaces can be generated using the CDS Maven Plugin. The generated accessor interfaces allow for hybrid access and easy serialization to JSON.
Renaming Elements in Java
Element names used in the CDS model might conflict with reserved Java keywords (class
, private
, transient
, etc.). In this case, the @cds.java.name
annotation must be used to specify an alternative property name that will be used for the generation of accessor interfaces and static model interfaces. The element name used as key in the underlying map for dynamic access isn’t affected by this annotation.
See the following example:
entity Equity {
@cds.java.name : 'clazz'
class : String;
...
}
interface Equity {
String getClazz();
void setClazz(String clazz);
...
}
Creating a Data Container for an Interface
To create an empty data container for an interface, use the Struct.create
method:
import static com.sap.cds.Struct.create;
...
Book book = create(Book.class);
book.setTitle("Dracula");
String title = book.getTitle(); // title: "Dracula"
Generated accessor interfaces contain a static create
method that further facilitates the usage:
Book book = Books.create();
book.setTitle("Dracula");
String title = book.getTitle(); // title: "Dracula"
Read-Only Access
Create a typed read-only view using access
. Calling a setter on the view throws an exception.
import static com.sap.cds.Struct.access;
...
Book book = access(data).asReadOnly(Book.class);
String title = book.getTitle();
book.setTitle("CDS4j"); // throws Exception
Typed Streaming of Data
Data given as Iterable<Map<String, Object>>
can also be streamed:
import static com.sap.cds.Struct.stream;
...
Stream<Book> books = stream(data).as(Book.class);
List<Book> bookList = books.collect(Collectors.toList());
Typed Access to Query Results
Typed access through custom or generated accessor interfaces eases the processing of query result.
CDS Data Processor
The CdsDataProcessor
allows to process deeply nested maps of CDS data, by executing a sequence of registered actions (validators, converters, and generators).
Using the create
method, a new instance of the CdsDataProcessor
can be created:
CdsDataProcessor processor = CdsDataProcessor.create();
Validators, converters, and generators can be added using the respective add
method, which takes a filter and an action as arguments and is executed when the filter
is matching.
processor.addValidator(filter, action);
When calling the process
method of the CdsDataProcessor
, the actions are executed sequentially in order of the registration.
List<Map<String, Object>> data; // data to be processed
CdsStructuredType rowType; // row type of the data
processor.process(data, rowType);
The process method can also be used on CDS.ql results that have a row type:
CqnSelect query; // some query
Result result = service.run(query);
processor.process(result);
Element Filters
Filters can be defined as lambda expressions on path
, element
, and type
, for the instance:
(path, element, type) -> element.isKey()
&& type.isSimpleType(CdsBaseType.STRING)
which is matching key elements of type String.
path
describes the path from the structured root type of the data to the parent type ofelement
and provides access to the data values of each path segmentelement
is the CDS elementtype
- for primitive elements the element’s CDS type
- for associations the association’s target type
- for arrayed elements the array’s item type
Data Validators
Validators validate the values of CDS elements matching the filter. New validators can be added using the addValidator
method.
The following example adds a validator that logs a warning if the CDS element amount
has a negative value. The warning message contains the path
to the element
.
processor.addValidator(
(path, element, type) -> element.getName().equals("amount"), // filter
(path, element, value) -> { // validator
if ((int) value < 0) {
log.warn("Negative amount: " + path.toRef());
}
});
Data Converters
Converters convert or remove values of CDS elements matching the filter. New converters can be added using the addConverter
method.
The following example adds a converter that formats elements with name price
.
processor.addConverter(
(path, element, type) -> element.getName().equals("price"), // filter
(path, element, value) -> formatter.format(value)); // converter
To remove a value from the data, return Converter.REMOVE
.
The following example adds a converter that removes values of associations and compositions.
processor.addConverter(
(path, element, type) -> element.getType().isAssociation(), // filter
(path, element, value) -> Converter.REMOVE); // remover
Data Generators
Generators generate the values for CDS elements matching the filter and are missing in the data or mapped to null.
New generators can be added using the addGenerator
method.
The following example adds a UUID generator for elements of type UUID that are missing in the data.
processor.addGenerator(
(path, element, type) -> type.isSimpleType(UUID), // filter
(path, element, isNull) -> isNull ? null : randomUUID()); // generator