Data Access Service Reference Guide
Repository Service
The repository service is for CRUD operations of aggregate objects. Unlike the query and the SQL service, the repository service supports multiple operations, including
- read, for retrieving objects
- create, for creating object in database
- update, for updating object in database in its entirety
- delete, for deleting object from database
- save, for creating or updating object in database, and
- merge, for merging object changes into database.
The repository service comprises:
- an object
- a read component, and
- a write component
The read component is a dynamic query for the object, comprising:
- an input for read
- a query for read
- an input bindings component that maps the query parameters to the input fields, and
- an output bindings component that maps the query columns to the object fields
The output of the read operation, by definition, is always an array of the repository objects, even for read by id. In this case, it returns an array of a single object if the object exists, and an empty array if not.
The write components comprises:
- table bindings that map the root object and its child structures to the root and child database tables, respectively, and
- columns bindings that map the columns of a mapped table to the data fields of the repository object.
File Structure
The following is the file structure of repository service:
myRepositoryObject
service.json
object.json
read/
input.json
query.sql
input-bindings.json
output-bindings.json
write/
tables.json
my-root-table.table-alias.columns.json
my-child-table.table-alias.columns.json
tests/
testReadMyRepositoryObject.json
testCreateMyRepositoryObject.json
testUpdateMyRepositoryObject.json
testDeleteMyRepositoryObject.json
This file structure is generated when the service is created. TThere is a file for each component of the repository service. The user is expected to complete the object component and all sub-components of the read and write components.
Example
The following is an exemplary repository service for an Order
object.
Object
{
"orderNumber": 23,
"orderDate": "2021-01-01T00:00:00.000Z",
"customerNumber": 12,
"customerName": "John",
"orderLines": [{
"orderLineNumber": 123,
"productCode": "PC",
"productName": "Computer",
"quantityOrdered": 10,
"priceEach": 599.00
}]
}
The repository object is generally a complex object with nested structure.
Input
{
"orderNumber": 1,
"customerNumber": 2,
"startDate": "2021-01-01T00:00:00.000Z",
"endDate": "2021-02-01T00:00:00.000Z"
}
The input is a simple object carrying the query parameters for read operation.
Query
select o.orderNumber, o.orderDate,
_c.customerNumber, _c.customerName,
od.orderLineNumber, _p.productCode, _p.productName,
od.quantityOrdered, od.priceEach
from classicmodels.orders o
join classicmodels.customers _c on o.customerNumber = _c.customerNumber
left join classicmodels.orderdetails od on od.orderNumber = o.orderNumber
left join classicmodels.products _p on _p.productCode = od.productCode
where 1 = 1
and o.orderNumber = :orderNumber
and o.customerNumber = :customerNumber
and o.orderDate between :startDate and :endDate
order by o.orderNumber, od.orderLineNumber
This query is a single SELECT statement for read like the query service. However, it must follow the rules for repository query to be discussed below.
Input Bindings
[
{
"parameter": "orderNumber",
"field": ".orderNumber"
},
{
"parameter": "customerNumber",
"field": ".customerNumber"
},
{
"parameter": "startDate",
"field": ".startDate"
},
{
"parameter": "endDate",
"field": ".endDate"
}
]
The input bindings map the query parameter to the read input field.
Output Bindings
[
{
"column": "orderNumber",
"field": ".orderNumber"
},
{
"column": "orderDate",
"field": ".orderDate"
},
{
"column": "customerNumber",
"field": ".customerNumber"
},
{
"column": "customerName",
"field": ".customerName"
},
{
"column": "orderLineNumber",
"field": ".orderLines..orderLineNumber"
},
{
"column": "productCode",
"field": ".orderLines..productCode"
},
{
"column": "productName",
"field": ".orderLines..productName"
},
{
"column": "quantityOrdered",
"field": ".orderLines..quantityOrdered"
},
{
"column": "priceEach",
"field": ".orderLines..priceEach"
}
]
The output bindings map the query column to the object fields.
Table Bindings
[
{
"name": "classicmodels.orders",
"alias": "o",
"object": ".",
"rootTable": true,
"mainTable": true,
"operationIndicator": null,
"columns": "./classicmodels.orders.columns.json"
},
{
"name": "classicmodels.orderdetails",
"alias": "od",
"object": ".orderLines",
"rootTable": false,
"mainTable": false,
"operationIndicator": null,
"columns": "./classicmodels.orderdetails.columns.json"
}
]
The following summarizes the properties of the table binding.
Property | Description | Generated by Service Builder | User Editable |
---|---|---|---|
name | name of table | Yes | No |
alias | alias of table | Yes | No |
object | path of root or child structure | Yes if match found | Yes |
root table | whether the root table | Yes | No |
main table | whether a main table | Yes | Yes |
operationIndicator | name of object field indicating merge operation | No | No |
columns | reference of column binding file | Yes | No |
The table bindings map the root object and its child structures to the database tables. In this example, the root object order
is mapped to the classicmodels.orders
table, and the child array orderLines
is mapped to the classicmodels.orderdetails
table. The table bindings specifies what tables shall be inserted with what objects/arrays, when the object is created. The order of inserts is dictated by the order of tables in the table bindings.
Column Bindings
There is a column bindings component for each of the mapped tables, which include the orders
and the orderdetails
tables in this case. However, only an abbreviated version of the column bindings for the orders
table is shown here for brevity.
[
{
"position": 1,
"column": "orderNumber",
"field": ".orderNumber",
"key": true,
"autoGenerate": false,
"inputField": ".orderNumber",
"insertValue": null,
"updateValue": null,
"version": false,
"softDelete": false,
"dataType": "number",
"notNull": true,
"keyEligible": false,
"versionEligible": true,
"softDeleteEligible": false
},
{
"position": 2,
"column": "orderDate",
"field": ".orderDate",
"key": false,
"autoGenerate": false,
"inputField": null,
"insertValue": null,
"updateValue": null,
"version": false,
"softDelete": false,
"dataType": "datetime",
"notNull": true,
"keyEligible": false,
"versionEligible": false,
"softDeleteEligible": false
},
...
]
The following summarizes the properties of the column binding.
Property | Description | Generated by Service Builder | User Editable |
---|---|---|---|
column | name of column | Yes | No |
field | path of field mapped to the column | Yes, if match found | Yes |
key | whether a key column | Yes, if primary key defined for table | Yes |
autoGenerate | whether an auto generate column | Yes | No |
inputField | path of input field mapped to the key column. Specify if you want the write operations to read back updated objects | No | Yes |
insertValue | insert value for the column | No | Yes |
updateValue | update value for the column | No | Yes |
version | wether a version control column | No | Yes |
softDelete | wether a soft delete column | No | Yes |
Meta Properties: | |||
position | column position in the table | Yes | No |
dataType | target json data type | Yes | No |
notNull | whether null value allowed | Yes | No |
keyEligible | whether eligible as a key column | Yes | No |
versionEligible | whether eligible as a version column | Yes | No |
softDeleteEligible | whether eligible as a soft delete column | Yes | No |
The columns bindings map the object field to the columns of a table. It specifies how a row should be inserted and updated.
When insert, the value of a column is from:
- database if an auto-generate column
- the insertValue if specified. The insertValue should be a DB expression
- the object field if mapped and the field exists in the input
- otherwise, the column is excluded from the insert statement if none of the above is true.
When update, the value of a column is from:
- the updateValue if specified, the updateValue should be an DB expression
- the object field if mapped and the field exists in the input
- otherwise, the column is excluded from the update statement if none of the above is true.
Both update and delete are by the key. The key is always the primary key of the table. However, if in the rare case where no primary key is defined, the user needs to select one or more columns as the key by setting their key
properties to true.
The repository service supports optimistic locking with a version column. The version column is designated by setting its version
property to true. The version column must be a number column. The versionEligible
property indicates if the column is a fit for version control.
The repository service supports soft delete. The soft-delete column is designated by setting its softDelete
property to true. The soft-delete column must be a character column. repository service updates this column to 'Y'
when delete occurs.
Read-Write Asymmetry
The read and write operations are often asymmetric, meaning that the read operations may read from more tables than the write operations write into.
For example, in the order
example above, the read operations read from four tables: orders
, orderdetails
, customers
and products
, but the write operations only write into the orders
and orderdetails
table.
The customers
and products
tables in this case are read-only, and are called reference table, as they are referenced by other tables.
Root, Child and Reference Table
The root table stores the root object. The child tables store the data that depends on the root object. The reference tables are read-only, with a different life-cycle than the root object. Table bindings are not generated for the reference tables.
In the above example, orders
is the root table, orderdetails
is a child table, and customers
and products
are reference tables.
Main Table
The root table is usually the main table, or the top parent table for the aggregate object, referenced by the child tables. However, there are cases where a child table is referenced by the root table. For example, we may have a root table customers
referencing the child table addresses
. When creating the following Customer
object:
{
"customerNumber": 123,
"customerName": "John",
"address": {
"addressId": 123,
"street": "123 Main st",
"city": "Los Angeles"
}
}
We need to config the addresses
table to be the main table, so that a record is inserted into this table before the record for the root table.
Repository Query
The query for repository read operation follows a few rules as listed below:
- The query should an an ANSI query, with the root table being the only table in the FROM clause, like
classicmodels.orders
in the example above; - The child tables shall be added using ANSI left join, like
classicmodels.orderDetails
in the example above, in the order that the records are to be inserted when creating the object - The reference tables shall be added using ANSI inner or left join as appropriate, like
classicmodels.customers
andclassicmodels.products
in the example above; - All table should be aliased, like
o
for theclassicmodels.orders
table in the example above; - The alias for the reference table should start with "_", like
_p
for theclassicmodels.products
table in the example above; - All table columns from the root and child tables should be prefixed with the respective table alias;
- The WHERE clause may need to include
1 = 1
as a base condition to supportreadAll
operation, as the query for read is dynamic by definition.
Database views can only be used as reference tables in repository query.
Repository Operations
The repository service is a multi-operation service. The chart below lists the supported operations, and the input and output for each operation:
Operation | Input | Output |
---|---|---|
read | input | array of objects |
create | object or objects | created object or objects |
delete | object or objects | none |
update | object or objects | updated object or objects |
save | object or objects | saved object or objects |
merge | object or objects | updated object or objects |
For write
operations, the objects can be batched. For delete
operation, the object only needs to contain the key fields of the root object. The write operation returns null by default, unless the inputField
property is specified for the key columns in the column bindings.
The inputField
is the field path to the key field in read input. The repository service retrieves the update object from the database using the readByKey
query, which is assumed to be supported by the read component of the service.
Read
The read operation is a query for the object. The input is an instance of the input object; the output is an array of the objects.
The query is dynamic. In the example above, if the input is
it is a query for all objects. If the input is
it is a query by order number. If the input is
{
"customerNumber": 123,
"startDate": "2021-01-01T00:00:00.000Z",
"endDate": "2021-02-01T00:00:00.000Z"
}
it is a query by customer number and date range.
Create
The create operation creates an instance of the aggregate object, including the root object and its children, in the database. The input is the object to be created, and the output is the object created. However, the input may also be an array of the objects in the service request, and the output will be an array of the objects created in this case.
Delete
The delete operation deletes the aggregate, including the root object and its children, from the database. The input is the object or the array of objects to be deleted, the output is none. The delete is by the key of the root object/table, and it is sufficient to include only the key fields of the object in the service request.
Update
The update operation updates the aggregate, including the root object and its children, in the database. The input is the object to be updated, and the output is the object updated. However, the input may also be an array of the objects, and the output will be an array of the objects updated in this case.
The update is done by the key of the root object/table. The object that is not found and thus not updated will be returned as {}
in the output.
In this operation, the old version of the object will be replaced by the given version in its entirety. As part of this process, the child objects of the aggregate may be created, updated or deleted from the database, to sync the stored object with the given object.
Save
The save operation is a create operation if the object does not yet exist or an update operation if the object already exists in the database.
Merge
The merge operation is an experimental feature that merges the changes to an aggregate into the current version in the database. The input is the object that carries the changes, and the output is the merged object. The input may also be an array of objects in the service request, and the output will be an array of merged objects in this case.
The input object should only include the fields that are changed. For arrays, an operation indicator fields should be included with the element to indicate the type of operation to be performed for the element. The valid values include: create
, update
or delete
.
The name of operation indicator field is specified in the table bindings. The operation indicator field itself will not be persisted. Its sole purpose is to indicate the type of change.